aboutsummaryrefslogblamecommitdiffstats
path: root/src/gun_socks.erl
blob: 16684f8e8480e889c48b1660ba1e9cc5333653b4 (plain) (tree)


















                                                                           
                              







































































                                                                                 


                                                              







                                                                             
                                                                                                    
                                                         
                                                                                      



























                                                                                                              






                                                                                                    
                                                      


                                                                                                      



                                                                        





































                                                                                                  
%% 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([switch_transport/3]).
-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}.

switch_transport(Transport, Socket, State) ->
	State#socks_state{socket=Socket, transport=Transport}.

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=#{auth := AuthMethods},
		version=5, status=auth_method_select}) ->
	[{username_password, Username, Password}] = [Method || Method <- AuthMethods],
	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
		#{transport := tls} ->
			HandshakeEvent = #{
				tls_opts => maps:get(tls_opts, Opts, []),
				timeout => maps:get(tls_handshake_timeout, Opts, infinity)
			},
			[{origin, <<"https">>, NewHost, NewPort, socks5},
				{tls_handshake, HandshakeEvent, maps:get(protocols, Opts, [http])}];
		#{protocols := [{socks, SockOpts}]} ->
			[{origin, <<"http">>, NewHost, NewPort, socks5},
				{switch_protocol, ?MODULE, init(Owner, Socket, Transport, SockOpts)}];
		#{protocols := [http2]} ->
			[{origin, <<"http">>, NewHost, NewPort, socks5},
				{switch_protocol, gun_http2, State},
				{mode, http}];
		_ ->
			[{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.