1%%% -*- erlang -*-
2%%%
3%%% This file is part of hackney released under the Apache 2 license.
4%%% See the NOTICE for more information.
5%%%
6
7%% @doc socks 5 transport
8
9-module(hackney_socks5).
10
11-export([messages/1,
12  connect/3, connect/4,
13  recv/2, recv/3,
14  send/2,
15  setopts/2,
16  controlling_process/2,
17  peername/1,
18  close/1,
19  shutdown/2,
20  sockname/1]).
21
22-define(TIMEOUT, infinity).
23
24-type socks5_socket() :: {atom(), inet:socket()}.
25-export_type([socks5_socket/0]).
26
27%% @doc Atoms used to identify messages in {active, once | true} mode.
28messages({hackney_ssl, _}) ->
29  {ssl, ssl_closed, ssl_error};
30messages({_, _}) ->
31  {tcp, tcp_closed, tcp_error}.
32
33
34connect(Host, Port, Opts) ->
35  connect(Host, Port, Opts, infinity).
36
37
38connect(Host, Port, Opts, Timeout) when is_list(Host), is_integer(Port),
39                                        (Timeout =:= infinity orelse is_integer(Timeout)) ->
40  %% get the proxy host and port from the options
41  ProxyHost = proplists:get_value(socks5_host, Opts),
42  ProxyPort = proplists:get_value(socks5_port, Opts),
43  Transport = proplists:get_value(socks5_transport, Opts),
44
45  %% filter connection options
46  AcceptedOpts =  [linger, nodelay, send_timeout,
47    send_timeout_close, raw, inet6],
48  BaseOpts = [binary, {active, false}, {packet, 0}, {keepalive,  true},
49    {nodelay, true}],
50  ConnectOpts = hackney_util:filter_options(Opts, AcceptedOpts, BaseOpts),
51
52  %% connect to the socks 5 proxy
53  case gen_tcp:connect(ProxyHost, ProxyPort, ConnectOpts, Timeout) of
54    {ok, Socket} ->
55      case do_handshake(Socket, Host, Port, Opts) of
56        ok ->
57          case Transport of
58            hackney_ssl ->
59              SSlOpts = hackney_connect:ssl_opts(Host, Opts),
60              %% upgrade the tcp connection
61              case ssl:connect(Socket, SSlOpts) of
62                {ok, SslSocket} ->
63                  {ok, {Transport, SslSocket}};
64                Error ->
65                  gen_tcp:close(Socket),
66                  Error
67              end;
68            _ ->
69              {ok, {Transport, Socket}}
70          end;
71        Error ->
72          gen_tcp:close(Socket),
73          Error
74      end;
75    Error ->
76      Error
77  end.
78
79
80recv(Socket, Length) ->
81  recv(Socket, Length, infinity).
82
83%% @doc Receive a packet from a socket in passive mode.
84%% @see gen_tcp:recv/3
85-spec recv(socks5_socket(), non_neg_integer(), timeout())
86    -> {ok, any()} | {error, closed | atom()}.
87recv({Transport, Socket}, Length, Timeout) ->
88  Transport:recv(Socket, Length, Timeout).
89
90
91%% @doc Send a packet on a socket.
92%% @see gen_tcp:send/2
93-spec send(socks5_socket(), iolist()) -> ok | {error, atom()}.
94send({Transport, Socket}, Packet) ->
95  Transport:send(Socket, Packet).
96
97%% @doc Set one or more options for a socket.
98%% @see inet:setopts/2
99-spec setopts(socks5_socket(), list()) -> ok | {error, atom()}.
100setopts({Transport, Socket}, Opts) ->
101  Transport:setopts(Socket, Opts).
102
103%% @doc Assign a new controlling process <em>Pid</em> to <em>Socket</em>.
104%% @see gen_tcp:controlling_process/2
105-spec controlling_process(socks5_socket(), pid())
106    -> ok | {error, closed | not_owner | atom()}.
107controlling_process({Transport, Socket}, Pid) ->
108  Transport:controlling_process(Socket, Pid).
109
110%% @doc Return the address and port for the other end of a connection.
111%% @see inet:peername/1
112-spec peername(socks5_socket())
113    -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
114peername({Transport, Socket}) ->
115  Transport:peername(Socket).
116
117%% @doc Close a socks5 socket.
118%% @see gen_tcp:close/1
119-spec close(socks5_socket()) -> ok.
120close({Transport, Socket}) ->
121  Transport:close(Socket).
122
123%% @doc Immediately close a socket in one or two directions.
124%% @see gen_tcp:shutdown/2
125-spec shutdown(socks5_socket(), read | write | read_write) -> ok.
126shutdown({Transport, Socket}, How) ->
127  Transport:shutdown(Socket, How).
128
129%% @doc Get the local address and port of a socket
130%% @see inet:sockname/1
131-spec sockname(socks5_socket())
132    -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
133sockname({Transport, Socket}) ->
134  Transport:sockname(Socket).
135
136%% private functions
137do_handshake(Socket, Host, Port, Options) ->
138  ProxyUser = proplists:get_value(socks5_user, Options),
139  ProxyPass = proplists:get_value(socks5_pass, Options, <<>>),
140  case ProxyUser of
141    undefined ->
142      %% no auth
143      ok = gen_tcp:send(Socket, << 5, 1, 0 >>),
144      case gen_tcp:recv(Socket, 2, ?TIMEOUT) of
145        {ok, << 5, 0 >>} ->
146          do_connection(Socket, Host, Port, Options);
147        {ok, _Reply} ->
148          {error, unknown_reply};
149        Error ->
150          Error
151      end;
152    _ ->
153      case do_authentication(Socket, ProxyUser, ProxyPass) of
154        ok ->
155          do_connection(Socket, Host, Port, Options);
156        Error ->
157          Error
158      end
159  end.
160
161do_authentication(Socket, User, Pass) ->
162  ok = gen_tcp:send(Socket, << 5, 1, 2 >>),
163  case gen_tcp:recv(Socket, 2, ?TIMEOUT) of
164    {ok, <<5, 0>>} ->
165      ok;
166    {ok, <<5, 2>>} ->
167      UserLength = byte_size(User),
168      PassLength = byte_size(Pass),
169      Msg = iolist_to_binary([<< 1, UserLength >>,
170        User, << PassLength >>,
171        Pass]),
172      ok = gen_tcp:send(Socket, Msg),
173      case gen_tcp:recv(Socket, 2, ?TIMEOUT) of
174        {ok, <<1, 0>>} ->
175          ok;
176        _ ->
177          {error, not_authenticated}
178      end;
179    _ ->
180      {error, not_authenticated}
181  end.
182
183
184do_connection(Socket, Host, Port, Options) ->
185  Resolve = proplists:get_value(socks5_resolve, Options, remote),
186  case addr(Host, Port, Resolve) of
187    Addr when is_binary(Addr) ->
188      ok = gen_tcp:send(Socket, << 5, 1, 0, Addr/binary >>),
189      case gen_tcp:recv(Socket, 10, ?TIMEOUT) of
190        {ok, << 5, 0, 0, BoundAddr/binary >>} ->
191          check_connection(BoundAddr);
192        {ok, _} ->
193          {error, badarg};
194        Error ->
195          Error
196      end;
197    Error ->
198      Error
199  end.
200
201addr(Host, Port, Resolve) ->
202  case inet_parse:address(Host) of
203    {ok, {IP1, IP2, IP3, IP4}} ->
204      << 1, IP1, IP2, IP3, IP4, Port:16 >>;
205    {ok, {IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}} ->
206      << 4, IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8, Port:16 >>;
207    _ -> %% domain name
208      case Resolve of
209        local ->
210          case inet:getaddr(Host, inet) of
211            {ok, {IP1, IP2, IP3, IP4}} ->
212              << 1, IP1, IP2, IP3, IP4, Port:16 >>;
213            Error ->
214              case inet:getaddr(Host, inet6) of
215                {ok, {IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}} ->
216                  << 4, IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8, Port:16 >>;
217                _ ->
218                  Error
219              end
220          end;
221        _Remote ->
222          Host1 = list_to_binary(Host),
223          HostLength = byte_size(Host1),
224          << 3, HostLength, Host1/binary, Port:16 >>
225      end
226  end.
227
228check_connection(<< 3, _DomainLen:8, _Domain/binary >>) ->
229  ok;
230check_connection(<< 1, _Addr:32, _Port:16 >>) ->
231  ok;
232check_connection(<< 4, _Addr:128, _Port:16 >>) ->
233  ok;
234check_connection(_) ->
235  {error, no_connection}.
236