From ca68d184abbf7bd1030b2f2035cc66c13d08dd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 19 Aug 2020 17:24:27 +0200 Subject: First working HTTPS over secure HTTP/2 Has a timer:sleep/1 though because there is currently no way to wait for the TLS handshake to complete. --- Makefile | 2 +- ebin/gun.app | 2 +- src/gun.erl | 62 ++++++++- src/gun_http2.erl | 257 ++++++++++++++++++++++++++++++++---- src/gun_tls_proxy.erl | 24 ++++ src/gun_tls_proxy_http2_connect.erl | 61 +++++++++ test/rfc7540_SUITE.erl | 21 +++ 7 files changed, 397 insertions(+), 32 deletions(-) create mode 100644 src/gun_tls_proxy_http2_connect.erl diff --git a/Makefile b/Makefile index 93c757f..a09039d 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ dep_ci.erlang.mk = git https://github.com/ninenines/ci.erlang.mk master DEP_EARLY_PLUGINS = ci.erlang.mk AUTO_CI_OTP ?= OTP-22+ -AUTO_CI_HIPE ?= OTP-LATEST +#AUTO_CI_HIPE ?= OTP-LATEST # AUTO_CI_ERLLVM ?= OTP-LATEST AUTO_CI_WINDOWS ?= OTP-22+ diff --git a/ebin/gun.app b/ebin/gun.app index c0f21d1..7ca0c8f 100644 --- a/ebin/gun.app +++ b/ebin/gun.app @@ -1,7 +1,7 @@ {application, 'gun', [ {description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."}, {vsn, "2.0.0-pre.2"}, - {modules, ['gun','gun_app','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_public_suffix','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']}, + {modules, ['gun','gun_app','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_public_suffix','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_ws','gun_ws_h']}, {registered, [gun_sup]}, {applications, [kernel,stdlib,ssl,cowlib]}, {mod, {gun_app, []}}, diff --git a/src/gun.erl b/src/gun.erl index 9d16f58..24ec9c0 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -102,7 +102,9 @@ -export([domain_lookup/3]). -export([connecting/3]). -export([initial_tls_handshake/3]). +-export([ensure_alpn_sni/3]). -export([tls_handshake/3]). +-export([protocol_negotiated/2]). -export([connected/3]). -export([connected_data_only/3]). -export([connected_no_input/3]). @@ -180,7 +182,10 @@ origin_port => inet:port_number(), %% Non-stream intermediaries (for example SOCKS). - intermediaries => [intermediary()] + intermediaries => [intermediary()], + + %% TLS proxy. + tls_proxy_pid => pid() }. -export_type([tunnel_info/0]). @@ -1049,10 +1054,10 @@ connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts, {next_event, internal, {retries, Retries, Reason}}} end. -initial_tls_handshake(_, {retries, Retries, Socket}, State0=#state{opts=Opts}) -> +initial_tls_handshake(_, {retries, Retries, Socket}, State0=#state{opts=Opts, origin_host=OriginHost}) -> Protocols = maps:get(protocols, Opts, [http2, http]), HandshakeEvent = #{ - tls_opts => ensure_alpn_sni(Protocols, maps:get(tls_opts, Opts, []), State0), + tls_opts => ensure_alpn_sni(Protocols, maps:get(tls_opts, Opts, []), OriginHost), timeout => maps:get(tls_handshake_timeout, Opts, infinity) }, case normal_tls_handshake(Socket, State0, HandshakeEvent, Protocols) of @@ -1064,7 +1069,7 @@ initial_tls_handshake(_, {retries, Retries, Socket}, State0=#state{opts=Opts}) - {next_event, internal, {retries, Retries, Reason}}} end. -ensure_alpn_sni(Protocols0, TransOpts0, #state{origin_host=OriginHost}) -> +ensure_alpn_sni(Protocols0, TransOpts0, OriginHost) -> %% ALPN. Protocols = [case P of http -> <<"http/1.1">>; @@ -1101,7 +1106,7 @@ tls_handshake(internal, {tls_handshake, HandshakeEvent0=#{tls_opts := TLSOpts0, timeout := TLSTimeout}, Protocols, ReplyTo}, State=#state{socket=Socket, transport=Transport, origin_host=OriginHost, origin_port=OriginPort, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> - TLSOpts = ensure_alpn_sni(Protocols, TLSOpts0, State), + TLSOpts = ensure_alpn_sni(Protocols, TLSOpts0, OriginHost), HandshakeEvent = HandshakeEvent0#{ tls_opts => TLSOpts, socket => Socket @@ -1130,9 +1135,10 @@ tls_handshake(info, {gun_tls_proxy, Socket, Error = {error, Reason}, {HandshakeE tls_handshake(Type, Event, State) -> handle_common_connected_no_input(Type, Event, ?FUNCTION_NAME, State). -normal_tls_handshake(Socket, State=#state{event_handler=EvHandler, event_handler_state=EvHandlerState0}, +normal_tls_handshake(Socket, State=#state{ + origin_host=OriginHost, event_handler=EvHandler, event_handler_state=EvHandlerState0}, HandshakeEvent0=#{tls_opts := TLSOpts0, timeout := TLSTimeout}, Protocols) -> - TLSOpts = ensure_alpn_sni(Protocols, TLSOpts0, State), + TLSOpts = ensure_alpn_sni(Protocols, TLSOpts0, OriginHost), HandshakeEvent = HandshakeEvent0#{ tls_opts => TLSOpts, socket => Socket @@ -1355,6 +1361,48 @@ handle_common_connected_no_input(info, {Closed, Socket}, _, handle_common_connected_no_input(info, {Error, Socket, Reason}, _, State=#state{socket=Socket, messages={_, _, Error}}) -> disconnect(State, {error, Reason}); +%% Socket events from TLS proxy sockets set up by HTTP/2 CONNECT. +%% 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, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:handle_continue(StreamRef, Msg, + ProtoState, EvHandler, EvHandlerState0), + case commands(Commands, State0#state{event_handler_state=EvHandlerState}) of + {keep_state, State} -> + {keep_state, active(State)}; + {next_state, closing, State, Actions} -> + {next_state, closing, active(State), Actions}; + Res -> + Res + end; +%% @todo +% NewProtocol = protocol_negotiated(Negotiated, Protocols), +% EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ +% socket => Socket, +% protocol => NewProtocol +% }, EvHandlerState0), +% commands([{switch_protocol, NewProtocol, ReplyTo}], State0#state{event_handler_state=EvHandlerState}); +%% +% State=#state{socket=Socket, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> +% EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ +% error => Reason +% }, EvHandlerState0), +% commands([Error], State#state{event_handler_state=EvHandlerState}); +handle_common_connected_no_input(info, {handle_continue, StreamRef, Msg}, _, + State0=#state{protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:handle_continue(StreamRef, Msg, + ProtoState, EvHandler, EvHandlerState0), + case commands(Commands, State0#state{event_handler_state=EvHandlerState}) of + {keep_state, State} -> + {keep_state, active(State)}; + {next_state, closing, State, Actions} -> + {next_state, closing, active(State), Actions}; + Res -> + Res + end; %% Timeouts. %% @todo HTTP/2 requires more timeouts than just the keepalive timeout. %% We should have a timeout function in protocols that deal with diff --git a/src/gun_http2.erl b/src/gun_http2.erl index 11dbb3d..bd74957 100644 --- a/src/gun_http2.erl +++ b/src/gun_http2.erl @@ -22,6 +22,7 @@ -export([init/4]). -export([switch_transport/3]). -export([handle/4]). +-export([handle_continue/5]). -export([update_flow/4]). -export([closing/4]). -export([close/4]). @@ -58,6 +59,7 @@ %% CONNECT tunnel. tunnel :: {module(), any(), gun:tunnel_info()} | {setup, gun:connect_destination(), gun:tunnel_info()} + | {tls_handshake, gun:connect_destination(), gun:tunnel_info()} | undefined }). @@ -311,6 +313,23 @@ data_frame(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0) -> case get_stream_by_id(State, StreamID) of Stream=#stream{tunnel=undefined} -> data_frame(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0, Stream); + #stream{ref=StreamRef, reply_to=ReplyTo, + tunnel={_Protocol, _ProtoState, #{tls_proxy_pid := ProxyPid}}} -> + %% 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. + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => stream_ref(State, StreamRef) + }, + ProxyPid ! {tls_proxy_http2_connect, OriginSocket, Data}, +io:format(user, "(~p) ~p:~p/~p: data ~p~n", + [self(), ?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY, Data]), + %% @todo What about IsFin? + {State, EvHandlerState0}; Stream=#stream{tunnel={Protocol, ProtoState0, TunnelInfo}} -> %% @todo Can't call Protocol:handle directly, may need to unwrap TLS first... @@ -373,6 +392,7 @@ tunnel_commands([{switch_protocol, Protocol0, ReplyTo}|Tail], Stream=#stream{ref _ -> ok end, OriginSocket = #{ + gun_pid => self(), reply_to => ReplyTo, stream_ref => StreamRef }, @@ -453,28 +473,61 @@ headers_frame(State0=#http2_state{content_handlers=Handlers0, commands_queue=Com status => Status, headers => Headers }, EvHandlerState0), - %% @todo Handle TLS over TCP and TLS over TLS. - tcp = maps:get(transport, Destination, tcp), - [Protocol0] = maps:get(protocols, Destination, [http]), - %% Options are either passed directly or #{} is used. Since the - %% protocol only applies to a stream we cannot use connection-wide options. - {Protocol, ProtoOpts} = case Protocol0 of - {P, PO} -> {gun:protocol_handler(P), PO}; - P -> {gun:protocol_handler(P), #{}} - end, - %% @todo What about the StateName returned? OriginSocket = #{ + gun_pid => self(), reply_to => ReplyTo, stream_ref => RealStreamRef }, - OriginTransport = gun_tcp_proxy, - {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, OriginTransport, - ProtoOpts#{stream_ref => RealStreamRef}), - %% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0), - %% @todo What about keepalive? - {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, - TunnelInfo#{origin_host => DestHost, origin_port => DestPort}}}), - EvHandlerState}; + case Destination of + #{transport := tls} -> + Protocols = maps:get(protocols, Destination, [http2, http]), + TLSOpts = gun:ensure_alpn_sni(Protocols, maps:get(tls_opts, Destination, []), DestHost), + TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity), +% HandshakeEvent = #{ +% stream_ref => StreamRef, +% reply_to => ReplyTo, +% tls_opts => maps:get(tls_opts, Destination, []), +% timeout => maps:get(tls_handshake_timeout, Destination, infinity) +% }, +%tls_handshake(internal, {tls_handshake, +% HandshakeEvent0=#{tls_opts := TLSOpts0, timeout := TLSTimeout}, Protocols, ReplyTo}, +% State=#state{socket=Socket, transport=Transport, origin_host=OriginHost, origin_port=OriginPort, +% event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> +% HandshakeEvent = HandshakeEvent0#{ +% tls_opts => TLSOpts, +% socket => Socket +% }, +% EvHandlerState = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), + HandshakeEvent = undefined, + {ok, ProxyPid} = gun_tls_proxy:start_link(DestHost, DestPort, + TLSOpts, TLSTimeout, OriginSocket, gun_tls_proxy_http2_connect, + %% @todo ? +% {HandshakeEvent, Protocols, ReplyTo}), + {handle_continue, RealStreamRef, HandshakeEvent, Protocols}), +% commands([{switch_transport, gun_tls_proxy, ProxyPid}], State#state{ +% socket=ProxyPid, transport=gun_tls_proxy, event_handler_state=EvHandlerState}); + %% @todo What about keepalive? + {store_stream(State, Stream#stream{tunnel={tls_handshake, Destination, + TunnelInfo#{origin_host => DestHost, origin_port => DestPort, + %% @todo Fine having it, but we want the socket pid to simulate active. + tls_proxy_pid => ProxyPid}}}), + EvHandlerState}; + _ -> + [Protocol0] = maps:get(protocols, Destination, [http]), + %% Options are either passed directly or #{} is used. Since the + %% protocol only applies to a stream we cannot use connection-wide options. + {Protocol, ProtoOpts} = case Protocol0 of + {P, PO} -> {gun:protocol_handler(P), PO}; + P -> {gun:protocol_handler(P), #{}} + end, + %% @todo What about the StateName returned? + {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts#{stream_ref => RealStreamRef}), + %% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0), + %% @todo What about keepalive? + {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, + TunnelInfo#{origin_host => DestHost, origin_port => DestPort}}}), + EvHandlerState} + end; true -> ReplyTo ! {gun_response, self(), stream_ref(State, StreamRef), IsFin, Status, Headers}, EvHandlerState1 = EvHandler:response_headers(#{ @@ -573,6 +626,140 @@ ignored_frame(State=#http2_state{http2_machine=HTTP2Machine0}) -> connection_error(State#http2_state{http2_machine=HTTP2Machine}, Error) end. +%% Continue handling or sending the data. +handle_continue(StreamRef, Msg, State, EvHandler, EvHandlerState0) + when is_reference(StreamRef) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{id=StreamID, reply_to=ReplyTo, + tunnel={tls_handshake, Destination, TunnelInfo=#{tls_proxy_pid := ProxyPid}}} -> + case Msg of + {gun_tls_proxy, ProxyPid, {ok, Negotiated}, + {handle_continue, _, _HandshakeEvent, Protocols}} -> + #{host := DestHost, port := DestPort} = Destination, + RealStreamRef = stream_ref(State, StreamRef), + NewProtocol = gun:protocol_negotiated(Negotiated, Protocols), +% EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ +% socket => Socket, +% protocol => NewProtocol +% }, EvHandlerState0), + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => RealStreamRef + }, + {Protocol, ProtoOpts} = case NewProtocol of + {P, PO} -> {gun:protocol_handler(P), PO}; + P -> {gun:protocol_handler(P), #{}} + end, + {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, gun_tcp_proxy, + ProtoOpts#{stream_ref => RealStreamRef}), + {{state, store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, + TunnelInfo#{origin_host => DestHost, origin_port => DestPort}}})}, + EvHandlerState0}; + {gun_tls_proxy, ProxyPid, {error, _Reason}, + {handle_continue, _, _HandshakeEvent, _}} -> +% EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ +% error => Reason +% }, EvHandlerState0), +%% @todo +% The TCP connection can be closed by either peer. The END_STREAM flag +% on a DATA frame is treated as being equivalent to the TCP FIN bit. A +% client is expected to send a DATA frame with the END_STREAM flag set +% after receiving a frame bearing the END_STREAM flag. A proxy that +% receives a DATA frame with the END_STREAM flag set sends the attached +% data with the FIN bit set on the last TCP segment. A proxy that +% 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. + {{state, State}, EvHandlerState0}; + %% Data that must be sent as a DATA frame. + {data, ReplyTo, _, IsFin, Data} -> + {State1, EvHandlerState} = maybe_send_data(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0), + {{state, State1}, EvHandlerState} + end; + Stream=#stream{id=StreamID, tunnel={Protocol, ProtoState0, TunnelInfo=#{tls_proxy_pid := ProxyPid}}} -> + case Msg of + %% Data that was received and decrypted. + {tls_proxy, ProxyPid, Data} -> +io:format(user, "(~p) ~p:~p/~p: data ~p~n", + [self(), ?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY, Data]), + {Commands, EvHandlerState} = Protocol:handle(Data, ProtoState0, EvHandler, EvHandlerState0), + {tunnel_commands(Commands, Stream, Protocol, TunnelInfo, State), EvHandlerState}; + %% @todo What to do about those? + {tls_proxy_closed, ProxyPid} -> + todo; + {tls_proxy_error, ProxyPid, _Reason} -> + todo; + %% Data that must be sent as a DATA frame. + {data, ReplyTo, _, IsFin, Data} -> + {State1, EvHandlerState} = maybe_send_data(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0), + {{state, State1}, EvHandlerState} + end + + +% {store_stream(State, Stream#stream{tunnel={Proto, ProtoState, TunnelInfo}}), 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} + end; + + + +% [Protocol0] = maps:get(protocols, Destination, [http]), +% %% Options are either passed directly or #{} is used. Since the +% %% protocol only applies to a stream we cannot use connection-wide options. +% {Protocol, ProtoOpts} = case Protocol0 of +% {P, PO} -> {gun:protocol_handler(P), PO}; +% P -> {gun:protocol_handler(P), #{}} +% end, +% %% @todo What about the StateName returned? +% {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, gun_tcp_proxy, +% ProtoOpts#{stream_ref => RealStreamRef}), +% %% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0), +% %% @todo What about keepalive? +% {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, +% TunnelInfo#{origin_host => DestHost, origin_port => DestPort}}}), +% EvHandlerState} +% +% +% todo; +%% Tunneled data. +handle_continue([StreamRef|Tail], Msg, State, EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{tunnel={Proto, ProtoState0, TunnelInfo}} -> + {ProtoState, EvHandlerState} = Proto:handle_continue(normalize_stream_ref(Tail), + Msg, ProtoState0, EvHandler, EvHandlerState0), + {store_stream(State, Stream#stream{tunnel={Proto, ProtoState, TunnelInfo}}), 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} + end. + + + + +%data(State=#http2_state{http2_machine=HTTP2Machine}, StreamRef, ReplyTo, IsFin, Data, +% EvHandler, EvHandlerState) when is_reference(StreamRef) -> +% case get_stream_by_ref(State, StreamRef) of +% #stream{id=StreamID} -> +% case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of +% {ok, fin, _} -> +% {error_stream_closed(State, StreamRef, ReplyTo), EvHandlerState}; +% {ok, _, fin} -> +% {error_stream_closed(State, StreamRef, ReplyTo), EvHandlerState}; +% {ok, _, _} -> +% maybe_send_data(State, StreamID, IsFin, Data, EvHandler, EvHandlerState) +% end; +% error -> +% {error_stream_not_found(State, StreamRef, ReplyTo), EvHandlerState} +% end; +%%% Tunneled data. +%data(State, [StreamRef|Tail], ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) -> + + + + + update_flow(State, _ReplyTo, StreamRef, Inc) -> case get_stream_by_ref(State, StreamRef) of Stream=#stream{id=StreamID, flow=Flow0} -> @@ -836,14 +1023,32 @@ normalize_stream_ref(StreamRef) -> StreamRef. data(State=#http2_state{http2_machine=HTTP2Machine}, StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState) when is_reference(StreamRef) -> case get_stream_by_ref(State, StreamRef) of - #stream{id=StreamID} -> + #stream{id=StreamID, tunnel=Tunnel} -> case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of {ok, fin, _} -> {error_stream_closed(State, StreamRef, ReplyTo), EvHandlerState}; {ok, _, fin} -> {error_stream_closed(State, StreamRef, ReplyTo), EvHandlerState}; {ok, _, _} -> - maybe_send_data(State, StreamID, IsFin, Data, EvHandler, EvHandlerState) +io:format(user, "(~p) ~p:~p/~p: data ~p~n", + [self(), ?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY, Data]), + +%% @todo The data to be sent on the tunnel neeeds to be encrypted as well! So we need +%% to have a different clause when we have a tunnel AND it has a tls_proxy_pid in TunnelInfo. +%% But we would need to differentiate between the incoming data and the encrypted data so +%% that we do not encrypt it in a loop. +%% +%% So I guess we need an handle_continue. + + case Tunnel of + %% We need to encrypt the data before we can send it. We send it + %% directly to the gun_tls_proxy process and then + {_, _, #{tls_proxy_pid := ProxyPid}} -> + ok = gun_tls_proxy:send(ProxyPid, Data), + {State, EvHandlerState}; + _ -> + maybe_send_data(State, StreamID, IsFin, Data, EvHandler, EvHandlerState) + end end; error -> {error_stream_not_found(State, StreamRef, ReplyTo), EvHandlerState} @@ -994,16 +1199,22 @@ timeout(State=#http2_state{http2_machine=HTTP2Machine0}, {cow_http2_machine, Nam stream_info(State, StreamRef) when is_reference(StreamRef) -> case get_stream_by_ref(State, StreamRef) of - #stream{reply_to=ReplyTo, tunnel={Protocol, _, #{ + #stream{reply_to=ReplyTo, tunnel={Protocol, _, TunnelInfo=#{ origin_host := OriginHost, origin_port := OriginPort}}} -> {ok, #{ ref => StreamRef, reply_to => ReplyTo, state => running, tunnel => #{ - transport => tcp, %% @todo + transport => case TunnelInfo of + #{tls_proxy_pid := _} -> tls; + _ -> tcp + end, protocol => Protocol:name(), - origin_scheme => <<"http">>, %% @todo + origin_scheme => case TunnelInfo of + #{tls_proxy_pid := _} -> <<"https">>; + _ -> <<"http">> + end, origin_host => OriginHost, origin_port => OriginPort } diff --git a/src/gun_tls_proxy.erl b/src/gun_tls_proxy.erl index 2b08088..35e83b1 100644 --- a/src/gun_tls_proxy.erl +++ b/src/gun_tls_proxy.erl @@ -95,6 +95,7 @@ extra :: any() }). +-define(DEBUG_PROXY, 1). -ifdef(DEBUG_PROXY). -define(DEBUG_LOG(Format, Args), io:format(user, "(~p) ~p:~p/~p:" ++ Format ++ "~n", @@ -114,6 +115,8 @@ start_link(Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra) -> {ok, Pid} when is_port(OutSocket) -> ok = gen_tcp:controlling_process(OutSocket, Pid), {ok, Pid}; + {ok, Pid} when is_map(OutSocket) -> + {ok, Pid}; {ok, Pid} when not is_pid(OutSocket) -> ok = ssl:controlling_process(OutSocket, Pid), {ok, Pid}; @@ -262,6 +265,27 @@ connected({call, From}, Msg={send, Data}, State=#state{proxy_socket=Socket}) -> ?DEBUG_LOG("spawned ~0p", [SpawnedPid]), keep_state_and_data; %% Messages from the proxy socket. +%% +%% When the out_socket is a #{stream_ref := _} map we know that processing +%% of the data isn't yet complete. We wrap the message in a handle_continue +%% tuple and provide the StreamRef for further processing. +connected(info, Msg={ssl, Socket, Data}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket, + out_socket=#{stream_ref := StreamRef}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {handle_continue, StreamRef, {tls_proxy, self(), Data}}, + keep_state_and_data; +connected(info, Msg={ssl_closed, Socket}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket, + out_socket=#{stream_ref := StreamRef}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {handle_continue, StreamRef, {tls_proxy_closed, self()}}, + keep_state_and_data; +connected(info, Msg={ssl_error, Socket, Reason}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket, + out_socket=#{stream_ref := StreamRef}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {handle_continue, StreamRef, {tls_proxy_error, self(), Reason}}, + keep_state_and_data; +%% When the out_socket is anything else then the data is sent like normal +%% socket data. It does not need to be handled specially. connected(info, Msg={ssl, Socket, Data}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), OwnerPid ! {tls_proxy, self(), Data}, diff --git a/src/gun_tls_proxy_http2_connect.erl b/src/gun_tls_proxy_http2_connect.erl new file mode 100644 index 0000000..e70454a --- /dev/null +++ b/src/gun_tls_proxy_http2_connect.erl @@ -0,0 +1,61 @@ +%% Copyright (c) 2020, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_tls_proxy_http2_connect). + +-export([name/0]). +-export([messages/0]). +-export([connect/3]). +-export([connect/4]). +-export([send/2]). +-export([setopts/2]). +-export([sockname/1]). +-export([close/1]). + +-type socket() :: #{ + reply_to := pid(), + stream_ref := reference() | [reference()] +}. + +name() -> tls_proxy_http2_connect. + +%% We need different message tags because the messages +%% must be propagated to the right stream. +messages() -> {tls_proxy_http2_connect, tls_proxy_http2_connect_closed, tls_proxy_http2_connect_error}. + +-spec connect(_, _, _) -> no_return(). +connect(_, _, _) -> + error(not_implemented). + +-spec connect(_, _, _, _) -> no_return(). +connect(_, _, _, _) -> + error(not_implemented). + +-spec send(socket(), iodata()) -> ok. +send(#{gun_pid := GunPid, reply_to := ReplyTo, stream_ref := StreamRef}, Data) -> + GunPid ! {handle_continue, StreamRef, {data, ReplyTo, StreamRef, nofin, Data}}, + ok. + +-spec setopts(_, _) -> no_return(). +setopts(_, _) -> +% error(not_implemented). + ok. + +-spec sockname(_) -> no_return(). +sockname(_) -> + error(not_implemented). + +-spec close(socket()) -> ok. +close(_) -> + ok. diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index ebc5392..a6bb440 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -435,11 +435,26 @@ connect_http_via_h2c(_) -> "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), do_connect_http(<<"http">>, tcp, http, <<"http">>, tcp). +%% @todo https + connect_http_via_h2(_) -> doc("CONNECT can be used to establish a TCP connection " "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), do_connect_http(<<"http">>, tcp, http, <<"https">>, tls). +connect_https_via_h2(_) -> + +%dbg:tracer(), +%dbg:tpl(gun, []), +%dbg:tpl(gun_http2, []), +%dbg:tpl(gun_tls_proxy, []), +%dbg:tpl(gun_tls_proxy_http2_connect, []), +%dbg:p(all, c), + + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"https">>, tls, http, <<"https">>, tls). + connect_h2c_via_h2c(_) -> doc("CONNECT can be used to establish a TCP connection " "to an HTTP/2 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), @@ -499,6 +514,12 @@ do_connect_http(OriginScheme, OriginTransport, OriginProtocol, ProxyScheme, Prox }} = receive_from(ProxyPid), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), handshake_completed = receive_from(OriginPid), + %% @todo The 200 response must not be sent before the TLS handshake completed successfully? + %% Or the coming request must be kept around until the tunnel is up? We probably need + %% to gun_tunnel_up or something to inform the user the tunnel is up. + %% + %% @todo QUEUE data until the tunnel is up? Send a gun_up of some kind? + timer:sleep(1000), ProxiedStreamRef = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef}), #{<<":authority">> := Authority} = receive_from(OriginPid), #{ -- cgit v1.2.3