Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gun adapter proxy auths #424

Merged
merged 6 commits into from
Dec 7, 2020
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 67 additions & 34 deletions lib/tesla/adapter/gun.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ if Code.ensure_loaded?(:gun) do
- `:close_conn` - Close connection or not after receiving full response body. Is used for reusing gun connections. Defaults to `true`.
- `:certificates_verification` - Add SSL certificates verification. [erlang-certifi](https://github.com/certifi/erlang-certifi) [ssl_verify_fun.erl](https://github.com/deadtrickster/ssl_verify_fun.erl)
- `:proxy` - Proxy for requests. **Socks proxy are supported only for gun master branch**. Examples: `{'localhost', 1234}`, `{{127, 0, 0, 1}, 1234}`, `{:socks5, 'localhost', 1234}`.
NOTE: By default GUN uses TLS as transport if the specified port is 443, if TLS is required for proxy conection on another port please specify transport using the Gun options below otherwise tcp will be used
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NOTE: By default GUN uses TLS as transport if the specified port is 443, if TLS is required for proxy conection on another port please specify transport using the Gun options below otherwise tcp will be used
NOTE: By default GUN uses TLS as transport if the specified port is 443, if TLS is required for proxy connection on another port please specify transport using the Gun options below otherwise tcp will be used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, corrected :)

- `:proxy_auth` - Auth to be passed along with the proxy opt, supports Basic auth for regular and Socks proxy. Format: `{proxy_username, proxy_password}`.

## [Gun options](https://ninenines.eu/docs/en/gun/1.3/manual/gun/)

Expand Down Expand Up @@ -178,9 +180,14 @@ if Code.ensure_loaded?(:gun) do
conn_scheme =
case info do
# gun master branch support, which has `origin_scheme` in connection info
%{origin_scheme: scheme} -> scheme
%{transport: :tls} -> "https"
_ -> "http"
%{origin_scheme: scheme} ->
scheme

%{transport: :tls} ->
"https"

_ ->
"http"
end

conn_host =
Expand Down Expand Up @@ -215,50 +222,31 @@ if Code.ensure_loaded?(:gun) do
end
end

defp maybe_add_transport(%URI{scheme: "https"}, opts), do: Map.put(opts, :transport, :tls)
defp maybe_add_transport(_, opts), do: opts

# Support for gun master branch where transport_opts, were splitted to tls_opts and tcp_opts
# https://github.com/ninenines/gun/blob/491ddf58c0e14824a741852fdc522b390b306ae2/doc/src/manual/gun.asciidoc#changelog
# TODO: remove after update to gun 2.0
defp fetch_tls_opts(%{tls_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
defp fetch_tls_opts(%{transport_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
defp fetch_tls_opts(_), do: []

defp maybe_add_verify_options(tls_opts, %{certificates_verification: true}, %{host: host}) do
charlist =
host
|> to_charlist()
|> :idna.encode()

security_opts = [
verify: :verify_peer,
cacertfile: CAStore.file_path(),
depth: 20,
reuse_sessions: false,
verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: charlist]}
]

Keyword.merge(security_opts, tls_opts)
end

defp maybe_add_verify_options(tls_opts, _, _), do: tls_opts

defp do_open_conn(uri, %{proxy: {proxy_host, proxy_port}}, gun_opts, tls_opts) do
defp do_open_conn(uri, %{proxy: {proxy_host, proxy_port}} = opts, gun_opts, tls_opts) do
connect_opts =
uri
|> tunnel_opts()
|> tunnel_tls_opts(uri.scheme, tls_opts)
|> add_proxy_auth_credentials(opts)

with {:ok, pid} <- :gun.open(proxy_host, proxy_port, gun_opts),
{:ok, _} <- :gun.await_up(pid),
stream <- :gun.connect(pid, connect_opts),
{:response, :fin, 200, _} <- :gun.await(pid, stream) do
{:ok, pid}
else
{:response, :nofin, 403, _} -> {:error, :unauthorized}
{:response, :nofin, 407, _} -> {:error, :proxy_auth_failed}
error -> error
end
end

defp do_open_conn(uri, %{proxy: {proxy_type, proxy_host, proxy_port}}, gun_opts, tls_opts) do
defp do_open_conn(
uri,
%{proxy: {proxy_type, proxy_host, proxy_port}} = opts,
gun_opts,
tls_opts
) do
version =
proxy_type
|> to_string()
Expand All @@ -273,6 +261,7 @@ if Code.ensure_loaded?(:gun) do
|> tunnel_opts()
|> tunnel_tls_opts(uri.scheme, tls_opts)
|> Map.put(:version, version)
|> add_socks_proxy_auth_credentials(opts)

gun_opts =
gun_opts
Expand All @@ -291,6 +280,38 @@ if Code.ensure_loaded?(:gun) do
end
end

# In case of a proxy being used the transport opt for initial gun open must be in accordance with the proxy host and port
# and not force TLS
defp maybe_add_transport(_, %{proxy: proxy_opts} = opts) when not is_nil(proxy_opts), do: opts
defp maybe_add_transport(%URI{scheme: "https"}, opts), do: Map.put(opts, :transport, :tls)
defp maybe_add_transport(_, opts), do: opts

# Support for gun master branch where transport_opts, were splitted to tls_opts and tcp_opts
# https://github.com/ninenines/gun/blob/491ddf58c0e14824a741852fdc522b390b306ae2/doc/src/manual/gun.asciidoc#changelog
# TODO: remove after update to gun 2.0
defp fetch_tls_opts(%{tls_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
defp fetch_tls_opts(%{transport_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
defp fetch_tls_opts(_), do: []

defp maybe_add_verify_options(tls_opts, %{certificates_verification: true}, %{host: host}) do
charlist =
host
|> to_charlist()
|> :idna.encode()

security_opts = [
verify: :verify_peer,
cacertfile: CAStore.file_path(),
depth: 20,
reuse_sessions: false,
verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: charlist]}
]

Keyword.merge(security_opts, tls_opts)
end

defp maybe_add_verify_options(tls_opts, _, _), do: tls_opts

@dialyzer [{:nowarn_function, do_open_conn: 4}, :no_match]
defp do_open_conn(uri, opts, gun_opts, tls_opts) do
tcp_opts = Map.get(opts, :tcp_opts, [])
Expand Down Expand Up @@ -345,6 +366,18 @@ if Code.ensure_loaded?(:gun) do

defp tunnel_tls_opts(opts, _, _), do: opts

defp add_proxy_auth_credentials(opts, %{proxy_auth: {username, password}})
when not is_nil(username) and not is_nil(password),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe is_binary/1 guard will be more suitable here for checking username and password?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are absolutely right, corrected.

do: Map.merge(opts, %{username: username, password: password})

defp add_proxy_auth_credentials(opts, _), do: opts

defp add_socks_proxy_auth_credentials(opts, %{proxy_auth: {username, password}})
when not is_nil(username) and not is_nil(password),
do: Map.put(opts, :auth, {:username_password, username, password})

defp add_socks_proxy_auth_credentials(opts, _), do: opts

defp open_stream(pid, method, path, headers, body, opts) do
req_opts = %{reply_to: opts[:reply_to] || self()}

Expand Down