diff options
-rw-r--r-- | ebin/gun.app | 2 | ||||
-rw-r--r-- | src/gun.erl | 84 | ||||
-rw-r--r-- | src/gun_http.erl | 19 | ||||
-rw-r--r-- | src/gun_raw.erl | 61 | ||||
-rw-r--r-- | src/gun_socks.erl | 3 | ||||
-rw-r--r-- | test/raw_SUITE.erl | 214 |
6 files changed, 348 insertions, 35 deletions
diff --git a/ebin/gun.app b/ebin/gun.app index c488c9a..9963c83 100644 --- a/ebin/gun.app +++ b/ebin/gun.app @@ -1,7 +1,7 @@ {application, 'gun', [ {description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."}, {vsn, "1.3.0"}, - {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']}, + {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']}, {registered, [gun_sup]}, {applications, [kernel,stdlib,ssl,cowlib]}, {mod, {gun_app, []}}, diff --git a/src/gun.erl b/src/gun.erl index 600327b..ee3a5f6 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -99,6 +99,7 @@ -export([initial_tls_handshake/3]). -export([tls_handshake/3]). -export([connected_no_input/3]). +-export([connected_data_only/3]). -export([connected/3]). -export([closing/3]). -export([terminate/3]). @@ -114,8 +115,8 @@ | {close, ws_close_code(), iodata()}. -export_type([ws_frame/0]). --type protocols() :: [http | http2 | socks - | {http, http_opts()} | {http2, http2_opts()} | {socks, socks_opts()}]. +-type protocols() :: [http | http2 | raw | socks + | {http, http_opts()} | {http2, http2_opts()} | {raw, raw_opts()} | {socks, socks_opts()}]. -export_type([protocols/0]). -type opts() :: #{ @@ -158,9 +159,12 @@ host := inet:hostname() | inet:ip_address(), port := inet:port_number(), transport := tcp | tls, - protocol := http | http2 | socks + protocol := http | http2 | raw | socks }. +-type raw_opts() :: #{}. +-export_type([raw_opts/0]). + %% @todo When/if HTTP/2 CONNECT gets implemented, we will want an option here %% to indicate that the request must be sent on an existing CONNECT stream. %% This is of course not required for HTTP/1.1 since the CONNECT takes over @@ -359,7 +363,7 @@ check_protocols_opt(Protocols) -> %% Protocols must not appear more than once, and they %% must be one of http, http2 or socks. ProtoNames0 = lists:usort([case P0 of {P, _} -> P; P -> P end || P0 <- Protocols]), - ProtoNames = [P || P <- ProtoNames0, lists:member(P, [http, http2, socks])], + ProtoNames = [P || P <- ProtoNames0, lists:member(P, [http, http2, raw, socks])], case length(Protocols) =:= length(ProtoNames) of false -> error; true -> @@ -368,6 +372,7 @@ check_protocols_opt(Protocols) -> TupleCheck = [case P of {http, Opts} -> gun_http:check_options(Opts); {http2, Opts} -> gun_http2:check_options(Opts); + {raw, Opts} -> gun_raw:check_options(Opts); {socks, Opts} -> gun_socks:check_options(Opts) end || P <- Protocols, is_tuple(P)], case lists:usort(TupleCheck) of @@ -382,6 +387,7 @@ consider_tracing(ServerPid, #{trace := true}) -> dbg:tpl(gun, [{'_', [], [{return_trace}]}]), dbg:tpl(gun_http, [{'_', [], [{return_trace}]}]), dbg:tpl(gun_http2, [{'_', [], [{return_trace}]}]), + dbg:tpl(gun_raw, [{'_', [], [{return_trace}]}]), dbg:tpl(gun_socks, [{'_', [], [{return_trace}]}]), dbg:tpl(gun_ws, [{'_', [], [{return_trace}]}]), dbg:p(ServerPid, all); @@ -688,14 +694,18 @@ await_body(ServerPid, StreamRef, Timeout, MRef, Acc) -> {error, timeout} end. --spec await_up(pid()) -> {ok, http | http2} | {error, {down, any()} | timeout}. +-spec await_up(pid()) + -> {ok, http | http2 | raw | socks} + | {error, {down, any()} | timeout}. await_up(ServerPid) -> MRef = monitor(process, ServerPid), Res = await_up(ServerPid, 5000, MRef), demonitor(MRef, [flush]), Res. --spec await_up(pid(), reference() | timeout()) -> {ok, http | http2} | {error, {down, any()} | timeout}. +-spec await_up(pid(), reference() | timeout()) + -> {ok, http | http2 | raw | socks} + | {error, {down, any()} | timeout}. await_up(ServerPid, MRef) when is_reference(MRef) -> await_up(ServerPid, 5000, MRef); await_up(ServerPid, Timeout) -> @@ -704,7 +714,9 @@ await_up(ServerPid, Timeout) -> demonitor(MRef, [flush]), Res. --spec await_up(pid(), timeout(), reference()) -> {ok, http | http2} | {error, {down, any()} | timeout}. +-spec await_up(pid(), timeout(), reference()) + -> {ok, http | http2 | raw | socks} + | {error, {down, any()} | timeout}. await_up(ServerPid, Timeout, MRef) -> receive {gun_up, ServerPid, Protocol} -> @@ -831,6 +843,7 @@ start_link(Owner, Host, Port, Opts) -> init({Owner, Host, Port, Opts}) -> Retry = maps:get(retry, Opts, 5), OriginTransport = maps:get(transport, Opts, default_transport(Port)), + %% @todo The OriginScheme is not http when we connect to socks/raw. {OriginScheme, Transport} = case OriginTransport of tcp -> {<<"http">>, gun_tcp}; tls -> {<<"https">>, gun_tls} @@ -963,7 +976,7 @@ ensure_alpn_sni(Protocols0, TransOpts0, #state{origin_host=OriginHost}) -> Protocols = [case P of http -> <<"http/1.1">>; http2 -> <<"h2">> - end || P <- Protocols0, is_atom(P)], + end || P <- Protocols0, lists:member(P, [http, http2])], TransOpts = [ {alpn_advertised_protocols, Protocols}, {client_preferred_next_protocols, {client, Protocols, <<"http/1.1">>}} @@ -1022,7 +1035,7 @@ tls_handshake(info, {gun_tls_proxy, Socket, Error = {error, Reason}, {HandshakeE }, EvHandlerState0), commands([Error], State#state{event_handler_state=EvHandlerState}); tls_handshake(Type, Event, State) -> - handle_common_connected(Type, Event, ?FUNCTION_NAME, State). + handle_common_connected_no_input(Type, Event, ?FUNCTION_NAME, State). normal_tls_handshake(Socket, State=#state{event_handler=EvHandler, event_handler_state=EvHandlerState0}, HandshakeEvent0=#{tls_opts := TLSOpts0, timeout := TLSTimeout}, Protocols) -> @@ -1053,6 +1066,9 @@ protocol_negotiated({error, protocol_not_negotiated}, [Protocol]) -> Protocol; protocol_negotiated({error, protocol_not_negotiated}, _) -> http. connected_no_input(Type, Event, State) -> + handle_common_connected_no_input(Type, Event, ?FUNCTION_NAME, State). + +connected_data_only(Type, Event, State) -> handle_common_connected(Type, Event, ?FUNCTION_NAME, State). connected(internal, {connected, Socket, Protocol0}, @@ -1151,8 +1167,22 @@ closing(Type, Event, State) -> %% Common events when we have a connection. %% +%% One function accepts new input, the other doesn't. + +%% @todo Do we want to reject ReplyTo if it's not the process +%% who initiated the connection? For both data and cancel. +handle_common_connected(cast, {data, ReplyTo, StreamRef, IsFin, Data}, _, + State=#state{protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {ProtoState2, EvHandlerState} = Protocol:data(ProtoState, + StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), + {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}}; +handle_common_connected(Type, Event, StateName, StateData) -> + handle_common_connected_no_input(Type, Event, StateName, StateData). + %% Socket events. -handle_common_connected(info, {OK, Socket, Data}, _, State0=#state{socket=Socket, messages={OK, _, _}, +handle_common_connected_no_input(info, {OK, Socket, Data}, _, + State0=#state{socket=Socket, messages={OK, _, _}, protocol=Protocol, protocol_state=ProtoState, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> {Commands, EvHandlerState} = Protocol:handle(Data, ProtoState, EvHandler, EvHandlerState0), @@ -1164,27 +1194,22 @@ handle_common_connected(info, {OK, Socket, Data}, _, State0=#state{socket=Socket Res -> Res end; -handle_common_connected(info, {Closed, Socket}, _, State=#state{socket=Socket, messages={_, Closed, _}}) -> +handle_common_connected_no_input(info, {Closed, Socket}, _, + State=#state{socket=Socket, messages={_, Closed, _}}) -> disconnect(State, closed); -handle_common_connected(info, {Error, Socket, Reason}, _, State=#state{socket=Socket, messages={_, _, Error}}) -> +handle_common_connected_no_input(info, {Error, Socket, Reason}, _, + State=#state{socket=Socket, messages={_, _, Error}}) -> disconnect(State, {error, Reason}); %% Timeouts. %% @todo HTTP/2 requires more timeouts than just the keepalive timeout. %% We should have a timeout function in protocols that deal with %% received timeouts. Currently the timeout messages are ignored. -handle_common_connected(info, keepalive, _, State=#state{protocol=Protocol, protocol_state=ProtoState}) -> +handle_common_connected_no_input(info, keepalive, _, + State=#state{protocol=Protocol, protocol_state=ProtoState}) -> ProtoState2 = Protocol:keepalive(ProtoState), {keep_state, keepalive_timeout(State#state{protocol_state=ProtoState2})}; -%% @todo Do we want to reject ReplyTo if it's not the process -%% who initiated the connection? For both data and cancel. -handle_common_connected(cast, {data, ReplyTo, StreamRef, IsFin, Data}, _, - State=#state{protocol=Protocol, protocol_state=ProtoState, - event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> - {ProtoState2, EvHandlerState} = Protocol:data(ProtoState, - StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), - {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}}; -handle_common_connected(cast, {update_flow, ReplyTo, StreamRef, Flow}, _, State0=#state{ - protocol=Protocol, protocol_state=ProtoState}) -> +handle_common_connected_no_input(cast, {update_flow, ReplyTo, StreamRef, Flow}, _, + State0=#state{protocol=Protocol, protocol_state=ProtoState}) -> Commands = Protocol:update_flow(ProtoState, ReplyTo, StreamRef, Flow), case commands(Commands, State0) of {keep_state, State} -> @@ -1192,16 +1217,16 @@ handle_common_connected(cast, {update_flow, ReplyTo, StreamRef, Flow}, _, State0 Res -> Res end; -handle_common_connected(cast, {cancel, ReplyTo, StreamRef}, _, State=#state{ - protocol=Protocol, protocol_state=ProtoState, +handle_common_connected_no_input(cast, {cancel, ReplyTo, StreamRef}, _, + State=#state{protocol=Protocol, protocol_state=ProtoState, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> {ProtoState2, EvHandlerState} = Protocol:cancel(ProtoState, StreamRef, ReplyTo, EvHandler, EvHandlerState0), {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}}; -handle_common_connected({call, From}, {stream_info, StreamRef}, _, +handle_common_connected_no_input({call, From}, {stream_info, StreamRef}, _, #state{protocol=Protocol, protocol_state=ProtoState}) -> {keep_state_and_data, {reply, From, Protocol:stream_info(ProtoState, StreamRef)}}; -handle_common_connected(Type, Event, StateName, State) -> +handle_common_connected_no_input(Type, Event, StateName, State) -> handle_common(Type, Event, StateName, State). %% Common events. @@ -1379,8 +1404,9 @@ disconnect_flush(State=#state{socket=Socket, messages={OK, Closed, Error}}) -> protocol_handler(http) -> gun_http; protocol_handler(http2) -> gun_http2; -protocol_handler(ws) -> gun_ws; -protocol_handler(socks) -> gun_socks. +protocol_handler(raw) -> gun_raw; +protocol_handler(socks) -> gun_socks; +protocol_handler(ws) -> gun_ws. active(State=#state{active=false}) -> State; diff --git a/src/gun_http.erl b/src/gun_http.erl index 59f4fe7..87b50c8 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -265,15 +265,26 @@ handle_head(Data, State=#http_state{version=ClientVersion, opts=Opts, {Version, Status, _, Rest} = cow_http:parse_status_line(Data), {Headers, Rest2} = cow_http:parse_headers(Rest), case {Status, StreamRef} of - {101, {websocket, RealStreamRef, WsKey, WsExtensions, WsOpts}} -> + {101, _} -> EvHandlerState = EvHandler:response_inform(#{ - stream_ref => RealStreamRef, + stream_ref => stream_ref(StreamRef), reply_to => ReplyTo, status => 101, headers => Headers }, EvHandlerState0), - {ws_handshake(Rest2, State, RealStreamRef, Headers, WsKey, WsExtensions, WsOpts), - EvHandlerState}; + %% @todo We might want to switch to the HTTP/2 protocol or to the TLS transport as well. + case StreamRef of + {websocket, RealStreamRef, WsKey, WsExtensions, WsOpts} -> + {ws_handshake(Rest2, State, RealStreamRef, Headers, WsKey, WsExtensions, WsOpts), + EvHandlerState}; + %% Any other 101 response results in us switching to the raw protocol. + %% @todo We should check that we asked for an upgrade before accepting it. + _ -> + {_, Upgrade0} = lists:keyfind(<<"upgrade">>, 1, Headers), + Upgrade = cow_http_hd:parse_upgrade(Upgrade0), + ReplyTo ! {gun_upgrade, self(), StreamRef, Upgrade, Headers}, + {{switch_protocol, raw}, EvHandlerState0} + end; %% @todo If the stream is cancelled we probably shouldn't finish the CONNECT setup. {_, {connect, RealStreamRef, Destination}} when Status >= 200, Status < 300 -> case IsAlive of diff --git a/src/gun_raw.erl b/src/gun_raw.erl new file mode 100644 index 0000000..da71cd6 --- /dev/null +++ b/src/gun_raw.erl @@ -0,0 +1,61 @@ +%% 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_raw). + +-export([check_options/1]). +-export([name/0]). +-export([opts_name/0]). +-export([has_keepalive/0]). +-export([init/4]). +-export([handle/4]). +-export([closing/4]). +-export([close/4]). +-export([data/7]). +%% @todo down + +-record(raw_state, { + owner :: pid(), + socket :: inet:socket() | ssl:sslsocket(), + transport :: module() +}). + +%% @todo Reject ALL options. +check_options(_) -> + ok. + +name() -> raw. +opts_name() -> raw_opts. +has_keepalive() -> false. + +init(Owner, Socket, Transport, _Opts) -> + {connected_data_only, #raw_state{owner=Owner, socket=Socket, transport=Transport}}. + +handle(Data, State=#raw_state{owner=Owner}, _, EvHandlerState) -> + %% When we take over the entire connection there is no stream reference. + Owner ! {gun_data, self(), undefined, nofin, Data}, + {{state, State}, EvHandlerState}. + +%% We can always close immediately. +closing(_, _, _, EvHandlerState) -> + {close, EvHandlerState}. + +close(_, _, _, EvHandlerState) -> + EvHandlerState. + +%% @todo Initiate closing on IsFin=fin. +data(State=#raw_state{socket=Socket, transport=Transport}, undefined, + _ReplyTo, _IsFin, Data, _EvHandler, EvHandlerState) -> + Transport:send(Socket, Data), + {State, EvHandlerState}. diff --git a/src/gun_socks.erl b/src/gun_socks.erl index 928af56..487e7c4 100644 --- a/src/gun_socks.erl +++ b/src/gun_socks.erl @@ -134,6 +134,7 @@ handle(<<5, 0, 0, Rest0/bits>>, #socks_state{opts=Opts, version=5, status=connec end, %% @todo Maybe an event indicating success. #{host := NewHost, port := NewPort} = Opts, + %% @todo The origin scheme is wrong when the next protocol is not HTTP. case Opts of #{transport := tls} -> HandshakeEvent = #{ @@ -141,7 +142,7 @@ handle(<<5, 0, 0, Rest0/bits>>, #socks_state{opts=Opts, version=5, status=connec timeout => maps:get(tls_handshake_timeout, Opts, infinity) }, [{origin, <<"https">>, NewHost, NewPort, socks5}, - {tls_handshake, HandshakeEvent, maps:get(protocols, Opts, [http])}]; + {tls_handshake, HandshakeEvent, maps:get(protocols, Opts, [http2, http])}]; _ -> [Protocol] = maps:get(protocols, Opts, [http]), [{origin, <<"http">>, NewHost, NewPort, socks5}, diff --git a/test/raw_SUITE.erl b/test/raw_SUITE.erl new file mode 100644 index 0000000..6a843ea --- /dev/null +++ b/test/raw_SUITE.erl @@ -0,0 +1,214 @@ +%% 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), + 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). + +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). + +%% 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. |