From 87d0bfff926892d2dc0a55a3dc45d8c5f8a682f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 11 Mar 2020 19:45:16 +0100 Subject: Make Gun use the cookie store when configured to --- src/gun.erl | 113 ++++++++++++++++++++++++++++++++++++++++++----- src/gun_cookies.erl | 69 +++++++++++++++++------------ src/gun_cookies_list.erl | 8 ++++ src/gun_http.erl | 109 +++++++++++++++++++++++++++++---------------- src/gun_http2.erl | 71 ++++++++++++++++++++--------- 5 files changed, 271 insertions(+), 99 deletions(-) (limited to 'src') 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 -> -- cgit v1.2.3