%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%% @doc socks 5 transport
-module(hackney_socks5).
-export([messages/1,
connect/3, connect/4,
recv/2, recv/3,
send/2,
setopts/2,
controlling_process/2,
peername/1,
close/1,
shutdown/2,
sockname/1]).
-define(TIMEOUT, infinity).
-type socks5_socket() :: {atom(), inet:socket()}.
-export_type([socks5_socket/0]).
-ifdef(no_proxy_sni_support).
ssl_opts(Host, Opts) ->
hackney_connect:ssl_opts(Host, Opts).
-else.
ssl_opts(Host, Opts) ->
[{server_name_indication, Host} | hackney_connect:ssl_opts(Host,Opts)].
-endif.
%% @doc Atoms used to identify messages in {active, once | true} mode.
messages({hackney_ssl, _}) ->
{ssl, ssl_closed, ssl_error};
messages({_, _}) ->
{tcp, tcp_closed, tcp_error}.
connect(Host, Port, Opts) ->
connect(Host, Port, Opts, infinity).
connect(Host, Port, Opts, Timeout) when is_list(Host), is_integer(Port),
(Timeout =:= infinity orelse is_integer(Timeout)) ->
%% get the proxy host and port from the options
ProxyHost = proplists:get_value(socks5_host, Opts),
ProxyPort = proplists:get_value(socks5_port, Opts),
Transport = proplists:get_value(socks5_transport, Opts),
%% filter connection options
AcceptedOpts = [linger, nodelay, send_timeout,
send_timeout_close, raw, inet6],
BaseOpts = [binary, {active, false}, {packet, 0}, {keepalive, true},
{nodelay, true}],
ConnectOpts = hackney_util:filter_options(Opts, AcceptedOpts, BaseOpts),
%% connect to the socks 5 proxy
case gen_tcp:connect(ProxyHost, ProxyPort, ConnectOpts, Timeout) of
{ok, Socket} ->
case do_handshake(Socket, Host, Port, Opts) of
ok ->
case Transport of
hackney_ssl ->
SSLOpts = ssl_opts(Host, Opts),
%% upgrade the tcp connection
case ssl:connect(Socket, SSLOpts) of
{ok, SslSocket} ->
{ok, {Transport, SslSocket}};
Error ->
gen_tcp:close(Socket),
Error
end;
_ ->
{ok, {Transport, Socket}}
end;
Error ->
gen_tcp:close(Socket),
Error
end;
Error ->
Error
end.
recv(Socket, Length) ->
recv(Socket, Length, infinity).
%% @doc Receive a packet from a socket in passive mode.
%% @see gen_tcp:recv/3
-spec recv(socks5_socket(), non_neg_integer(), timeout())
-> {ok, any()} | {error, closed | atom()}.
recv({Transport, Socket}, Length, Timeout) ->
Transport:recv(Socket, Length, Timeout).
%% @doc Send a packet on a socket.
%% @see gen_tcp:send/2
-spec send(socks5_socket(), iolist()) -> ok | {error, atom()}.
send({Transport, Socket}, Packet) ->
Transport:send(Socket, Packet).
%% @doc Set one or more options for a socket.
%% @see inet:setopts/2
-spec setopts(socks5_socket(), list()) -> ok | {error, atom()}.
setopts({Transport, Socket}, Opts) ->
Transport:setopts(Socket, Opts).
%% @doc Assign a new controlling process Pid to Socket.
%% @see gen_tcp:controlling_process/2
-spec controlling_process(socks5_socket(), pid())
-> ok | {error, closed | not_owner | atom()}.
controlling_process({Transport, Socket}, Pid) ->
Transport:controlling_process(Socket, Pid).
%% @doc Return the address and port for the other end of a connection.
%% @see inet:peername/1
-spec peername(socks5_socket())
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
peername({Transport, Socket}) ->
Transport:peername(Socket).
%% @doc Close a socks5 socket.
%% @see gen_tcp:close/1
-spec close(socks5_socket()) -> ok.
close({Transport, Socket}) ->
Transport:close(Socket).
%% @doc Immediately close a socket in one or two directions.
%% @see gen_tcp:shutdown/2
-spec shutdown(socks5_socket(), read | write | read_write) -> ok.
shutdown({Transport, Socket}, How) ->
Transport:shutdown(Socket, How).
%% @doc Get the local address and port of a socket
%% @see inet:sockname/1
-spec sockname(socks5_socket())
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
sockname({Transport, Socket}) ->
Transport:sockname(Socket).
%% private functions
do_handshake(Socket, Host, Port, Options) ->
ProxyUser = proplists:get_value(socks5_user, Options),
ProxyPass = proplists:get_value(socks5_pass, Options, <<>>),
case ProxyUser of
undefined ->
%% no auth
ok = gen_tcp:send(Socket, << 5, 1, 0 >>),
case gen_tcp:recv(Socket, 2, ?TIMEOUT) of
{ok, << 5, 0 >>} ->
do_connection(Socket, Host, Port, Options);
{ok, _Reply} ->
{error, unknown_reply};
Error ->
Error
end;
_ ->
case do_authentication(Socket, ProxyUser, ProxyPass) of
ok ->
do_connection(Socket, Host, Port, Options);
Error ->
Error
end
end.
do_authentication(Socket, User, Pass) ->
ok = gen_tcp:send(Socket, << 5, 1, 2 >>),
case gen_tcp:recv(Socket, 2, ?TIMEOUT) of
{ok, <<5, 0>>} ->
ok;
{ok, <<5, 2>>} ->
UserLength = byte_size(User),
PassLength = byte_size(Pass),
Msg = iolist_to_binary([<< 1, UserLength >>,
User, << PassLength >>,
Pass]),
ok = gen_tcp:send(Socket, Msg),
case gen_tcp:recv(Socket, 2, ?TIMEOUT) of
{ok, <<1, 0>>} ->
ok;
_ ->
{error, not_authenticated}
end;
_ ->
{error, not_authenticated}
end.
do_connection(Socket, Host, Port, Options) ->
Resolve = proplists:get_value(socks5_resolve, Options, remote),
case addr(Host, Port, Resolve) of
Addr when is_binary(Addr) ->
ok = gen_tcp:send(Socket, << 5, 1, 0, Addr/binary >>),
case gen_tcp:recv(Socket, 4, ?TIMEOUT) of
{ok, << 5, 0, 0, AType>>} ->
BoundAddr = recv_addr_port(AType, gen_tcp, Socket),
check_connection(BoundAddr);
{ok, _} ->
{error, badarg};
Error ->
Error
end;
Error ->
Error
end.
addr(Host, Port, Resolve) ->
case inet_parse:address(Host) of
{ok, {IP1, IP2, IP3, IP4}} ->
<< 1, IP1, IP2, IP3, IP4, Port:16 >>;
{ok, {IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}} ->
<< 4, IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8, Port:16 >>;
_ -> %% domain name
case Resolve of
local ->
case inet:getaddr(Host, inet) of
{ok, {IP1, IP2, IP3, IP4}} ->
<< 1, IP1, IP2, IP3, IP4, Port:16 >>;
Error ->
case inet:getaddr(Host, inet6) of
{ok, {IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}} ->
<< 4, IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8, Port:16 >>;
_ ->
Error
end
end;
_Remote ->
Host1 = list_to_binary(Host),
HostLength = byte_size(Host1),
<< 3, HostLength, Host1/binary, Port:16 >>
end
end.
recv_addr_port(1 = AType, Transport, Socket) -> % IPv4
{ok, Data} = Transport:recv(Socket, 6, ?TIMEOUT),
<>;
recv_addr_port(4 = AType, Transport, Socket) -> % IPv6
{ok, Data} = Transport:recv(Socket, 18, ?TIMEOUT),
<>;
recv_addr_port(3 = AType, Transport, Socket) -> % Domain
{ok, <>} = Transport:recv(Socket, 1, ?TIMEOUT),
{ok, AddrPort} = Transport:recv(Socket, DLen + 2, ?TIMEOUT),
<>;
recv_addr_port(_, _, _) ->
error.
check_connection(<< 3, _DomainLen:8, _Domain/binary >>) ->
ok;
check_connection(<< 1, _Addr:32, _Port:16 >>) ->
ok;
check_connection(<< 4, _Addr:128, _Port:16 >>) ->
ok;
check_connection(_) ->
{error, no_connection}.