Skip to content

Commit f8c8b17

Browse files
v0idpwnpolvalente
andauthored
Accept URIs with scheme, add default SSL options for https, deprecate URLs without scheme (#357)
* Accept URIs with scheme, add default SSL options for https, deprecate URLs without scheme This commit changes how addresses are handled by GRPC.Stub.connect/2. First, by accepting URIs with scheme. Before, `http://localhost:50051" would cause a crash. Second, by using the URI scheme to automatically set a GRPC credential if no GRPC credentials were provided. If we were to remove this part, we could at least validate that a GRPC credential was provided when the scheme is https. Finally, by deprecating receiving URLs without scheme, such as `localhost:50051`. This isn't strictly necessary, but in my opinion makes things more consistent when we want to support both local sockets and remote urls. * Remove superflous guards * Improve error message * Update lib/grpc/stub.ex * Compute default ssl option in compile_time Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com> * format * Fix mistake * Add error when trying to use :cred with http * Minor fixes/improvements * Test for opening channels with ips, restructure channel test suite * Fix typespec for connect/2 * Update lib/grpc/stub.ex * Add scheme in all non-unix socket connect/2 calls Makes all examples in the documentation include `http` or `https`. Changes tests to avoid using deprecated code. * Revert "Add scheme in all non-unix socket connect/2 calls" This reverts commit ec02a94. * Remove deprecation warning * Apply suggestions from code review --------- Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com>
1 parent c677e75 commit f8c8b17

File tree

4 files changed

+114
-18
lines changed

4 files changed

+114
-18
lines changed

lib/grpc/stub.ex

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,62 @@ defmodule GRPC.Stub do
131131
"""
132132
@spec connect(String.t(), keyword()) :: {:ok, Channel.t()} | {:error, any()}
133133
def connect(addr, opts \\ []) when is_binary(addr) and is_list(opts) do
134-
{host, port} =
135-
case String.split(addr, ":") do
136-
[host, port] -> {host, port}
137-
[socket_path] -> {{:local, socket_path}, 0}
138-
end
134+
# This works because we only accept `http` and `https` schemes (allowlisted below explicitly)
135+
# addresses like "localhost:1234" parse as if `localhost` is the scheme for URI, and this falls through to
136+
# the base case. Accepting only `http/https` is a trait of `connect/3`.
137+
138+
case URI.parse(addr) do
139+
%URI{scheme: @secure_scheme, host: host, port: port} ->
140+
opts = Keyword.put_new_lazy(opts, :cred, &default_ssl_option/0)
141+
connect(host, port, opts)
142+
143+
%URI{scheme: @insecure_scheme, host: host, port: port} ->
144+
if opts[:cred] do
145+
raise ArgumentError, "invalid option for insecure (http) address: :cred"
146+
end
147+
148+
connect(host, port, opts)
149+
150+
# For compatibility with previous versions, we accept URIs in
151+
# the "#{address}:#{port}" format
152+
_ ->
153+
case String.split(addr, ":") do
154+
[socket_path] ->
155+
connect({:local, socket_path}, 0, opts)
156+
157+
[address, port] ->
158+
port = String.to_integer(port)
159+
connect(address, port, opts)
160+
end
161+
end
162+
end
139163

140-
connect(host, port, opts)
164+
if {:module, CAStore} == Code.ensure_loaded(CAStore) do
165+
defp default_ssl_option do
166+
%GRPC.Credential{
167+
ssl: [
168+
verify: :verify_peer,
169+
depth: 99,
170+
cacert_file: CAStore.file_path()
171+
]
172+
}
173+
end
174+
else
175+
defp default_ssl_option do
176+
raise """
177+
no GRPC credentials provided. Please either:
178+
179+
- Pass the `:cred` option to `GRPC.Stub.connect/2,3`
180+
- Add `:castore` to your list of dependencies in `mix.exs`
181+
"""
182+
end
141183
end
142184

143-
@spec connect(String.t(), binary() | non_neg_integer(), keyword()) ::
144-
{:ok, Channel.t()} | {:error, any()}
185+
@spec connect(
186+
String.t() | {:local, String.t()},
187+
binary() | non_neg_integer(),
188+
keyword()
189+
) :: {:ok, Channel.t()} | {:error, any()}
145190
def connect(host, port, opts) when is_binary(port) do
146191
connect(host, String.to_integer(port), opts)
147192
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ defmodule GRPC.Mixfile do
4545
{:gun, "~> 2.0"},
4646
{:jason, ">= 0.0.0", optional: true},
4747
{:cowlib, "~> 2.12"},
48+
{:castore, "~> 0.1 or ~> 1.0", optional: true},
4849
{:protobuf, "~> 0.11"},
4950
{:protobuf_generate, "~> 0.1.1", only: [:dev, :test]},
5051
{:googleapis,

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
%{
2+
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
23
"cowboy": {:hex, :cowboy, "2.11.0", "356bf784599cf6f2cdc6ad12fdcfb8413c2d35dab58404cf000e1feaed3f5645", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0fa395437f1b0e104e0e00999f39d2ac5f4082ac5049b67a5b6d56ecc31b1403"},
34
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
45
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},

test/grpc/channel_test.exs

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,69 @@ defmodule GRPC.ChannelTest do
33
alias GRPC.Test.ClientAdapter
44
alias GRPC.Channel
55

6-
test "connect/2 works for insecure" do
7-
{:ok, channel} = GRPC.Stub.connect("10.1.0.0:50051", adapter: ClientAdapter)
8-
assert %Channel{host: "10.1.0.0", port: 50051, scheme: "http", cred: nil} = channel
9-
end
6+
for {kind, addr} <- [{"ip", "10.0.0.1"}, {"hostname", "example.com"}] do
7+
describe "connect/2 with http and #{kind}" do
8+
test "works" do
9+
{:ok, channel} =
10+
GRPC.Stub.connect("http://#{unquote(addr)}:50051", adapter: ClientAdapter)
11+
12+
assert %Channel{host: unquote(addr), port: 50051, scheme: "http", cred: nil} = channel
13+
end
14+
15+
test "errors if credential is provided" do
16+
cred = %GRPC.Credential{ssl: []}
17+
18+
assert_raise ArgumentError, "invalid option for insecure (http) address: :cred", fn ->
19+
GRPC.Stub.connect("http://#{unquote(addr)}:50051", adapter: ClientAdapter, cred: cred)
20+
end
21+
end
22+
end
23+
24+
describe "connect/2 with https and #{kind}" do
25+
test "sets default credential" do
26+
{:ok, channel} =
27+
GRPC.Stub.connect("https://#{unquote(addr)}:50051", adapter: ClientAdapter)
28+
29+
assert %Channel{host: unquote(addr), port: 50051, scheme: "https", cred: cred} = channel
30+
31+
assert Keyword.has_key?(cred.ssl, :verify)
32+
assert Keyword.has_key?(cred.ssl, :depth)
33+
assert Keyword.has_key?(cred.ssl, :cacert_file)
34+
end
35+
36+
test "allows overriding default credentials" do
37+
cred = %GRPC.Credential{ssl: []}
1038

11-
test "connect/2 works for ssl" do
12-
cred = %{ssl: []}
13-
{:ok, channel} = GRPC.Stub.connect("10.1.0.0:50051", adapter: ClientAdapter, cred: cred)
14-
assert %Channel{host: "10.1.0.0", port: 50051, scheme: "https", cred: ^cred} = channel
39+
{:ok, channel} =
40+
GRPC.Stub.connect("https://#{unquote(addr)}:50051", adapter: ClientAdapter, cred: cred)
41+
42+
assert %Channel{host: unquote(addr), port: 50051, scheme: "https", cred: ^cred} = channel
43+
end
44+
end
45+
46+
describe "connect/2 with no scheme, #{kind} and" do
47+
test "no cred uses http" do
48+
{:ok, channel} = GRPC.Stub.connect("#{unquote(addr)}:50051", adapter: ClientAdapter)
49+
assert %Channel{host: unquote(addr), port: 50051, scheme: "http", cred: nil} = channel
50+
end
51+
52+
test "cred uses https" do
53+
cred = %{ssl: []}
54+
55+
{:ok, channel} =
56+
GRPC.Stub.connect("#{unquote(addr)}:50051", adapter: ClientAdapter, cred: cred)
57+
58+
assert %Channel{host: unquote(addr), port: 50051, scheme: "https", cred: ^cred} = channel
59+
end
60+
end
1561
end
1662

1763
test "connect/2 allows setting default headers" do
1864
headers = [{"authorization", "Bearer TOKEN"}]
19-
{:ok, channel} = GRPC.Stub.connect("10.1.0.0:50051", adapter: ClientAdapter, headers: headers)
20-
assert %Channel{host: "10.1.0.0", port: 50051, headers: ^headers} = channel
65+
66+
{:ok, channel} =
67+
GRPC.Stub.connect("http://10.0.0.1:50051", adapter: ClientAdapter, headers: headers)
68+
69+
assert %Channel{host: "10.0.0.1", port: 50051, headers: ^headers} = channel
2170
end
2271
end

0 commit comments

Comments
 (0)