Skip to content

Instantly share code, notes, and snippets.

@essen
Created December 12, 2018 16:38
Show Gist options
  • Select an option

  • Save essen/0f5bc9cb0b45d79423582d94ac90b318 to your computer and use it in GitHub Desktop.

Select an option

Save essen/0f5bc9cb0b45d79423582d94ac90b318 to your computer and use it in GitHub Desktop.

Revisions

  1. essen created this gist Dec 12, 2018.
    160 changes: 160 additions & 0 deletions gun_proxied_tls.erl
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,160 @@
    -module(gun_proxied_tls).

    -behaviour(gen_server).

    %% Gun-specific interface.
    -export([ssl_connect/2]).
    -export([ssl_connect/3]).
    -export([proxy_received/2]).

    %% Transport callback.
    -export([connect/4]).
    -export([controlling_process/2]).
    -export([send/2]).
    -export([setopts/2]).

    %% gen_server.
    -export([init/1]).
    -export([handle_call/3]).
    -export([handle_cast/2]).
    -export([handle_info/2]).

    -record(state, {
    %% The pid of the Gun process.
    gun_pid :: pid(),

    %% The pid of the TLS connection.
    controlling_pid :: pid(),

    %% Metadata about the connection.
    meta :: map(),

    %% Active mode state.
    active = false :: false | true | pos_integer(),

    %% Buffer for incoming data.
    buffer = <<>> :: binary()
    }).

    %% Gun-specific interface.

    ssl_connect(Host, Port) ->
    spawn_link(?MODULE, ssl_connect, [self(), Host, Port]).

    ssl_connect(Parent, Host, Port) ->
    {ok, Socket} = ssl:connect(Host, Port, [
    {cb_info, {gun_proxied_tls, gun_data, gun_closed, gun_error}},
    {?MODULE, Parent}
    ]),
    ssl:controlling_process(Socket, Parent),
    Parent ! {?MODULE, {ssl_connect, Socket}},
    ok.

    proxy_received(Pid, Data) ->
    gen_server:cast(Pid, {?FUNCTION_NAME, Data}).

    %% Transport callback.

    %% The connect/4 function is called by the process
    %% that calls ssl:connect/2,3,4.
    connect(Address, Port, Opts, _Timeout) ->
    gen_server:start_link(?MODULE, {self(), Opts, #{
    address => Address,
    port => Port
    }}, []).

    %% Nothing to do, we're just a callback module.
    controlling_process(Pid, ControllingPid) ->
    gen_server:cast(Pid, {?FUNCTION_NAME, ControllingPid}).

    send(Pid, Data) ->
    gen_server:cast(Pid, {?FUNCTION_NAME, Data}).

    setopts(Pid, Opts) ->
    gen_server:cast(Pid, {?FUNCTION_NAME, Opts}).

    %% gen_server.

    -spec init({pid(), [inet:socket_setopt()], map()}) -> {ok, passive, #state{}}.
    init({ControllingPid, Opts, Meta}) ->
    {_, GunPid} = lists:keyfind(?MODULE, 1, Opts),
    GunPid ! {?MODULE, ControllingPid, {proxied_pid, self()}},
    {ok, handle_setopts(Opts, #state{
    gun_pid=GunPid,
    controlling_pid=ControllingPid,
    meta=Meta
    })}.

    handle_call(_, _, State) ->
    {reply, {error, bad_call}, State}.

    handle_cast({proxy_received, Data}, State=#state{buffer=Buffer}) ->
    {noreply, active(State#state{buffer= <<Buffer/binary, Data/binary>>})};
    handle_cast({controlling_process, ControllingPid}, State) ->
    {noreply, State#state{controlling_pid=ControllingPid}};
    handle_cast(Event={send, _}, State=#state{gun_pid=GunPid}) ->
    GunPid ! {?MODULE, self(), Event},
    {noreply, State};
    handle_cast({setopts, Opts}, State0) ->
    {noreply, active(handle_setopts(Opts, State0))};
    handle_cast(_, State) ->
    {noreply, State}.

    handle_info(_, State) ->
    {noreply, State}.

    handle_setopts(Opts, State0) ->
    case [A || {active, A} <- Opts] of
    [] -> State0;
    [false] -> State0#state{active=false};
    [0] -> State0#state{active=false};
    [once] -> State0#state{active=1};
    [Active] -> active(State0#state{active=Active})
    end.

    active(State=#state{buffer= <<>>}) ->
    State;
    active(State=#state{active=false}) ->
    State;
    active(State=#state{controlling_pid=Pid, active=Active0, buffer=Buffer}) ->
    Pid ! {gun_data, self(), Buffer},
    Active = case Active0 of
    true -> true;
    1 -> false;
    N -> N - 1
    end,
    State#state{active=Active, buffer= <<>>}.

    -ifdef(TEST).
    my_test() ->
    ssl:start(),
    dbg:tracer(),
    dbg:tpl(?MODULE, []),
    dbg:p(all, c),
    Self = self(),
    ConnectPid = ssl_connect("google.com", 443),
    {ok, Socket} = gen_tcp:connect("google.com", 443, [binary, {active, true}]),
    ProxyPid = my_receive_loop(Socket, ConnectPid),
    io:format(user, "~p~n", [erlang:process_info(Self, messages)]),
    io:format(user, "~p~n", [erlang:process_info(ProxyPid, messages)]),
    ok.

    my_receive_loop(Socket, Pid) ->
    receive
    {?MODULE, {ssl_connect, SSL}} ->
    io:format(user, "~p~n", [SSL]),
    ssl:send(SSL, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
    ssl:setopts(SSL, [{active, true}]),
    my_receive_loop(Socket, Pid);
    {?MODULE, Pid, {proxied_pid, ProxiedPid}} ->
    my_receive_loop(Socket, ProxiedPid);
    {?MODULE, Pid, {send, Data}} ->
    gen_tcp:send(Socket, Data),
    my_receive_loop(Socket, Pid);
    {tcp, Socket, Data} ->
    proxy_received(Pid, Data),
    my_receive_loop(Socket, Pid)
    after 1000 ->
    Pid
    end.
    -endif.