aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ebin/gun.app2
-rw-r--r--src/gun.erl84
-rw-r--r--src/gun_http.erl19
-rw-r--r--src/gun_raw.erl61
-rw-r--r--src/gun_socks.erl3
-rw-r--r--test/raw_SUITE.erl214
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.