diff options
-rw-r--r-- | src/gun.erl | 29 | ||||
-rw-r--r-- | src/gun_http.erl | 15 | ||||
-rw-r--r-- | src/gun_tls_proxy.erl | 36 | ||||
-rw-r--r-- | test/rfc7231_SUITE.erl | 143 |
4 files changed, 167 insertions, 56 deletions
diff --git a/src/gun.erl b/src/gun.erl index 1fd7e37..2f023e1 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -173,12 +173,13 @@ owner_ref :: reference(), host :: inet:hostname() | inet:ip_address(), port :: inet:port_number(), + origin_scheme :: binary(), origin_host :: inet:hostname() | inet:ip_address(), origin_port :: inet:port_number(), intermediaries = [] :: [intermediary()], opts :: opts(), keepalive_ref :: undefined | reference(), - socket :: undefined | inet:socket() | ssl:sslsocket(), + socket :: undefined | inet:socket() | ssl:sslsocket() | pid(), transport :: module(), messages :: {atom(), atom(), atom()}, protocol :: module(), @@ -292,6 +293,7 @@ info(ServerPid) -> socket=Socket, transport=Transport, protocol=Protocol, + origin_scheme=OriginScheme, origin_host=OriginHost, origin_port=OriginPort, intermediaries=Intermediaries @@ -299,10 +301,15 @@ info(ServerPid) -> {ok, {SockIP, SockPort}} = Transport:sockname(Socket), #{ socket => Socket, - transport => Transport:name(), + transport => case OriginScheme of + <<"http">> -> tcp; + <<"https">> -> tls + end, protocol => Protocol:name(), sock_ip => SockIP, sock_port => SockPort, + %% @todo Add origin_scheme to documentation/tests. + origin_scheme => OriginScheme, origin_host => OriginHost, origin_port => OriginPort, %% Intermediaries are listed in the order data goes through them. @@ -685,14 +692,16 @@ start_link(Owner, Host, Port, Opts) -> init({Owner, Host, Port, Opts}) -> Retry = maps:get(retry, Opts, 5), - Transport = case maps:get(transport, Opts, default_transport(Port)) of - tcp -> gun_tcp; - tls -> gun_tls + OriginTransport = maps:get(transport, Opts, default_transport(Port)), + {OriginScheme, Transport} = case OriginTransport of + tcp -> {<<"http">>, gun_tcp}; + tls -> {<<"https">>, gun_tls} end, OwnerRef = monitor(process, Owner), State = #state{owner=Owner, owner_ref=OwnerRef, - host=Host, port=Port, origin_host=Host, origin_port=Port, - opts=Opts, transport=Transport, messages=Transport:messages()}, + host=Host, port=Port, origin_scheme=OriginScheme, + origin_host=Host, origin_port=Port, opts=Opts, + transport=Transport, messages=Transport:messages()}, {ok, not_connected, State, {next_event, internal, {retries, Retry}}}. @@ -901,7 +910,7 @@ commands([{state, ProtoState}|Tail], State) -> %% Order is important: the origin must be changed before %% the transport and/or protocol in order to keep track %% of the intermediaries properly. -commands([{origin, _Scheme, Host, Port, Type}|Tail], +commands([{origin, Scheme, Host, Port, Type}|Tail], State=#state{transport=Transport, protocol=Protocol, origin_host=IntermediateHost, origin_port=IntermediatePort, intermediaries=Intermediaries}) -> @@ -912,8 +921,8 @@ commands([{origin, _Scheme, Host, Port, Type}|Tail], transport => Transport:name(), protocol => Protocol:name() }, - commands(Tail, State#state{origin_host=Host, origin_port=Port, - intermediaries=[Info|Intermediaries]}); + commands(Tail, State#state{origin_scheme=Scheme, + origin_host=Host, origin_port=Port, intermediaries=[Info|Intermediaries]}); commands([{switch_transport, Transport, Socket}|Tail], State) -> commands(Tail, active(State#state{socket=Socket, transport=Transport, messages=Transport:messages()})); diff --git a/src/gun_http.erl b/src/gun_http.erl index 719307c..efcea35 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -202,8 +202,8 @@ handle(Data, State=#http_state{in={body, Length}, connection=Conn}) -> end end. -handle_head(Data, State=#http_state{socket=Socket, version=ClientVersion, - content_handlers=Handlers0, connection=Conn, +handle_head(Data, State=#http_state{socket=Socket, transport=Transport, + version=ClientVersion, content_handlers=Handlers0, connection=Conn, streams=[Stream=#stream{ref=StreamRef, reply_to=ReplyTo, method=Method, is_alive=IsAlive}|Tail]}) -> {Version, Status, _, Rest} = cow_http:parse_status_line(Data), @@ -226,6 +226,17 @@ handle_head(Data, State=#http_state{socket=Socket, version=ClientVersion, NewHost = maps:get(host, Destination), NewPort = maps:get(port, Destination), case Destination of + #{transport := tls} when Transport =:= gun_tls -> + TLSOpts = maps:get(tls_opts, Destination, []), + TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity), + {ok, ProxyPid} = gun_tls_proxy:start_link(NewHost, NewPort, + TLSOpts, TLSTimeout, Socket, gun_tls), + [{state, State2#http_state{socket=ProxyPid, transport=gun_tls_proxy}}, + {origin, <<"https">>, NewHost, NewPort, connect}, + {switch_transport, gun_tls_proxy, ProxyPid}]; + %% @todo Might also need to switch protocol, but gotta wait + %% @todo for the TLS connection to be established first. + %% @todo Should have a gun_tls_proxy event indicating connection success. #{transport := tls} -> TLSOpts = maps:get(tls_opts, Destination, []), TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity), diff --git a/src/gun_tls_proxy.erl b/src/gun_tls_proxy.erl index 591411b..23737ab 100644 --- a/src/gun_tls_proxy.erl +++ b/src/gun_tls_proxy.erl @@ -102,9 +102,19 @@ start_link(Host, Port, Opts, Timeout, OutSocket, OutTransport) -> ?DEBUG_LOG("host ~0p port ~0p opts ~0p timeout ~0p out_socket ~0p out_transport ~0p", [Host, Port, Opts, Timeout, OutSocket, OutTransport]), - gen_server:start_link(?MODULE, - {self(), Host, Port, Opts, Timeout, OutSocket, OutTransport}, - []). + + case gen_server:start_link(?MODULE, + {self(), Host, Port, Opts, Timeout, OutSocket, OutTransport}, + []) of + {ok, Pid} when is_port(OutSocket) -> + gen_tcp:controlling_process(OutSocket, Pid), + {ok, Pid}; + {ok, Pid} when not is_pid(OutSocket) -> + ssl:controlling_process(OutSocket, Pid), + {ok, Pid}; + Other -> + Other + end. %% gun_tls_proxy_cb interface. @@ -114,11 +124,21 @@ cb_controlling_process(Pid, ControllingPid) -> cb_send(Pid, Data) -> ?DEBUG_LOG("pid ~0p data ~0p", [Pid, Data]), - gen_server:call(Pid, {?FUNCTION_NAME, Data}). + try + gen_server:call(Pid, {?FUNCTION_NAME, Data}) + catch + exit:{noproc, _} -> + {error, closed} + end. cb_setopts(Pid, Opts) -> ?DEBUG_LOG("pid ~0p opts ~0p", [Pid, Opts]), - gen_server:call(Pid, {?FUNCTION_NAME, Opts}). + try + gen_server:call(Pid, {?FUNCTION_NAME, Opts}) + catch + exit:{noproc, _} -> + {error, einval} + end. %% Transport. @@ -156,6 +176,7 @@ close(Pid) -> gen_server:call(Pid, ?FUNCTION_NAME). %% gen_server. +%% @todo Probably need to gen_statem it to avoid trying to send stuff before being connected. init({OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport}) -> if @@ -244,6 +265,8 @@ handle_cast(Msg={connect_proc, Error}, State) -> {stop, Error, State}; handle_cast(Msg={cb_controlling_process, ProxyPid}, State) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + %% We link so that the ssl process terminates when we do. + link(ProxyPid), {noreply, State#state{proxy_pid=ProxyPid}}; handle_cast(Msg={setopts, Opts}, State) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), @@ -342,7 +365,6 @@ tcp_test() -> ssl:start(), {ok, Socket} = gen_tcp:connect("google.com", 443, [binary, {active, false}]), {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, gen_tcp), - gen_tcp:controlling_process(Socket, ProxyPid1), timer:sleep(500), send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>), timer:sleep(1000), @@ -355,7 +377,6 @@ ssl_test() -> {ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}]), timer:sleep(500), {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, ssl), - ssl:controlling_process(Socket, ProxyPid1), timer:sleep(500), send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>), timer:sleep(1000), @@ -369,7 +390,6 @@ ssl2_test() -> {ok, Socket} = ssl:connect("localhost", Port2, [binary, {active, false}]), timer:sleep(500), {ok, ProxyPid1} = start_link("localhost", Port1, [], 5000, Socket, ssl), - ssl:controlling_process(Socket, ProxyPid1), timer:sleep(500), {ok, ProxyPid2} = start_link("google.com", 443, [], 5000, ProxyPid1, ?MODULE), timer:sleep(500), diff --git a/test/rfc7231_SUITE.erl b/test/rfc7231_SUITE.erl index 769ce98..66ada7f 100644 --- a/test/rfc7231_SUITE.erl +++ b/test/rfc7231_SUITE.erl @@ -27,33 +27,54 @@ all() -> %% Proxy helpers. -do_proxy_start() -> - do_proxy_start(200, []). +do_proxy_start(Transport) -> + do_proxy_start(Transport, 200, []). -do_proxy_start(Status) -> - do_proxy_start(Status, []). +do_proxy_start(Transport, Status) -> + do_proxy_start(Transport, Status, []). -do_proxy_start(Status, ConnectRespHeaders) -> - do_proxy_start(Status, ConnectRespHeaders, 0). +do_proxy_start(Transport, Status, ConnectRespHeaders) -> + do_proxy_start(Transport, Status, ConnectRespHeaders, 0). -do_proxy_start(Status, ConnectRespHeaders, Delay) -> +do_proxy_start(Transport0, Status, ConnectRespHeaders, Delay) -> + Transport = case Transport0 of + tcp -> gun_tcp; + tls -> gun_tls + end, Self = self(), - Pid = spawn_link(fun() -> do_proxy_init(Self, Status, ConnectRespHeaders, Delay) end), + Pid = spawn_link(fun() -> do_proxy_init(Self, Transport, 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), +do_proxy_init(Parent, Transport, Status, ConnectRespHeaders, Delay) -> + {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}|Opts]) + end, + {ok, {_, Port}} = Transport:sockname(ListenSocket), Parent ! {self(), Port}, - {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 1000), - {ok, Data} = gen_tcp:recv(ClientSocket, 0, 1000), + {ok, ClientSocket} = case Transport of + gun_tcp -> + gen_tcp:accept(ListenSocket, 1000); + gun_tls -> + {ok, ClientSocket0} = ssl:transport_accept(ListenSocket, 1000), + ssl:handshake(ClientSocket0, 1000) + end, + {ok, Data} = case Transport of + gun_tcp -> + gen_tcp:recv(ClientSocket, 0, 1000); + gun_tls -> + ssl:recv(ClientSocket, 0, 1000) + end, {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, [ + ok = Transport:send(ClientSocket, [ <<"HTTP/1.1 ">>, integer_to_binary(Status), <<" Reason phrase\r\n">>, @@ -65,28 +86,36 @@ do_proxy_init(Parent, Status, ConnectRespHeaders, Delay) -> {ok, OriginSocket} = gen_tcp:connect( binary_to_list(OriginHost), OriginPort, [binary, {active, false}]), - inet:setopts(ClientSocket, [{active, true}]), + Transport:setopts(ClientSocket, [{active, true}]), inet:setopts(OriginSocket, [{active, true}]), - do_proxy_loop(ClientSocket, OriginSocket); + do_proxy_loop(Transport, ClientSocket, OriginSocket); true -> %% We send a 501 to the subsequent request. - {ok, _} = gen_tcp:recv(ClientSocket, 0, 1000), - ok = gen_tcp:send(ClientSocket, << + {ok, _} = case Transport of + gun_tcp -> + gen_tcp:recv(ClientSocket, 0, 1000); + gun_tls -> + ssl:recv(ClientSocket, 0, 1000) + end, + ok = Transport: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) -> +do_proxy_loop(Transport, ClientSocket, OriginSocket) -> + {OK, _, _} = Transport:messages(), receive - {tcp, ClientSocket, Data} -> + {OK, ClientSocket, Data} -> ok = gen_tcp:send(OriginSocket, Data), - do_proxy_loop(ClientSocket, OriginSocket); + do_proxy_loop(Transport, ClientSocket, OriginSocket); {tcp, OriginSocket, Data} -> - ok = gen_tcp:send(ClientSocket, Data), - do_proxy_loop(ClientSocket, OriginSocket); + ok = Transport:send(ClientSocket, Data), + do_proxy_loop(Transport, ClientSocket, OriginSocket); {tcp_closed, _} -> ok; + {ssl_closed, _} -> + ok; Msg -> error(Msg) end. @@ -105,7 +134,7 @@ connect_https(_) -> do_connect_http(Transport) -> {ok, OriginPid, OriginPort} = init_origin(Transport, http), - {ok, ProxyPid, ProxyPort} = do_proxy_start(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -134,6 +163,48 @@ do_connect_http(Transport) -> }]} = gun:info(ConnPid), gun:close(ConnPid). +connect_http_over_https_proxy(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via an HTTPS proxy. (RFC7231 4.3.6)"), + do_connect_http_over_https_proxy(tcp). + +connect_https_over_https_proxy(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via an HTTPS proxy. (RFC7231 4.3.6)"), + do_connect_http_over_https_proxy(tls). + +do_connect_http_over_https_proxy(Transport) -> + {ok, OriginPid, OriginPort} = init_origin(Transport, http), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tls), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{transport => tls}), + {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), +% timer:sleep(2000), + _ = 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 := tls, + 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)"), @@ -146,7 +217,7 @@ connect_h2(_) -> do_connect_h2(Transport) -> {ok, OriginPid, OriginPort} = init_origin(Transport, http2), - {ok, ProxyPid, ProxyPort} = do_proxy_start(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -180,8 +251,8 @@ connect_through_multiple_proxies(_) -> "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, Proxy1Pid, Proxy1Port} = do_proxy_start(tcp), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(tcp), {ok, ConnPid} = gun:open("localhost", Proxy1Port), {ok, http} = gun:await_up(ConnPid), Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]), @@ -225,7 +296,7 @@ connect_through_multiple_proxies(_) -> connect_delay(_) -> doc("The CONNECT response may not be immediate."), {ok, OriginPid, OriginPort} = init_origin(tcp), - {ok, ProxyPid, ProxyPort} = do_proxy_start(201, [], 2000), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 201, [], 2000), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort, #{http_opts => #{keepalive => 1000}}), @@ -258,7 +329,7 @@ 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), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 201), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -304,7 +375,7 @@ connect_response_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), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, Status, Headers), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -328,7 +399,7 @@ do_connect_failure(Status) -> 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(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -343,7 +414,7 @@ connect_authority_form(_) -> 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(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -363,7 +434,7 @@ 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(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -379,7 +450,7 @@ 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(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -396,7 +467,7 @@ connect_response_ignore_transfer_encoding(_) -> "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), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 200, Headers), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), @@ -417,7 +488,7 @@ connect_response_ignore_content_length(_) -> "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), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 200, Headers), Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), {ok, ConnPid} = gun:open("localhost", ProxyPort), {ok, http} = gun:await_up(ConnPid), |