aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-03-11 19:45:16 +0100
committerLoïc Hoguin <[email protected]>2020-03-12 18:08:16 +0100
commit87d0bfff926892d2dc0a55a3dc45d8c5f8a682f3 (patch)
tree5b381d3d2c2f691699baeddad78c781026d237b7 /src
parent04790d3a281a42fbd65c9a44fe88f437cfe025f3 (diff)
downloadgun-87d0bfff926892d2dc0a55a3dc45d8c5f8a682f3.tar.gz
gun-87d0bfff926892d2dc0a55a3dc45d8c5f8a682f3.tar.bz2
gun-87d0bfff926892d2dc0a55a3dc45d8c5f8a682f3.zip
Make Gun use the cookie store when configured to
Diffstat (limited to 'src')
-rw-r--r--src/gun.erl113
-rw-r--r--src/gun_cookies.erl69
-rw-r--r--src/gun_cookies_list.erl8
-rw-r--r--src/gun_http.erl109
-rw-r--r--src/gun_http2.erl71
5 files changed, 271 insertions, 99 deletions
diff --git a/src/gun.erl b/src/gun.erl
index b08057f..7e468f3 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -65,6 +65,10 @@
-export([connect/3]).
-export([connect/4]).
+%% Cookies.
+%% @todo -export([gc_cookies/1]).
+%% @todo -export([session_gc_cookies/1]).
+
%% Awaiting gun messages.
-export([await/2]).
-export([await/3]).
@@ -123,6 +127,8 @@
-type opts() :: #{
connect_timeout => timeout(),
+ cookie_ignore_informational => boolean(),
+ cookie_store => gun_cookies:store(),
domain_lookup_timeout => timeout(),
event_handler => {module(), any()},
http_opts => http_opts(),
@@ -252,7 +258,8 @@
protocol :: module(),
protocol_state :: any(),
event_handler :: module(),
- event_handler_state :: any()
+ event_handler_state :: any(),
+ cookie_store :: undefined | {module(), any()}
}).
%% Connection.
@@ -301,6 +308,8 @@ check_options([{connect_timeout, infinity}|Opts]) ->
check_options(Opts);
check_options([{connect_timeout, T}|Opts]) when is_integer(T), T >= 0 ->
check_options(Opts);
+check_options([{cookie_store, {Mod, _}}|Opts]) when is_atom(Mod) ->
+ check_options(Opts);
check_options([{domain_lookup_timeout, infinity}|Opts]) ->
check_options(Opts);
check_options([{domain_lookup_timeout, T}|Opts]) when is_integer(T), T >= 0 ->
@@ -412,7 +421,8 @@ info(ServerPid) ->
origin_scheme=OriginScheme,
origin_host=OriginHost,
origin_port=OriginPort,
- intermediaries=Intermediaries
+ intermediaries=Intermediaries,
+ cookie_store=CookieStore
}} = sys:get_state(ServerPid),
Info0 = #{
owner => Owner,
@@ -425,7 +435,8 @@ info(ServerPid) ->
origin_host => OriginHost,
origin_port => OriginPort,
%% Intermediaries are listed in the order data goes through them.
- intermediaries => lists:reverse(Intermediaries)
+ intermediaries => lists:reverse(Intermediaries),
+ cookie_store => CookieStore
},
Info = case Socket of
undefined ->
@@ -543,6 +554,8 @@ put(ServerPid, Path, Headers, Body, ReqOpts) ->
request(ServerPid, <<"PUT">>, Path, Headers, Body, ReqOpts).
%% Generic requests interface.
+%%
+%% @todo Accept a TargetURI map as well as a normal Path.
-spec headers(pid(), iodata(), iodata(), req_headers()) -> reference().
headers(ServerPid, Method, Path, Headers) ->
@@ -880,11 +893,13 @@ init({Owner, Host, Port, Opts}) ->
origin_port => Port,
opts => Opts
}, EvHandlerState0),
+ CookieStore = maps:get(cookie_store, Opts, undefined),
State = #state{owner=Owner, status={up, OwnerRef},
host=Host, port=Port, origin_scheme=OriginScheme,
origin_host=Host, origin_port=Port, opts=Opts,
transport=Transport, messages=Transport:messages(),
- event_handler=EvHandler, event_handler_state=EvHandlerState},
+ event_handler=EvHandler, event_handler_state=EvHandlerState,
+ cookie_store=CookieStore},
{ok, domain_lookup, State,
{next_event, internal, {retries, Retry, not_connected}}}.
@@ -1141,18 +1156,23 @@ connected(internal, {connected, Socket, Protocol0},
false -> {next_state, StateName, State}
end;
%% Public HTTP interface.
-connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers, InitialFlow},
- State=#state{origin_host=Host, origin_port=Port,
+%%
+%% @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,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
+ {Headers, State} = add_cookie_header(Path, Headers0, State0),
{ProtoState2, EvHandlerState} = Protocol:headers(ProtoState,
StreamRef, 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, Headers, Body, InitialFlow},
- State=#state{origin_host=Host, origin_port=Port,
+connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers0, Body, InitialFlow},
+ State0=#state{origin_host=Host, origin_port=Port,
protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
+ {Headers, State} = add_cookie_header(Path, Headers0, State0),
{ProtoState2, EvHandlerState} = Protocol:request(ProtoState,
StreamRef, ReplyTo, Method, Host, Port, Path, Headers, Body,
InitialFlow, EvHandler, EvHandlerState0),
@@ -1167,8 +1187,8 @@ connected(cast, {connect, ReplyTo, StreamRef, Destination, Headers, InitialFlow}
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, Headers, WsOpts},
- State=#state{origin_host=Host, origin_port=Port,
+connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers0, WsOpts},
+ State0=#state{origin_host=Host, origin_port=Port,
protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0})
when Protocol =:= gun_http ->
@@ -1178,6 +1198,7 @@ connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers, 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,
StreamRef, ReplyTo, Host, Port, Path, Headers, WsOpts,
EvHandler, EvHandlerState1),
@@ -1195,6 +1216,35 @@ 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}}.
+
%% Switch to the graceful connection close state.
closing(State=#state{protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason) ->
@@ -1355,6 +1405,41 @@ commands([{active, Active}|Tail], State) when is_boolean(Active) ->
commands(Tail, State#state{active=Active});
commands([{state, ProtoState}|Tail], State) ->
commands(Tail, State#state{protocol_state=ProtoState});
+%% Don't set cookies when cookie store isn't configured.
+commands([{set_cookie, _, _, _, _}|Tail], State=#state{cookie_store=undefined}) ->
+ commands(Tail, State);
+%% Ignore cookies set on informational responses when configured to do so.
+%% This includes cookies set to Websocket upgrade responses!
+commands([{set_cookie, _, _, Status, _}|Tail], State=#state{opts=#{cookie_ignore_informational := true}})
+ when Status >= 100, Status =< 199 ->
+ commands(Tail, State);
+commands([{set_cookie, Authority, PathWithQs, _, Headers}|Tail], State=#state{
+ transport=Transport, cookie_store=Store0}) ->
+ Scheme = case Transport of
+ gun_tls -> <<"https">>;
+ gun_tls_proxy -> <<"https">>;
+ gun_tcp -> <<"http">>
+ end,
+ %% @todo Not sure if this is best done here or in the protocol code or elsewhere.
+ #{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]),
+ SetCookies = [SC || {<<"set-cookie">>, SC} <- Headers],
+ Store = lists:foldl(fun(SC, Store1) ->
+ case cow_cookie:parse_set_cookie(SC) of
+ {ok, N, V, A} ->
+ case gun_cookies:set_cookie(Store1, URIMap, N, V, A) of
+ {ok, Store2} -> Store2;
+ {error, _} -> Store1
+ end;
+ ignore ->
+ Store1
+ end
+ end, Store0, SetCookies),
+ commands(Tail, State#state{cookie_store=Store});
%% Order is important: the origin must be changed before
%% the transport and/or protocol in order to keep track
%% of the intermediaries properly.
@@ -1499,7 +1584,13 @@ owner_down(Shutdown = {shutdown, _}, State) -> {stop, Shutdown, State};
owner_down(Reason, State) -> {stop, {shutdown, {owner_down, Reason}}, State}.
terminate(Reason, StateName, #state{event_handler=EvHandler,
- event_handler_state=EvHandlerState}) ->
+ event_handler_state=EvHandlerState, cookie_store=Store}) ->
+ case Store of
+ undefined -> ok;
+ %% Optimization: gun_cookies_list isn't a persistent cookie store.
+ {gun_cookies_list, _} -> ok;
+ _ -> gun_cookies:session_gc(Store)
+ end,
TerminateEvent = #{
state => StateName,
reason => Reason
diff --git a/src/gun_cookies.erl b/src/gun_cookies.erl
index 9f377a3..d4c5423 100644
--- a/src/gun_cookies.erl
+++ b/src/gun_cookies.erl
@@ -21,6 +21,10 @@
-export([session_gc/1]).
-export([set_cookie/5]).
+-ifdef(TEST).
+-export([wpt_http_state_test_files/1]). %% Also used in rfc6265bis_SUITE.
+-endif.
+
-type store_state() :: any().
-type store() :: {module(), store_state()}.
@@ -42,7 +46,7 @@
}.
-export_type([cookie/0]).
--callback init() -> store().
+-callback init(any()) -> store().
-callback query(State, uri_string:uri_map())
-> {ok, [{binary(), binary()}], State}
@@ -66,6 +70,14 @@
-> {ok, State} | {error, any()}
when State::store_state().
+-callback gc(State)
+ -> {ok, State}
+ when State::store_state().
+
+-callback session_gc(State)
+ -> {ok, State}
+ when State::store_state().
+
-spec domain_match(binary(), binary()) -> boolean().
domain_match(String, String) ->
true;
@@ -387,35 +399,36 @@ wpt_domain_missing_test() ->
{ok, [], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}),
ok.
-%% WPT: http-state/general-tests
-%%
-%% The WPT http-state test suite is either broken or complicated to setup.
-%% The original http-state test suite is a better reference at the time
-%% of writing. The server running these tests is at
-%% https://github.com/abarth/http-state/blob/master/tools/testserver/testserver.py
+%% WPT: http-state/*-tests
+wpt_http_state_test_files() ->
+ wpt_http_state_test_files("test/").
+
+wpt_http_state_test_files(TestPath) ->
+ filelib:wildcard(TestPath ++ "wpt/cookies/*-test") -- [
+ TestPath ++ "wpt/cookies/attribute0023-test", %% Doesn't match the spec (path override).
+ TestPath ++ "wpt/cookies/chromium0009-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/chromium0010-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/chromium0012-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/disabled-chromium0020-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/disabled-chromium0022-test", %% Nonsense.
+ TestPath ++ "wpt/cookies/mozilla0012-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/mozilla0014-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/mozilla0015-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/mozilla0016-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/mozilla0017-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/name0017-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/name0023-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/name0025-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/name0028-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/name0031-test", %% Doesn't match the spec (name with quotes).
+ TestPath ++ "wpt/cookies/name0032-test", %% Doesn't match the spec (name with quotes).
+ TestPath ++ "wpt/cookies/name0033-test", %% Doesn't match the spec (empty names).
+ TestPath ++ "wpt/cookies/optional-domain0042-test" %% Doesn't match the spec (empty domain override).
+ ].
+
wpt_http_state_test_() ->
URIMap0 = #{scheme => <<"http">>, host => <<"home.example.org">>, path => <<"/cookie-parser">>},
- TestFiles = filelib:wildcard("test/wpt/cookies/*-test") -- [
- "test/wpt/cookies/attribute0023-test", %% Doesn't match the spec (path override).
- "test/wpt/cookies/chromium0009-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/chromium0010-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/chromium0012-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/disabled-chromium0020-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/disabled-chromium0022-test", %% Nonsense.
- "test/wpt/cookies/mozilla0012-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/mozilla0014-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/mozilla0015-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/mozilla0016-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/mozilla0017-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/name0017-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/name0023-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/name0025-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/name0028-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/name0031-test", %% Doesn't match the spec (name with quotes).
- "test/wpt/cookies/name0032-test", %% Doesn't match the spec (name with quotes).
- "test/wpt/cookies/name0033-test", %% Doesn't match the spec (empty names).
- "test/wpt/cookies/optional-domain0042-test" %% Doesn't match the spec (empty domain override).
- ],
+ TestFiles = wpt_http_state_test_files(),
[{F, fun() ->
{ok, Test} = file:read_file(F),
%% We don't want the final empty line.
diff --git a/src/gun_cookies_list.erl b/src/gun_cookies_list.erl
index ccd2292..e8cf17a 100644
--- a/src/gun_cookies_list.erl
+++ b/src/gun_cookies_list.erl
@@ -17,6 +17,7 @@
-module(gun_cookies_list).
-export([init/0]).
+-export([init/1]).
-export([query/2]).
-export([set_cookie_secure_match/2]).
-export([set_cookie_take_exact_match/2]).
@@ -30,8 +31,15 @@
%% @todo max_cookies => non_neg_integer() | infinity
}.
+-type opts() :: #{
+}.
+
-spec init() -> {?MODULE, state()}.
init() ->
+ init(#{}).
+
+-spec init(opts()) -> {?MODULE, state()}.
+init(_Opts) ->
{?MODULE, #{cookies => []}}.
-spec query(State, uri_string:uri_map())
diff --git a/src/gun_http.erl b/src/gun_http.erl
index 79124c3..401e23a 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -56,6 +56,11 @@
reply_to :: pid(),
flow :: integer() | infinity,
method :: binary(),
+
+ %% Request target URI.
+ authority :: iodata(),
+ path :: iodata(),
+
is_alive :: boolean(),
handler_state :: undefined | gun_content_handler:state()
}).
@@ -70,7 +75,10 @@
streams = [] :: [#stream{}],
in = head :: io(),
in_state = {0, 0} :: {non_neg_integer(), non_neg_integer()},
- out = head :: io()
+ out = head :: io(),
+
+ %% We must queue commands when parsing the incoming data.
+ commands_queue = [] :: [{set_cookie, iodata(), iodata(), cow_http:status(), cow_http:headers()}]
}).
check_options(Opts) ->
@@ -113,12 +121,20 @@ init(_ReplyTo, Socket, Transport, Opts) ->
switch_transport(Transport, Socket, State) ->
State#http_state{socket=Socket, transport=Transport}.
+%% This function is called before returning from handle/4.
+handle_ret(CommandOrCommands, #http_state{commands_queue=[]}) ->
+ CommandOrCommands;
+handle_ret(Commands, #http_state{commands_queue=Queue}) when is_list(Commands) ->
+ lists:reverse(Queue, Commands);
+handle_ret(Command, #http_state{commands_queue=Queue}) ->
+ lists:reverse([Command|Queue]).
+
%% Stop looping when we got no more data.
handle(<<>>, State, _, EvHandlerState) ->
- {{state, State}, EvHandlerState};
+ {handle_ret({state, State}, State), EvHandlerState};
%% Close when server responds and we don't have any open streams.
-handle(_, #http_state{streams=[]}, _, EvHandlerState) ->
- {close, EvHandlerState};
+handle(_, State=#http_state{streams=[]}, _, EvHandlerState) ->
+ {handle_ret(close, State), EvHandlerState};
%% Wait for the full response headers before trying to parse them.
handle(Data, State=#http_state{in=head, buffer=Buffer,
streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, EvHandler, EvHandlerState0) ->
@@ -135,12 +151,12 @@ handle(Data, State=#http_state{in=head, buffer=Buffer,
end,
Data2 = << Buffer/binary, Data/binary >>,
case binary:match(Data2, <<"\r\n\r\n">>) of
- nomatch -> {{state, State#http_state{buffer=Data2}}, EvHandlerState};
+ nomatch -> {handle_ret({state, State#http_state{buffer=Data2}}, State), EvHandlerState};
{_, _} -> handle_head(Data2, State#http_state{buffer= <<>>}, EvHandler, EvHandlerState)
end;
%% Everything sent to the socket until it closes is part of the response body.
handle(Data, State=#http_state{in=body_close}, _, EvHandlerState) ->
- {send_data(Data, State, nofin), EvHandlerState};
+ {handle_ret(send_data(Data, State, nofin), State), EvHandlerState};
%% Chunked transfer-encoding may contain both data and trailers.
handle(Data, State=#http_state{in=body_chunked, in_state=InState,
buffer=Buffer, streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_],
@@ -148,15 +164,18 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState,
Buffer2 = << Buffer/binary, Data/binary >>,
case cow_http_te:stream_chunked(Buffer2, InState) of
more ->
- {{state, State#http_state{buffer=Buffer2}}, EvHandlerState0};
+ {handle_ret({state, State#http_state{buffer=Buffer2}}, State), EvHandlerState0};
{more, Data2, InState2} ->
- {send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), EvHandlerState0};
+ {handle_ret(send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), State),
+ EvHandlerState0};
{more, Data2, Length, InState2} when is_integer(Length) ->
%% @todo See if we can recv faster than one message at a time.
- {send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), EvHandlerState0};
+ {handle_ret(send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), State),
+ EvHandlerState0};
{more, Data2, Rest, InState2} ->
%% @todo See if we can recv faster than one message at a time.
- {send_data(Data2, State#http_state{buffer=Rest, in_state=InState2}, nofin), EvHandlerState0};
+ {handle_ret(send_data(Data2, State#http_state{buffer=Rest, in_state=InState2}, nofin), State),
+ EvHandlerState0};
{done, HasTrailers, Rest} ->
%% @todo response_end should be called AFTER send_data
{IsFin, EvHandlerState} = case HasTrailers of
@@ -178,7 +197,7 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState,
{no_trailers, keepalive} ->
handle(Rest, end_stream(State1#http_state{buffer= <<>>}), EvHandler, EvHandlerState);
{no_trailers, close} ->
- {[{state, end_stream(State1)}, close], EvHandlerState}
+ {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState}
end;
{done, Data2, HasTrailers, Rest} ->
%% @todo response_end should be called AFTER send_data
@@ -200,7 +219,7 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState,
{no_trailers, keepalive} ->
handle(Rest, end_stream(State1#http_state{buffer= <<>>}), EvHandler, EvHandlerState);
{no_trailers, close} ->
- {[{state, end_stream(State1)}, close], EvHandlerState}
+ {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState}
end
end;
handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn,
@@ -208,7 +227,7 @@ handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn,
Data2 = << Buffer/binary, Data/binary >>,
case binary:match(Data2, <<"\r\n\r\n">>) of
nomatch ->
- {{state, State#http_state{buffer=Data2}}, EvHandlerState0};
+ {handle_ret({state, State#http_state{buffer=Data2}}, State), EvHandlerState0};
{_, _} ->
{Trailers, Rest} = cow_http:parse_headers(Data2),
%% @todo We probably want to pass this to gun_content_handler?
@@ -223,7 +242,7 @@ handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn,
keepalive ->
handle(Rest, end_stream(State#http_state{buffer= <<>>}), EvHandler, EvHandlerState);
close ->
- {[{state, end_stream(State)}, close], EvHandlerState}
+ {handle_ret([{state, end_stream(State)}, close], State), EvHandlerState}
end
end;
%% We know the length of the rest of the body.
@@ -234,7 +253,8 @@ handle(Data, State=#http_state{in={body, Length}, connection=Conn,
if
%% More data coming.
DataSize < Length ->
- {send_data(Data, State#http_state{in={body, Length - DataSize}}, nofin), EvHandlerState0};
+ {handle_ret(send_data(Data, State#http_state{in={body, Length - DataSize}}, nofin), State),
+ EvHandlerState0};
%% Stream finished, no rest.
DataSize =:= Length ->
%% We ignore the active command because the stream ended.
@@ -244,8 +264,10 @@ handle(Data, State=#http_state{in={body, Length}, connection=Conn,
reply_to => ReplyTo
}, EvHandlerState0),
case Conn of
- keepalive -> {[{state, end_stream(State1)}, {active, true}], EvHandlerState};
- close -> {[{state, end_stream(State1)}, close], EvHandlerState}
+ keepalive ->
+ {handle_ret([{state, end_stream(State1)}, {active, true}], State1), EvHandlerState};
+ close ->
+ {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState}
end;
%% Stream finished, rest.
true ->
@@ -258,14 +280,15 @@ handle(Data, State=#http_state{in={body, Length}, connection=Conn,
}, EvHandlerState0),
case Conn of
keepalive -> handle(Rest, end_stream(State1), EvHandler, EvHandlerState);
- close -> {[{state, end_stream(State1)}, close], EvHandlerState}
+ close -> {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState}
end
end.
-handle_head(Data, State=#http_state{streams=[#stream{ref=StreamRef}|_]},
- EvHandler, EvHandlerState) ->
+handle_head(Data, State0=#http_state{streams=[#stream{ref=StreamRef, authority=Authority, path=Path}|_],
+ commands_queue=Commands}, EvHandler, EvHandlerState) ->
{Version, Status, _, Rest0} = cow_http:parse_status_line(Data),
{Headers, Rest} = cow_http:parse_headers(Rest0),
+ State = State0#http_state{commands_queue=[{set_cookie, Authority, Path, Status, Headers}|Commands]},
case StreamRef of
{connect, _, _} when Status >= 200, Status < 300 ->
handle_connect(Rest, State, EvHandler, EvHandlerState, Version, Status, Headers);
@@ -305,12 +328,16 @@ handle_connect(Rest, State=#http_state{
timeout => maps:get(tls_handshake_timeout, Destination, infinity)
},
Protocols = maps:get(protocols, Destination, [http2, http]),
- {[{origin, <<"https">>, NewHost, NewPort, connect},
- {tls_handshake, HandshakeEvent, Protocols, ReplyTo}], EvHandlerState1};
+ {handle_ret([
+ {origin, <<"https">>, NewHost, NewPort, connect},
+ {tls_handshake, HandshakeEvent, Protocols, ReplyTo}
+ ], State), EvHandlerState1};
_ ->
[Protocol] = maps:get(protocols, Destination, [http]),
- {[{origin, <<"http">>, NewHost, NewPort, connect},
- {switch_protocol, Protocol, ReplyTo}], EvHandlerState1}
+ {handle_ret([
+ {origin, <<"http">>, NewHost, NewPort, connect},
+ {switch_protocol, Protocol, ReplyTo}
+ ], State), EvHandlerState1}
end.
%% @todo We probably shouldn't send info messages if the stream is not alive.
@@ -326,7 +353,7 @@ handle_inform(Rest, State=#http_state{
%% @todo We might want to switch to the HTTP/2 protocol or to the TLS transport as well.
case {Version, Status, StreamRef} of
{'HTTP/1.1', 101, #websocket{}} ->
- {ws_handshake(Rest, State, StreamRef, Headers), EvHandlerState};
+ {handle_ret(ws_handshake(Rest, State, StreamRef, Headers), State), 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.
{'HTTP/1.1', 101, _} when is_reference(StreamRef) ->
@@ -335,7 +362,7 @@ handle_inform(Rest, State=#http_state{
{_, Upgrade0} = lists:keyfind(<<"upgrade">>, 1, Headers),
Upgrade = cow_http_hd:parse_upgrade(Upgrade0),
ReplyTo ! {gun_upgrade, self(), StreamRef, Upgrade, Headers},
- {{switch_protocol, raw, ReplyTo}, EvHandlerState0}
+ {handle_ret({switch_protocol, raw, ReplyTo}, State), EvHandlerState0}
catch _:_ ->
%% When the Upgrade header is missing or invalid we treat
%% the response as any other informational response.
@@ -391,7 +418,7 @@ handle_response(Rest, State=#http_state{version=ClientVersion, opts=Opts, connec
%% We always reset in_state even if not chunked.
if
IsFin =:= fin, Conn2 =:= close ->
- {close, EvHandlerState};
+ {handle_ret(close, State), EvHandlerState};
IsFin =:= fin ->
handle(Rest, end_stream(State#http_state{in=In,
in_state={0, 0}, connection=Conn2,
@@ -501,22 +528,22 @@ keepalive(State, _, EvHandlerState) ->
headers(State=#http_state{opts=Opts, out=head},
StreamRef, ReplyTo, Method, Host, Port, Path, Headers,
InitialFlow0, EvHandler, EvHandlerState0) ->
- {Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
+ {Authority, Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
Method, Host, Port, Path, Headers, undefined,
EvHandler, EvHandlerState0, ?FUNCTION_NAME),
InitialFlow = initial_flow(InitialFlow0, Opts),
- {new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo, Method, InitialFlow),
- EvHandlerState}.
+ {new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo,
+ Method, Authority, Path, InitialFlow), EvHandlerState}.
request(State=#http_state{opts=Opts, out=head}, StreamRef, ReplyTo,
Method, Host, Port, Path, Headers, Body,
InitialFlow0, EvHandler, EvHandlerState0) ->
- {Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
+ {Authority, Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
Method, Host, Port, Path, Headers, Body,
EvHandler, EvHandlerState0, ?FUNCTION_NAME),
InitialFlow = initial_flow(InitialFlow0, Opts),
- {new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo, Method, InitialFlow),
- EvHandlerState}.
+ {new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo,
+ Method, Authority, Path, InitialFlow), EvHandlerState}.
initial_flow(infinity, #{flow := InitialFlow}) -> InitialFlow;
initial_flow(InitialFlow, _) -> InitialFlow.
@@ -536,6 +563,7 @@ send_request(State=#http_state{socket=Socket, transport=Transport, version=Versi
undefined -> request_io_from_headers(Headers2);
_ -> head
end,
+ %% @todo Move this inside the case clause.
Authority0 = host_header(Transport, Host, Port),
{Authority, Headers3} = case lists:keyfind(<<"host">>, 1, Headers2) of
false -> {Authority0, [{<<"host">>, Authority0}|Headers2]};
@@ -572,7 +600,7 @@ send_request(State=#http_state{socket=Socket, transport=Transport, version=Versi
_ ->
EvHandlerState2
end,
- {Conn, Out, EvHandlerState}.
+ {Authority, Conn, Out, EvHandlerState}.
host_header(Transport, Host0, Port) ->
Host = case Host0 of
@@ -681,7 +709,8 @@ connect(State=#http_state{socket=Socket, transport=Transport, opts=Opts, version
cow_http:request(<<"CONNECT">>, Authority, Version, Headers)
]),
InitialFlow = initial_flow(InitialFlow0, Opts),
- new_stream(State, {connect, StreamRef, Destination}, ReplyTo, <<"CONNECT">>, InitialFlow).
+ new_stream(State, {connect, StreamRef, Destination}, ReplyTo,
+ <<"CONNECT">>, Authority, <<>>, InitialFlow).
%% We can't cancel anything, we can just stop forwarding messages to the owner.
cancel(State0, StreamRef, ReplyTo, EvHandler, EvHandlerState0) ->
@@ -780,10 +809,12 @@ response_io_from_headers(_, Version, _Status, Headers) ->
%% Streams.
-new_stream(State=#http_state{streams=Streams}, StreamRef, ReplyTo, Method, InitialFlow) ->
+new_stream(State=#http_state{streams=Streams}, StreamRef, ReplyTo,
+ Method, Authority, Path, InitialFlow) ->
State#http_state{streams=Streams
++ [#stream{ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow,
- method=iolist_to_binary(Method), is_alive=true}]}.
+ method=iolist_to_binary(Method), authority=Authority,
+ path=iolist_to_binary(Path), is_alive=true}]}.
is_stream(#http_state{streams=Streams}, StreamRef) ->
lists:keymember(StreamRef, #stream.ref, Streams).
@@ -830,13 +861,13 @@ ws_upgrade(State=#http_state{out=head}, StreamRef, ReplyTo,
{<<"sec-websocket-key">>, Key}
|Headers2
],
- {Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
+ {Authority, Conn, Out, EvHandlerState} = send_request(State, StreamRef, ReplyTo,
<<"GET">>, Host, Port, Path, Headers, undefined,
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">>, InitialFlow), EvHandlerState}.
+ ReplyTo, <<"GET">>, Authority, Path, InitialFlow), 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 76ba75c..9edabaa 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -46,6 +46,10 @@
%% Flow control.
flow :: integer() | infinity,
+ %% Request target URI.
+ authority :: iodata(),
+ path :: iodata(),
+
%% Content handlers state.
handler_state :: undefined | gun_content_handler:state()
}).
@@ -72,7 +76,10 @@
%% the idea, that's why the main map has the ID as key. Then we also
%% have a Ref->ID index for faster lookup when we only have the Ref.
streams = #{} :: #{cow_http2:streamid() => #stream{}},
- stream_refs = #{} :: #{reference() => cow_http2:streamid()}
+ stream_refs = #{} :: #{reference() => cow_http2:streamid()},
+
+ %% We must queue commands when parsing the incoming data.
+ commands_queue = [] :: [{set_cookie, iodata(), iodata(), cow_http:status(), cow_http:headers()}]
}).
check_options(Opts) ->
@@ -144,6 +151,14 @@ init(_ReplyTo, Socket, Transport, Opts0) ->
switch_transport(Transport, Socket, State) ->
State#http2_state{socket=Socket, transport=Transport}.
+%% This function is called before returning from handle/4.
+handle_ret(CommandOrCommands, #http2_state{commands_queue=[]}) ->
+ CommandOrCommands;
+handle_ret(Commands, #http2_state{commands_queue=Queue}) when is_list(Commands) ->
+ lists:reverse(Queue, Commands);
+handle_ret(Command, #http2_state{commands_queue=Queue}) ->
+ lists:reverse([Command|Queue]).
+
handle(Data, State=#http2_state{buffer=Buffer}, EvHandler, EvHandlerState) ->
parse(<< Buffer/binary, Data/binary >>, State#http2_state{buffer= <<>>},
EvHandler, EvHandlerState).
@@ -154,11 +169,11 @@ parse(Data, State0=#http2_state{status=preface, http2_machine=HTTP2Machine},
case cow_http2:parse(Data, MaxFrameSize) of
{ok, Frame, Rest} when element(1, Frame) =:= settings ->
case frame(State0#http2_state{status=connected}, Frame, EvHandler, EvHandlerState0) of
- Error = {{error, _}, _} -> Error;
+ {Error={error, _}, EvHandlerState} -> {handle_ret(Error, State0), EvHandlerState};
{State, EvHandlerState} -> parse(Rest, State, EvHandler, EvHandlerState)
end;
more ->
- {{state, State0#http2_state{buffer=Data}}, EvHandlerState0};
+ {handle_ret({state, State0#http2_state{buffer=Data}}, State0), EvHandlerState0};
%% Any error in the preface is converted to this specific error
%% to make debugging the problem easier (it's the server's fault).
_ ->
@@ -168,7 +183,8 @@ parse(Data, State0=#http2_state{status=preface, http2_machine=HTTP2Machine},
_ ->
'Invalid connection preface received. (RFC7540 3.5)'
end,
- {connection_error(State0, {connection_error, protocol_error, Reason}), EvHandlerState0}
+ {handle_ret(connection_error(State0, {connection_error, protocol_error, Reason}), State0),
+ EvHandlerState0}
end;
parse(Data, State0=#http2_state{status=Status, http2_machine=HTTP2Machine, streams=Streams},
EvHandler, EvHandlerState0) ->
@@ -176,28 +192,30 @@ parse(Data, State0=#http2_state{status=Status, http2_machine=HTTP2Machine, strea
case cow_http2:parse(Data, MaxFrameSize) of
{ok, Frame, Rest} ->
case frame(State0, Frame, EvHandler, EvHandlerState0) of
- Error = {{error, _}, _} -> Error;
+ {Error={error, _}, EvHandlerState} -> {handle_ret(Error, State0), EvHandlerState};
{State, EvHandlerState} -> parse(Rest, State, EvHandler, EvHandlerState)
end;
{ignore, Rest} ->
case ignored_frame(State0) of
- Error = {error, _} -> {Error, EvHandlerState0};
+ Error = {error, _} -> {handle_ret(Error, State0), EvHandlerState0};
State -> parse(Rest, State, EvHandler, EvHandlerState0)
end;
{stream_error, StreamID, Reason, Human, Rest} ->
parse(Rest, reset_stream(State0, StreamID, {stream_error, Reason, Human}),
EvHandler, EvHandlerState0);
Error = {connection_error, _, _} ->
- {connection_error(State0, Error), EvHandlerState0};
+ {handle_ret(connection_error(State0, Error), State0), EvHandlerState0};
%% If we both received and sent a GOAWAY frame and there are no streams
%% currently running, we can close the connection immediately.
more when Status =/= connected, Streams =:= #{} ->
- {[{state, State0#http2_state{buffer=Data, status=closing}}, close], EvHandlerState0};
+ {handle_ret([{state, State0#http2_state{buffer=Data, status=closing}}, close], State0),
+ EvHandlerState0};
%% Otherwise we enter the closing state.
more when Status =:= goaway ->
- {[{state, State0#http2_state{buffer=Data, status=closing}}, closing(State0)], EvHandlerState0};
+ {handle_ret([{state, State0#http2_state{buffer=Data, status=closing}}, closing(State0)], State0),
+ EvHandlerState0};
more ->
- {{state, State0#http2_state{buffer=Data}}, EvHandlerState0}
+ {handle_ret({state, State0#http2_state{buffer=Data}}, State0), EvHandlerState0}
end.
%% Frames received.
@@ -308,12 +326,19 @@ data_frame(State0, StreamID, IsFin, Data, EvHandler, EvHandlerState0) ->
end,
{maybe_delete_stream(State, StreamID, remote, IsFin), EvHandlerState}.
-headers_frame(State=#http2_state{content_handlers=Handlers0},
- StreamID, IsFin, Headers, PseudoHeaders, _BodyLen,
+headers_frame(State0=#http2_state{content_handlers=Handlers0, commands_queue=Commands},
+ StreamID, IsFin, Headers, #{status := Status}, _BodyLen,
EvHandler, EvHandlerState0) ->
- Stream = #stream{ref=StreamRef, reply_to=ReplyTo} = get_stream_by_id(State, StreamID),
- case PseudoHeaders of
- #{status := Status} when Status >= 100, Status =< 199 ->
+ Stream = get_stream_by_id(State0, StreamID),
+ #stream{
+ ref=StreamRef,
+ reply_to=ReplyTo,
+ authority=Authority,
+ path=Path
+ } = Stream,
+ State = State0#http2_state{commands_queue=[{set_cookie, Authority, Path, Status, Headers}|Commands]},
+ if
+ Status >= 100, Status =< 199 ->
ReplyTo ! {gun_inform, self(), StreamRef, Status, Headers},
EvHandlerState = EvHandler:response_inform(#{
stream_ref => StreamRef,
@@ -322,7 +347,7 @@ headers_frame(State=#http2_state{content_handlers=Handlers0},
headers => Headers
}, EvHandlerState0),
{State, EvHandlerState};
- #{status := Status} ->
+ true ->
ReplyTo ! {gun_response, self(), StreamRef, IsFin, Status, Headers},
EvHandlerState1 = EvHandler:response_headers(#{
stream_ref => StreamRef,
@@ -402,7 +427,7 @@ push_promise_frame(State=#http2_state{socket=Socket, transport=Transport,
case Status of
connected ->
NewStream = #stream{id=PromisedStreamID, ref=PromisedStreamRef,
- reply_to=ReplyTo, flow=InitialFlow},
+ reply_to=ReplyTo, flow=InitialFlow, authority=Authority, path=Path},
{create_stream(State, NewStream), EvHandlerState};
%% We cancel the push_promise immediately when we are shutting down.
_ ->
@@ -545,12 +570,13 @@ headers(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
{ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream(
iolist_to_binary(Method), HTTP2Machine0),
{ok, PseudoHeaders, Headers} = prepare_headers(State, Method, Host, Port, Path, Headers0),
+ Authority = maps:get(authority, PseudoHeaders),
RequestEvent = #{
stream_ref => StreamRef,
reply_to => ReplyTo,
function => ?FUNCTION_NAME,
method => Method,
- authority => maps:get(authority, PseudoHeaders),
+ authority => Authority,
path => Path,
headers => Headers
},
@@ -560,7 +586,8 @@ headers(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)),
EvHandlerState = EvHandler:request_headers(RequestEvent, EvHandlerState1),
InitialFlow = initial_flow(InitialFlow0, Opts),
- Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow},
+ 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}.
request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts,
@@ -571,12 +598,13 @@ request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts,
{ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream(
iolist_to_binary(Method), HTTP2Machine0),
{ok, PseudoHeaders, Headers} = prepare_headers(State0, Method, Host, Port, Path, Headers1),
+ Authority = maps:get(authority, PseudoHeaders),
RequestEvent = #{
stream_ref => StreamRef,
reply_to => ReplyTo,
function => ?FUNCTION_NAME,
method => Method,
- authority => maps:get(authority, PseudoHeaders),
+ authority => Authority,
path => Path,
headers => Headers
},
@@ -590,7 +618,8 @@ request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts,
Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)),
EvHandlerState = EvHandler:request_headers(RequestEvent, EvHandlerState1),
InitialFlow = initial_flow(InitialFlow0, Opts),
- Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow},
+ Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow,
+ authority=Authority, path=Path},
State = create_stream(State0#http2_state{http2_machine=HTTP2Machine}, Stream),
case IsFin of
fin ->