aboutsummaryrefslogblamecommitdiffstats
path: root/test/rfc7231_SUITE.erl
blob: 769ce9838283de920b005657a5fb63d6c06e00f3 (plain) (tree)
1
                                                             
















                                                                           
                            



                                    












                                             


                                                      
                      
                                                                                              
                                 

                        
                                                           






                                                                                              
                           







































                                                                          












                                                                            
                                                                   








                                                                                    
                                                                                    

                                                                
                                       

                                                                                  











                                            












                                                                          
                                                                    







                                                                                    
                                    
           
                                                                                    
                                                                
                                                                                           
                                         
                                                        











                                            





                                                                   
                                                       








                                                                                     
                                                                                      





                                                                                     
                                                                                      

                                                                 
                                       

                                                                                   

















                                            

                           

                                                          
                                                       








                                                                                    
                                                                                          

                                                                
                                       

                                                                                  













                                            


                                                                       
                                                       







                                                                                    
                                                                                    

                                                                
                                       

                                                                                  











                                            



























                                                                                    
                                                                                    


                                                                         






                                           



                                                                             
                                                        







                                                                                    
                                                                                    




                                                                                          
                                                        









                                                                                    
                                                                                          







                                                                                       
                                                        







                                                                                    
                                                                                          





                                                                                 
                                                        







                                                                                    
                                                                                          





                                                                         
                                                       








                                                                                    
                                                                                    

                                                                      
                                       

                                                                                  




                                                                      
                                                       








                                                                                    
                                                                                    

                                                                      
                                       

                                                                                  
                           
%% Copyright (c) 2018-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(rfc7231_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() ->
	do_proxy_start(200, []).

do_proxy_start(Status) ->
	do_proxy_start(Status, []).

do_proxy_start(Status, ConnectRespHeaders) ->
	do_proxy_start(Status, ConnectRespHeaders, 0).

do_proxy_start(Status, ConnectRespHeaders, Delay) ->
	Self = self(),
	Pid = spawn_link(fun() -> do_proxy_init(Self, Status, ConnectRespHeaders, Delay) end),
	Port = receive_from(Pid),
	{ok, Pid, Port}.

do_proxy_init(Parent, Status, ConnectRespHeaders, Delay) ->
	{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
	{ok, {_, Port}} = inet:sockname(ListenSocket),
	Parent ! {self(), Port},
	{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 1000),
	{ok, Data} = gen_tcp:recv(ClientSocket, 0, 1000),
	{Method= <<"CONNECT">>, Authority, Version, Rest} = cow_http:parse_request_line(Data),
	{Headers, <<>>} = cow_http:parse_headers(Rest),
	timer:sleep(Delay),
	Parent ! {self(), {request, Method, Authority, Version, Headers}},
	{OriginHost, OriginPort} = cow_http_hd:parse_host(Authority),
	ok = gen_tcp:send(ClientSocket, [
		<<"HTTP/1.1 ">>,
		integer_to_binary(Status),
		<<" Reason phrase\r\n">>,
		cow_http:headers(ConnectRespHeaders),
		<<"\r\n">>
	]),
	if
		Status >= 200, Status < 300 ->
			{ok, OriginSocket} = gen_tcp:connect(
				binary_to_list(OriginHost), OriginPort,
				[binary, {active, false}]),
			inet:setopts(ClientSocket, [{active, true}]),
			inet:setopts(OriginSocket, [{active, true}]),
			do_proxy_loop(ClientSocket, OriginSocket);
		true ->
			%% We send a 501 to the subsequent request.
			{ok, _} = gen_tcp:recv(ClientSocket, 0, 1000),
			ok = gen_tcp:send(ClientSocket, <<
				"HTTP/1.1 501 Not Implemented\r\n"
				"content-length: 0\r\n\r\n">>),
			timer:sleep(2000)
	end.

do_proxy_loop(ClientSocket, OriginSocket) ->
	receive
		{tcp, ClientSocket, Data} ->
			ok = gen_tcp:send(OriginSocket, Data),
			do_proxy_loop(ClientSocket, OriginSocket);
		{tcp, OriginSocket, Data} ->
			ok = gen_tcp:send(ClientSocket, Data),
			do_proxy_loop(ClientSocket, OriginSocket);
		{tcp_closed, _} ->
			ok;
		Msg ->
			error(Msg)
	end.

%% Tests.

connect_http(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via an HTTP proxy. (RFC7231 4.3.6)"),
	do_connect_http(tcp).

connect_https(_) ->
	doc("CONNECT can be used to establish a TLS connection "
		"to an HTTP/1.1 server via an HTTP proxy. (RFC7231 4.3.6)"),
	do_connect_http(tls).

do_connect_http(Transport) ->
	{ok, OriginPid, OriginPort} = init_origin(Transport, http),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort,
		transport => Transport
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
	_ = gun:get(ConnPid, "/proxied"),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	#{
		transport := Transport,
		protocol := http,
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := ProxyPort,
			transport := tcp,
			protocol := http
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_h2c(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/2 server via an HTTP proxy. (RFC7231 4.3.6)"),
	do_connect_h2(tcp).

connect_h2(_) ->
	doc("CONNECT can be used to establish a TLS connection "
		"to an HTTP/2 server via an HTTP proxy. (RFC7231 4.3.6)"),
	do_connect_h2(tls).

do_connect_h2(Transport) ->
	{ok, OriginPid, OriginPort} = init_origin(Transport, http2),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort,
		transport => Transport,
		protocols => [http2]
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
	timer:sleep(1000), %% Give enough time for the ssl/h2 handshakes to fully complete.
	_ = gun:get(ConnPid, "/proxied"),
	<<_:24, 1:8, _/bits>> = receive_from(OriginPid),
	#{
		transport := Transport,
		protocol := http2,
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := ProxyPort,
			transport := tcp,
			protocol := http
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_through_multiple_proxies(_) ->
	doc("CONNECT can be used to establish a TCP connection "
		"to an HTTP/1.1 server via a tunnel going through "
		"two separate HTTP proxies. (RFC7231 4.3.6)"),
	{ok, OriginPid, OriginPort} = init_origin(tcp),
	{ok, Proxy1Pid, Proxy1Port} = do_proxy_start(),
	{ok, Proxy2Pid, Proxy2Port} = do_proxy_start(),
	{ok, ConnPid} = gun:open("localhost", Proxy1Port),
	{ok, http} = gun:await_up(ConnPid),
	Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]),
	StreamRef1 = gun:connect(ConnPid, #{
		host => "localhost",
		port => Proxy2Port
	}),
	{request, <<"CONNECT">>, Authority1, 'HTTP/1.1', _} = receive_from(Proxy1Pid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef1),
	Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	StreamRef2 = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority2, 'HTTP/1.1', _} = receive_from(Proxy2Pid),
	{response, fin, 200, _} = gun:await(ConnPid, StreamRef2),
	_ = gun:get(ConnPid, "/proxied"),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority2/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	#{
		transport := tcp,
		protocol := http,
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := Proxy1Port,
			transport := tcp,
			protocol := http
		}, #{
			type := connect,
			host := "localhost",
			port := Proxy2Port,
			transport := tcp,
			protocol := http
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_delay(_) ->
	doc("The CONNECT response may not be immediate."),
	{ok, OriginPid, OriginPort} = init_origin(tcp),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(201, [], 2000),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort,
		#{http_opts => #{keepalive => 1000}}),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid, 3000),
	{response, fin, 201, _} = gun:await(ConnPid, StreamRef),
	_ = gun:get(ConnPid, "/proxied"),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	#{
		transport := tcp,
		protocol := http,
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := ProxyPort,
			transport := tcp,
			protocol := http
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_response_201(_) ->
	doc("2xx responses to CONNECT requests indicate "
		"the tunnel was set up successfully. (RFC7231 4.3.6)"),
	{ok, OriginPid, OriginPort} = init_origin(tcp),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(201),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, 201, _} = gun:await(ConnPid, StreamRef),
	_ = gun:get(ConnPid, "/proxied"),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	#{
		transport := tcp,
		protocol := http,
		origin_host := "localhost",
		origin_port := OriginPort,
		intermediaries := [#{
			type := connect,
			host := "localhost",
			port := ProxyPort,
			transport := tcp,
			protocol := http
	}]} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_response_302(_) ->
	doc("3xx responses to CONNECT requests indicate "
		"the tunnel was not set up. (RFC7231 4.3.6)"),
	do_connect_failure(302).

connect_response_403(_) ->
	doc("4xx responses to CONNECT requests indicate "
		"the tunnel was not set up. (RFC7231 4.3.6)"),
	do_connect_failure(403).

connect_response_500(_) ->
	doc("5xx responses to CONNECT requests indicate "
		"the tunnel was not set up. (RFC7231 4.3.6)"),
	do_connect_failure(500).

do_connect_failure(Status) ->
	OriginPort = 33333, %% Doesn't matter because we won't try to connect.
	Headers = [{<<"content-length">>, <<"0">>}],
	{ok, ProxyPid, ProxyPort} = do_proxy_start(Status, Headers),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, Status, Headers} = gun:await(ConnPid, StreamRef),
	FailedStreamRef = gun:get(ConnPid, "/proxied"),
	{response, fin, 501, _} = gun:await(ConnPid, FailedStreamRef),
	#{
		transport := tcp,
		protocol := http,
		origin_host := "localhost",
		origin_port := ProxyPort,
		intermediaries := []
	} = gun:info(ConnPid),
	gun:close(ConnPid).

connect_authority_form(_) ->
	doc("CONNECT requests must use the authority-form. (RFC7231 4.3.6)"),
	{ok, _OriginPid, OriginPort} = init_origin(tcp),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	_StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{<<"localhost">>, OriginPort} = cow_http_hd:parse_host(Authority),
	gun:close(ConnPid).

connect_proxy_authorization(_) ->
	doc("CONNECT requests may include a proxy-authorization header. (RFC7231 4.3.6)"),
	{ok, _OriginPid, OriginPort} = init_origin(tcp),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	_StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort,
		username => "essen",
		password => "myrealpasswordis"
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', Headers} = receive_from(ProxyPid),
	{_, ProxyAuthorization} = lists:keyfind(<<"proxy-authorization">>, 1, Headers),
	{basic, <<"essen">>, <<"myrealpasswordis">>}
		= cow_http_hd:parse_proxy_authorization(ProxyAuthorization),
	gun:close(ConnPid).

connect_request_no_transfer_encoding(_) ->
	doc("The payload for CONNECT requests has no defined semantics. "
		"The transfer-encoding header should not be sent. (RFC7231 4.3.6)"),
	{ok, _OriginPid, OriginPort} = init_origin(tcp),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	_StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', Headers} = receive_from(ProxyPid),
	false = lists:keyfind(<<"transfer-encoding">>, 1, Headers),
	gun:close(ConnPid).

connect_request_no_content_length(_) ->
	doc("The payload for CONNECT requests has no defined semantics. "
		"The content-length header should not be sent. (RFC7231 4.3.6)"),
	{ok, _OriginPid, OriginPort} = init_origin(tcp),
	{ok, ProxyPid, ProxyPort} = do_proxy_start(),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	_StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', Headers} = receive_from(ProxyPid),
	false = lists:keyfind(<<"content-length">>, 1, Headers),
	gun:close(ConnPid).

connect_response_ignore_transfer_encoding(_) ->
	doc("Clients must ignore transfer-encoding headers in responses "
		"to CONNECT requests. (RFC7231 4.3.6)"),
	{ok, OriginPid, OriginPort} = init_origin(tcp),
	Headers = [{<<"transfer-encoding">>, <<"chunked">>}],
	{ok, ProxyPid, ProxyPort} = do_proxy_start(200, Headers),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, 200, Headers} = gun:await(ConnPid, StreamRef),
	_ = gun:get(ConnPid, "/proxied"),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	gun:close(ConnPid).

connect_response_ignore_content_length(_) ->
	doc("Clients must ignore content-length headers in responses "
		"to CONNECT requests. (RFC7231 4.3.6)"),
	{ok, OriginPid, OriginPort} = init_origin(tcp),
	Headers = [{<<"content-length">>, <<"1000">>}],
	{ok, ProxyPid, ProxyPort} = do_proxy_start(200, Headers),
	Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
	{ok, ConnPid} = gun:open("localhost", ProxyPort),
	{ok, http} = gun:await_up(ConnPid),
	StreamRef = gun:connect(ConnPid, #{
		host => "localhost",
		port => OriginPort
	}),
	{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid),
	{response, fin, 200, Headers} = gun:await(ConnPid, StreamRef),
	_ = gun:get(ConnPid, "/proxied"),
	Data = receive_from(OriginPid),
	Lines = binary:split(Data, <<"\r\n">>, [global]),
	[<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
	gun:close(ConnPid).