aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-10-19 18:01:40 +0200
committerLoïc Hoguin <[email protected]>2020-10-19 18:01:40 +0200
commit3047f0a5ef1872a1d8533c90bccb434d575d98f0 (patch)
treef8d592acfc2eef6e7b86abeee7c675db882f8e99
parent91c1820b9ad8812b2a8c9960da0a460b0522b6e0 (diff)
downloadgun-3047f0a5ef1872a1d8533c90bccb434d575d98f0.tar.gz
gun-3047f0a5ef1872a1d8533c90bccb434d575d98f0.tar.bz2
gun-3047f0a5ef1872a1d8533c90bccb434d575d98f0.zip
Fix cookies for tunnels
There are still small issues left to fix. In particular the set_cookie command should be replaced with doing the same in the protocol itself so that the scheme is correct. So CookieStore must be propagated to all callbacks.
-rw-r--r--src/gun.erl79
-rw-r--r--src/gun_cookies.erl26
-rw-r--r--src/gun_http.erl70
-rw-r--r--src/gun_http2.erl87
-rw-r--r--src/gun_tunnel.erl110
-rw-r--r--test/rfc6265bis_SUITE.erl33
6 files changed, 244 insertions, 161 deletions
diff --git a/src/gun.erl b/src/gun.erl
index 2af6b94..73779c2 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -297,9 +297,9 @@
messages :: {atom(), atom(), atom()},
protocol :: module(),
protocol_state :: any(),
+ cookie_store :: undefined | {module(), any()},
event_handler :: module(),
- event_handler_state :: any(),
- cookie_store :: undefined | {module(), any()}
+ event_handler_state :: any()
}).
%% Connection.
@@ -1251,26 +1251,26 @@ connected(internal, {connected, Socket, NewProtocol},
%%
%% @todo It might be better, internally, to pass around a URIMap
%% containing the target URI, instead of separate Host/Port/PathWithQs.
-connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers0, InitialFlow},
- State0=#state{origin_host=Host, origin_port=Port,
- protocol=Protocol, protocol_state=ProtoState,
+connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers, InitialFlow},
+ State=#state{origin_host=Host, origin_port=Port,
+ protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
- {Headers, State} = add_cookie_header(Path, Headers0, State0),
- {ProtoState2, EvHandlerState} = Protocol:headers(ProtoState,
+ {ProtoState2, CookieStore, EvHandlerState} = Protocol:headers(ProtoState,
dereference_stream_ref(StreamRef, State), ReplyTo,
Method, Host, Port, Path, Headers,
- InitialFlow, EvHandler, EvHandlerState0),
- {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
-connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers0, Body, InitialFlow},
- State0=#state{origin_host=Host, origin_port=Port,
- protocol=Protocol, protocol_state=ProtoState,
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
+ {keep_state, State#state{protocol_state=ProtoState2, cookie_store=CookieStore,
+ event_handler_state=EvHandlerState}};
+connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers, Body, InitialFlow},
+ State=#state{origin_host=Host, origin_port=Port,
+ protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
- {Headers, State} = add_cookie_header(Path, Headers0, State0),
- {ProtoState2, EvHandlerState} = Protocol:request(ProtoState,
+ {ProtoState2, CookieStore, EvHandlerState} = Protocol:request(ProtoState,
dereference_stream_ref(StreamRef, State), ReplyTo,
Method, Host, Port, Path, Headers, Body,
- InitialFlow, EvHandler, EvHandlerState0),
- {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
+ {keep_state, State#state{protocol_state=ProtoState2, cookie_store=CookieStore,
+ event_handler_state=EvHandlerState}};
connected(cast, {connect, ReplyTo, StreamRef, Destination, Headers, InitialFlow},
State=#state{origin_host=Host, origin_port=Port,
protocol=Protocol, protocol_state=ProtoState,
@@ -1279,16 +1279,17 @@ connected(cast, {connect, ReplyTo, StreamRef, Destination, Headers, InitialFlow}
dereference_stream_ref(StreamRef, State), ReplyTo,
Destination, #{host => Host, port => Port},
Headers, InitialFlow, EvHandler, EvHandlerState0),
- {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
+ {keep_state, State#state{protocol_state=ProtoState2,
+ event_handler_state=EvHandlerState}};
%% Public Websocket interface.
%% @todo Maybe make an interface in the protocol module instead of checking on protocol name.
%% An interface would also make sure that HTTP/1.0 can't upgrade.
connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers}, State=#state{opts=Opts}) ->
WsOpts = maps:get(ws_opts, Opts, #{}),
connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers, WsOpts}, State);
-connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers0, WsOpts},
- State0=#state{origin_host=Host, origin_port=Port,
- protocol=Protocol, protocol_state=ProtoState,
+connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers, WsOpts},
+ State=#state{origin_host=Host, origin_port=Port,
+ protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
EvHandlerState1 = EvHandler:ws_upgrade(#{
stream_ref => StreamRef,
@@ -1296,11 +1297,10 @@ connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers0, WsOpts},
opts => WsOpts
}, EvHandlerState0),
%% @todo Can fail if HTTP/1.0.
- {Headers, State} = add_cookie_header(Path, Headers0, State0),
- {ProtoState2, EvHandlerState} = Protocol:ws_upgrade(ProtoState,
+ {ProtoState2, CookieStore, EvHandlerState} = Protocol:ws_upgrade(ProtoState,
dereference_stream_ref(StreamRef, State), ReplyTo,
- Host, Port, Path, Headers, WsOpts, EvHandler, EvHandlerState1),
- {keep_state, State#state{protocol_state=ProtoState2,
+ Host, Port, Path, Headers, WsOpts, CookieStore0, EvHandler, EvHandlerState1),
+ {keep_state, State#state{protocol_state=ProtoState2, cookie_store=CookieStore,
event_handler_state=EvHandlerState}};
%% @todo Maybe better standardize the protocol callbacks argument orders.
connected(cast, {ws_send, ReplyTo, StreamRef, Frames}, State=#state{
@@ -1319,35 +1319,6 @@ connected(cast, {ws_send, ReplyTo, _}, _) ->
connected(Type, Event, State) ->
handle_common_connected(Type, Event, ?FUNCTION_NAME, State).
-add_cookie_header(_, Headers, State=#state{cookie_store=undefined}) ->
- {Headers, State};
-add_cookie_header(PathWithQs, Headers0, State=#state{
- origin_host=OriginHost, transport=Transport, cookie_store=Store0}) ->
- Scheme = case Transport of
- gun_tls -> <<"https">>;
- gun_tls_proxy -> <<"https">>;
- gun_tcp -> <<"http">>
- end,
- #{path := Path} = uri_string:parse(PathWithQs),
- URIMap = uri_string:normalize(#{
- scheme => Scheme,
- host => case lists:keyfind(<<"host">>, 1, Headers0) of
- false -> iolist_to_binary(OriginHost); %% @todo Probably not enough for atoms and such.
- {_, HeaderHost} -> iolist_to_binary(HeaderHost)
- end,
- path => iolist_to_binary(Path)
- }, [return_map]),
- {ok, Cookies0, Store} = gun_cookies:query(Store0, URIMap),
- Headers = case Cookies0 of
- [] ->
- Headers0;
- _ ->
- Cookies = [{Name, Value} || #{name := Name, value := Value} <- Cookies0],
- %% We put cookies at the end of the headers list as it's the least important header.
- Headers0 ++ [{<<"cookie">>, cow_cookie:cookie(Cookies)}]
- end,
- {Headers, State#state{cookie_store=Store}}.
-
%% When the origin is using raw we do not dereference the stream_ref
%% because it expects the full stream_ref to function (there's no
%% other stream involved for this connection).
@@ -1655,6 +1626,8 @@ commands([{set_cookie, _, _, Status, _}|Tail], State=#state{opts=#{cookie_ignore
%% @todo Make sure this works for proxied requests too.
commands([{set_cookie, Authority, PathWithQs, _, Headers}|Tail], State=#state{
transport=Transport, cookie_store=Store0}) ->
+ %% @todo This is wrong. Also we should probably not do a command for this.
+ %% We should instead give the CookieStore to all callbacks.
Scheme = case Transport of
gun_tls -> <<"https">>;
gun_tls_proxy -> <<"https">>;
diff --git a/src/gun_cookies.erl b/src/gun_cookies.erl
index ce33b49..965c3a5 100644
--- a/src/gun_cookies.erl
+++ b/src/gun_cookies.erl
@@ -14,6 +14,7 @@
-module(gun_cookies).
+-export([add_cookie_header/5]).
-export([domain_match/2]).
-export([gc/1]).
-export([path_match/2]).
@@ -79,6 +80,31 @@
-> {ok, State}
when State::store_state().
+-spec add_cookie_header(binary(), iodata(), iodata(), Headers, Store)
+ -> {Headers, Store} when Headers :: [{binary(), iodata()}], Store :: undefined | store().
+add_cookie_header(_, _, _, Headers, Store=undefined) ->
+ {Headers, Store};
+add_cookie_header(Scheme, Authority, PathWithQs, Headers0, Store0) ->
+ #{
+ host := Host,
+ path := Path
+ } = uri_string:parse([Scheme, <<"://">>, Authority, PathWithQs]),
+ URIMap = uri_string:normalize(#{
+ scheme => Scheme,
+ host => iolist_to_binary(Host),
+ path => iolist_to_binary(Path)
+ }, [return_map]),
+ {ok, Cookies0, Store} = query(Store0, URIMap),
+ Headers = case Cookies0 of
+ [] ->
+ Headers0;
+ _ ->
+ Cookies = [{Name, Value} || #{name := Name, value := Value} <- Cookies0],
+ %% We put cookies at the end of the headers list as it's the least important header.
+ Headers0 ++ [{<<"cookie">>, cow_cookie:cookie(Cookies)}]
+ end,
+ {Headers, Store}.
+
-spec domain_match(binary(), binary()) -> boolean().
domain_match(String, String) ->
true;
diff --git a/src/gun_http.erl b/src/gun_http.erl
index d877068..62d9490 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -26,14 +26,14 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([headers/11]).
--export([request/12]).
+-export([headers/12]).
+-export([request/13]).
-export([data/7]).
-export([connect/9]).
-export([cancel/5]).
-export([stream_info/2]).
-export([down/1]).
--export([ws_upgrade/10]).
+-export([ws_upgrade/11]).
%% Functions shared with gun_http2.
-export([host_header/3]).
@@ -541,42 +541,44 @@ keepalive(State=#http_state{socket=Socket, transport=Transport, out=head}, _, Ev
keepalive(State, _, EvHandlerState) ->
{State, EvHandlerState}.
-headers(State, StreamRef, ReplyTo, _, _, _, _, _, _, _, EvHandlerState)
+headers(State, StreamRef, ReplyTo, _, _, _, _, _, _, CookieStore, _, EvHandlerState)
when is_list(StreamRef) ->
ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef),
{badstate, "The stream is not a tunnel."}},
- {State, EvHandlerState};
+ {State, CookieStore, EvHandlerState};
headers(State=#http_state{opts=Opts, out=head},
StreamRef, ReplyTo, Method, Host, Port, Path, Headers,
- InitialFlow0, EvHandler, EvHandlerState0) ->
- {Authority, Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
- Method, Host, Port, Path, Headers, undefined,
- EvHandler, EvHandlerState0, ?FUNCTION_NAME),
+ InitialFlow0, CookieStore0, EvHandler, EvHandlerState0) ->
+ {Authority, Conn, Out, CookieStore, EvHandlerState} = send_request(State,
+ StreamRef, ReplyTo, Method, Host, Port, Path, Headers, undefined,
+ CookieStore0, EvHandler, EvHandlerState0, ?FUNCTION_NAME),
InitialFlow = initial_flow(InitialFlow0, Opts),
{new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo,
- Method, Authority, Path, InitialFlow), EvHandlerState}.
+ Method, Authority, Path, InitialFlow),
+ CookieStore, EvHandlerState}.
-request(State, StreamRef, ReplyTo, _, _, _, _, _, _, _, _, EvHandlerState)
+request(State, StreamRef, ReplyTo, _, _, _, _, _, _, _, CookieStore, _, EvHandlerState)
when is_list(StreamRef) ->
ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef),
{badstate, "The stream is not a tunnel."}},
- {State, EvHandlerState};
+ {State, CookieStore, EvHandlerState};
request(State=#http_state{opts=Opts, out=head}, StreamRef, ReplyTo,
Method, Host, Port, Path, Headers, Body,
- InitialFlow0, EvHandler, EvHandlerState0) ->
- {Authority, Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
- Method, Host, Port, Path, Headers, Body,
- EvHandler, EvHandlerState0, ?FUNCTION_NAME),
+ InitialFlow0, CookieStore0, EvHandler, EvHandlerState0) ->
+ {Authority, Conn, Out, CookieStore, EvHandlerState} = send_request(State,
+ StreamRef, ReplyTo, Method, Host, Port, Path, Headers, Body,
+ CookieStore0, EvHandler, EvHandlerState0, ?FUNCTION_NAME),
InitialFlow = initial_flow(InitialFlow0, Opts),
{new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo,
- Method, Authority, Path, InitialFlow), EvHandlerState}.
+ Method, Authority, Path, InitialFlow),
+ CookieStore, EvHandlerState}.
initial_flow(infinity, #{flow := InitialFlow}) -> InitialFlow;
initial_flow(InitialFlow, _) -> InitialFlow.
send_request(State=#http_state{socket=Socket, transport=Transport, version=Version},
StreamRef, ReplyTo, Method, Host, Port, Path, Headers0, Body,
- EvHandler, EvHandlerState0, Function) ->
+ CookieStore0, EvHandler, EvHandlerState0, Function) ->
Headers1 = lists:keydelete(<<"transfer-encoding">>, 1, Headers0),
Headers2 = case Body of
undefined -> Headers1;
@@ -596,12 +598,14 @@ send_request(State=#http_state{socket=Socket, transport=Transport, version=Versi
{_, Authority1} -> {Authority1, Headers2}
end,
Headers4 = transform_header_names(State, Headers3),
- Headers = case {Body, Out} of
+ Headers5 = case {Body, Out} of
{undefined, body_chunked} when Version =:= 'HTTP/1.0' -> Headers4;
{undefined, body_chunked} -> [{<<"transfer-encoding">>, <<"chunked">>}|Headers4];
{undefined, _} -> Headers4;
_ -> [{<<"content-length">>, integer_to_binary(iolist_size(Body))}|Headers4]
end,
+ {Headers, CookieStore} = gun_cookies:add_cookie_header(
+ scheme(State), Authority, Path, Headers5, CookieStore0),
RealStreamRef = stream_ref(State, StreamRef),
RequestEvent = #{
stream_ref => RealStreamRef,
@@ -627,7 +631,7 @@ send_request(State=#http_state{socket=Socket, transport=Transport, version=Versi
_ ->
EvHandlerState2
end,
- {Authority, Conn, Out, EvHandlerState}.
+ {Authority, Conn, Out, CookieStore, EvHandlerState}.
host_header(Transport, Host0, Port) ->
Host = case Host0 of
@@ -649,6 +653,13 @@ transform_header_names(#http_state{opts=Opts}, Headers) ->
Fun -> lists:keymap(Fun, 1, Headers)
end.
+scheme(#http_state{transport=Transport}) ->
+ case Transport of
+ gun_tls -> <<"https">>;
+ gun_tls_proxy -> <<"https">>;
+ _ -> <<"http">>
+ end.
+
%% We are expecting a new stream.
data(State=#http_state{out=head}, StreamRef, ReplyTo, _, _, _, EvHandlerState) ->
{error_stream_closed(State, StreamRef, ReplyTo), EvHandlerState};
@@ -897,18 +908,18 @@ end_stream(State=#http_state{streams=[_|Tail]}) ->
%% Websocket upgrade.
-ws_upgrade(State, StreamRef, ReplyTo, _, _, _, _, _, _, EvHandlerState)
+ws_upgrade(State, StreamRef, ReplyTo, _, _, _, _, _, CookieStore, _, EvHandlerState)
when is_list(StreamRef) ->
ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef),
{badstate, "The stream is not a tunnel."}},
- {State, EvHandlerState};
+ {State, CookieStore, EvHandlerState};
ws_upgrade(State=#http_state{version='HTTP/1.0'},
- StreamRef, ReplyTo, _, _, _, _, _, _, EvHandlerState) ->
+ StreamRef, ReplyTo, _, _, _, _, _, CookieStore, _, EvHandlerState) ->
ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate,
"Websocket cannot be used over an HTTP/1.0 connection."}},
- {[], EvHandlerState};
+ {State, CookieStore, EvHandlerState};
ws_upgrade(State=#http_state{out=head}, StreamRef, ReplyTo,
- Host, Port, Path, Headers0, WsOpts, EvHandler, EvHandlerState0) ->
+ Host, Port, Path, Headers0, WsOpts, CookieStore0, EvHandler, EvHandlerState0) ->
{Headers1, GunExtensions} = case maps:get(compress, WsOpts, false) of
true -> {[{<<"sec-websocket-extensions">>,
<<"permessage-deflate; client_max_window_bits; server_max_window_bits=15">>}
@@ -930,13 +941,14 @@ ws_upgrade(State=#http_state{out=head}, StreamRef, ReplyTo,
{<<"sec-websocket-key">>, Key}
|Headers2
],
- {Authority, Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
- <<"GET">>, Host, Port, Path, Headers, undefined,
- EvHandler, EvHandlerState0, ?FUNCTION_NAME),
+ {Authority, Conn, Out, CookieStore, EvHandlerState} = send_request(State,
+ StreamRef, ReplyTo, <<"GET">>, Host, Port, Path, Headers, undefined,
+ CookieStore0, EvHandler, EvHandlerState0, ?FUNCTION_NAME),
InitialFlow = maps:get(flow, WsOpts, infinity),
{new_stream(State#http_state{connection=Conn, out=Out},
#websocket{ref=StreamRef, reply_to=ReplyTo, key=Key, extensions=GunExtensions, opts=WsOpts},
- ReplyTo, <<"GET">>, Authority, Path, InitialFlow), EvHandlerState}.
+ ReplyTo, <<"GET">>, Authority, Path, InitialFlow),
+ CookieStore, EvHandlerState}.
ws_handshake(Buffer, State, Ws=#websocket{key=Key}, Headers) ->
%% @todo check upgrade, connection
diff --git a/src/gun_http2.erl b/src/gun_http2.erl
index c5521ce..e8eb3aa 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -27,15 +27,15 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([headers/11]).
--export([request/12]).
+-export([headers/12]).
+-export([request/13]).
-export([data/7]).
-export([connect/9]).
-export([cancel/5]).
-export([timeout/3]).
-export([stream_info/2]).
-export([down/1]).
--export([ws_upgrade/10]).
+-export([ws_upgrade/11]).
-export([ws_send/6]).
-record(tunnel, {
@@ -622,7 +622,7 @@ handle_continue(ContinueStreamRef, Msg, State0, EvHandler, EvHandlerState0) ->
{Commands, EvHandlerState1} = Proto:handle_continue(ContinueStreamRef,
Msg, ProtoState0, EvHandler, EvHandlerState0),
{State, EvHandlerState} = tunnel_commands(Commands, Stream, State0, EvHandler, EvHandlerState1),
- {{state, State}, EvHandlerState}
+ {handle_ret({state, State}, State), EvHandlerState}
%% The stream may have ended while TLS was being decoded. @todo What should we do?
% error ->
% {error_stream_not_found(State, StreamRef, ReplyTo), EvHandlerState0}
@@ -750,11 +750,12 @@ keepalive(State=#http2_state{socket=Socket, transport=Transport}, _, EvHandlerSt
headers(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, Method, Host, Port,
- Path, Headers0, InitialFlow0, EvHandler, EvHandlerState0)
+ Path, Headers0, InitialFlow0, CookieStore0, EvHandler, EvHandlerState0)
when is_reference(StreamRef) ->
{ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream(
iolist_to_binary(Method), HTTP2Machine0),
- {ok, PseudoHeaders, Headers} = prepare_headers(State, Method, Host, Port, Path, Headers0),
+ {ok, PseudoHeaders, Headers, CookieStore} = prepare_headers(
+ State, Method, Host, Port, Path, Headers0, CookieStore0),
Authority = maps:get(authority, PseudoHeaders),
RequestEvent = #{
stream_ref => stream_ref(State, StreamRef),
@@ -773,36 +774,39 @@ headers(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
InitialFlow = initial_flow(InitialFlow0, Opts),
Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow,
authority=Authority, path=Path},
- {create_stream(State#http2_state{http2_machine=HTTP2Machine}, Stream), EvHandlerState};
+ {create_stream(State#http2_state{http2_machine=HTTP2Machine}, Stream),
+ CookieStore, EvHandlerState};
%% Tunneled request.
headers(State, RealStreamRef=[StreamRef|_], ReplyTo, Method, _Host, _Port,
- Path, Headers, InitialFlow, EvHandler, EvHandlerState0) ->
+ Path, Headers, InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
case get_stream_by_ref(State, StreamRef) of
%% @todo We should send an error to the user if the stream isn't ready.
Stream=#stream{tunnel=Tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0, info=#{
origin_host := OriginHost, origin_port := OriginPort}}} ->
- {ProtoState, EvHandlerState} = Proto:headers(ProtoState0, RealStreamRef,
+ {ProtoState, CookieStore, EvHandlerState} = Proto:headers(ProtoState0, RealStreamRef,
ReplyTo, Method, OriginHost, OriginPort, Path, Headers,
- InitialFlow, EvHandler, EvHandlerState0),
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
{store_stream(State, Stream#stream{tunnel=Tunnel#tunnel{protocol_state=ProtoState}}),
- EvHandlerState};
+ CookieStore, EvHandlerState};
#stream{tunnel=undefined} ->
ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate,
"The stream is not a tunnel."}},
- {State, EvHandlerState0};
+ {State, CookieStore0, EvHandlerState0};
error ->
- {error_stream_not_found(State, StreamRef, ReplyTo), EvHandlerState0}
+ {error_stream_not_found(State, StreamRef, ReplyTo),
+ CookieStore0, EvHandlerState0}
end.
request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts,
http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, Method, Host, Port,
- Path, Headers0, Body, InitialFlow0, EvHandler, EvHandlerState0)
+ Path, Headers0, Body, InitialFlow0, CookieStore0, EvHandler, EvHandlerState0)
when is_reference(StreamRef) ->
Headers1 = lists:keystore(<<"content-length">>, 1, Headers0,
{<<"content-length">>, integer_to_binary(iolist_size(Body))}),
{ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream(
iolist_to_binary(Method), HTTP2Machine0),
- {ok, PseudoHeaders, Headers} = prepare_headers(State0, Method, Host, Port, Path, Headers1),
+ {ok, PseudoHeaders, Headers, CookieStore} = prepare_headers(
+ State0, Method, Host, Port, Path, Headers1, CookieStore0),
Authority = maps:get(authority, PseudoHeaders),
RealStreamRef = stream_ref(State0, StreamRef),
RequestEvent = #{
@@ -833,60 +837,66 @@ request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts,
stream_ref => RealStreamRef,
reply_to => ReplyTo
},
- {State, EvHandler:request_end(RequestEndEvent, EvHandlerState)};
+ {State, CookieStore, EvHandler:request_end(RequestEndEvent, EvHandlerState)};
nofin ->
- maybe_send_data(State, StreamID, fin, Body, EvHandler, EvHandlerState)
+ {StateRet, EvHandlerStateRet} = maybe_send_data(
+ State, StreamID, fin, Body, EvHandler, EvHandlerState),
+ {StateRet, CookieStore, EvHandlerStateRet}
end;
%% Tunneled request.
request(State, RealStreamRef=[StreamRef|_], ReplyTo, Method, _Host, _Port,
- Path, Headers, Body, InitialFlow, EvHandler, EvHandlerState0) ->
+ Path, Headers, Body, InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
case get_stream_by_ref(State, StreamRef) of
%% @todo We should send an error to the user if the stream isn't ready.
Stream=#stream{tunnel=Tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0, info=#{
origin_host := OriginHost, origin_port := OriginPort}}} ->
- {ProtoState, EvHandlerState} = Proto:request(ProtoState0, RealStreamRef,
+ {ProtoState, CookieStore, EvHandlerState} = Proto:request(ProtoState0, RealStreamRef,
ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body,
- InitialFlow, EvHandler, EvHandlerState0),
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
{store_stream(State, Stream#stream{tunnel=Tunnel#tunnel{protocol_state=ProtoState}}),
- EvHandlerState};
+ CookieStore, EvHandlerState};
#stream{tunnel=undefined} ->
ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate,
"The stream is not a tunnel."}},
- {State, EvHandlerState0};
+ {State, CookieStore0, EvHandlerState0};
error ->
- {error_stream_not_found(State, StreamRef, ReplyTo), EvHandlerState0}
+ {error_stream_not_found(State, StreamRef, ReplyTo),
+ CookieStore0, EvHandlerState0}
end.
initial_flow(infinity, #{flow := InitialFlow}) -> InitialFlow;
initial_flow(InitialFlow, _) -> InitialFlow.
-prepare_headers(#http2_state{transport=Transport}, Method, Host0, Port, Path, Headers0) ->
+prepare_headers(#http2_state{transport=Transport}, Method, Host0, Port, Path, Headers0, CookieStore0) ->
+ Scheme = case Transport of
+ gun_tls -> <<"https">>;
+ gun_tls_proxy -> <<"https">>;
+ gun_tcp -> <<"http">>;
+ gun_tcp_proxy -> <<"http">>;
+ gun_tls_proxy_http2_connect -> <<"http">>
+ end,
Authority = case lists:keyfind(<<"host">>, 1, Headers0) of
{_, Host} -> Host;
_ -> gun_http:host_header(Transport, Host0, Port)
end,
%% @todo We also must remove any header found in the connection header.
%% @todo Much of this is duplicated in cow_http2_machine; sort things out.
- Headers =
+ Headers1 =
lists:keydelete(<<"host">>, 1,
lists:keydelete(<<"connection">>, 1,
lists:keydelete(<<"keep-alive">>, 1,
lists:keydelete(<<"proxy-connection">>, 1,
lists:keydelete(<<"transfer-encoding">>, 1,
lists:keydelete(<<"upgrade">>, 1, Headers0)))))),
+ {Headers, CookieStore} = gun_cookies:add_cookie_header(
+ Scheme, Authority, Path, Headers1, CookieStore0),
PseudoHeaders = #{
method => Method,
- scheme => case Transport of
- gun_tls -> <<"https">>;
- gun_tls_proxy -> <<"https">>;
- gun_tcp -> <<"http">>;
- gun_tcp_proxy -> <<"http">>;
- gun_tls_proxy_http2_connect -> <<"http">>
- end,
+ scheme => Scheme,
authority => Authority,
path => Path
},
- {ok, PseudoHeaders, Headers}.
+ {ok, PseudoHeaders, Headers, CookieStore}.
%% @todo Make all calls go through this clause.
data(State=#http2_state{http2_machine=HTTP2Machine}, StreamRef, ReplyTo, IsFin, Data,
@@ -1181,13 +1191,16 @@ down(#http2_state{stream_refs=Refs}) ->
%% Websocket upgrades are currently only accepted when tunneled.
ws_upgrade(State, RealStreamRef=[StreamRef|_], ReplyTo,
- Host, Port, Path, Headers, WsOpts, EvHandler, EvHandlerState0) ->
+ Host, Port, Path, Headers, WsOpts, CookieStore0, EvHandler, EvHandlerState0) ->
case get_stream_by_ref(State, StreamRef) of
Stream=#stream{tunnel=Tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} ->
- {ProtoState, EvHandlerState} = Proto:ws_upgrade(ProtoState0, RealStreamRef, ReplyTo,
- Host, Port, Path, Headers, WsOpts, EvHandler, EvHandlerState0),
+ {ProtoState, CookieStore, EvHandlerState} = Proto:ws_upgrade(
+ ProtoState0, RealStreamRef, ReplyTo,
+ Host, Port, Path, Headers, WsOpts,
+ CookieStore0, EvHandler, EvHandlerState0),
{store_stream(State, Stream#stream{
- tunnel=Tunnel#tunnel{protocol_state=ProtoState}}), EvHandlerState}
+ tunnel=Tunnel#tunnel{protocol_state=ProtoState}}),
+ CookieStore, EvHandlerState}
%% @todo Error conditions?
end.
diff --git a/src/gun_tunnel.erl b/src/gun_tunnel.erl
index cc58351..a1435f3 100644
--- a/src/gun_tunnel.erl
+++ b/src/gun_tunnel.erl
@@ -24,8 +24,8 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([headers/11]).
--export([request/12]).
+-export([headers/12]).
+-export([request/13]).
-export([data/7]).
-export([connect/9]).
-export([cancel/5]).
@@ -33,7 +33,7 @@
-export([stream_info/2]).
-export([tunneled_name/2]).
-export([down/1]).
--export([ws_upgrade/10]).
+-export([ws_upgrade/11]).
-export([ws_send/6]).
-record(tunnel_state, {
@@ -87,7 +87,10 @@
%% We keep the new information to provide it in TunnelInfo of
%% the new protocol when the switch occurs.
protocol_origin = undefined :: undefined
- | {origin, binary(), inet:hostname() | inet:ip_address(), inet:port_number(), connect | socks5}
+ | {origin, binary(), inet:hostname() | inet:ip_address(), inet:port_number(), connect | socks5},
+
+ %% We must queue some commands given by the sub-protocol.
+ commands_queue = [] :: [{set_cookie, iodata(), iodata(), cow_http:status(), cow_http:headers()}]
}).
%% Socket is the "origin socket" and Transport the "origin transport".
@@ -158,7 +161,7 @@ handle(Data, State0=#tunnel_state{transport=gun_tcp_proxy,
EvHandler, EvHandlerState0) ->
{Commands, EvHandlerState1} = Proto:handle(Data, ProtoState0, EvHandler, EvHandlerState0),
{State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1),
- {{state, State}, EvHandlerState};
+ {ret({state, State}, State), EvHandlerState};
handle(Data, State=#tunnel_state{transport=gun_tls_proxy,
socket=ProxyPid, tls_origin_socket=OriginSocket},
_EvHandler, EvHandlerState) ->
@@ -168,7 +171,7 @@ handle(Data, State=#tunnel_state{transport=gun_tls_proxy,
%% message and forward it to the right stream via the handle_continue
%% callback.
ProxyPid ! {tls_proxy_http2_connect, OriginSocket, Data},
- {{state, State}, EvHandlerState}.
+ {ret({state, State}, State), EvHandlerState}.
%% This callback will only be called for TLS.
%%
@@ -199,10 +202,11 @@ handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {ok, Negotiated},
{_, ProtoState} = Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy,
ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tls}),
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
- {{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, EvHandlerState};
+ {ret({state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, State),
+ EvHandlerState};
handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, Reason},
{handle_continue, _, HandshakeEvent, _}},
- #tunnel_state{socket=ProxyPid}, EvHandler, EvHandlerState0)
+ State=#tunnel_state{socket=ProxyPid}, EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
error => Reason
@@ -217,29 +221,29 @@ handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, Reason},
%% receives a TCP segment with the FIN bit set sends a DATA frame with
%% the END_STREAM flag set. Note that the final TCP segment or DATA
%% frame could be empty.
- {{error, Reason}, EvHandlerState};
+ {ret({error, Reason}, State), EvHandlerState};
%% Send the data. This causes TLS to encrypt the data and send it to the inner layer.
handle_continue(ContinueStreamRef, {data, _ReplyTo, _StreamRef, IsFin, Data},
- #tunnel_state{}, _EvHandler, EvHandlerState)
+ State=#tunnel_state{}, _EvHandler, EvHandlerState)
when is_reference(ContinueStreamRef) ->
- {{send, IsFin, Data}, EvHandlerState};
+ {ret({send, IsFin, Data}, State), EvHandlerState};
handle_continue(ContinueStreamRef, {tls_proxy, ProxyPid, Data},
State0=#tunnel_state{socket=ProxyPid, protocol=Proto, protocol_state=ProtoState},
EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
{Commands, EvHandlerState1} = Proto:handle(Data, ProtoState, EvHandler, EvHandlerState0),
{State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1),
- {{state, State}, EvHandlerState};
+ {ret({state, State}, State), EvHandlerState};
handle_continue(ContinueStreamRef, {tls_proxy_closed, ProxyPid},
- #tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0)
+ State=#tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
%% @todo All sub-streams must produce a stream_error.
- {{error, closed}, EvHandlerState0};
+ {ret({error, closed}, State), EvHandlerState0};
handle_continue(ContinueStreamRef, {tls_proxy_error, ProxyPid, Reason},
- #tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0)
+ State=#tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
%% @todo All sub-streams must produce a stream_error.
- {{error, Reason}, EvHandlerState0};
+ {ret({error, Reason}, State), EvHandlerState0};
%% We always dereference the ContinueStreamRef because it includes a
%% reference() for Socks layers too.
%%
@@ -254,7 +258,7 @@ handle_continue([_StreamRef|ContinueStreamRef0], Msg,
{Commands, EvHandlerState1} = Proto:handle_continue(ContinueStreamRef,
Msg, ProtoState, EvHandler, EvHandlerState0),
{State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1),
- {{state, State}, EvHandlerState}.
+ {ret({state, State}, State), EvHandlerState}.
%% @todo This function will need EvHandler/EvHandlerState?
update_flow(State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
@@ -262,11 +266,11 @@ update_flow(State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
StreamRef = maybe_dereference(State0, StreamRef0),
Commands = Proto:update_flow(ProtoState, ReplyTo, StreamRef, Inc),
{State, undefined} = commands(Commands, State0, undefined, undefined),
- {state, State}.
+ ret({state, State}, State).
-closing(_Reason, _State, _EvHandler, EvHandlerState) ->
+closing(_Reason, State, _EvHandler, EvHandlerState) ->
%% @todo Graceful shutdown must be propagated to tunnels.
- {[], EvHandlerState}.
+ {ret([], State), EvHandlerState}.
close(_Reason, _State, _EvHandler, EvHandlerState) ->
%% @todo Closing must be propagated to tunnels.
@@ -279,23 +283,25 @@ keepalive(State, _EvHandler, EvHandlerState) ->
%% We pass the headers forward and optionally dereference StreamRef.
headers(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, Method, Host, Port, Path, Headers,
- InitialFlow, EvHandler, EvHandlerState0) ->
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
- {ProtoState, EvHandlerState} = Proto:headers(ProtoState0, StreamRef,
+ {ProtoState, CookieStore, EvHandlerState} = Proto:headers(ProtoState0, StreamRef,
ReplyTo, Method, Host, Port, Path, Headers,
- InitialFlow, EvHandler, EvHandlerState0),
- {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}.
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState},
+ CookieStore, EvHandlerState}.
%% We pass the request forward and optionally dereference StreamRef.
request(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
info=#{origin_host := OriginHost, origin_port := OriginPort}},
StreamRef0, ReplyTo, Method, _Host, _Port, Path, Headers, Body,
- InitialFlow, EvHandler, EvHandlerState0) ->
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
- {ProtoState, EvHandlerState} = Proto:request(ProtoState0, StreamRef,
+ {ProtoState, CookieStore, EvHandlerState} = Proto:request(ProtoState0, StreamRef,
ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body,
- InitialFlow, EvHandler, EvHandlerState0),
- {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}.
+ InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState},
+ CookieStore, EvHandlerState}.
%% When the next tunnel is SOCKS we pass the data forward directly.
%% This is needed because SOCKS has no StreamRef and the data cannot
@@ -340,14 +346,14 @@ cancel(State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
StreamRef = maybe_dereference(State0, StreamRef0),
{Commands, EvHandlerState1} = Proto:cancel(ProtoState, StreamRef, ReplyTo, EvHandler, EvHandlerState0),
{State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1),
- {{state, State}, EvHandlerState}.
+ {ret({state, State}, State), EvHandlerState}.
timeout(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, Msg, TRef) ->
case Proto:timeout(ProtoState0, Msg, TRef) of
{state, ProtoState} ->
- {state, State#tunnel_state{protocol_state=ProtoState}};
+ ret({state, State#tunnel_state{protocol_state=ProtoState}}, State);
Other ->
- Other
+ ret(Other, State)
end.
stream_info(#tunnel_state{transport=Transport0, stream_ref=TunnelStreamRef, reply_to=ReplyTo,
@@ -409,20 +415,23 @@ tunneled_name(#tunnel_state{tunnel_protocol=TunnelProto}, false) ->
tunneled_name(#tunnel_state{protocol=Proto}, _) ->
Proto:name().
-down(_State) ->
+down(State) ->
%% @todo Tunnels must be included in the gun_down message.
- [].
+ ret([], State).
ws_upgrade(State=#tunnel_state{info=TunnelInfo, protocol=Proto, protocol_state=ProtoState0},
- StreamRef0, ReplyTo, _, _, Path, Headers, WsOpts, EvHandler, EvHandlerState0) ->
+ StreamRef0, ReplyTo, _, _, Path, Headers, WsOpts,
+ CookieStore0, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
#{
origin_host := Host,
origin_port := Port
} = TunnelInfo,
- {ProtoState, EvHandlerState} = Proto:ws_upgrade(ProtoState0, StreamRef, ReplyTo,
- Host, Port, Path, Headers, WsOpts, EvHandler, EvHandlerState0),
- {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}.
+ {ProtoState, CookieStore, EvHandlerState} = Proto:ws_upgrade(ProtoState0, StreamRef, ReplyTo,
+ Host, Port, Path, Headers, WsOpts,
+ CookieStore0, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState},
+ CookieStore, EvHandlerState}.
ws_send(Frames, State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
StreamRef0, ReplyTo, EvHandler, EvHandlerState0) ->
@@ -430,7 +439,7 @@ ws_send(Frames, State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
{Commands, EvHandlerState1} = Proto:ws_send(Frames,
ProtoState, StreamRef, ReplyTo, EvHandler, EvHandlerState0),
{State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1),
- {{state, State}, EvHandlerState}.
+ {ret({state, State}, State), EvHandlerState}.
%% Internal.
@@ -440,11 +449,14 @@ commands([], State, _, EvHandlerState) ->
{State, EvHandlerState};
commands([{state, ProtoState}|Tail], State, EvHandler, EvHandlerState) ->
commands(Tail, State#tunnel_state{protocol_state=ProtoState}, EvHandler, EvHandlerState);
-%% @todo We must pass down the set_cookie commands. Have a commands_queue.
-commands([_SetCookie={set_cookie, _, _, _, _}|Tail], State=#tunnel_state{}, EvHandler, EvHandlerState) ->
- commands(Tail, State, EvHandler, EvHandlerState);
+commands([SetCookie={set_cookie, _, _, _, _}|Tail],
+ State=#tunnel_state{commands_queue=Queue},
+ EvHandler, EvHandlerState) ->
+ commands(Tail, State#tunnel_state{commands_queue=[SetCookie|Queue]},
+ EvHandler, EvHandlerState);
%% @todo What to do about IsFin?
-commands([{send, _IsFin, Data}|Tail], State=#tunnel_state{socket=Socket, transport=Transport},
+commands([{send, _IsFin, Data}|Tail],
+ State=#tunnel_state{socket=Socket, transport=Transport},
EvHandler, EvHandlerState) ->
Transport:send(Socket, Data),
commands(Tail, State, EvHandler, EvHandlerState);
@@ -592,3 +604,17 @@ outer_stream_ref(StreamRef) when is_list(StreamRef) ->
lists:last(StreamRef);
outer_stream_ref(StreamRef) ->
StreamRef.
+
+%% This function is called before returning commands.
+ret(CommandOrCommands, #tunnel_state{commands_queue=[]}) ->
+ empty_commands_queue(CommandOrCommands);
+ret(Commands, #tunnel_state{commands_queue=Queue}) when is_list(Commands) ->
+ lists:reverse(Queue, empty_commands_queue(Commands));
+ret(Command, #tunnel_state{commands_queue=Queue}) ->
+ lists:reverse([empty_commands_queue(Command)|Queue]).
+
+empty_commands_queue([{state, State}|Tail]) -> [{state, State#tunnel_state{commands_queue=[]}}|Tail];
+empty_commands_queue([Command|Tail]) -> [Command|empty_commands_queue(Tail)];
+empty_commands_queue([]) -> [];
+empty_commands_queue({state, State}) -> {state, State#tunnel_state{commands_queue=[]}};
+empty_commands_queue(Command) -> Command.
diff --git a/test/rfc6265bis_SUITE.erl b/test/rfc6265bis_SUITE.erl
index ecaf45f..8beee58 100644
--- a/test/rfc6265bis_SUITE.erl
+++ b/test/rfc6265bis_SUITE.erl
@@ -176,6 +176,39 @@ do_informational_set_cookie(Config, Boolean) ->
gun:close(ConnPid),
Res.
+set_cookie_connect(Config) ->
+ doc("Cookies may also be set in responses going through CONNECT tunnels."),
+ Transport = config(transport, Config),
+ Protocol = config(protocol, Config),
+ {ok, ProxyPid, ProxyPort} = event_SUITE:do_proxy_start(Protocol, Transport),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ transport => Transport,
+ protocols => [Protocol],
+ cookie_store => gun_cookies_list:init()
+ }),
+ {ok, Protocol} = gun:await_up(ConnPid),
+ tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid),
+ StreamRef1 = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => config(port, Config),
+ transport => Transport,
+ protocols => [Protocol]
+ }),
+ %% @todo _IsFin is 'fin' for HTTP and 'nofin' for HTTP/2...
+ {response, _IsFin, 200, _} = gun:await(ConnPid, StreamRef1),
+ {up, Protocol} = gun:await(ConnPid, StreamRef1),
+ StreamRef2 = gun:get(ConnPid, "/cookie-set?prefix", #{
+ <<"please-set-cookie">> => <<"a=b">>
+ }, #{tunnel => StreamRef1}),
+ {response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef2),
+ ct:log("Headers2:~n~p", [Headers2]),
+ StreamRef3 = gun:get(ConnPid, "/cookie-echo", [], #{tunnel => StreamRef1}),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3),
+ {ok, Body3} = gun:await_body(ConnPid, StreamRef3),
+ ct:log("Body3:~n~p", [Body3]),
+ [{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body3),
+ gun:close(ConnPid).
+
-define(HOST, "web-platform.test").
%% WPT: domain/domain-attribute-host-with-and-without-leading-period