%% 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/6]).
-export([handle/5]).
-export([handle_continue/6]).
-export([update_flow/4]).
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
-export([connect/9]).
-export([cancel/5]).
-export([timeout/3]).
-export([stream_info/2]).
-export([tunneled_name/2]).
-export([down/1]).
-export([ws_upgrade/11]).
-export([ws_send/6]).
-record(tunnel_state, {
%% Fake socket and transport.
%% We accept 'undefined' only to simplify the init code.
socket = undefined :: #{
gun_pid := pid(),
reply_to := pid(),
stream_ref := gun:stream_ref(),
handle_continue_stream_ref := gun:stream_ref()
} | pid() | undefined,
transport = undefined :: gun_tcp_proxy | gun_tls_proxy | undefined,
%% 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(),
%% The pid we send messages to.
reply_to = undefined :: pid(),
%% 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 | socks5,
%% Transport and protocol name of the tunnel layer.
tunnel_transport = undefined :: tcp | tls,
tunnel_protocol = undefined :: http | http2 | 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(),
%% When the protocol is being switched the origin may change.
%% 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}
}).
%% 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},
EvHandler, EvHandlerState0) ->
#{
type := TunnelType,
transport_name := TunnelTransport,
protocol_name := TunnelProtocol,
info := TunnelInfo
} = Tunnel,
State = #tunnel_state{stream_ref=StreamRef, reply_to=ReplyTo, type=TunnelType,
tunnel_transport=TunnelTransport, tunnel_protocol=TunnelProtocol,
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),
case Proto:init(ReplyTo, OriginSocket, OriginTransport,
ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tcp}) of
{ok, _, ProtoState} ->
EvHandlerState = EvHandler:protocol_changed(#{
stream_ref => StreamRef,
protocol => Proto:name()
}, EvHandlerState0),
%% When the tunnel protocol is HTTP/1.1 or SOCKS
%% the gun_tunnel_up message was already sent.
_ = case TunnelProtocol of
http -> ok;
socks -> ok;
_ -> ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()}
end,
{tunnel, State#tunnel_state{socket=OriginSocket, transport=OriginTransport,
protocol=Proto, protocol_state=ProtoState},
EvHandlerState};
Error={error, _} ->
Error
end;
%% We can't initialize the protocol until the TLS handshake has completed.
#{handshake_event := HandshakeEvent0, protocols := Protocols} ->
#{handle_continue_stream_ref := ContinueStreamRef} = OriginSocket,
#{
origin_host := DestHost,
origin_port := DestPort
} = TunnelInfo,
#{
tls_opts := TLSOpts,
timeout := TLSTimeout
} = HandshakeEvent0,
HandshakeEvent = HandshakeEvent0#{socket => OriginSocket},
EvHandlerState = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0),
{ok, ProxyPid} = gun_tls_proxy:start_link(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}, EvHandlerState}
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},
CookieStore0, EvHandler, EvHandlerState0) ->
{Commands, CookieStore, EvHandlerState1} = Proto:handle(
Data, ProtoState0, CookieStore0, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, CookieStore, EvHandlerState};
handle(Data, State=#tunnel_state{transport=gun_tls_proxy,
socket=ProxyPid, tls_origin_socket=OriginSocket},
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},
{{state, State}, CookieStore, 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},
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 => Proto:name()
}, EvHandlerState0),
EvHandlerState = EvHandler:protocol_changed(#{
stream_ref => StreamRef,
protocol => Proto:name()
}, EvHandlerState1),
%% @todo Terminate the current protocol or something?
OriginSocket = #{
gun_pid => self(),
reply_to => ReplyTo,
stream_ref => StreamRef
},
case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy,
ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tls}) of
{ok, _, ProtoState} ->
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
{{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}},
CookieStore, EvHandlerState};
Error={error, _} ->
{Error, CookieStore, EvHandlerState}
end;
handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, Reason},
{handle_continue, _, HandshakeEvent, _}},
#tunnel_state{socket=ProxyPid}, CookieStore, 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.
{{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},
#tunnel_state{}, CookieStore, _EvHandler, EvHandlerState)
when is_reference(ContinueStreamRef) ->
{{send, IsFin, Data}, CookieStore, EvHandlerState};
handle_continue(ContinueStreamRef, {tls_proxy, ProxyPid, Data},
State=#tunnel_state{socket=ProxyPid, protocol=Proto, protocol_state=ProtoState},
CookieStore0, EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
{Commands, CookieStore, EvHandlerState1} = Proto:handle(
Data, ProtoState, CookieStore0, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, CookieStore, EvHandlerState};
handle_continue(ContinueStreamRef, {tls_proxy_closed, ProxyPid},
#tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
%% @todo All sub-streams must produce a stream_error.
{{error, closed}, CookieStore, EvHandlerState0};
handle_continue(ContinueStreamRef, {tls_proxy_error, ProxyPid, Reason},
#tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0)
when is_reference(ContinueStreamRef) ->
%% @todo All sub-streams must produce a stream_error.
{{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,
State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
CookieStore0, EvHandler, EvHandlerState0) ->
ContinueStreamRef = case ContinueStreamRef0 of
[CSR] -> CSR;
_ -> ContinueStreamRef0
end,
{Commands, CookieStore, EvHandlerState1} = Proto:handle_continue(
ContinueStreamRef, Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, CookieStore, EvHandlerState}.
%% @todo This function will need EvHandler/EvHandlerState?
update_flow(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
ReplyTo, StreamRef0, Inc) ->
StreamRef = maybe_dereference(State, StreamRef0),
Commands = Proto:update_flow(ProtoState, ReplyTo, StreamRef, Inc),
{ResCommands, undefined} = commands(Commands, State, undefined, undefined),
ResCommands.
closing(_Reason, _State, _EvHandler, EvHandlerState) ->
%% @todo Graceful shutdown must be propagated to tunnels.
{[], EvHandlerState}.
close(_Reason, _State, _EvHandler, EvHandlerState) ->
%% @todo Closing must be propagated to tunnels.
EvHandlerState.
keepalive(_State, _EvHandler, EvHandlerState) ->
%% @todo Need to figure out how to handle keepalive for tunnels.
{[], EvHandlerState}.
%% 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, CookieStore0, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
{Commands, CookieStore, EvHandlerState1} = Proto:headers(ProtoState0, StreamRef,
ReplyTo, Method, Host, Port, Path, Headers,
InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, CookieStore, EvHandlerState}.
%% We pass the request forward and optionally dereference StreamRef.
request(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
info=#{origin_host := OriginHost, origin_port := OriginPort}},
StreamRef0, ReplyTo, Method, _Host, _Port, Path, Headers, Body,
InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
{Commands, CookieStore, EvHandlerState1} = Proto:request(ProtoState0, StreamRef,
ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body,
InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, CookieStore, EvHandlerState}.
%% When the next tunnel is SOCKS we pass the data forward directly.
%% This is needed because SOCKS has no StreamRef and the data cannot
%% therefore be passed forward through the usual method.
data(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
protocol_origin={origin, _, _, _, socks5}},
StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
{Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef,
ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, EvHandlerState};
%% CONNECT tunnels pass the data forward and dereference StreamRef
%% unless they are the recipient of the callback, in which case the
%% data is sent to the socket.
data(State=#tunnel_state{socket=Socket, transport=Transport,
stream_ref=TunnelStreamRef0, protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
TunnelStreamRef = outer_stream_ref(TunnelStreamRef0),
case StreamRef0 of
TunnelStreamRef ->
case Transport:send(Socket, Data) of
ok -> {[], EvHandlerState0};
Error={error, _} -> {Error, EvHandlerState0}
end;
_ ->
StreamRef = maybe_dereference(State, StreamRef0),
{Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef,
ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State,
EvHandler, EvHandlerState1),
{ResCommands, EvHandlerState}
end.
%% We pass the CONNECT request forward and optionally dereference StreamRef.
connect(State=#tunnel_state{info=#{origin_host := Host, origin_port := Port},
protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, Destination, _, Headers, InitialFlow,
EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
{Commands, EvHandlerState1} = Proto:connect(ProtoState0, StreamRef,
ReplyTo, Destination, #{host => Host, port => Port}, Headers, InitialFlow,
EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, EvHandlerState}.
cancel(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
{Commands, EvHandlerState1} = Proto:cancel(ProtoState0, StreamRef,
ReplyTo, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, EvHandlerState}.
timeout(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, Msg, TRef) ->
case Proto:timeout(ProtoState0, Msg, TRef) of
{state, ProtoState} ->
{state, State#tunnel_state{protocol_state=ProtoState}};
Other ->
Other
end.
stream_info(#tunnel_state{transport=Transport0, stream_ref=TunnelStreamRef, reply_to=ReplyTo,
tunnel_protocol=TunnelProtocol,
info=#{origin_host := OriginHost, origin_port := OriginPort},
protocol=Proto, protocol_state=ProtoState}, StreamRef)
when is_reference(StreamRef), TunnelProtocol =/= socks ->
Transport = case Transport0 of
gun_tcp_proxy -> tcp;
gun_tls_proxy -> tls
end,
Protocol = case Proto of
gun_tunnel -> Proto:tunneled_name(ProtoState, false);
_ -> Proto:name()
end,
{ok, #{
ref => TunnelStreamRef,
reply_to => ReplyTo,
state => running,
tunnel => #{
transport => Transport,
protocol => Protocol,
origin_scheme => case {Transport, Protocol} of
{_, raw} -> undefined;
{tcp, _} -> <<"http">>;
{tls, _} -> <<"https">>
end,
origin_host => OriginHost,
origin_port => OriginPort
}
}};
stream_info(State=#tunnel_state{type=Type,
tunnel_transport=IntermediaryTransport, tunnel_protocol=IntermediaryProtocol,
info=TunnelInfo, protocol=Proto, protocol_state=ProtoState}, StreamRef0) ->
StreamRef = maybe_dereference(State, StreamRef0),
case Proto:stream_info(ProtoState, StreamRef) of
{ok, undefined} ->
{ok, undefined};
{ok, Info} ->
#{
host := IntermediateHost,
port := IntermediatePort
} = TunnelInfo,
IntermediaryInfo = #{
type => Type,
host => IntermediateHost,
port => IntermediatePort,
transport => IntermediaryTransport,
protocol => IntermediaryProtocol
},
Intermediaries = maps:get(intermediaries, Info, []),
{ok, Info#{
intermediaries => [IntermediaryInfo|Intermediaries]
}}
end.
tunneled_name(#tunnel_state{protocol=Proto=gun_tunnel, protocol_state=ProtoState}, true) ->
Proto:tunneled_name(ProtoState, false);
tunneled_name(#tunnel_state{tunnel_protocol=TunnelProto}, false) ->
TunnelProto;
tunneled_name(#tunnel_state{protocol=Proto}, _) ->
Proto:name().
down(_State) ->
%% @todo Tunnels must be included in the gun_down message.
[].
ws_upgrade(State=#tunnel_state{info=TunnelInfo, protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, _, _, Path, Headers, WsOpts,
CookieStore0, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
#{
origin_host := Host,
origin_port := Port
} = TunnelInfo,
{Commands, CookieStore, EvHandlerState1} = Proto:ws_upgrade(ProtoState0, StreamRef, ReplyTo,
Host, Port, Path, Headers, WsOpts,
CookieStore0, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, CookieStore, EvHandlerState}.
ws_send(Frames, State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
StreamRef0, ReplyTo, EvHandler, EvHandlerState0) ->
StreamRef = maybe_dereference(State, StreamRef0),
{Commands, EvHandlerState1} = Proto:ws_send(Frames,
ProtoState, StreamRef, ReplyTo, EvHandler, EvHandlerState0),
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
{ResCommands, EvHandlerState}.
%% Internal.
%% Returns an error on send errors, a state otherwise
commands(Command, State, EvHandler, EvHandlerState) when not is_list(Command) ->
commands([Command], State, EvHandler, EvHandlerState);
commands([], State, _, EvHandlerState) ->
{{state, State}, EvHandlerState};
commands([Error = {error, _}|_],
State=#tunnel_state{socket=Socket, transport=Transport},
_, EvHandlerState) ->
%% We must terminate the TLS proxy pid if any.
case Transport of
gun_tls_proxy -> gun_tls_proxy:close(Socket);
_ -> ok
end,
{[{state, State}, Error], EvHandlerState};
commands([{state, ProtoState}|Tail], State, EvHandler, EvHandlerState) ->
commands(Tail, State#tunnel_state{protocol_state=ProtoState}, EvHandler, EvHandlerState);
%% @todo What to do about IsFin?
commands([{send, _IsFin, Data}|Tail],
State=#tunnel_state{socket=Socket, transport=Transport},
EvHandler, EvHandlerState) ->
case Transport:send(Socket, Data) of
ok -> commands(Tail, State, EvHandler, EvHandlerState);
Error={error, _} -> {Error, EvHandlerState}
end;
commands([Origin={origin, Scheme, Host, Port, Type}|Tail],
State=#tunnel_state{stream_ref=StreamRef},
EvHandler, EvHandlerState0) ->
EvHandlerState = EvHandler:origin_changed(#{
stream_ref => StreamRef,
type => Type,
origin_scheme => Scheme,
origin_host => Host,
origin_port => Port
}, EvHandlerState0),
commands(Tail, State#tunnel_state{protocol_origin=Origin}, EvHandler, EvHandlerState);
commands([{switch_protocol, NewProtocol, ReplyTo}|Tail],
State=#tunnel_state{socket=Socket, transport=Transport, opts=Opts,
protocol_origin=undefined},
EvHandler, EvHandlerState0) ->
{Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
%% This should only apply to Websocket for the time being.
case Proto:init(ReplyTo, Socket, Transport, ProtoOpts) of
{ok, connected_ws_only, ProtoState} ->
#{stream_ref := StreamRef} = ProtoOpts,
EvHandlerState = EvHandler:protocol_changed(#{
stream_ref => StreamRef,
protocol => Proto:name()
}, EvHandlerState0),
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState},
EvHandler, EvHandlerState);
Error={error, _} ->
{Error, EvHandlerState0}
end;
commands([{switch_protocol, NewProtocol, ReplyTo}|Tail],
State=#tunnel_state{transport=Transport, stream_ref=TunnelStreamRef,
info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto,
protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}},
EvHandler, EvHandlerState0) ->
StreamRef = case Type of
socks5 -> TunnelStreamRef;
connect -> gun_protocols:stream_ref(NewProtocol)
end,
ContinueStreamRef0 = continue_stream_ref(State),
ContinueStreamRef = case Type of
socks5 -> 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,
transport_name => case Transport of
gun_tcp_proxy -> tcp;
gun_tls_proxy -> tls
end,
protocol_name => CurrentProto:name(),
info => #{
host => Host,
port => Port,
origin_host => OriginHost,
origin_port => OriginPort
},
new_protocol => NewProtocol
}
},
Proto = gun_tunnel,
case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState0) of
{tunnel, ProtoState, EvHandlerState} ->
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState},
EvHandler, EvHandlerState);
Error={error, _} ->
{Error, EvHandlerState0}
end;
commands([{tls_handshake, HandshakeEvent0, Protocols, ReplyTo}|Tail],
State=#tunnel_state{transport=Transport,
info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto,
protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}},
EvHandler, EvHandlerState0) ->
#{
stream_ref := StreamRef,
tls_opts := TLSOpts0
} = HandshakeEvent0,
TLSOpts = gun:ensure_alpn_sni(Protocols, TLSOpts0, OriginHost),
HandshakeEvent = HandshakeEvent0#{
tls_opts => TLSOpts
},
ContinueStreamRef0 = continue_stream_ref(State),
ContinueStreamRef = case Type of
socks5 -> 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,
transport_name => case Transport of
gun_tcp_proxy -> tcp;
gun_tls_proxy -> tls
end,
protocol_name => CurrentProto:name(),
info => #{
host => Host,
port => Port,
origin_host => OriginHost,
origin_port => OriginPort
},
handshake_event => HandshakeEvent,
protocols => Protocols
}
},
Proto = gun_tunnel,
case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState0) of
{tunnel, ProtoState, EvHandlerState} ->
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState},
EvHandler, EvHandlerState);
Error={error, _} ->
{Error, EvHandlerState0}
end;
commands([{active, true}|Tail], State, EvHandler, EvHandlerState) ->
commands(Tail, State, EvHandler, EvHandlerState).
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}, [StreamRef|Tail]) ->
%% We ensure that the stream_ref is correct.
StreamRef = outer_stream_ref(RealStreamRef),
case Tail of
[Ref] -> Ref;
_ -> Tail
end;
maybe_dereference(#tunnel_state{type=socks5}, StreamRef) ->
StreamRef.
outer_stream_ref(StreamRef) when is_list(StreamRef) ->
lists:last(StreamRef);
outer_stream_ref(StreamRef) ->
StreamRef.