aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-08-19 17:24:27 +0200
committerLoïc Hoguin <[email protected]>2020-09-21 15:51:57 +0200
commitca68d184abbf7bd1030b2f2035cc66c13d08dd5d (patch)
tree77a5800cbfc3da76e7863e1bbb51d83442bddf50
parenta1729d5584364412f72d0d6337447da653da865b (diff)
downloadgun-ca68d184abbf7bd1030b2f2035cc66c13d08dd5d.tar.gz
gun-ca68d184abbf7bd1030b2f2035cc66c13d08dd5d.tar.bz2
gun-ca68d184abbf7bd1030b2f2035cc66c13d08dd5d.zip
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.
-rw-r--r--Makefile2
-rw-r--r--ebin/gun.app2
-rw-r--r--src/gun.erl62
-rw-r--r--src/gun_http2.erl257
-rw-r--r--src/gun_tls_proxy.erl24
-rw-r--r--src/gun_tls_proxy_http2_connect.erl61
-rw-r--r--test/rfc7540_SUITE.erl21
7 files changed, 397 insertions, 32 deletions
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 <[email protected]>
+%%
+%% 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),
#{