%% Copyright (c) Loïc Hoguin <essen@ninenines.eu>
%%
%% 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}, {verify, verify_none},
				{fail_if_no_peer_cert, 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),
			{ok, ClientSocket1} = ssl:handshake(ClientSocket0, 5000),
			{ok, ClientSocket1}
	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}},
	AuthMethod = do_auth_method(Auth),
	ok = case {AuthMethod, lists:member(AuthMethod, Auths)} of
		{none, true} ->
			Transport:send(ClientSocket, <<5, 0>>);
		{username_password, true} ->
			Transport:send(ClientSocket, <<5, 2>>),
			{ok, <<1, ULen, User:ULen/binary, PLen, Pass:PLen/binary>>} = Recv(ClientSocket, 0, 1000),
			Parent ! {self(), {username_password, User, Pass}},
			%% @todo Test errors too (byte 2).
			Transport:send(ClientSocket, <<1, 0>>);
		{_, 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.

do_auth_method(none) -> none;
do_auth_method({username_password, _, _}) -> username_password.

%% Tests.

socks5_tcp_http_none(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTP server."),
	do_socks5(<<"http">>, tcp, http, tcp, none).

socks5_tcp_http_username_password(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTP server."),
	do_socks5(<<"http">>, tcp, http, tcp, {username_password, <<"user">>, <<"password">>}).

socks5_tcp_https_none(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTPS server."),
	do_socks5(<<"https">>, tls, http, tcp, none).

socks5_tcp_https_username_password(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTPS server."),
	do_socks5(<<"https">>, tls, http, tcp, {username_password, <<"user">>, <<"password">>}).

socks5_tls_http_none(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTP server."),
	do_socks5(<<"http">>, tcp, http, tls, none).

socks5_tls_http_username_password(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTP server."),
	do_socks5(<<"http">>, tcp, http, tls, {username_password, <<"user">>, <<"password">>}).

socks5_tls_https_none(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTPS server."),
	do_socks5(<<"https">>, tls, http, tls, none).

socks5_tls_https_username_password(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTPS server."),
	do_socks5(<<"https">>, tls, http, tls, {username_password, <<"user">>, <<"password">>}).

socks5_tcp_h2c_none(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TCP."),
	do_socks5(<<"http">>, tcp, http2, tcp, none).

socks5_tcp_h2c_username_password(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TCP."),
	do_socks5(<<"http">>, tcp, http2, tcp, {username_password, <<"user">>, <<"password">>}).

socks5_tcp_h2_none(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TLS."),
	do_socks5(<<"https">>, tls, http2, tcp, none).

socks5_tcp_h2_username_password(_) ->
	doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TLS."),
	do_socks5(<<"https">>, tls, http2, tcp, {username_password, <<"user">>, <<"password">>}).

socks5_tls_h2c_none(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TCP."),
	do_socks5(<<"http">>, tcp, http2, tls, none).

socks5_tls_h2c_username_password(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TCP."),
	do_socks5(<<"http">>, tcp, http2, tls, {username_password, <<"user">>, <<"password">>}).

socks5_tls_h2_none(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TLS."),
	do_socks5(<<"https">>, tls, http2, tls, none).

socks5_tls_h2_username_password(_) ->
	doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TLS."),
	do_socks5(<<"https">>, tls, http2, tls, {username_password, <<"user">>, <<"password">>}).

do_socks5(OriginScheme, OriginTransport, OriginProtocol, ProxyTransport, SocksAuth) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, OriginProtocol),
	{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,
		tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
		protocols => [{socks, #{
			auth => [SocksAuth],
			host => "localhost",
			port => OriginPort,
			transport => OriginTransport,
			tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
			protocols => [OriginProtocol]
		}}]
	}),
	%% We receive a gun_up and a gun_tunnel_up.
	{ok, socks} = gun:await_up(ConnPid),
	{up, OriginProtocol} = gun:await(ConnPid, undefined),
	%% The proxy received two packets.
	AuthMethod = do_auth_method(SocksAuth),
	{auth_methods, 1, [AuthMethod]} = receive_from(ProxyPid),
	_ = case AuthMethod of
		none -> ok;
		username_password -> SocksAuth = receive_from(ProxyPid)
	end,
	{connect, <<"localhost">>, OriginPort} = receive_from(ProxyPid),
	handshake_completed = receive_from(OriginPid),
	_ = gun:get(ConnPid, "/proxied"),
	_ = case OriginProtocol of
		http ->
			Data = receive_from(OriginPid),
			Lines = binary:split(Data, <<"\r\n">>, [global]),
			[<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines];
		http2 ->
			<<_:24, 1:8, _/bits>> = receive_from(OriginPid)
	end,
	#{
		transport := OriginTransport,
		protocol := OriginProtocol,
		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).

socks5_tcp_through_multiple_tcp_proxies(_) ->
	doc("Gun can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"two separate TCP Socks5 proxies."),
	do_socks5_through_multiple_proxies(<<"http">>, tcp, tcp).

socks5_tcp_through_multiple_tls_proxies(_) ->
	doc("Gun can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"two separate TLS Socks5 proxies."),
	do_socks5_through_multiple_proxies(<<"http">>, tcp, tls).

socks5_tls_through_multiple_tcp_proxies(_) ->
	doc("Gun can be used to establish a TLS connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"two separate TCP Socks5 proxies."),
	do_socks5_through_multiple_proxies(<<"https">>, tls, tcp).

socks5_tls_through_multiple_tls_proxies(_) ->
	doc("Gun can be used to establish a TLS connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"two separate TLS Socks5 proxies."),
	do_socks5_through_multiple_proxies(<<"https">>, tls, tls).

do_socks5_through_multiple_proxies(OriginScheme, OriginTransport, ProxyTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, http),
	{ok, Proxy1Pid, Proxy1Port} = do_proxy_start(ProxyTransport, none),
	{ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyTransport, none),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", Proxy1Port, #{
		transport => ProxyTransport,
		tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
		protocols => [{socks, #{
			host => "localhost",
			port => Proxy2Port,
			transport => ProxyTransport,
			tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
			protocols => [{socks, #{
				host => "localhost",
				port => OriginPort,
				transport => OriginTransport,
				tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}]
			}}]
		}}]
	}),
	%% We receive a gun_up and two gun_tunnel_up.
	{ok, socks} = gun:await_up(ConnPid),
	{up, socks} = gun:await(ConnPid, undefined),
	{up, http} = gun:await(ConnPid, undefined),
	%% The first proxy received two packets.
	{auth_methods, 1, [none]} = receive_from(Proxy1Pid),
	{connect, <<"localhost">>, Proxy2Port} = receive_from(Proxy1Pid),
	%% So did the second proxy.
	{auth_methods, 1, [none]} = receive_from(Proxy2Pid),
	{connect, <<"localhost">>, OriginPort} = receive_from(Proxy2Pid),
	handshake_completed = receive_from(OriginPid),
	_ = 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 := Proxy1Port,
			transport := ProxyTransport,
			protocol := socks
		}, #{
			type := socks5,
			host := "localhost",
			port := Proxy2Port,
			transport := ProxyTransport,
			protocol := socks
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

socks5_tcp_through_connect_tcp_to_tcp_origin(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"an HTTP proxy followed by a Socks5 proxy."),
	do_socks5_through_connect_proxy(<<"http">>, tcp, tcp).

socks5_tls_through_connect_tls_to_tcp_origin(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"an HTTPS proxy followed by a TLS Socks5 proxy."),
	do_socks5_through_connect_proxy(<<"http">>, tcp, tls).

socks5_tcp_through_connect_tcp_to_tls_origin(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"an HTTP proxy followed by a Socks5 proxy."),
	do_socks5_through_connect_proxy(<<"https">>, tls, tcp).

socks5_tls_through_connect_tls_to_tls_origin(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"an HTTPS proxy followed by a TLS Socks5 proxy."),
	do_socks5_through_connect_proxy(<<"https">>, tls, tls).

do_socks5_through_connect_proxy(OriginScheme, OriginTransport, ProxyTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, http),
	{ok, Proxy1Pid, Proxy1Port} = rfc7231_SUITE:do_proxy_start(ProxyTransport),
	{ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyTransport, none),
	{ok, ConnPid} = gun:open("localhost", Proxy1Port, #{
		transport => ProxyTransport,
		tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}]
	}),
	%% We receive a gun_up first. This is the HTTP proxy.
	{ok, http} = gun:await_up(ConnPid),
	Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => Proxy2Port,
		transport => ProxyTransport,
		tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
		protocols => [{socks, #{
			host => "localhost",
			port => OriginPort,
			transport => OriginTransport,
			tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}]
		}}]
	}),
	{request, <<"CONNECT">>, Authority1, 'HTTP/1.1', _} = receive_from(Proxy1Pid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
	%% We receive two gun_tunnel_up messages. First the SOCKS server and then the origin HTTP server.
	{up, socks} = gun:await(ConnPid, StreamRef),
	{up, http} = gun:await(ConnPid, StreamRef),
	%% The second proxy receives a Socks5 auth/connect request.
	{auth_methods, 1, [none]} = receive_from(Proxy2Pid),
	{connect, <<"localhost">>, OriginPort} = receive_from(Proxy2Pid),
	handshake_completed = receive_from(OriginPid),
	Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	_ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority2/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	#{
		transport := OriginTransport,
		protocol := http,
		origin_scheme := OriginScheme,
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := Proxy1Port,
			transport := ProxyTransport,
			protocol := http
		}, #{
			type := socks5,
			host := "localhost",
			port := Proxy2Port,
			transport := ProxyTransport,
			protocol := socks
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

socks5_tcp_through_h2_connect_tcp_to_tcp_origin(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"a TCP HTTP/2 proxy followed by a Socks5 proxy."),
	do_socks5_through_h2_connect_proxy(<<"http">>, tcp, <<"http">>, tcp).

do_socks5_through_h2_connect_proxy(_OriginScheme, OriginTransport, ProxyScheme, ProxyTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, http),
	{ok, Proxy1Pid, Proxy1Port} = rfc7540_SUITE:do_proxy_start(ProxyTransport, [
		{proxy_stream, 1, 200, [], 0, undefined}
	]),
	{ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyTransport, none),
	{ok, ConnPid} = gun:open("localhost", Proxy1Port, #{
		transport => ProxyTransport,
		protocols => [http2]
	}),
	%% We receive a gun_up first. This is the HTTP proxy.
	{ok, http2} = gun:await_up(ConnPid),
	handshake_completed = receive_from(Proxy1Pid),
	Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => Proxy2Port,
		transport => ProxyTransport,
		protocols => [{socks, #{
			host => "localhost",
			port => OriginPort,
			transport => OriginTransport
		}}]
	}),
	{request, #{
		<<":method">> := <<"CONNECT">>,
		<<":authority">> := Authority1
	}} = receive_from(Proxy1Pid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
	%% First the HTTP/2 tunnel is up, then the SOCKS tunnel to the origin HTTP server.
	{up, socks} = gun:await(ConnPid, StreamRef),
	{up, http} = gun:await(ConnPid, StreamRef),
	%% The second proxy receives a Socks5 auth/connect request.
	{auth_methods, 1, [none]} = receive_from(Proxy2Pid),
	{connect, <<"localhost">>, OriginPort} = receive_from(Proxy2Pid),
	handshake_completed = receive_from(OriginPid),
	ProxiedStreamRef = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef}),
	Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority2/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	#{
		transport := ProxyTransport,
		protocol := http2,
		origin_scheme := ProxyScheme,
		origin_host := "localhost",
		origin_port := Proxy1Port,
		intermediaries := [] %% Intermediaries are specific to the CONNECT stream.
	} = gun:info(ConnPid),
	{ok, #{
		ref := StreamRef,
		reply_to := Self,
		state := running,
		tunnel := #{
			transport := ProxyTransport,
			protocol := socks,
			%% @todo They're not necessarily the origin. Should be named scheme/host/port.
			origin_scheme := ProxyScheme,
			origin_host := "localhost",
			origin_port := Proxy2Port
		}
	}} = gun:stream_info(ConnPid, StreamRef),
	{ok, #{
		ref := ProxiedStreamRef,
		reply_to := Self,
		state := running,
%% @todo Add "authority" when the stream is not a tunnel.
%		authority := #{
%			scheme := OriginScheme
%			transport :=
%			protocol :=
%			host :=
%			port :=
%		},
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := Proxy1Port,
			transport := ProxyTransport,
			protocol := http2
		}, #{
			type := socks5,
			host := "localhost",
			port := Proxy2Port,
			transport := ProxyTransport,
			protocol := socks
		}]
	}} = gun:stream_info(ConnPid, ProxiedStreamRef),
	gun:close(ConnPid).
