From 465d072abf4a76104d4562ed15345b27fe9a0cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 21 Oct 2020 19:15:48 +0200 Subject: Fix cookie handling when tunnel and origin schemes mismatch The cookie_ignore_informational has been moved to http_opts and http2_opts. Also fix an issue when using 'protocols' in gun:open. When connecting via TLS the protocol's options were discarded. --- src/gun.erl | 87 +++++++--------------- src/gun_cookies.erl | 32 ++++++++ src/gun_http.erl | 147 ++++++++++++++++++------------------- src/gun_http2.erl | 182 ++++++++++++++++++++++++---------------------- src/gun_protocols.erl | 22 ++++-- src/gun_raw.erl | 6 +- src/gun_socks.erl | 6 +- src/gun_tunnel.erl | 98 ++++++++++--------------- src/gun_ws.erl | 6 +- test/rfc6265bis_SUITE.erl | 42 +++++++++-- 10 files changed, 330 insertions(+), 298 deletions(-) diff --git a/src/gun.erl b/src/gun.erl index 177d395..69dbb6b 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -135,7 +135,6 @@ -type opts() :: #{ connect_timeout => timeout(), - cookie_ignore_informational => boolean(), cookie_store => gun_cookies:store(), domain_lookup_timeout => timeout(), event_handler => {module(), any()}, @@ -212,6 +211,7 @@ -type http_opts() :: #{ closing_timeout => timeout(), content_handlers => gun_content_handler:opt(), + cookie_ignore_informational => boolean(), flow => pos_integer(), keepalive => timeout(), transform_header_name => fun((binary()) -> binary()), @@ -226,6 +226,7 @@ -type http2_opts() :: #{ closing_timeout => timeout(), content_handlers => gun_content_handler:opt(), + cookie_ignore_informational => boolean(), flow => pos_integer(), keepalive => timeout(), @@ -350,8 +351,6 @@ 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([{cookie_ignore_informational, B}|Opts]) when is_boolean(B) -> - 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 -> @@ -1097,10 +1096,13 @@ initial_tls_handshake(_, {retries, Retries, Socket}, State0=#state{opts=Opts, or ensure_alpn_sni(Protocols0, TransOpts0, OriginHost) -> %% ALPN. - Protocols = [case P of - http -> <<"http/1.1">>; - http2 -> <<"h2">> - end || P <- Protocols0, lists:member(P, [http, http2])], + Protocols = lists:foldl(fun + (http, Acc) -> [<<"http/1.1">>|Acc]; + ({http, _}, Acc) -> [<<"http/1.1">>|Acc]; + (http2, Acc) -> [<<"h2">>|Acc]; + ({http2, _}, Acc) -> [<<"h2">>|Acc]; + (_, Acc) -> Acc + end, [], Protocols0), TransOpts = [ {alpn_advertised_protocols, Protocols}, {client_preferred_next_protocols, {client, Protocols, <<"http/1.1">>}} @@ -1188,12 +1190,13 @@ normal_tls_handshake(Socket, State=#state{ EvHandlerState1 = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of {ok, TLSSocket} -> - Protocol = gun_protocols:negotiated(ssl:negotiated_protocol(TLSSocket), Protocols), + NewProtocol = gun_protocols:negotiated(ssl:negotiated_protocol(TLSSocket), Protocols), + Protocol = gun_protocols:handler(NewProtocol), EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ socket => TLSSocket, - protocol => Protocol + protocol => Protocol:name() }, EvHandlerState1), - {ok, TLSSocket, Protocol, State#state{event_handler_state=EvHandlerState}}; + {ok, TLSSocket, NewProtocol, State#state{event_handler_state=EvHandlerState}}; {error, Reason} -> EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ error => Reason @@ -1381,10 +1384,12 @@ handle_common_connected(Type, Event, StateName, StateData) -> %% Socket events. handle_common_connected_no_input(info, {OK, Socket, Data}, _, State0=#state{socket=Socket, messages={OK, _, _}, - protocol=Protocol, protocol_state=ProtoState, + protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> - {Commands, EvHandlerState} = Protocol:handle(Data, ProtoState, EvHandler, EvHandlerState0), - maybe_active(commands(Commands, State0#state{event_handler_state=EvHandlerState})); + {Commands, CookieStore, EvHandlerState} = Protocol:handle(Data, + ProtoState, CookieStore0, EvHandler, EvHandlerState0), + maybe_active(commands(Commands, State0#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState})); handle_common_connected_no_input(info, {Closed, Socket}, _, State=#state{socket=Socket, messages={_, Closed, _}}) -> disconnect(State, closed); @@ -1395,19 +1400,21 @@ handle_common_connected_no_input(info, {Error, Socket, Reason}, _, %% We always forward the messages to Protocol:handle_continue. handle_common_connected_no_input(info, Msg={gun_tls_proxy, _, _, {handle_continue, StreamRef, _, _}}, _, - State0=#state{protocol=Protocol, protocol_state=ProtoState, + State0=#state{protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> - {Commands, EvHandlerState} = Protocol:handle_continue( + {Commands, CookieStore, EvHandlerState} = Protocol:handle_continue( dereference_stream_ref(StreamRef, State0), - Msg, ProtoState, EvHandler, EvHandlerState0), - maybe_active(commands(Commands, State0#state{event_handler_state=EvHandlerState})); + Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0), + maybe_active(commands(Commands, State0#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState})); handle_common_connected_no_input(info, {handle_continue, StreamRef, Msg}, _, - State0=#state{protocol=Protocol, protocol_state=ProtoState, + State0=#state{protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> - {Commands, EvHandlerState} = Protocol:handle_continue( + {Commands, CookieStore, EvHandlerState} = Protocol:handle_continue( dereference_stream_ref(StreamRef, State0), - Msg, ProtoState, EvHandler, EvHandlerState0), - maybe_active(commands(Commands, State0#state{event_handler_state=EvHandlerState})); + Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0), + maybe_active(commands(Commands, State0#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState})); %% Timeouts. %% @todo HTTP/2 requires more timeouts than just the keepalive timeout. %% We should have a timeout function in protocols that deal with @@ -1622,44 +1629,6 @@ 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); -%% @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">>; - 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. diff --git a/src/gun_cookies.erl b/src/gun_cookies.erl index 965c3a5..0537832 100644 --- a/src/gun_cookies.erl +++ b/src/gun_cookies.erl @@ -21,6 +21,7 @@ -export([query/2]). -export([session_gc/1]). -export([set_cookie/5]). +-export([set_cookie_header/7]). -ifdef(TEST). -export([wpt_http_state_test_files/1]). %% Also used in rfc6265bis_SUITE. @@ -341,6 +342,37 @@ store({Mod, State0}, Cookie) -> Error end. +-spec set_cookie_header(binary(), iodata(), iodata(), cow_http:status(), + Headers, Store, #{cookie_ignore_informational := boolean()}) + -> {Headers, Store} when Headers :: [{binary(), iodata()}], Store :: undefined | store(). +%% Don't set cookies when cookie store isn't configured. +set_cookie_header(_, _, _, _, _, Store=undefined, _) -> + Store; +%% Ignore cookies set on informational responses when configured to do so. +%% This includes cookies set to Websocket upgrade responses! +set_cookie_header(_, _, _, Status, _, Store, #{cookie_ignore_informational := true}) + when Status >= 100, Status =< 199 -> + Store; +set_cookie_header(Scheme, Authority, PathWithQs, _, Headers, 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]), + SetCookies = [SC || {<<"set-cookie">>, SC} <- Headers], + lists:foldl(fun(SC, Store1) -> + case cow_cookie:parse_set_cookie(SC) of + {ok, N, V, A} -> + case set_cookie(Store1, URIMap, N, V, A) of + {ok, Store} -> Store; + {error, _} -> Store1 + end; + ignore -> + Store1 + end + end, Store0, SetCookies). + -ifdef(TEST). gc_test() -> URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>}, diff --git a/src/gun_http.erl b/src/gun_http.erl index 62d9490..8b716a5 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -21,7 +21,7 @@ -export([default_keepalive/0]). -export([init/4]). -export([switch_transport/3]). --export([handle/4]). +-export([handle/5]). -export([update_flow/4]). -export([closing/4]). -export([close/4]). @@ -80,10 +80,7 @@ streams = [] :: [#stream{}], in = head :: io(), in_state = {0, 0} :: {non_neg_integer(), non_neg_integer()}, - out = head :: io(), - - %% We must queue commands when parsing the incoming data. - commands_queue = [] :: [{set_cookie, iodata(), iodata(), cow_http:status(), cow_http:headers()}] + out = head :: io() }). check_options(Opts) -> @@ -100,6 +97,8 @@ do_check_options([Opt={content_handlers, Handlers}|Opts]) -> ok -> do_check_options(Opts); error -> {error, {options, {http, Opt}}} end; +do_check_options([{cookie_ignore_informational, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); do_check_options([{flow, InitialFlow}|Opts]) when is_integer(InitialFlow), InitialFlow > 0 -> do_check_options(Opts); do_check_options([{keepalive, infinity}|Opts]) -> @@ -127,29 +126,16 @@ 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=[]}) -> - empty_commands_queue(CommandOrCommands); -handle_ret(Commands, #http_state{commands_queue=Queue}) when is_list(Commands) -> - lists:reverse(Queue, empty_commands_queue(Commands)); -handle_ret(Command, #http_state{commands_queue=Queue}) -> - lists:reverse([empty_commands_queue(Command)|Queue]). - -empty_commands_queue([{state, State}|Tail]) -> [{state, State#http_state{commands_queue=[]}}|Tail]; -empty_commands_queue([Command|Tail]) -> [Command|empty_commands_queue(Tail)]; -empty_commands_queue([]) -> []; -empty_commands_queue({state, State}) -> {state, State#http_state{commands_queue=[]}}; -empty_commands_queue(Command) -> Command. - %% Stop looping when we got no more data. -handle(<<>>, State, _, EvHandlerState) -> - {handle_ret({state, State}, State), EvHandlerState}; +handle(<<>>, State, CookieStore, _, EvHandlerState) -> + {{state, State}, CookieStore, EvHandlerState}; %% Close when server responds and we don't have any open streams. -handle(_, State=#http_state{streams=[]}, _, EvHandlerState) -> - {handle_ret(close, State), EvHandlerState}; +handle(_, #http_state{streams=[]}, CookieStore, _, EvHandlerState) -> + {close, CookieStore, 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) -> + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, + CookieStore, EvHandler, EvHandlerState0) -> %% Send the event only if there was no data in the buffer. %% If there is data in the buffer then we already sent the event. EvHandlerState = case Buffer of @@ -163,31 +149,34 @@ 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 -> {handle_ret({state, State#http_state{buffer=Data2}}, State), EvHandlerState}; - {_, _} -> handle_head(Data2, State#http_state{buffer= <<>>}, EvHandler, EvHandlerState) + nomatch -> + {{state, State#http_state{buffer=Data2}}, CookieStore, EvHandlerState}; + {_, _} -> + handle_head(Data2, State#http_state{buffer= <<>>}, + CookieStore, 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) -> - {handle_ret(send_data(Data, State, nofin), State), EvHandlerState}; +handle(Data, State=#http_state{in=body_close}, CookieStore, _, EvHandlerState) -> + {send_data(Data, State, nofin), CookieStore, 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}|_], - connection=Conn}, EvHandler, EvHandlerState0) -> +handle(Data, State=#http_state{in=body_chunked, in_state=InState, buffer=Buffer, + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_], connection=Conn}, + CookieStore, EvHandler, EvHandlerState0) -> Buffer2 = << Buffer/binary, Data/binary >>, case cow_http_te:stream_chunked(Buffer2, InState) of more -> - {handle_ret({state, State#http_state{buffer=Buffer2}}, State), EvHandlerState0}; + {{state, State#http_state{buffer=Buffer2}}, CookieStore, EvHandlerState0}; {more, Data2, InState2} -> - {handle_ret(send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), State), - EvHandlerState0}; + {send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), + CookieStore, EvHandlerState0}; {more, Data2, Length, InState2} when is_integer(Length) -> %% @todo See if we can recv faster than one message at a time. - {handle_ret(send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), State), - EvHandlerState0}; + {send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), + CookieStore, EvHandlerState0}; {more, Data2, Rest, InState2} -> %% @todo See if we can recv faster than one message at a time. - {handle_ret(send_data(Data2, State#http_state{buffer=Rest, in_state=InState2}, nofin), State), - EvHandlerState0}; + {send_data(Data2, State#http_state{buffer=Rest, in_state=InState2}, nofin), + CookieStore, EvHandlerState0}; {done, HasTrailers, Rest} -> %% @todo response_end should be called AFTER send_data {IsFin, EvHandlerState} = case HasTrailers of @@ -205,11 +194,13 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState, [{state, State1}|_] = send_data(<<>>, State, IsFin), case {HasTrailers, Conn} of {trailers, _} -> - handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}, EvHandler, EvHandlerState); + handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}, + CookieStore, EvHandler, EvHandlerState); {no_trailers, keepalive} -> - handle(Rest, end_stream(State1#http_state{buffer= <<>>}), EvHandler, EvHandlerState); + handle(Rest, end_stream(State1#http_state{buffer= <<>>}), + CookieStore, EvHandler, EvHandlerState); {no_trailers, close} -> - {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState} + {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} end; {done, Data2, HasTrailers, Rest} -> %% @todo response_end should be called AFTER send_data @@ -227,19 +218,22 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState, [{state, State1}|_] = send_data(Data2, State, IsFin), case {HasTrailers, Conn} of {trailers, _} -> - handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}, EvHandler, EvHandlerState); + handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}, + CookieStore, EvHandler, EvHandlerState); {no_trailers, keepalive} -> - handle(Rest, end_stream(State1#http_state{buffer= <<>>}), EvHandler, EvHandlerState); + handle(Rest, end_stream(State1#http_state{buffer= <<>>}), + CookieStore, EvHandler, EvHandlerState); {no_trailers, close} -> - {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState} + {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} end end; handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn, - streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, EvHandler, EvHandlerState0) -> + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, + CookieStore, EvHandler, EvHandlerState0) -> Data2 = << Buffer/binary, Data/binary >>, case binary:match(Data2, <<"\r\n\r\n">>) of nomatch -> - {handle_ret({state, State#http_state{buffer=Data2}}, State), EvHandlerState0}; + {{state, State#http_state{buffer=Data2}}, CookieStore, EvHandlerState0}; {_, _} -> {Trailers, Rest} = cow_http:parse_headers(Data2), %% @todo We probably want to pass this to gun_content_handler? @@ -253,21 +247,22 @@ handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn, EvHandlerState = EvHandler:response_end(ResponseEvent, EvHandlerState1), case Conn of keepalive -> - handle(Rest, end_stream(State#http_state{buffer= <<>>}), EvHandler, EvHandlerState); + handle(Rest, end_stream(State#http_state{buffer= <<>>}), + CookieStore, EvHandler, EvHandlerState); close -> - {handle_ret([{state, end_stream(State)}, close], State), EvHandlerState} + {[{state, end_stream(State)}, close], CookieStore, EvHandlerState} end end; %% We know the length of the rest of the body. handle(Data, State=#http_state{in={body, Length}, connection=Conn, streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, - EvHandler, EvHandlerState0) -> + CookieStore, EvHandler, EvHandlerState0) -> DataSize = byte_size(Data), if %% More data coming. DataSize < Length -> - {handle_ret(send_data(Data, State#http_state{in={body, Length - DataSize}}, nofin), State), - EvHandlerState0}; + {send_data(Data, State#http_state{in={body, Length - DataSize}}, nofin), + CookieStore, EvHandlerState0}; %% Stream finished, no rest. DataSize =:= Length -> %% We ignore the active command because the stream ended. @@ -278,9 +273,9 @@ handle(Data, State=#http_state{in={body, Length}, connection=Conn, }, EvHandlerState0), case Conn of keepalive -> - {handle_ret([{state, end_stream(State1)}, {active, true}], State1), EvHandlerState}; + {[{state, end_stream(State1)}, {active, true}], CookieStore, EvHandlerState}; close -> - {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState} + {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} end; %% Stream finished, rest. true -> @@ -292,28 +287,30 @@ handle(Data, State=#http_state{in={body, Length}, connection=Conn, reply_to => ReplyTo }, EvHandlerState0), case Conn of - keepalive -> handle(Rest, end_stream(State1), EvHandler, EvHandlerState); - close -> {handle_ret([{state, end_stream(State1)}, close], State1), EvHandlerState} + keepalive -> handle(Rest, end_stream(State1), CookieStore, EvHandler, EvHandlerState); + close -> {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} end end. -handle_head(Data, State0=#http_state{streams=[#stream{ref=StreamRef, authority=Authority, path=Path}|_], - commands_queue=Commands}, EvHandler, EvHandlerState) -> +handle_head(Data, State=#http_state{opts=Opts, + streams=[#stream{ref=StreamRef, authority=Authority, path=Path}|_]}, + CookieStore0, 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]}, + CookieStore = gun_cookies:set_cookie_header(scheme(State), + Authority, Path, Status, Headers, CookieStore0, Opts), case StreamRef of {connect, _, _} when Status >= 200, Status < 300 -> - handle_connect(Rest, State, EvHandler, EvHandlerState, Version, Status, Headers); + handle_connect(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers); _ when Status >= 100, Status =< 199 -> - handle_inform(Rest, State, EvHandler, EvHandlerState, Version, Status, Headers); + handle_inform(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers); _ -> - handle_response(Rest, State, EvHandler, EvHandlerState, Version, Status, Headers) + handle_response(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers) end. handle_connect(Rest, State=#http_state{ streams=[Stream=#stream{ref={_, StreamRef, Destination}, reply_to=ReplyTo}|Tail]}, - EvHandler, EvHandlerState0, 'HTTP/1.1', Status, Headers) -> + CookieStore, EvHandler, EvHandlerState0, 'HTTP/1.1', Status, Headers) -> RealStreamRef = stream_ref(State, StreamRef), %% @todo If the stream is cancelled we probably shouldn't finish the CONNECT setup. _ = case Stream of @@ -342,25 +339,25 @@ handle_connect(Rest, State=#http_state{ timeout => maps:get(tls_handshake_timeout, Destination, infinity) }, Protocols = maps:get(protocols, Destination, [http2, http]), - {handle_ret([ + {[ {origin, <<"https">>, NewHost, NewPort, connect}, {tls_handshake, HandshakeEvent, Protocols, ReplyTo} - ], State), EvHandlerState1}; + ], CookieStore, EvHandlerState1}; _ -> [NewProtocol0] = maps:get(protocols, Destination, [http]), NewProtocol = gun_protocols:add_stream_ref(NewProtocol0, RealStreamRef), Protocol = gun_protocols:handler(NewProtocol), ReplyTo ! {gun_tunnel_up, self(), RealStreamRef, Protocol:name()}, - {handle_ret([ + {[ {origin, <<"http">>, NewHost, NewPort, connect}, {switch_protocol, NewProtocol, ReplyTo} - ], State), EvHandlerState1} + ], CookieStore, EvHandlerState1} end. %% @todo We probably shouldn't send info messages if the stream is not alive. handle_inform(Rest, State=#http_state{ streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, - EvHandler, EvHandlerState0, Version, Status, Headers) -> + CookieStore, EvHandler, EvHandlerState0, Version, Status, Headers) -> EvHandlerState = EvHandler:response_inform(#{ stream_ref => stream_ref(State, StreamRef), reply_to => ReplyTo, @@ -370,7 +367,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{}} -> - {handle_ret(ws_handshake(Rest, State, StreamRef, Headers), State), EvHandlerState}; + {ws_handshake(Rest, State, StreamRef, Headers), CookieStore, 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) -> @@ -380,21 +377,21 @@ handle_inform(Rest, State=#http_state{ Upgrade = cow_http_hd:parse_upgrade(Upgrade0), ReplyTo ! {gun_upgrade, self(), stream_ref(State, StreamRef), Upgrade, Headers}, %% @todo We probably need to add_stream_ref? - {handle_ret({switch_protocol, raw, ReplyTo}, State), EvHandlerState0} + {{switch_protocol, raw, ReplyTo}, CookieStore, EvHandlerState0} catch _:_ -> %% When the Upgrade header is missing or invalid we treat %% the response as any other informational response. ReplyTo ! {gun_inform, self(), stream_ref(State, StreamRef), Status, Headers}, - handle(Rest, State, EvHandler, EvHandlerState) + handle(Rest, State, CookieStore, EvHandler, EvHandlerState) end; _ -> ReplyTo ! {gun_inform, self(), stream_ref(State, StreamRef), Status, Headers}, - handle(Rest, State, EvHandler, EvHandlerState) + handle(Rest, State, CookieStore, EvHandler, EvHandlerState) end. handle_response(Rest, State=#http_state{version=ClientVersion, opts=Opts, connection=Conn, streams=[Stream=#stream{ref=StreamRef, reply_to=ReplyTo, method=Method, is_alive=IsAlive}|Tail]}, - EvHandler, EvHandlerState0, Version, Status, Headers) -> + CookieStore, EvHandler, EvHandlerState0, Version, Status, Headers) -> In = response_io_from_headers(Method, Version, Status, Headers), IsFin = case In of head -> fin; _ -> nofin end, RealStreamRef = stream_ref(State, StreamRef), @@ -436,17 +433,17 @@ 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 -> - {handle_ret(close, State), EvHandlerState}; + {close, CookieStore, EvHandlerState}; IsFin =:= fin -> handle(Rest, end_stream(State#http_state{in=In, in_state={0, 0}, connection=Conn2, streams=[Stream#stream{handler_state=Handlers}|Tail]}), - EvHandler, EvHandlerState); + CookieStore, EvHandler, EvHandlerState); true -> handle(Rest, State#http_state{in=In, in_state={0, 0}, connection=Conn2, streams=[Stream#stream{handler_state=Handlers}|Tail]}, - EvHandler, EvHandlerState) + CookieStore, EvHandler, EvHandlerState) end. %% The state must be first in order to retrieve it when the stream ended. diff --git a/src/gun_http2.erl b/src/gun_http2.erl index e8eb3aa..cb10029 100644 --- a/src/gun_http2.erl +++ b/src/gun_http2.erl @@ -21,8 +21,8 @@ -export([default_keepalive/0]). -export([init/4]). -export([switch_transport/3]). --export([handle/4]). --export([handle_continue/5]). +-export([handle/5]). +-export([handle_continue/6]). -export([update_flow/4]). -export([closing/4]). -export([close/4]). @@ -110,10 +110,7 @@ %% 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()}, - - %% We must queue commands when parsing the incoming data. - commands_queue = [] :: [{set_cookie, iodata(), iodata(), cow_http:status(), cow_http:headers()}] + stream_refs = #{} :: #{reference() => cow_http2:streamid()} }). check_options(Opts) -> @@ -131,6 +128,8 @@ do_check_options([Opt={content_handlers, Handlers}|Opts]) -> ok -> do_check_options(Opts); error -> {error, {options, {http2, Opt}}} end; +do_check_options([{cookie_ignore_informational, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); do_check_options([{flow, InitialFlow}|Opts]) when is_integer(InitialFlow), InitialFlow > 0 -> do_check_options(Opts); do_check_options([{keepalive, infinity}|Opts]) -> @@ -188,35 +187,23 @@ 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=[]}) -> - empty_commands_queue(CommandOrCommands); -handle_ret(Commands, #http2_state{commands_queue=Queue}) when is_list(Commands) -> - lists:reverse(Queue, empty_commands_queue(Commands)); -handle_ret(Command, #http2_state{commands_queue=Queue}) -> - lists:reverse([empty_commands_queue(Command)|Queue]). - -empty_commands_queue([{state, State}|Tail]) -> [{state, State#http2_state{commands_queue=[]}}|Tail]; -empty_commands_queue([Command|Tail]) -> [Command|empty_commands_queue(Tail)]; -empty_commands_queue([]) -> []; -empty_commands_queue({state, State}) -> {state, State#http2_state{commands_queue=[]}}; -empty_commands_queue(Command) -> Command. - -handle(Data, State=#http2_state{buffer=Buffer}, EvHandler, EvHandlerState) -> +handle(Data, State=#http2_state{buffer=Buffer}, CookieStore, EvHandler, EvHandlerState) -> parse(<< Buffer/binary, Data/binary >>, State#http2_state{buffer= <<>>}, - EvHandler, EvHandlerState). + CookieStore, EvHandler, EvHandlerState). parse(Data, State0=#http2_state{status=preface, http2_machine=HTTP2Machine}, - EvHandler, EvHandlerState0) -> + CookieStore0, EvHandler, EvHandlerState0) -> MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, 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, _}, EvHandlerState} -> {handle_ret(Error, State0), EvHandlerState}; - {State, EvHandlerState} -> parse(Rest, State, EvHandler, EvHandlerState) + case frame(State0#http2_state{status=connected}, Frame, CookieStore0, EvHandler, EvHandlerState0) of + {Error={error, _}, CookieStore, EvHandlerState} -> + {Error, CookieStore, EvHandlerState}; + {State, CookieStore, EvHandlerState} -> + parse(Rest, State, CookieStore, EvHandler, EvHandlerState) end; more -> - {handle_ret({state, State0#http2_state{buffer=Data}}, State0), EvHandlerState0}; + {{state, State0#http2_state{buffer=Data}}, CookieStore0, EvHandlerState0}; %% Any error in the preface is converted to this specific error %% to make debugging the problem easier (it's the server's fault). _ -> @@ -226,44 +213,49 @@ parse(Data, State0=#http2_state{status=preface, http2_machine=HTTP2Machine}, _ -> 'Invalid connection preface received. (RFC7540 3.5)' end, - {handle_ret(connection_error(State0, {connection_error, protocol_error, Reason}), State0), - EvHandlerState0} + {connection_error(State0, {connection_error, protocol_error, Reason}), + CookieStore0, EvHandlerState0} end; parse(Data, State0=#http2_state{status=Status, http2_machine=HTTP2Machine, streams=Streams}, - EvHandler, EvHandlerState0) -> + CookieStore0, EvHandler, EvHandlerState0) -> MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), case cow_http2:parse(Data, MaxFrameSize) of {ok, Frame, Rest} -> - case frame(State0, Frame, EvHandler, EvHandlerState0) of - {Error={error, _}, EvHandlerState} -> {handle_ret(Error, State0), EvHandlerState}; - {State, EvHandlerState} -> parse(Rest, State, EvHandler, EvHandlerState) + case frame(State0, Frame, CookieStore0, EvHandler, EvHandlerState0) of + {Error={error, _}, CookieStore, EvHandlerState} -> + {Error, CookieStore, EvHandlerState}; + {State, CookieStore, EvHandlerState} -> + parse(Rest, State, CookieStore, EvHandler, EvHandlerState) end; {ignore, Rest} -> case ignored_frame(State0) of - Error = {error, _} -> {handle_ret(Error, State0), EvHandlerState0}; - State -> parse(Rest, State, EvHandler, EvHandlerState0) + Error = {error, _} -> + {Error, CookieStore0, EvHandlerState0}; + State -> + parse(Rest, State, CookieStore0, EvHandler, EvHandlerState0) end; {stream_error, StreamID, Reason, Human, Rest} -> parse(Rest, reset_stream(State0, StreamID, {stream_error, Reason, Human}), - EvHandler, EvHandlerState0); + CookieStore0, EvHandler, EvHandlerState0); Error = {connection_error, _, _} -> - {handle_ret(connection_error(State0, Error), State0), EvHandlerState0}; + {connection_error(State0, Error), CookieStore0, 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 =:= #{} -> - {handle_ret([{state, State0#http2_state{buffer=Data, status=closing}}, close], State0), - EvHandlerState0}; + {[{state, State0#http2_state{buffer=Data, status=closing}}, close], + CookieStore0, EvHandlerState0}; %% Otherwise we enter the closing state. more when Status =:= goaway -> - {handle_ret([{state, State0#http2_state{buffer=Data, status=closing}}, closing(State0)], State0), - EvHandlerState0}; + {[{state, State0#http2_state{buffer=Data, status=closing}}, closing(State0)], + CookieStore0, EvHandlerState0}; more -> - {handle_ret({state, State0#http2_state{buffer=Data}}, State0), EvHandlerState0} + {{state, State0#http2_state{buffer=Data}}, + CookieStore0, EvHandlerState0} end. %% Frames received. -frame(State=#http2_state{http2_machine=HTTP2Machine0}, Frame, EvHandler, EvHandlerState0) -> +frame(State=#http2_state{http2_machine=HTTP2Machine0}, Frame, CookieStore, EvHandler, EvHandlerState0) -> EvHandlerState = if element(1, Frame) =:= headers; element(1, Frame) =:= push_promise -> EvStreamID = element(2, Frame), @@ -288,40 +280,49 @@ frame(State=#http2_state{http2_machine=HTTP2Machine0}, Frame, EvHandler, EvHandl case cow_http2_machine:frame(Frame, HTTP2Machine0) of %% We only update the connection's window when receiving a lingering data frame. {ok, HTTP2Machine} when element(1, Frame) =:= data -> - {update_window(State#http2_state{http2_machine=HTTP2Machine}), EvHandlerState}; + {update_window(State#http2_state{http2_machine=HTTP2Machine}), + CookieStore, EvHandlerState}; {ok, HTTP2Machine} -> {maybe_ack(State#http2_state{http2_machine=HTTP2Machine}, Frame), - EvHandlerState}; + CookieStore, EvHandlerState}; {ok, {data, StreamID, IsFin, Data}, HTTP2Machine} -> data_frame(State#http2_state{http2_machine=HTTP2Machine}, StreamID, IsFin, Data, - EvHandler, EvHandlerState); + CookieStore, EvHandler, EvHandlerState); {ok, {headers, StreamID, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP2Machine} -> headers_frame(State#http2_state{http2_machine=HTTP2Machine}, StreamID, IsFin, Headers, PseudoHeaders, BodyLen, - EvHandler, EvHandlerState); + CookieStore, EvHandler, EvHandlerState); {ok, {trailers, StreamID, Trailers}, HTTP2Machine} -> - trailers_frame(State#http2_state{http2_machine=HTTP2Machine}, - StreamID, Trailers, EvHandler, EvHandlerState); + {StateRet, EvHandlerStateRet} = trailers_frame( + State#http2_state{http2_machine=HTTP2Machine}, + StreamID, Trailers, EvHandler, EvHandlerState), + {StateRet, CookieStore, EvHandlerStateRet}; {ok, {rst_stream, StreamID, Reason}, HTTP2Machine} -> - rst_stream_frame(State#http2_state{http2_machine=HTTP2Machine}, - StreamID, Reason, EvHandler, EvHandlerState); + {StateRet, EvHandlerStateRet} = rst_stream_frame( + State#http2_state{http2_machine=HTTP2Machine}, + StreamID, Reason, EvHandler, EvHandlerState), + {StateRet, CookieStore, EvHandlerStateRet}; {ok, {push_promise, StreamID, PromisedStreamID, Headers, PseudoHeaders}, HTTP2Machine} -> - push_promise_frame(State#http2_state{http2_machine=HTTP2Machine}, + {StateRet, EvHandlerStateRet} = push_promise_frame( + State#http2_state{http2_machine=HTTP2Machine}, StreamID, PromisedStreamID, Headers, PseudoHeaders, - EvHandler, EvHandlerState); + EvHandler, EvHandlerState), + {StateRet, CookieStore, EvHandlerStateRet}; {ok, GoAway={goaway, _, _, _}, HTTP2Machine} -> {goaway(State#http2_state{http2_machine=HTTP2Machine}, GoAway), - EvHandlerState}; + CookieStore, EvHandlerState}; {send, SendData, HTTP2Machine} -> - send_data(maybe_ack(State#http2_state{http2_machine=HTTP2Machine}, Frame), SendData, - EvHandler, EvHandlerState); + {StateRet, EvHandlerStateRet} = send_data( + maybe_ack(State#http2_state{http2_machine=HTTP2Machine}, Frame), + SendData, EvHandler, EvHandlerState), + {StateRet, CookieStore, EvHandlerStateRet}; {error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} -> {reset_stream(State#http2_state{http2_machine=HTTP2Machine}, StreamID, {stream_error, Reason, Human}), - EvHandlerState}; + CookieStore, EvHandlerState}; {error, Error={connection_error, _, _}, HTTP2Machine} -> {connection_error(State#http2_state{http2_machine=HTTP2Machine}, Error), - EvHandlerState} + CookieStore, EvHandlerState} end. maybe_ack(State=#http2_state{socket=Socket, transport=Transport}, Frame) -> @@ -332,14 +333,18 @@ maybe_ack(State=#http2_state{socket=Socket, transport=Transport}, Frame) -> end, State. -data_frame(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0) -> - case get_stream_by_id(State, StreamID) of +data_frame(State0, StreamID, IsFin, Data, CookieStore0, EvHandler, EvHandlerState0) -> + case get_stream_by_id(State0, StreamID) of Stream=#stream{tunnel=undefined} -> - data_frame(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0, Stream); + {State, EvHandlerState} = data_frame1(State0, + StreamID, IsFin, Data, EvHandler, EvHandlerState0, Stream), + {State, CookieStore0, EvHandlerState}; Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> % %% @todo What about IsFin? - {Commands, EvHandlerState} = Proto:handle(Data, ProtoState0, EvHandler, EvHandlerState0), - tunnel_commands(Commands, Stream, State, EvHandler, EvHandlerState) + {Commands, CookieStore, EvHandlerState1} = Proto:handle(Data, + ProtoState0, CookieStore0, EvHandler, EvHandlerState0), + {State, EvHandlerState} = tunnel_commands(Commands, Stream, State0, EvHandler, EvHandlerState1), + {State, CookieStore, EvHandlerState} end. tunnel_commands(Command, Stream, State, EvHandler, EvHandlerState) @@ -356,10 +361,6 @@ tunnel_commands([{state, ProtoState}|Tail], Stream=#stream{tunnel=Tunnel}, State, EvHandler, EvHandlerState) -> tunnel_commands(Tail, Stream#stream{tunnel=Tunnel#tunnel{protocol_state=ProtoState}}, State, EvHandler, EvHandlerState); -tunnel_commands([SetCookie={set_cookie, _, _, _, _}|Tail], Stream, - State=#http2_state{commands_queue=Queue}, EvHandler, EvHandlerState) -> - tunnel_commands(Tail, Stream, State#http2_state{commands_queue=[SetCookie|Queue]}, - EvHandler, EvHandlerState); tunnel_commands([{error, _Reason}|_], #stream{id=StreamID}, State, _EvHandler, EvHandlerState) -> {delete_stream(State, StreamID), EvHandlerState}. @@ -372,7 +373,7 @@ continue_stream_ref(#http2_state{socket=#{handle_continue_stream_ref := Continue continue_stream_ref(State, StreamRef) -> stream_ref(State, StreamRef). -data_frame(State0, StreamID, IsFin, Data, EvHandler, EvHandlerState0, +data_frame1(State0, StreamID, IsFin, Data, EvHandler, EvHandlerState0, Stream=#stream{ref=StreamRef, reply_to=ReplyTo, flow=Flow0, handler_state=Handlers0}) -> {ok, Dec, Handlers} = gun_content_handler:handle(IsFin, Data, Handlers0), Flow = case Flow0 of @@ -409,11 +410,11 @@ data_frame(State0, StreamID, IsFin, Data, EvHandler, EvHandlerState0, {maybe_delete_stream(State, StreamID, remote, IsFin), EvHandlerState}. %% @todo Make separate functions for inform/connect/normal. -headers_frame(State0=#http2_state{transport=Transport, opts=Opts, - tunnel_transport=TunnelTransport, content_handlers=Handlers0, commands_queue=Commands}, +headers_frame(State=#http2_state{transport=Transport, opts=Opts, + tunnel_transport=TunnelTransport, content_handlers=Handlers0}, StreamID, IsFin, Headers, #{status := Status}, _BodyLen, - EvHandler, EvHandlerState0) -> - Stream = get_stream_by_id(State0, StreamID), + CookieStore0, EvHandler, EvHandlerState0) -> + Stream = get_stream_by_id(State, StreamID), #stream{ ref=StreamRef, reply_to=ReplyTo, @@ -421,7 +422,8 @@ headers_frame(State0=#http2_state{transport=Transport, opts=Opts, path=Path, tunnel=Tunnel } = Stream, - State = State0#http2_state{commands_queue=[{set_cookie, Authority, Path, Status, Headers}|Commands]}, + CookieStore = gun_cookies:set_cookie_header(scheme(State), + Authority, Path, Status, Headers, CookieStore0, Opts), RealStreamRef = stream_ref(State, StreamRef), if Status >= 100, Status =< 199 -> @@ -432,7 +434,7 @@ headers_frame(State0=#http2_state{transport=Transport, opts=Opts, status => Status, headers => Headers }, EvHandlerState0), - {State, EvHandlerState}; + {State, CookieStore, EvHandlerState}; Status >= 200, Status =< 299, element(#tunnel.state, Tunnel) =:= requested -> #tunnel{destination=Destination, info=TunnelInfo0} = Tunnel, #{host := DestHost, port := DestPort} = Destination, @@ -509,7 +511,7 @@ headers_frame(State0=#http2_state{transport=Transport, opts=Opts, ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState2), {store_stream(State, Stream#stream{tunnel=Tunnel#tunnel{ info=TunnelInfo, protocol=Proto, protocol_state=ProtoState}}), - EvHandlerState}; + CookieStore, EvHandlerState}; true -> ReplyTo ! {gun_response, self(), RealStreamRef, IsFin, Status, Headers}, EvHandlerState1 = EvHandler:response_headers(#{ @@ -532,7 +534,7 @@ headers_frame(State0=#http2_state{transport=Transport, opts=Opts, %% @todo Disable the tunnel if any. {maybe_delete_stream(store_stream(State, Stream#stream{handler_state=Handlers}), StreamID, remote, IsFin), - EvHandlerState} + CookieStore, EvHandlerState} end. trailers_frame(State, StreamID, Trailers, EvHandler, EvHandlerState0) -> @@ -612,17 +614,17 @@ ignored_frame(State=#http2_state{http2_machine=HTTP2Machine0}) -> end. %% We always pass handle_continue messages to the tunnel. -handle_continue(ContinueStreamRef, Msg, State0, EvHandler, EvHandlerState0) -> +handle_continue(ContinueStreamRef, Msg, State0, CookieStore0, EvHandler, EvHandlerState0) -> StreamRef = case ContinueStreamRef of [SR|_] -> SR; _ -> ContinueStreamRef end, case get_stream_by_ref(State0, StreamRef) of Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> - {Commands, EvHandlerState1} = Proto:handle_continue(ContinueStreamRef, - Msg, ProtoState0, EvHandler, EvHandlerState0), + {Commands, CookieStore, EvHandlerState1} = Proto:handle_continue(ContinueStreamRef, + Msg, ProtoState0, CookieStore0, EvHandler, EvHandlerState0), {State, EvHandlerState} = tunnel_commands(Commands, Stream, State0, EvHandler, EvHandlerState1), - {handle_ret({state, State}, State), EvHandlerState} + {{state, State}, CookieStore, 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} @@ -867,14 +869,9 @@ request(State, RealStreamRef=[StreamRef|_], ReplyTo, Method, _Host, _Port, initial_flow(infinity, #{flow := InitialFlow}) -> InitialFlow; initial_flow(InitialFlow, _) -> InitialFlow. -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, +prepare_headers(State=#http2_state{transport=Transport}, + Method, Host0, Port, Path, Headers0, CookieStore0) -> + Scheme = scheme(State), Authority = case lists:keyfind(<<"host">>, 1, Headers0) of {_, Host} -> Host; _ -> gun_http:host_header(Transport, Host0, Port) @@ -898,6 +895,15 @@ prepare_headers(#http2_state{transport=Transport}, Method, Host0, Port, Path, He }, {ok, PseudoHeaders, Headers, CookieStore}. +scheme(#http2_state{transport=Transport}) -> + case Transport of + gun_tls -> <<"https">>; + gun_tls_proxy -> <<"https">>; + gun_tcp -> <<"http">>; + gun_tcp_proxy -> <<"http">>; + gun_tls_proxy_http2_connect -> <<"http">> + end. + %% @todo Make all calls go through this clause. data(State=#http2_state{http2_machine=HTTP2Machine}, StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState) when is_reference(StreamRef) -> diff --git a/src/gun_protocols.erl b/src/gun_protocols.erl index 65d5211..d297019 100644 --- a/src/gun_protocols.erl +++ b/src/gun_protocols.erl @@ -49,11 +49,23 @@ handler_and_opts(ProtocolName, Opts) -> {Protocol, maps:get(Protocol:opts_name(), Opts, #{})}. -spec negotiated({ok, binary()} | {error, protocol_not_negotiated}, gun:protocols()) - -> http | http2 | raw | socks. -negotiated({ok, <<"h2">>}, _) -> http2; -negotiated({ok, <<"http/1.1">>}, _) -> http; -negotiated({error, protocol_not_negotiated}, [Protocol]) -> Protocol; -negotiated({error, protocol_not_negotiated}, _) -> http. + -> gun:protocol(). +negotiated({ok, <<"h2">>}, Protocols) -> + lists:foldl(fun + (E = http2, _) -> E; + (E = {http2, _}, _) -> E; + (_, Acc) -> Acc + end, http2, Protocols); +negotiated({ok, <<"http/1.1">>}, Protocols) -> + lists:foldl(fun + (E = http, _) -> E; + (E = {http, _}, _) -> E; + (_, Acc) -> Acc + end, http, Protocols); +negotiated({error, protocol_not_negotiated}, [Protocol]) -> + Protocol; +negotiated({error, protocol_not_negotiated}, _) -> + http. -spec stream_ref(gun:protocol()) -> undefined | gun:stream_ref(). stream_ref({_, ProtocolOpts}) -> maps:get(stream_ref, ProtocolOpts, undefined); diff --git a/src/gun_raw.erl b/src/gun_raw.erl index 8d0aa42..11b875c 100644 --- a/src/gun_raw.erl +++ b/src/gun_raw.erl @@ -19,7 +19,7 @@ -export([opts_name/0]). -export([has_keepalive/0]). -export([init/4]). --export([handle/4]). +-export([handle/5]). -export([closing/4]). -export([close/4]). -export([data/7]). @@ -44,10 +44,10 @@ init(ReplyTo, Socket, Transport, Opts) -> StreamRef = maps:get(stream_ref, Opts, undefined), {connected_data_only, #raw_state{ref=StreamRef, reply_to=ReplyTo, socket=Socket, transport=Transport}}. -handle(Data, State=#raw_state{ref=StreamRef, reply_to=ReplyTo}, _, EvHandlerState) -> +handle(Data, State=#raw_state{ref=StreamRef, reply_to=ReplyTo}, CookieStore, _, EvHandlerState) -> %% When we take over the entire connection there is no stream reference. ReplyTo ! {gun_data, self(), StreamRef, nofin, Data}, - {{state, State}, EvHandlerState}. + {{state, State}, CookieStore, EvHandlerState}. %% We can always close immediately. closing(_, _, _, EvHandlerState) -> diff --git a/src/gun_socks.erl b/src/gun_socks.erl index 752daec..5c89e26 100644 --- a/src/gun_socks.erl +++ b/src/gun_socks.erl @@ -20,7 +20,7 @@ -export([has_keepalive/0]). -export([init/4]). -export([switch_transport/3]). --export([handle/4]). +-export([handle/5]). -export([closing/4]). -export([close/4]). %% @todo down @@ -99,8 +99,8 @@ init(ReplyTo, Socket, Transport, Opts) -> switch_transport(Transport, Socket, State) -> State#socks_state{socket=Socket, transport=Transport}. -handle(Data, State, _, EvHandlerState) -> - {handle(Data, State), EvHandlerState}. +handle(Data, State, CookieStore, _, EvHandlerState) -> + {handle(Data, State), CookieStore, EvHandlerState}. %% No authentication. handle(<<5, 0>>, State=#socks_state{version=5, status=auth_method_select}) -> diff --git a/src/gun_tunnel.erl b/src/gun_tunnel.erl index a1435f3..7c29684 100644 --- a/src/gun_tunnel.erl +++ b/src/gun_tunnel.erl @@ -18,8 +18,8 @@ -module(gun_tunnel). -export([init/6]). --export([handle/4]). --export([handle_continue/5]). +-export([handle/5]). +-export([handle_continue/6]). -export([update_flow/4]). -export([closing/4]). -export([close/4]). @@ -87,10 +87,7 @@ %% 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}, - - %% We must queue some commands given by the sub-protocol. - commands_queue = [] :: [{set_cookie, iodata(), iodata(), cow_http:status(), cow_http:headers()}] + | {origin, binary(), inet:hostname() | inet:ip_address(), inet:port_number(), connect | socks5} }). %% Socket is the "origin socket" and Transport the "origin transport". @@ -158,20 +155,21 @@ init(ReplyTo, OriginSocket, OriginTransport, Opts=#{stream_ref := StreamRef, tun %% or we decrypt it and pass it via handle_continue for TLS. handle(Data, State0=#tunnel_state{transport=gun_tcp_proxy, protocol=Proto, protocol_state=ProtoState0}, - EvHandler, EvHandlerState0) -> - {Commands, EvHandlerState1} = Proto:handle(Data, ProtoState0, EvHandler, EvHandlerState0), + CookieStore0, EvHandler, EvHandlerState0) -> + {Commands, CookieStore, EvHandlerState1} = Proto:handle( + Data, ProtoState0, CookieStore0, EvHandler, EvHandlerState0), {State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1), - {ret({state, State}, State), EvHandlerState}; + {{state, State}, CookieStore, EvHandlerState}; handle(Data, State=#tunnel_state{transport=gun_tls_proxy, socket=ProxyPid, tls_origin_socket=OriginSocket}, - _EvHandler, EvHandlerState) -> + CookieStore, _EvHandler, EvHandlerState) -> %% When we receive a DATA frame that contains TLS-encoded data, %% we must first forward it to the ProxyPid to be decoded. The %% Gun process will receive it back as a tls_proxy_http2_connect %% message and forward it to the right stream via the handle_continue %% callback. ProxyPid ! {tls_proxy_http2_connect, OriginSocket, Data}, - {ret({state, State}, State), EvHandlerState}. + {{state, State}, CookieStore, EvHandlerState}. %% This callback will only be called for TLS. %% @@ -180,18 +178,18 @@ handle(Data, State=#tunnel_state{transport=gun_tls_proxy, handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {ok, Negotiated}, {handle_continue, _, HandshakeEvent, Protocols}}, State=#tunnel_state{socket=ProxyPid, stream_ref=StreamRef, opts=Opts}, - EvHandler, EvHandlerState0) + CookieStore, EvHandler, EvHandlerState0) when is_reference(ContinueStreamRef) -> #{reply_to := ReplyTo} = HandshakeEvent, NewProtocol = gun_protocols:negotiated(Negotiated, Protocols), {Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts), EvHandlerState1 = EvHandler:tls_handshake_end(HandshakeEvent#{ socket => ProxyPid, - protocol => NewProtocol + protocol => Proto:name() }, EvHandlerState0), EvHandlerState = EvHandler:protocol_changed(#{ stream_ref => StreamRef, - protocol => NewProtocol + protocol => Proto:name() }, EvHandlerState1), %% @todo Terminate the current protocol or something? OriginSocket = #{ @@ -202,11 +200,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()}, - {ret({state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, State), - EvHandlerState}; + {{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, + CookieStore, EvHandlerState}; handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, Reason}, {handle_continue, _, HandshakeEvent, _}}, - State=#tunnel_state{socket=ProxyPid}, EvHandler, EvHandlerState0) + #tunnel_state{socket=ProxyPid}, CookieStore, EvHandler, EvHandlerState0) when is_reference(ContinueStreamRef) -> EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ error => Reason @@ -221,44 +219,45 @@ 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. - {ret({error, Reason}, State), EvHandlerState}; + {{error, Reason}, CookieStore, 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}, - State=#tunnel_state{}, _EvHandler, EvHandlerState) + #tunnel_state{}, CookieStore, _EvHandler, EvHandlerState) when is_reference(ContinueStreamRef) -> - {ret({send, IsFin, Data}, State), EvHandlerState}; + {{send, IsFin, Data}, CookieStore, EvHandlerState}; handle_continue(ContinueStreamRef, {tls_proxy, ProxyPid, Data}, State0=#tunnel_state{socket=ProxyPid, protocol=Proto, protocol_state=ProtoState}, - EvHandler, EvHandlerState0) + CookieStore0, EvHandler, EvHandlerState0) when is_reference(ContinueStreamRef) -> - {Commands, EvHandlerState1} = Proto:handle(Data, ProtoState, EvHandler, EvHandlerState0), + {Commands, CookieStore, EvHandlerState1} = Proto:handle( + Data, ProtoState, CookieStore0, EvHandler, EvHandlerState0), {State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1), - {ret({state, State}, State), EvHandlerState}; + {{state, State}, CookieStore, EvHandlerState}; handle_continue(ContinueStreamRef, {tls_proxy_closed, ProxyPid}, - State=#tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0) + #tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0) when is_reference(ContinueStreamRef) -> %% @todo All sub-streams must produce a stream_error. - {ret({error, closed}, State), EvHandlerState0}; + {{error, closed}, CookieStore, EvHandlerState0}; handle_continue(ContinueStreamRef, {tls_proxy_error, ProxyPid, Reason}, - State=#tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0) + #tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0) when is_reference(ContinueStreamRef) -> %% @todo All sub-streams must produce a stream_error. - {ret({error, Reason}, State), EvHandlerState0}; + {{error, Reason}, CookieStore, EvHandlerState0}; %% We always dereference the ContinueStreamRef because it includes a %% reference() for Socks layers too. %% %% @todo Assert StreamRef to be our reference(). handle_continue([_StreamRef|ContinueStreamRef0], Msg, State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState}, - EvHandler, EvHandlerState0) -> + CookieStore0, EvHandler, EvHandlerState0) -> ContinueStreamRef = case ContinueStreamRef0 of [CSR] -> CSR; _ -> ContinueStreamRef0 end, - {Commands, EvHandlerState1} = Proto:handle_continue(ContinueStreamRef, - Msg, ProtoState, EvHandler, EvHandlerState0), + {Commands, CookieStore, EvHandlerState1} = Proto:handle_continue( + ContinueStreamRef, Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0), {State, EvHandlerState} = commands(Commands, State0, EvHandler, EvHandlerState1), - {ret({state, State}, State), EvHandlerState}. + {{state, State}, CookieStore, EvHandlerState}. %% @todo This function will need EvHandler/EvHandlerState? update_flow(State0=#tunnel_state{protocol=Proto, protocol_state=ProtoState}, @@ -266,11 +265,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), - ret({state, State}, State). + {state, State}. -closing(_Reason, State, _EvHandler, EvHandlerState) -> +closing(_Reason, _State, _EvHandler, EvHandlerState) -> %% @todo Graceful shutdown must be propagated to tunnels. - {ret([], State), EvHandlerState}. + {[], EvHandlerState}. close(_Reason, _State, _EvHandler, EvHandlerState) -> %% @todo Closing must be propagated to tunnels. @@ -346,14 +345,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), - {ret({state, State}, State), EvHandlerState}. + {{state, State}, EvHandlerState}. timeout(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, Msg, TRef) -> case Proto:timeout(ProtoState0, Msg, TRef) of {state, ProtoState} -> - ret({state, State#tunnel_state{protocol_state=ProtoState}}, State); + {state, State#tunnel_state{protocol_state=ProtoState}}; Other -> - ret(Other, State) + Other end. stream_info(#tunnel_state{transport=Transport0, stream_ref=TunnelStreamRef, reply_to=ReplyTo, @@ -415,9 +414,9 @@ 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, @@ -439,7 +438,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), - {ret({state, State}, State), EvHandlerState}. + {{state, State}, EvHandlerState}. %% Internal. @@ -449,11 +448,6 @@ commands([], State, _, EvHandlerState) -> {State, EvHandlerState}; commands([{state, ProtoState}|Tail], State, EvHandler, EvHandlerState) -> commands(Tail, State#tunnel_state{protocol_state=ProtoState}, 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}, @@ -604,17 +598,3 @@ 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/src/gun_ws.erl b/src/gun_ws.erl index cd81d65..f413f94 100644 --- a/src/gun_ws.erl +++ b/src/gun_ws.erl @@ -20,7 +20,7 @@ -export([has_keepalive/0]). -export([default_keepalive/0]). -export([init/4]). --export([handle/4]). +-export([handle/5]). -export([update_flow/4]). -export([closing/4]). -export([close/4]). @@ -101,6 +101,10 @@ init(ReplyTo, Socket, Transport, #{stream_ref := StreamRef, headers := Headers, socket=Socket, transport=Transport, opts=Opts, extensions=Extensions, flow=InitialFlow, handler=Handler, handler_state=HandlerState}}. +handle(Data, State, CookieStore, EvHandler, EvHandlerState0) -> + {Commands, EvHandlerState} = handle(Data, State, EvHandler, EvHandlerState0), + {Commands, CookieStore, EvHandlerState}. + %% Do not handle anything if we received a close frame. %% Initiate or terminate the closing state depending on whether we sent a close yet. handle(_, State=#ws_state{in=close, out=close}, _, EvHandlerState) -> diff --git a/test/rfc6265bis_SUITE.erl b/test/rfc6265bis_SUITE.erl index 8beee58..b4b443d 100644 --- a/test/rfc6265bis_SUITE.erl +++ b/test/rfc6265bis_SUITE.erl @@ -158,9 +158,8 @@ do_informational_set_cookie(Config, Boolean) -> Protocol = config(protocol, Config), {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ transport => config(transport, Config), - protocols => [Protocol], - cookie_store => gun_cookies_list:init(), - cookie_ignore_informational => Boolean + protocols => [{Protocol, #{cookie_ignore_informational => Boolean}}], + cookie_store => gun_cookies_list:init() }), {ok, Protocol} = gun:await_up(ConnPid), StreamRef1 = gun:get(ConnPid, "/informational"), @@ -176,13 +175,46 @@ do_informational_set_cookie(Config, Boolean) -> gun:close(ConnPid), Res. -set_cookie_connect(Config) -> +set_cookie_connect_tcp(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, ProxyPid, ProxyPort} = event_SUITE:do_proxy_start(Protocol, tcp), {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => tcp, + 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). + +set_cookie_connect_tls(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, tls), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => tls, protocols => [Protocol], cookie_store => gun_cookies_list:init() }), -- cgit v1.2.3