aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2019-09-11 07:22:07 +0200
committerLoïc Hoguin <[email protected]>2019-09-22 16:46:28 +0200
commit92fd84f61f95a0ecb8aea75c28207d81a9c6f94d (patch)
treef93a875d3ab457e8f4189a7a2a86377f6e900349
parent4194682d4edaee3da34783c46a513698eb1e8d05 (diff)
downloadgun-92fd84f61f95a0ecb8aea75c28207d81a9c6f94d.tar.gz
gun-92fd84f61f95a0ecb8aea75c28207d81a9c6f94d.tar.bz2
gun-92fd84f61f95a0ecb8aea75c28207d81a9c6f94d.zip
Initial support for Socks5
-rw-r--r--doc/src/manual/gun_up.asciidoc4
-rw-r--r--ebin/gun.app2
-rw-r--r--src/gun.erl95
-rw-r--r--src/gun_socks.erl184
-rw-r--r--src/gun_tls_proxy.erl1
-rw-r--r--test/socks_SUITE.erl179
6 files changed, 443 insertions, 22 deletions
diff --git a/doc/src/manual/gun_up.asciidoc b/doc/src/manual/gun_up.asciidoc
index c7089fb..a103594 100644
--- a/doc/src/manual/gun_up.asciidoc
+++ b/doc/src/manual/gun_up.asciidoc
@@ -25,6 +25,10 @@ then this may not be desirable for all requests. Those requests
should be cancelled when the connection goes down, and any
subsequent messages ignored.
+// @todo Gun doesn't process messages immediately if it
+// is using the socks protocol, there are gun_socks_connected
+// messages coming up before reaching HTTP.
+
== Elements
ConnPid::
diff --git a/ebin/gun.app b/ebin/gun.app
index e3ad981..c488c9a 100644
--- a/ebin/gun.app
+++ b/ebin/gun.app
@@ -1,7 +1,7 @@
{application, 'gun', [
{description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."},
{vsn, "1.3.0"},
- {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']},
+ {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']},
{registered, [gun_sup]},
{applications, [kernel,stdlib,ssl,cowlib]},
{mod, {gun_app, []}},
diff --git a/src/gun.erl b/src/gun.erl
index bb659e7..d0cf2c3 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -97,6 +97,7 @@
-export([domain_lookup/3]).
-export([connecting/3]).
-export([tls_handshake/3]).
+-export([not_fully_connected/3]).
-export([connected/3]).
-export([closing/3]).
-export([terminate/3]).
@@ -118,11 +119,13 @@
event_handler => {module(), any()},
http_opts => http_opts(),
http2_opts => http2_opts(),
- protocols => [http | http2],
+ protocols => [http | http2 | {socks, socks_opts()}],
retry => non_neg_integer(),
retry_fun => fun((non_neg_integer(), opts())
-> #{retries => non_neg_integer(), timeout => pos_integer()}),
retry_timeout => pos_integer(),
+ %% @todo Not sure this should be allowed, there could be loops.
+% socks_opts => socks_opts(),
supervise => boolean(),
tcp_opts => [gen_tcp:connect_option()],
tls_handshake_timeout => timeout(),
@@ -140,7 +143,9 @@
username => iodata(),
password => iodata(),
protocol => http | http2, %% @todo Remove in Gun 2.0.
- protocols => [http | http2],
+ %% @todo It could be interesting to accept {http, http_opts()}
+ %% as well since we may want different options for proxy and origin.
+ protocols => [http | http2 | {socks, socks_opts()}],
transport => tcp | tls,
tls_opts => [ssl:tls_client_option()],
tls_handshake_timeout => timeout()
@@ -148,11 +153,11 @@
-export_type([connect_destination/0]).
-type intermediary() :: #{
- type := connect,
+ type := connect | socks5,
host := inet:hostname() | inet:ip_address(),
port := inet:port_number(),
transport := tcp | tls,
- protocol := http | http2
+ protocol := http | http2 | socks
}.
%% @todo When/if HTTP/2 CONNECT gets implemented, we will want an option here
@@ -181,6 +186,18 @@
}.
-export_type([http2_opts/0]).
+-type socks_opts() :: #{
+ version => 5,
+ auth => [{username_password, binary(), binary()} | none],
+ host := inet:hostname() | inet:ip_address(),
+ port := inet:port_number(),
+ protocols => [http | http2 | {socks, socks_opts()}],
+ transport => tcp | tls,
+ tls_opts => [ssl:tls_client_option()],
+ tls_handshake_timeout => timeout()
+}.
+-export_type([socks_opts/0]).
+
%% @todo keepalive
-type ws_opts() :: #{
closing_timeout => timeout(),
@@ -278,18 +295,9 @@ check_options([{http2_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) ->
Error
end;
check_options([Opt = {protocols, L}|Opts]) when is_list(L) ->
- Len = length(L),
- case length(lists:usort(L)) of
- Len when Len > 0 ->
- Check = lists:usort([(P =:= http) orelse (P =:= http2) || P <- L]),
- case Check of
- [true] ->
- check_options(Opts);
- _ ->
- {error, {options, Opt}}
- end;
- _ ->
- {error, {options, Opt}}
+ case check_protocols_opt(L) of
+ ok -> check_options(Opts);
+ error -> {error, {options, Opt}}
end;
check_options([{retry, R}|Opts]) when is_integer(R), R >= 0 ->
check_options(Opts);
@@ -321,11 +329,33 @@ check_options([{ws_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) ->
check_options([Opt|_]) ->
{error, {options, Opt}}.
+check_protocols_opt(Protocols) ->
+ %% Protocols must not appear more than once, and they
+ %% must be one of http, http2 or socks.
+ ProtoNames0 = lists:usort([case P0 of {P, _} -> P; P -> P end || P0 <- Protocols]),
+ ProtoNames = [P || P <- ProtoNames0, lists:member(P, [http, http2, socks])],
+ case length(Protocols) =:= length(ProtoNames) of
+ false -> error;
+ true ->
+ %% When options are given alongside a protocol, they
+ %% must be checked as well.
+ %% @todo It may be interesting to allow more than just socks here.
+ TupleCheck = [case P of
+ {socks, Opts} -> gun_socks:check_options(Opts)
+ end || P <- Protocols, is_tuple(P)],
+ case lists:usort(TupleCheck) of
+ [] -> ok;
+ [ok] -> ok;
+ _ -> error
+ end
+ end.
+
consider_tracing(ServerPid, #{trace := true}) ->
dbg:tracer(),
dbg:tpl(gun, [{'_', [], [{return_trace}]}]),
dbg:tpl(gun_http, [{'_', [], [{return_trace}]}]),
dbg:tpl(gun_http2, [{'_', [], [{return_trace}]}]),
+ dbg:tpl(gun_socks, [{'_', [], [{return_trace}]}]),
dbg:tpl(gun_ws, [{'_', [], [{return_trace}]}]),
dbg:p(ServerPid, all);
consider_tracing(_, _) ->
@@ -652,6 +682,8 @@ await_up(ServerPid, Timeout, MRef) ->
receive
{gun_up, ServerPid, Protocol} ->
{ok, Protocol};
+ {gun_socks_connected, ServerPid, Protocol} ->
+ {ok, Protocol};
{'DOWN', MRef, process, ServerPid, Reason} ->
{error, {down, Reason}}
after Timeout ->
@@ -861,7 +893,8 @@ connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts,
{ok, Socket} when Transport =:= gun_tcp ->
Protocol = case maps:get(protocols, Opts, [http]) of
[http] -> gun_http;
- [http2] -> gun_http2
+ [http2] -> gun_http2;
+ [{socks, _}] -> gun_socks
end,
EvHandlerState = EvHandler:connect_end(ConnectEvent#{
socket => Socket,
@@ -885,8 +918,9 @@ connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts,
tls_handshake(_, {retries, Retries, Socket0}, State=#state{opts=Opts,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
+ Protocols = maps:get(protocols, Opts, [http2, http]),
TransOpts0 = maps:get(tls_opts, Opts, []),
- TransOpts = ensure_alpn(maps:get(protocols, Opts, [http2, http]), TransOpts0),
+ TransOpts = ensure_alpn(Protocols, TransOpts0),
HandshakeTimeout = maps:get(tls_handshake_timeout, Opts, infinity),
HandshakeEvent = #{
socket => Socket0,
@@ -898,7 +932,12 @@ tls_handshake(_, {retries, Retries, Socket0}, State=#state{opts=Opts,
{ok, Socket} ->
Protocol = case ssl:negotiated_protocol(Socket) of
{ok, <<"h2">>} -> gun_http2;
- _ -> gun_http
+ {ok, <<"http/1.1">>} -> gun_http;
+ {error, protocol_not_negotiated} ->
+ case Protocols of
+ [{socks, _}] -> gun_socks;
+ _ -> gun_http
+ end
end,
EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
socket => Socket,
@@ -918,12 +957,22 @@ ensure_alpn(Protocols0, TransOpts) ->
Protocols = [case P of
http -> <<"http/1.1">>;
http2 -> <<"h2">>
- end || P <- Protocols0],
+ end || P <- Protocols0, is_atom(P)],
[
{alpn_advertised_protocols, Protocols},
{client_preferred_next_protocols, {client, Protocols, <<"http/1.1">>}}
|TransOpts].
+not_fully_connected(Type, Event, State) ->
+ handle_common_connected(Type, Event, ?FUNCTION_NAME, State).
+
+connected(internal, {connected, Socket, Protocol=gun_socks},
+ State=#state{owner=Owner, opts=Opts, transport=Transport}) ->
+ [{socks, ProtoOpts}] = [Proto || Proto = {socks, _} <- maps:get(protocols, Opts)],
+ ProtoState = Protocol:init(Owner, Socket, Transport, ProtoOpts),
+ Owner ! {gun_up, self(), Protocol:name()},
+ {next_state, not_fully_connected, active(State#state{socket=Socket,
+ protocol=Protocol, protocol_state=ProtoState})};
connected(internal, {connected, Socket, Protocol},
State=#state{owner=Owner, opts=Opts, transport=Transport}) ->
ProtoOptsKey = case Protocol of
@@ -1211,6 +1260,7 @@ commands([{switch_protocol, Protocol=gun_ws, ProtoState}], State=#state{
{keep_state, keepalive_cancel(State#state{protocol=Protocol, protocol_state=ProtoState,
event_handler_state=EvHandlerState})};
%% @todo And this state should probably not be ignored.
+%% @todo Socks is switching to *http* and we don't seem to support it properly yet.
commands([{switch_protocol, Protocol, _ProtoState0}|Tail], State=#state{
owner=Owner, opts=Opts, socket=Socket, transport=Transport,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
@@ -1218,7 +1268,10 @@ commands([{switch_protocol, Protocol, _ProtoState0}|Tail], State=#state{
ProtoState = Protocol:init(Owner, Socket, Transport, ProtoOpts),
EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
commands(Tail, keepalive_timeout(State#state{protocol=Protocol, protocol_state=ProtoState,
- event_handler_state=EvHandlerState})).
+ event_handler_state=EvHandlerState}));
+%% Switch from not_fully_connected to connected.
+commands([{mode, http}], State) ->
+ {next_state, connected, State}.
disconnect(State0=#state{owner=Owner, status=Status, opts=Opts,
socket=Socket, transport=Transport,
diff --git a/src/gun_socks.erl b/src/gun_socks.erl
new file mode 100644
index 0000000..6c3e6fc
--- /dev/null
+++ b/src/gun_socks.erl
@@ -0,0 +1,184 @@
+%% Copyright (c) 2019, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(gun_socks).
+
+-export([check_options/1]).
+-export([name/0]).
+-export([init/4]).
+-export([handle/4]).
+-export([closing/4]).
+-export([close/4]).
+%% @todo down
+
+-record(socks_state, {
+ owner :: pid(),
+ socket :: inet:socket() | ssl:sslsocket(),
+ transport :: module(),
+ opts = #{} :: map(), %% @todo
+ %% We only support version 5 at the moment.
+ version :: 5,
+ status :: auth_method_select | auth_username_password | connect
+}).
+
+check_options(Opts=#{host := _, port := _}) ->
+ do_check_options(maps:to_list(maps:without([host, port], Opts)));
+%% Host and port are not optional.
+check_options(#{host := _}) ->
+ {error, options, {socks, port}};
+check_options(#{}) ->
+ {error, options, {socks, host}}.
+
+do_check_options([]) ->
+ ok;
+do_check_options([Opt={auth, L}|Opts]) ->
+ case check_auth_opt(L) of
+ ok -> do_check_options(Opts);
+ error -> {error, {options, {socks, Opt}}}
+ end;
+%% @todo Proper protocols check.
+do_check_options([{protocols, P}|Opts]) when is_list(P) ->
+ do_check_options(Opts);
+do_check_options([{tls_handshake_timeout, infinity}|Opts]) ->
+ do_check_options(Opts);
+do_check_options([{tls_handshake_timeout, T}|Opts]) when is_integer(T), T >= 0 ->
+ do_check_options(Opts);
+do_check_options([{tls_opts, L}|Opts]) when is_list(L) ->
+ do_check_options(Opts);
+do_check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls ->
+ do_check_options(Opts);
+do_check_options([{version, 5}|Opts]) ->
+ do_check_options(Opts);
+do_check_options([Opt|_]) ->
+ {error, {options, {socks, Opt}}}.
+
+check_auth_opt(Methods) ->
+ %% Methods must not appear more than once, and they
+ %% must be one of none or {username_password, binary(), binary()}.
+ Check = lists:usort([case M of
+ none -> ok;
+ {username_password, U, P} when is_binary(U), is_binary(P) -> ok
+ end || M <- Methods]),
+ case {length(Methods) =:= length(Check), lists:usort(Check)} of
+ {true, []} -> ok;
+ {true, [ok]} -> ok;
+ _ -> error
+ end.
+
+name() -> socks.
+
+init(Owner, Socket, Transport, Opts) ->
+ 5 = Version = maps:get(version, Opts, 5),
+ Auth = maps:get(auth, Opts, [none]),
+ Methods = <<case A of
+ {username_password, _, _} -> <<2>>;
+ none -> <<0>>
+ end || A <- Auth>>,
+ Transport:send(Socket, [<<5, (length(Auth))>>, Methods]),
+ #socks_state{owner=Owner, socket=Socket, transport=Transport, opts=Opts,
+ version=Version, status=auth_method_select}.
+
+handle(Data, State, _, EvHandlerState) ->
+ {handle(Data, State), EvHandlerState}.
+
+%% No authentication.
+handle(<<5, 0>>, State=#socks_state{version=5, status=auth_method_select}) ->
+ send_socks5_connect(State),
+ {state, State#socks_state{status=connect}};
+%% Username/password authentication.
+handle(<<5, 2>>, State=#socks_state{socket=Socket, transport=Transport, opts=Opts,
+ version=5, status=auth_method_select}) ->
+ #{auth := {username_password, Username, Password}} = Opts,
+ ULen = byte_size(Username),
+ PLen = byte_size(Password),
+ Transport:send(Socket, <<1, ULen, Username/binary, PLen, Password/binary>>),
+ {state, State#socks_state{status=auth_username_password}};
+%% Username/password authentication successful.
+handle(<<1, 0>>, State=#socks_state{version=5, status=auth_username_password}) ->
+ send_socks5_connect(State),
+ {state, State#socks_state{status=connect}};
+%% Username/password authentication error.
+handle(<<1, _>>, #socks_state{version=5, status=auth_username_password}) ->
+ {error, {socks5, username_password_auth_failure}};
+%% Connect reply.
+handle(<<5, 0, 0, Rest0/bits>>, State=#socks_state{owner=Owner, socket=Socket, transport=Transport, opts=Opts,
+ version=5, status=connect}) ->
+ %% @todo What to do with BoundAddr and BoundPort? Add as metadata to origin info?
+ {_BoundAddr, _BoundPort} = case Rest0 of
+ %% @todo Seen a server with <<1, 0:48>>.
+ <<1, A, B, C, D, Port:16>> ->
+ {{A, B, C, D}, Port};
+ <<3, Len, Host:Len/binary, Port:16>> ->
+ %% We convert to list to get an inet:hostname().
+ {unicode:characters_to_list(Host), Port};
+ <<4, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16, Port:16>> ->
+ {{A, B, C, D, E, F, G, H}, Port}
+ end,
+ %% @todo Maybe an event indicating success.
+ #{host := NewHost, port := NewPort} = Opts,
+ case Opts of
+ %% @todo TLS over TLS here as well.
+% #{protocols := Protocols, transport := tls} ->
+% TLSOpts = maps:get(tls_opts, Destination, []),
+% TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity),
+ %%
+ #{protocols := [{socks, SockOpts}]} ->
+ Owner ! {gun_socks_connected, self(), name()},
+ [{origin, <<"http">>, NewHost, NewPort, socks5},
+ {switch_protocol, ?MODULE, init(Owner, Socket, Transport, SockOpts)}];
+ #{protocols := [http2]} ->
+ Owner ! {gun_socks_connected, self(), gun_http2:name()},
+ [{origin, <<"http">>, NewHost, NewPort, socks5},
+ {switch_protocol, gun_http2, State},
+ {mode, http}];
+ _ ->
+ Owner ! {gun_socks_connected, self(), gun_http:name()},
+ [{origin, <<"http">>, NewHost, NewPort, socks5},
+ {switch_protocol, gun_http, State},
+ {mode, http}]
+ end;
+handle(<<5, Error, _/bits>>, #socks_state{version=5, status=connect}) ->
+ Reason = case Error of
+ 1 -> general_socks_server_failure;
+ 2 -> connection_not_allowed_by_ruleset;
+ 3 -> network_unreachable;
+ 4 -> host_unreachable;
+ 5 -> connection_refused;
+ 6 -> ttl_expired;
+ 7 -> command_not_supported;
+ 8 -> address_type_not_supported;
+ _ -> {unknown_error, Error}
+ end,
+ {error, {socks5, Reason}}.
+
+send_socks5_connect(#socks_state{socket=Socket, transport=Transport, opts=Opts}) ->
+ ATypeAndDestAddr = case maps:get(host, Opts) of
+ {A, B, C, D} -> <<1, A, B, C, D>>;
+ {A, B, C, D, E, F, G, H} -> <<4, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>;
+ Host when is_atom(Host) ->
+ DestAddr0 = atom_to_binary(Host, utf8),
+ <<3, (byte_size(DestAddr0)), DestAddr0/binary>>;
+ Host ->
+ DestAddr0 = unicode:characters_to_binary(Host, utf8),
+ <<3, (byte_size(DestAddr0)), DestAddr0/binary>>
+ end,
+ DestPort = maps:get(port, Opts),
+ Transport:send(Socket, <<5, 1, 0, ATypeAndDestAddr/binary, DestPort:16>>).
+
+%% We can always close immediately.
+closing(_, _, _, EvHandlerState) ->
+ {close, EvHandlerState}.
+
+close(_, _, _, EvHandlerState) ->
+ EvHandlerState.
diff --git a/src/gun_tls_proxy.erl b/src/gun_tls_proxy.erl
index 3978822..d519919 100644
--- a/src/gun_tls_proxy.erl
+++ b/src/gun_tls_proxy.erl
@@ -237,6 +237,7 @@ not_connected(cast, Msg={setopts, _}, State) ->
{keep_state_and_data, postpone};
not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
+ %% @todo We should distinguish case when no protocol was negotiated (for socks).
Protocol = case ssl:negotiated_protocol(Socket) of
{ok, <<"h2">>} -> gun_http2;
_ -> gun_http
diff --git a/test/socks_SUITE.erl b/test/socks_SUITE.erl
new file mode 100644
index 0000000..01bc760
--- /dev/null
+++ b/test/socks_SUITE.erl
@@ -0,0 +1,179 @@
+%% Copyright (c) 2019, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% This test suite covers the following RFCs and specifications:
+%%
+%% * RFC 1928
+%% * RFC 1929
+%% * http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol
+%% * https://www.openssh.com/txt/socks4a.protocol
+
+-module(socks_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-import(ct_helper, [doc/1]).
+-import(gun_test, [init_origin/1]).
+-import(gun_test, [init_origin/2]).
+-import(gun_test, [receive_from/1]).
+-import(gun_test, [receive_from/2]).
+
+all() ->
+ ct_helper:all(?MODULE).
+
+%% Proxy helpers.
+
+do_proxy_start(Transport0, Auth) ->
+ Transport = case Transport0 of
+ tcp -> gun_tcp;
+ tls -> gun_tls
+ end,
+ Self = self(),
+ Pid = spawn_link(fun() -> do_proxy_init(Self, Transport, Auth) end),
+ Port = receive_from(Pid),
+ {ok, Pid, Port}.
+
+do_proxy_init(Parent, Transport, Auth) ->
+ {ok, ListenSocket} = case Transport of
+ gun_tcp ->
+ gen_tcp:listen(0, [binary, {active, false}]);
+ gun_tls ->
+ Opts = ct_helper:get_certs_from_ets(),
+ ssl:listen(0, [binary, {active, false}|Opts])
+ end,
+ {ok, {_, Port}} = Transport:sockname(ListenSocket),
+ Parent ! {self(), Port},
+ {ok, ClientSocket} = case Transport of
+ gun_tcp ->
+ gen_tcp:accept(ListenSocket, 5000);
+ gun_tls ->
+ {ok, ClientSocket0} = ssl:transport_accept(ListenSocket, 5000),
+ ssl:ssl_accept(ClientSocket0, 5000),
+ {ok, ClientSocket0}
+ end,
+ Recv = case Transport of
+ gun_tcp -> fun gen_tcp:recv/3;
+ gun_tls -> fun ssl:recv/3
+ end,
+ %% Authentication method.
+ {ok, <<5, NumAuths, Auths0/bits>>} = Recv(ClientSocket, 0, 1000),
+ Auths = [case A of
+ 0 -> none;
+ 2 -> username_password
+ end || <<A>> <= Auths0],
+ Parent ! {self(), {auth_methods, NumAuths, Auths}},
+ ok = case {Auth, lists:member(Auth, Auths)} of
+ {none, true} ->
+ Transport:send(ClientSocket, <<5, 0>>);
+ {username_password, true} ->
+ %% @todo
+ ok;
+ {_, false} ->
+ %% @todo
+ not_ok
+ end,
+ %% Connection request.
+ {ok, <<5, 1, 0, AType, Rest/bits>>} = Recv(ClientSocket, 0, 1000),
+ {OriginHost, OriginPort} = case AType of
+ 1 ->
+ <<A, B, C, D, P:16>> = Rest,
+ {{A, B, C, D}, P};
+ 3 ->
+ <<L, H:L/binary, P:16>> = Rest,
+ {H, P};
+ 4 ->
+ <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16, P:16>> = Rest,
+ {{A, B, C, D, E, F, G, H}, P}
+ end,
+ Parent ! {self(), {connect, OriginHost, OriginPort}},
+ %% @todo Test errors too (byte 2).
+ %% @todo Configurable bound address.
+ Transport:send(ClientSocket, <<5, 0, 0, 1, 1, 2, 3, 4, 33333:16>>),
+ if
+ true ->
+ {ok, OriginSocket} = gen_tcp:connect(
+ binary_to_list(OriginHost), OriginPort,
+ [binary, {active, false}]),
+ Transport:setopts(ClientSocket, [{active, true}]),
+ inet:setopts(OriginSocket, [{active, true}]),
+ do_proxy_loop(Transport, ClientSocket, OriginSocket)
+ end.
+
+do_proxy_loop(Transport, ClientSocket, OriginSocket) ->
+ {OK, _, _} = Transport:messages(),
+ receive
+ {OK, ClientSocket, Data} ->
+ case gen_tcp:send(OriginSocket, Data) of
+ ok ->
+ do_proxy_loop(Transport, ClientSocket, OriginSocket);
+ {error, _} ->
+ ok
+ end;
+ {tcp, OriginSocket, Data} ->
+ case Transport:send(ClientSocket, Data) of
+ ok ->
+ do_proxy_loop(Transport, ClientSocket, OriginSocket);
+ {error, _} ->
+ ok
+ end;
+ {tcp_closed, _} ->
+ ok;
+ {ssl_closed, _} ->
+ ok;
+ Msg ->
+ error(Msg)
+ end.
+
+%% Tests.
+
+socks5_tcp_http_none(_) ->
+ doc("Use Socks5 over TCP and without authentication to connect to an HTTP server."),
+ do_socks5_tcp_http(<<"http">>, tcp, tcp, none).
+
+do_socks5_tcp_http(OriginScheme, OriginTransport, ProxyTransport, SocksAuth) ->
+ {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http),
+ {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport, SocksAuth),
+ Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ transport => ProxyTransport,
+ protocols => [{socks, #{
+ auth => [SocksAuth],
+ host => "localhost",
+ port => OriginPort
+ }}]
+ }),
+ %% We receive a gun_up and a gun_socks_connected.
+ {ok, socks} = gun:await_up(ConnPid),
+ {ok, http} = gun:await_up(ConnPid),
+ %% The proxy received two packets.
+ {auth_methods, 1, [SocksAuth]} = receive_from(ProxyPid),
+ {connect, <<"localhost">>, OriginPort} = receive_from(ProxyPid),
+ _ = gun:get(ConnPid, "/proxied"),
+ Data = receive_from(OriginPid),
+ Lines = binary:split(Data, <<"\r\n">>, [global]),
+ [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
+ #{
+ transport := OriginTransport,
+ protocol := http,
+ origin_scheme := OriginScheme,
+ origin_host := "localhost",
+ origin_port := OriginPort,
+ intermediaries := [#{
+ type := socks5,
+ host := "localhost",
+ port := ProxyPort,
+ transport := ProxyTransport,
+ protocol := socks
+ }]} = gun:info(ConnPid),
+ gun:close(ConnPid).