aboutsummaryrefslogtreecommitdiffstats
path: root/src/gun_tunnel.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-09-18 17:01:25 +0200
committerLoïc Hoguin <[email protected]>2020-09-21 15:52:26 +0200
commit8033850ab81ca0639489636bb8760d93900d4a80 (patch)
tree94c2df630a4c6fce97f6192a63a663a25f43266c /src/gun_tunnel.erl
parente740356b5881c39a95715d6081689802edf469a0 (diff)
downloadgun-8033850ab81ca0639489636bb8760d93900d4a80.tar.gz
gun-8033850ab81ca0639489636bb8760d93900d4a80.tar.bz2
gun-8033850ab81ca0639489636bb8760d93900d4a80.zip
Initial success for h2 CONNECT -> https CONNECT -> https
Diffstat (limited to 'src/gun_tunnel.erl')
-rw-r--r--src/gun_tunnel.erl414
1 files changed, 414 insertions, 0 deletions
diff --git a/src/gun_tunnel.erl b/src/gun_tunnel.erl
new file mode 100644
index 0000000..8da4c5a
--- /dev/null
+++ b/src/gun_tunnel.erl
@@ -0,0 +1,414 @@
+%% 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.
+
+%% This module is used when a tunnel is established and either
+%% StreamRef dereference or a TLS proxy process must be handled
+%% by the tunnel layer.
+-module(gun_tunnel).
+
+-export([init/4]).
+-export([handle/4]).
+-export([handle_continue/5]).
+-export([update_flow/4]).
+-export([closing/4]).
+-export([close/4]).
+-export([keepalive/3]).
+-export([headers/11]).
+-export([request/12]).
+-export([data/7]).
+-export([connect/7]).
+-export([cancel/5]).
+-export([timeout/3]).
+-export([stream_info/2]).
+-export([tunneled_name/1]).
+-export([down/1]).
+%-export([ws_upgrade/10]).
+
+-record(tunnel_state, {
+ %% Fake socket and transport.
+ socket = undefined :: #{
+ gun_pid := pid(),
+ reply_to := pid(),
+ stream_ref := gun:stream_ref(),
+ handle_continue_stream_ref := gun:stream_ref()
+ } | pid(),
+ transport = undefined :: gun_tcp_proxy | gun_tls_proxy,
+
+ %% The stream_ref from which the stream was created. When
+ %% the tunnel exists as a result of HTTP/2 CONNECT -> HTTP/1.1 CONNECT
+ %% the stream_ref is the same as the HTTP/1.1 CONNECT one.
+ stream_ref = undefined :: gun:stream_ref(),
+
+ %% When the tunnel is a 'connect' tunnel we must dereference the
+ %% stream_ref. When it is 'socks' we must not as there was no
+ %% stream involved in creating the tunnel.
+ type = undefined :: connect | socks,
+
+ %% Tunnel information.
+ info = undefined :: gun:tunnel_info(),
+
+ %% The origin socket of the TLS proxy, if any. This is used to forward
+ %% messages to the proxy process in order to decrypt the data.
+ tls_origin_socket = undefined :: undefined | #{
+ gun_pid := pid(),
+ reply_to := pid(),
+ stream_ref := gun:stream_ref(),
+ handle_continue_stream_ref => gun:stream_ref()
+ },
+
+ opts = undefined :: undefined | any(), %% @todo Opts type.
+
+ %% Protocol module and state of the outer layer. Only initialized
+ %% after the TLS handshake has completed when TLS is involved.
+ protocol = undefined :: module(),
+ protocol_state = undefined :: any()
+}).
+
+%% Socket is the "origin socket" and Transport the "origin transport".
+%% When the Transport indicate a TLS handshake was requested, the socket
+%% and transport are given to the intermediary TLS proxy process.
+%%
+%% Opts is the options for the underlying HTTP/2 connection,
+%% with some extra information added for the tunnel.
+%%
+%% @todo Mark the tunnel options as reserved.
+init(ReplyTo, OriginSocket, OriginTransport, Opts=#{stream_ref := StreamRef, tunnel := Tunnel}) ->
+ #{
+ type := TunnelType,
+ info := TunnelInfo
+ } = Tunnel,
+ State = #tunnel_state{stream_ref=StreamRef, type=TunnelType, info=TunnelInfo,
+ opts=maps:without([stream_ref, tunnel], Opts)},
+ case Tunnel of
+ %% Initialize the protocol.
+ #{new_protocol := NewProtocol} ->
+ {Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
+ {_, ProtoState} = Proto:init(ReplyTo, OriginSocket, OriginTransport,
+ ProtoOpts#{stream_ref => StreamRef}),
+%% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
+ ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
+ {tunnel, State#tunnel_state{socket=OriginSocket, transport=OriginTransport,
+ protocol=Proto, protocol_state=ProtoState}};
+ %% We can't initialize the protocol until the TLS handshake has completed.
+ #{handshake_event := HandshakeEvent, protocols := Protocols} ->
+ #{handle_continue_stream_ref := ContinueStreamRef} = OriginSocket,
+ %% @todo FIX THIS!!
+ % #{
+ % origin_host := DestHost,
+ % origin_port := DestPort
+ % } = TunnelInfo,
+%% @todo OK so Protocol:init/4 will need to have EvHandler/EvHandlerState!
+%% Otherwise we can't do the TLS events.
+ #{
+ tls_opts := TLSOpts,
+ timeout := TLSTimeout
+ } = HandshakeEvent,
+ {ok, ProxyPid} = gun_tls_proxy:start_link("fake", 12345,% @todo FIX THIS!! DestHost, DestPort,
+ TLSOpts, TLSTimeout, OriginSocket, gun_tls_proxy_http2_connect,
+ {handle_continue, ContinueStreamRef, HandshakeEvent, Protocols}),
+ {tunnel, State#tunnel_state{socket=ProxyPid, transport=gun_tls_proxy,
+ tls_origin_socket=OriginSocket}}
+ end.
+
+%% When we receive data we pass it forward directly for TCP;
+%% or we decrypt it and pass it via handle_continue for TLS.
+handle(Data, State=#tunnel_state{transport=gun_tcp_proxy,
+ protocol=Proto, protocol_state=ProtoState0},
+ EvHandler, EvHandlerState0) ->
+ {Commands, EvHandlerState} = Proto:handle(Data, ProtoState0, EvHandler, EvHandlerState0),
+ {{state, commands(Commands, State)}, EvHandlerState};
+handle(Data, State=#tunnel_state{transport=gun_tls_proxy,
+ socket=ProxyPid, tls_origin_socket=OriginSocket},
+ _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},
+ {{state, State}, EvHandlerState}.
+
+%% This callback will only be called for TLS.
+%%
+%% The StreamRef in this callback is special because it includes
+%% a reference() for Socks layers as well.
+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)
+ when is_reference(ContinueStreamRef) ->
+ #{reply_to := ReplyTo} = HandshakeEvent,
+ NewProtocol = gun_protocols:negotiated(Negotiated, Protocols),
+ {Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
+% EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
+% socket => Socket,
+% protocol => NewProtocol
+% }, EvHandlerState0),
+ %% @todo Terminate the current protocol or something?
+ OriginSocket = #{
+ gun_pid => self(),
+ reply_to => ReplyTo,
+ stream_ref => StreamRef%,
+% handle_continue_stream_ref => ContinueStreamRef
+ },
+ {_, ProtoState} = Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy,
+ ProtoOpts#{stream_ref => StreamRef}),
+ ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
+ {{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, EvHandlerState0};
+handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, _Reason},
+ {handle_continue, _, _HandshakeEvent, _}},
+ #tunnel_state{socket=ProxyPid}, _EvHandler, EvHandlerState0)
+ when is_reference(ContinueStreamRef) ->
+%% 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.
+ {[], EvHandlerState0};
+%% 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},
+ #tunnel_state{socket=Socket, transport=Transport}, _EvHandler, EvHandlerState)
+ when is_reference(ContinueStreamRef) ->
+ {[{send, IsFin, Data}], EvHandlerState};
+handle_continue(ContinueStreamRef, {tls_proxy, ProxyPid, Data},
+ State=#tunnel_state{socket=ProxyPid, protocol=Proto, protocol_state=ProtoState},
+ EvHandler, EvHandlerState0)
+ when is_reference(ContinueStreamRef) ->
+ {Commands, EvHandlerState} = Proto:handle(Data, ProtoState, EvHandler, EvHandlerState0),
+ {{state, commands(Commands, State)}, EvHandlerState};
+%% @todo What to do about those? Does it matter which one closes/errors out?
+handle_continue(ContinueStreamRef, {tls_proxy_closed, ProxyPid},
+ #tunnel_state{socket=ProxyPid}, _EvHandler, _EvHandlerState0)
+ when is_reference(ContinueStreamRef) ->
+ todo;
+handle_continue(ContinueStreamRef, {tls_proxy_error, ProxyPid, _Reason},
+ #tunnel_state{socket=ProxyPid}, _EvHandler, _EvHandlerState0)
+ when is_reference(ContinueStreamRef) ->
+ todo;
+%% 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,
+ State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
+ EvHandler, EvHandlerState0) ->
+ ContinueStreamRef = case ContinueStreamRef0 of
+ [CSR] -> CSR;
+ _ -> ContinueStreamRef0
+ end,
+ {Commands, EvHandlerState} = Proto:handle_continue(ContinueStreamRef,
+ Msg, ProtoState, EvHandler, EvHandlerState0),
+ {{state, commands(Commands, State)}, EvHandlerState}.
+
+%% @todo Probably just pass it forward?
+update_flow(_State, _ReplyTo, _StreamRef, _Inc) ->
+ todo.
+
+%% @todo ?
+closing(_Reason, _State, _EvHandler, _EvHandlerState) ->
+ todo.
+
+%% @todo ?
+close(_Reason, _State, _EvHandler, _EvHandlerState) ->
+ todo.
+
+%% @todo ?
+keepalive(_State, _EvHandler, _EvHandlerState) ->
+ todo.
+
+%% We pass the headers forward and optionally dereference StreamRef.
+headers(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
+ StreamRef0, ReplyTo, Method, Host, Port, Path, Headers,
+ InitialFlow, EvHandler, EvHandlerState0) ->
+ StreamRef = maybe_dereference(State, StreamRef0),
+ {ProtoState, EvHandlerState} = Proto:headers(ProtoState0, StreamRef,
+ ReplyTo, Method, Host, Port, Path, Headers,
+ InitialFlow, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}.
+
+%% We pass the request forward and optionally dereference StreamRef.
+request(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
+ StreamRef0, ReplyTo, Method, Host, Port, Path, Headers, Body,
+ InitialFlow, EvHandler, EvHandlerState0) ->
+ StreamRef = maybe_dereference(State, StreamRef0),
+ {ProtoState, EvHandlerState} = Proto:request(ProtoState0, StreamRef,
+ ReplyTo, Method, Host, Port, Path, Headers, Body,
+ InitialFlow, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}.
+
+%% We pass the data forward and optionally dereference StreamRef.
+data(State=#tunnel_state{socket=Socket, transport=Transport,
+ stream_ref=TunnelStreamRef0, protocol=Proto, protocol_state=ProtoState0},
+ StreamRef0, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
+ TunnelStreamRef = if
+ is_list(TunnelStreamRef0) -> lists:last(TunnelStreamRef0);
+ true -> TunnelStreamRef0
+ end,
+ case StreamRef0 of
+ TunnelStreamRef ->
+ ok = Transport:send(Socket, Data),
+ {State, EvHandlerState0};
+ _ ->
+ StreamRef = maybe_dereference(State, StreamRef0),
+ {ProtoState, EvHandlerState} = Proto:data(ProtoState0, StreamRef,
+ ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}
+ end.
+
+%% We pass the CONNECT request forward and optionally dereference StreamRef.
+connect(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
+ StreamRef0, ReplyTo, Destination, TunnelInfo, Headers, InitialFlow) ->
+ StreamRef = maybe_dereference(State, StreamRef0),
+ ProtoState = Proto:connect(ProtoState0, StreamRef,
+ ReplyTo, Destination, TunnelInfo, Headers, InitialFlow),
+ State#tunnel_state{protocol_state=ProtoState}.
+
+%% @todo ?
+cancel(_State, _StreamRef, _ReplyTo, _EvHandler, _EvHandlerState) ->
+ todo.
+
+%% @todo ?
+%% ... we might have to do update Cowlib there...
+timeout(_State, {cow_http2_machine, _Name}, _TRef) ->
+ todo.
+
+stream_info(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState}, StreamRef0) ->
+ StreamRef = maybe_dereference(State, StreamRef0),
+ Proto:stream_info(ProtoState, StreamRef).
+
+tunneled_name(#tunnel_state{protocol=Proto}) ->
+ Proto:name().
+
+%% @todo ?
+down(_State) ->
+ todo.
+
+%% Internal.
+
+commands(Command, State) when not is_list(Command) ->
+ commands([Command], State);
+commands([], State) ->
+ State;
+commands([{state, ProtoState}|Tail], State) ->
+ commands(Tail, State#tunnel_state{protocol_state=ProtoState});
+%% @todo We must pass down the set_cookie commands. Have a commands_queue.
+commands([_SetCookie={set_cookie, _, _, _, _}|Tail], State=#tunnel_state{}) ->
+ commands(Tail, State);
+commands([{send, IsFin, Data}|Tail], State=#tunnel_state{socket=Socket, transport=Transport}) ->
+ Transport:send(Socket, Data),
+ commands(Tail, State);
+%% @todo How to handle origin changes?
+commands([{origin, _, _NewHost, _NewPort, _Type}|Tail], State) ->
+ commands(Tail, State);
+commands([{switch_protocol, NewProtocol, ReplyTo}|Tail],
+ State=#tunnel_state{stream_ref=TunnelStreamRef, opts=Opts, protocol=CurrentProto}) ->
+ Type = case CurrentProto:name() of
+ socks -> socks;
+ _ -> connect
+ end,
+ StreamRef = case Type of
+ socks -> TunnelStreamRef;
+ connect -> gun_protocols:stream_ref(NewProtocol)
+ end,
+ ContinueStreamRef0 = continue_stream_ref(State),
+ ContinueStreamRef = case Type of
+ socks -> ContinueStreamRef0 ++ [make_ref()];
+ connect -> ContinueStreamRef0 ++ [if is_list(StreamRef) -> lists:last(StreamRef); true -> StreamRef end]
+ end,
+ OriginSocket = #{
+ gun_pid => self(),
+ reply_to => ReplyTo,
+ stream_ref => StreamRef,
+ handle_continue_stream_ref => ContinueStreamRef
+ },
+ ProtoOpts = Opts#{
+ stream_ref => StreamRef,
+ tunnel => #{
+ type => Type,
+ info => #{}, %% @todo
+ new_protocol => NewProtocol
+ }
+ },
+ Proto = gun_tunnel,
+ {_, ProtoState} = Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts),
+%% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
+ commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState});
+commands([{tls_handshake, HandshakeEvent, Protocols, ReplyTo}|Tail],
+ State=#tunnel_state{opts=Opts, protocol=CurrentProto}) ->
+ Type = case CurrentProto:name() of
+ socks -> socks;
+ _ -> connect
+ end,
+ #{
+ stream_ref := StreamRef
+ } = HandshakeEvent,
+ ContinueStreamRef0 = continue_stream_ref(State),
+ ContinueStreamRef = case Type of
+ socks -> ContinueStreamRef0 ++ [make_ref()];
+ connect -> ContinueStreamRef0 ++ [lists:last(StreamRef)]
+ end,
+ OriginSocket = #{
+ gun_pid => self(),
+ reply_to => ReplyTo,
+ stream_ref => StreamRef,
+ handle_continue_stream_ref => ContinueStreamRef
+ },
+ ProtoOpts = Opts#{
+ stream_ref => StreamRef,
+ tunnel => #{
+ type => Type,
+ info => #{}, %% @todo
+ handshake_event => HandshakeEvent,
+ protocols => Protocols
+ }
+ },
+ Proto = gun_tunnel,
+ {_, ProtoState} = Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts),
+ commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState});
+commands([{active, true}|Tail], State) ->
+ commands(Tail, State).
+
+continue_stream_ref(#tunnel_state{socket=#{handle_continue_stream_ref := ContinueStreamRef}}) ->
+ if
+ is_list(ContinueStreamRef) -> ContinueStreamRef;
+ true -> [ContinueStreamRef]
+ end;
+continue_stream_ref(#tunnel_state{tls_origin_socket=#{handle_continue_stream_ref := ContinueStreamRef}}) ->
+ if
+ is_list(ContinueStreamRef) -> ContinueStreamRef;
+ true -> [ContinueStreamRef]
+ end.
+
+maybe_dereference(#tunnel_state{stream_ref=RealStreamRef,
+ type=connect, protocol=gun_tunnel}, [_StreamRef|Tail]) ->
+ %% @todo Assert that we got the right stream.
+% StreamRef = if is_list(RealStreamRef) -> lists:last(RealStreamRef); true -> RealStreamRef end,
+ case Tail of
+ [Ref] -> Ref;
+ _ -> Tail
+ end;
+%% We do not dereference when we are the target.
+%% For example when creating a new stream on the origin via tunnel(s).
+maybe_dereference(#tunnel_state{type=connect}, StreamRef) ->
+ StreamRef;
+maybe_dereference(#tunnel_state{type=socks}, StreamRef) ->
+ StreamRef.