aboutsummaryrefslogblamecommitdiffstats
path: root/test/raw_SUITE.erl
blob: 29717e68cb1cbbaad1575cd14fc0fa392b286bbe (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(raw_SUITE).
-compile(export_all).
-compile(nowarn_export_all).

-import(ct_helper, [doc/1]).
-import(gun_test, [init_origin/3]).
-import(gun_test, [receive_from/1]).

all() ->
	[{group, raw}].

groups() ->
	[{raw, [parallel], ct_helper:all(?MODULE)}].

%% Tests.

direct_raw_tcp(_) ->
	doc("Directly connect to a remote endpoint using the raw protocol over TCP."),
	do_direct_raw(tcp).

direct_raw_tls(_) ->
	doc("Directly connect to a remote endpoint using the raw protocol over TLS."),
	do_direct_raw(tls).

do_direct_raw(OriginTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/3),
	{ok, ConnPid} = gun:open("localhost", OriginPort, #{
		transport => OriginTransport,
		protocols => [raw]
	}),
	{ok, raw} = gun:await_up(ConnPid),
	handshake_completed = receive_from(OriginPid),
	%% When we take over the entire connection there is no stream reference.
	gun:data(ConnPid, undefined, nofin, <<"Hello world!">>),
	{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined),
	#{
		transport := OriginTransport,
		protocol := raw,
		origin_scheme := _, %% @todo This should be 'undefined'.
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := []
	} = gun:info(ConnPid),
	gun:close(ConnPid).

socks5_tcp_raw_tcp(_) ->
	doc("Use Socks5 over TCP to connect to a remote endpoint using the raw protocol over TCP."),
	do_socks5_raw(tcp, tcp).

socks5_tcp_raw_tls(_) ->
	doc("Use Socks5 over TCP to connect to a remote endpoint using the raw protocol over TLS."),
	do_socks5_raw(tcp, tls).

socks5_tls_raw_tcp(_) ->
	doc("Use Socks5 over TLS to connect to a remote endpoint using the raw protocol over TCP."),
	do_socks5_raw(tls, tcp).

socks5_tls_raw_tls(_) ->
	doc("Use Socks5 over TLS to connect to a remote endpoint using the raw protocol over TLS."),
	do_socks5_raw(tls, tls).

do_socks5_raw(OriginTransport, ProxyTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/3),
	{ok, ProxyPid, ProxyPort} = socks_SUITE:do_proxy_start(ProxyTransport, none),
	{ok, ConnPid} = gun:open("localhost", ProxyPort, #{
		transport => ProxyTransport,
		protocols => [{socks, #{
			host => "localhost",
			port => OriginPort,
			transport => OriginTransport,
			protocols => [raw]
		}}]
	}),
	%% We receive a gun_up and a gun_socks_up.
	{ok, socks} = gun:await_up(ConnPid),
	{ok, raw} = gun:await_up(ConnPid),
	%% The proxy received two packets.
	{auth_methods, 1, [none]} = receive_from(ProxyPid),
	{connect, <<"localhost">>, OriginPort} = receive_from(ProxyPid),
	handshake_completed = receive_from(OriginPid),
	%% When we take over the entire connection there is no stream reference.
	gun:data(ConnPid, undefined, nofin, <<"Hello world!">>),
	{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined),
	#{
		transport := OriginTransport,
		protocol := raw,
		origin_scheme := _, %% @todo This should be 'undefined'.
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := socks5,
			host := "localhost",
			port := ProxyPort,
			transport := ProxyTransport,
			protocol := socks
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_tcp_raw_tcp(_) ->
	doc("Use CONNECT over TCP to connect to a remote endpoint using the raw protocol over TCP."),
	do_connect_raw(tcp, tcp).

connect_tcp_raw_tls(_) ->
	doc("Use CONNECT over TCP to connect to a remote endpoint using the raw protocol over TLS."),
	do_connect_raw(tcp, tls).

connect_tls_raw_tcp(_) ->
	doc("Use CONNECT over TLS to connect to a remote endpoint using the raw protocol over TCP."),
	do_connect_raw(tls, tcp).

connect_tls_raw_tls(_) ->
	doc("Use CONNECT over TLS to connect to a remote endpoint using the raw protocol over TLS."),
	do_connect_raw(tls, tls).

do_connect_raw(OriginTransport, ProxyTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/3),
	{ok, ProxyPid, ProxyPort} = rfc7231_SUITE:do_proxy_start(ProxyTransport),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort, #{transport => ProxyTransport}),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort,
		transport => OriginTransport,
		protocols => [raw]
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef), %% @todo Why fin?
	handshake_completed = receive_from(OriginPid),
	%% When we take over the entire connection there is no stream reference.
	gun:data(ConnPid, undefined, nofin, <<"Hello world!">>),
	{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined),
	#{
		transport := OriginTransport,
		protocol := raw,
		origin_scheme := _, %% @todo This should be 'undefined'.
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := ProxyPort,
			transport := ProxyTransport,
			protocol := http
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_raw_reply_to(_) ->
	doc("When using CONNECT to establish a connection with the reply_to option set, "
		"Gun must honor this option in the raw protocol."),
	Self = self(),
	ReplyTo = spawn(fun() ->
		{ConnPid, StreamRef} = receive Msg -> Msg after 1000 -> error(timeout) end,
		{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
		Self ! {self(), ready},
		{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined),
		Self ! {self(), ok}
	end),
	{ok, OriginPid, OriginPort} = init_origin(tcp, raw, fun do_echo/3),
	{ok, ProxyPid, ProxyPort} = rfc7231_SUITE:do_proxy_start(tcp),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort,
		protocols => [raw]
	}, [], #{reply_to => ReplyTo}),
	ReplyTo ! {ConnPid, StreamRef},
	{request, <<"CONNECT">>, _, 'HTTP/1.1', _} = receive_from(ProxyPid),
	handshake_completed = receive_from(OriginPid),
	receive {ReplyTo, ready} -> ok after 1000 -> error(timeout) end,
	gun:data(ConnPid, undefined, nofin, <<"Hello world!">>),
	receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end.

h2_connect_tcp_raw_tcp(_) ->
	doc("Use CONNECT over clear HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."),
	do_h2_connect_raw(tcp, <<"http">>, tcp).

h2_connect_tls_raw_tcp(_) ->
	doc("Use CONNECT over secure HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."),
	do_h2_connect_raw(tcp, <<"https">>, tls).

do_h2_connect_raw(OriginTransport, ProxyScheme, ProxyTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/3),
	{ok, ProxyPid, ProxyPort} = rfc7540_SUITE:do_proxy_start(ProxyTransport, [
		{proxy_stream, 1, 200, [], 0, undefined}
	]),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort, #{
		transport => ProxyTransport,
		protocols => [http2]
	}),
	{ok, http2} = gun:await_up(ConnPid),
	handshake_completed = receive_from(ProxyPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort,
		transport => OriginTransport,
		protocols => [raw]
	}),
	{request, #{
		<<":method">> := <<"CONNECT">>,
		<<":authority">> := Authority
	}} = receive_from(ProxyPid),
	{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
	handshake_completed = receive_from(OriginPid),
	gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>),
	{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef),
	#{
		transport := ProxyTransport,
		protocol := http2,
		origin_scheme := ProxyScheme,
		origin_host := "localhost",
		origin_port := ProxyPort,
		intermediaries := []
	} = gun:info(ConnPid),
	Self = self(),
	{ok, #{
		ref := StreamRef,
		reply_to := Self,
		state := running,
		tunnel := #{
			transport := OriginTransport,
			protocol := raw,
			origin_scheme := _, %% @todo This should be 'undefined'.
			origin_host := "localhost",
			origin_port := OriginPort
		}
	}} = gun:stream_info(ConnPid, StreamRef),
	gun:close(ConnPid).

http11_upgrade_raw_tcp(_) ->
	doc("Use the HTTP Upgrade mechanism to switch to the raw protocol over TCP."),
	do_http11_upgrade_raw(tcp).

http11_upgrade_raw_tls(_) ->
	doc("Use the HTTP Upgrade mechanism to switch to the raw protocol over TLS."),
	do_http11_upgrade_raw(tls).

do_http11_upgrade_raw(OriginTransport) ->
	{ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw,
		fun (Parent, ClientSocket, ClientTransport) ->
			%% We skip the request and send a 101 response unconditionally.
			{ok, _} = ClientTransport:recv(ClientSocket, 0, 5000),
			ClientTransport:send(ClientSocket,
				"HTTP/1.1 101 Switching Protocols\r\n"
				"Connection: upgrade\r\n"
				"Upgrade: custom/1.0\r\n"
				"\r\n"),
			do_echo(Parent, ClientSocket, ClientTransport)
		end),
	{ok, ConnPid} = gun:open("localhost", OriginPort, #{
		transport => OriginTransport
	}),
	{ok, http} = gun:await_up(ConnPid),
	handshake_completed = receive_from(OriginPid),
	StreamRef = gun:get(ConnPid, "/", #{
		<<"connection">> => <<"upgrade">>,
		<<"upgrade">> => <<"custom/1.0">>
	}),
	{upgrade, [<<"custom/1.0">>], _} = gun:await(ConnPid, StreamRef),
	%% When we take over the entire connection there is no stream reference.
	gun:data(ConnPid, undefined, nofin, <<"Hello world!">>),
	{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined),
	#{
		transport := OriginTransport,
		protocol := raw,
		origin_scheme := _, %% @todo This should be 'undefined'.
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := []
	} = gun:info(ConnPid),
	gun:close(ConnPid).

http11_upgrade_raw_reply_to(_) ->
	doc("When upgrading an HTTP/1.1 connection with the reply_to option set, "
		"Gun must honor this option in the raw protocol."),
	Self = self(),
	ReplyTo = spawn(fun() ->
		{ConnPid, StreamRef} = receive Msg -> Msg after 1000 -> error(timeout) end,
		{upgrade, [<<"custom/1.0">>], _} = gun:await(ConnPid, StreamRef),
		Self ! {self(), ready},
		{data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined),
		Self ! {self(), ok}
	end),
	{ok, OriginPid, OriginPort} = init_origin(tcp, raw,
		fun (Parent, ClientSocket, ClientTransport) ->
			%% We skip the request and send a 101 response unconditionally.
			{ok, _} = ClientTransport:recv(ClientSocket, 0, 5000),
			ClientTransport:send(ClientSocket,
				"HTTP/1.1 101 Switching Protocols\r\n"
				"Connection: upgrade\r\n"
				"Upgrade: custom/1.0\r\n"
				"\r\n"),
			do_echo(Parent, ClientSocket, ClientTransport)
		end),
	{ok, ConnPid} = gun:open("localhost", OriginPort),
	{ok, http} = gun:await_up(ConnPid),
	handshake_completed = receive_from(OriginPid),
	StreamRef = gun:get(ConnPid, "/", #{
		<<"connection">> => <<"upgrade">>,
		<<"upgrade">> => <<"custom/1.0">>
	}, #{reply_to => ReplyTo}),
	ReplyTo ! {ConnPid, StreamRef},
	receive {ReplyTo, ready} -> ok after 1000 -> error(timeout) end,
	gun:data(ConnPid, undefined, nofin, <<"Hello world!">>),
	receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end.

%% The origin server will echo everything back.

do_echo(Parent, ClientSocket, ClientTransport) ->
	case ClientTransport:recv(ClientSocket, 0, 5000) of
		{ok, Data} ->
			ClientTransport:send(ClientSocket, Data),
			do_echo(Parent, ClientSocket, ClientTransport);
		{error, closed} ->
			ok
	end.