-module(cabal_transport).
-behavior(gen_server).
-export([start_link/1, stop/1, listener_port/1, get_address/1, get_listener_info/1]).
-export([register_handler/2, dial/3, send/3]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
-record(state, {
listener_pid,
listen_port,
key_pair,
event_handler,
connections = #{}
}).
start_link(Args) ->
gen_server:start_link(?MODULE, Args, []).
stop(Pid) ->
gen_server:call(Pid, stop).
register_handler(Pid, HandlerPid) ->
gen_server:call(Pid, {register_handler, HandlerPid}).
dial(Pid, Host, Port) ->
gen_server:call(Pid, {dial, Host, Port}).
send(Pid, ConnPid, Binary) ->
gen_server:call(Pid, {send, ConnPid, Binary}).
listener_port(Pid) ->
gen_server:call(Pid, {listener_port}).
get_address(Pid) ->
gen_server:call(Pid, {get_address}).
get_listener_info(Pid) ->
gen_server:call(Pid, {get_listener_info}).
init(Args) ->
{_IP, Port} = proplists:get_value(listen_addr, Args),
KeyPair = proplists:get_value(key_pair, Args),
Handler = proplists:get_value(event_handler, Args, undefined),
if
KeyPair =:= undefined ->
{stop, {error, no_keypair_provided}};
true ->
TcpOpts = [{reuseaddr, true}],
case enoise_cable:listen(Port, TcpOpts) of
{ok, ListenerPid} ->
{ok, ListenPort} = enoise_cable:port(ListenerPid),
TransportPid = self(),
io:format("[Transport] Starting encrypted server on port ~p (pid: ~p)~n", [
ListenPort, TransportPid
]),
{ok, #state{
listener_pid = ListenerPid,
listen_port = ListenPort,
key_pair = KeyPair,
event_handler = Handler,
connections = #{}
}};
{error, Reason} ->
{stop, Reason}
end
end.
handle_call({register_handler, HandlerPid}, _From, State) ->
io:format("[Transport] Registering handler: ~p~n", [HandlerPid]),
{reply, ok, State#state{event_handler = HandlerPid}};
handle_call({dial, Host, Port}, From, State = #state{key_pair = KeyPair}) ->
ActualHost =
case Host of
"unnamed" -> {127, 0, 0, 1};
_ -> Host
end,
io:format("[Transport] Attempting encrypted connection to ~p:~p~n", [ActualHost, Port]),
TransportPid = self(),
spawn_link(fun() -> do_dial(ActualHost, Port, KeyPair, TransportPid, From) end),
{noreply, State};
handle_call({send, ConnPid, Binary}, _From, State = #state{connections = Conns}) ->
case maps:get(ConnPid, Conns, undefined) of
undefined ->
io:format("[Transport] Cannot send to unknown connection: ~p~n", [ConnPid]),
{reply, {error, unknown_connection}, State};
_ConnInfo ->
Result = enoise_cable:send(ConnPid, Binary),
{reply, Result, State}
end;
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call({listener_port}, _From, State = #state{listen_port = Port}) ->
{reply, {ok, Port}, State};
handle_call({get_address}, _From, State = #state{listen_port = Port}) ->
{reply, {ok, {{127, 0, 0, 1}, Port}}, State};
handle_call(
{get_listener_info}, _From, State = #state{listener_pid = ListenerPid, key_pair = KeyPair}
) ->
{reply, {ok, {ListenerPid, KeyPair}}, State};
handle_call(Request, _From, State) ->
io:format("Unhandled call: ~p~n", [Request]),
{reply, {error, unknown_call}, State}.
handle_cast(Msg, State) ->
io:format("Unhandled cast: ~p~n", [Msg]),
{noreply, State}.
handle_info(
{new_connection, ConnPid, PeerAddr},
State = #state{event_handler = Handler, connections = Conns}
) ->
io:format("[Transport] New encrypted connection established with ~p (conn: ~p)~n", [
PeerAddr, ConnPid
]),
erlang:monitor(process, ConnPid),
ConnInfo = #{addr => PeerAddr},
NewConns = maps:put(ConnPid, ConnInfo, Conns),
Handler ! {peerNew, ConnPid, PeerAddr},
{noreply, State#state{connections = NewConns}};
handle_info(
{cable_transport, ConnPid, Data}, State = #state{event_handler = Handler, connections = Conns}
) ->
case maps:get(ConnPid, Conns, undefined) of
undefined ->
io:format("[Transport] Received data from unknown connection ~p~n", [ConnPid]),
{noreply, State};
_ConnInfo ->
Handler ! {peerData, ConnPid, Data},
{noreply, State}
end;
handle_info(
{'DOWN', _Ref, process, ConnPid, _Reason},
State = #state{
event_handler = Handler,
connections = Conns
}
) ->
case maps:get(ConnPid, Conns, undefined) of
undefined ->
io:format("[Transport] Unknown connection ~p terminated~n", [ConnPid]),
{noreply, State};
ConnInfo ->
PeerAddr = maps:get(addr, ConnInfo),
io:format("[Transport] Connection ~p closed with peer ~p~n", [ConnPid, PeerAddr]),
NewConns = maps:remove(ConnPid, Conns),
Handler ! {peerLost, ConnPid, PeerAddr},
{noreply, State#state{connections = NewConns}}
end;
handle_info(Info, State) ->
io:format("Unhandled info: ~p~n", [Info]),
{noreply, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
terminate(_Reason, #state{listener_pid = ListenerPid, connections = Connections}) ->
maps:foreach(
fun(ConnPid, _ConnInfo) ->
catch enoise_cable:close(ConnPid)
end,
Connections
),
catch enoise_cable:close(ListenerPid),
ok.
do_dial(Host, Port, KeyPair, TransportPid, From) ->
Opts = [{keypair, KeyPair}],
case enoise_cable:connect(Host, Port, Opts) of
{ok, ConnPid} ->
io:format("[Transport] Encrypted connection established to ~p:~p (conn: ~p)~n", [
Host, Port, ConnPid
]),
ok = enoise_cable:controlling_process(ConnPid, TransportPid),
PeerAddr = {Host, Port},
TransportPid ! {new_connection, ConnPid, PeerAddr},
gen_server:reply(From, {ok, ConnPid});
{error, Reason} ->
io:format("[Transport] Connection error to ~p:~p: ~p~n", [Host, Port, Reason]),
gen_server:reply(From, {error, Reason})
end.