-module(enoise_cable).
-behavior(gen_server).
-export([
listen/1, listen/2,
accept/1, accept/2,
connect/3,
send/2,
close/1,
controlling_process/2,
port/1,
peername/1
]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(DEFAULT_PROTOCOL, "Noise_XXpsk0_25519_ChaChaPoly_BLAKE2b").
-define(DEFAULT_PROLOGUE, <<"CABLE1.0">>).
-define(DEFAULT_PSK,
hex:hexstr_to_bin("0808080808080808080808080808080808080808080808080808080808080808")
).
-record(state, {
mode,
socket,
enoise_conn,
handler_pid
}).
-spec listen(inet:port_number()) -> {ok, pid()} | {error, term()}.
listen(Port) ->
listen(Port, []).
-spec listen(inet:port_number(), proplists:proplist()) -> {ok, pid()} | {error, term()}.
listen(Port, TcpOpts) ->
gen_server:start_link(?MODULE, {listen, Port, TcpOpts}, []).
-spec accept(pid()) -> {ok, pid()} | {error, term()}.
accept(ListenerPid) ->
accept(ListenerPid, []).
-spec accept(pid(), proplists:proplist()) -> {ok, pid()} | {error, term()}.
accept(ListenerPid, Opts) ->
gen_server:call(ListenerPid, {accept_connection, Opts, self()}, infinity).
-spec connect(string() | inet:ip_address(), inet:port_number(), proplists:proplist()) ->
{ok, pid()} | {error, term()}.
connect(Host, Port, Opts) ->
gen_server:start_link(?MODULE, {connect, Host, Port, Opts, self()}, []).
-spec send(pid(), binary()) -> ok | {error, term()}.
send(Pid, Message) when is_binary(Message) ->
gen_server:call(Pid, {send, Message}).
-spec close(pid()) -> ok.
close(Pid) ->
gen_server:call(Pid, close).
-spec controlling_process(pid(), pid()) -> ok.
controlling_process(Pid, NewHandler) ->
gen_server:call(Pid, {controlling_process, NewHandler}).
-spec port(pid()) -> {ok, inet:port_number()} | {error, term()}.
port(ListenerPid) ->
gen_server:call(ListenerPid, get_port).
-spec peername(pid()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}.
peername(ConnPid) ->
gen_server:call(ConnPid, get_peername).
init({listen, Port, TcpOpts}) ->
DefaultOpts = [binary, {packet, 0}, {active, once}, {reuseaddr, true}],
case gen_tcp:listen(Port, DefaultOpts ++ TcpOpts) of
{ok, ListenSocket} ->
{ok, #state{
mode = listener,
socket = ListenSocket,
enoise_conn = undefined,
handler_pid = undefined
}};
{error, Reason} ->
{stop, Reason}
end;
init({connect, Host, Port, Opts, HandlerPid}) ->
TcpOpts = [binary, {packet, 0}, {active, true}, {nodelay, true}],
case gen_tcp:connect(Host, Port, TcpOpts) of
{ok, Socket} ->
EnoiseOpts = build_enoise_opts(Opts),
case enoise:connect(Socket, EnoiseOpts) of
{ok, EConn, _HandshakeState} ->
{ok, #state{
mode = connection,
socket = Socket,
enoise_conn = EConn,
handler_pid = HandlerPid
}};
{error, Reason} ->
gen_tcp:close(Socket),
{stop, Reason}
end;
{error, Reason} ->
{stop, Reason}
end;
init({do_accept, ListenSocket, Opts, HandlerPid, CallerFrom}) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
inet:setopts(Socket, [{active, true}]),
EnoiseOpts = build_enoise_opts(Opts),
case enoise:accept(Socket, EnoiseOpts) of
{ok, EConn, _HandshakeState} ->
gen_server:reply(CallerFrom, {ok, self()}),
{ok, #state{
mode = connection,
socket = Socket,
enoise_conn = EConn,
handler_pid = HandlerPid
}};
{error, Reason} ->
gen_tcp:close(Socket),
gen_server:reply(CallerFrom, {error, Reason}),
{stop, Reason}
end;
{error, Reason} ->
io:format(user, "[enoise_cable] TCP accept failed: ~p~n", [Reason]),
gen_server:reply(CallerFrom, {error, Reason}),
{stop, Reason}
end.
handle_call(
{accept_connection, Opts, HandlerPid},
From,
State = #state{mode = listener, socket = ListenSocket}
) ->
case gen_server:start_link(?MODULE, {do_accept, ListenSocket, Opts, HandlerPid, From}, []) of
{ok, _ConnPid} ->
{noreply, State};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call({send, Message}, _From, State = #state{mode = connection, enoise_conn = EConn}) ->
Res = enoise:send(EConn, Message),
{reply, Res, State};
handle_call(close, _From, State = #state{mode = connection, enoise_conn = EConn}) ->
enoise:close(EConn),
{stop, normal, ok, State};
handle_call(close, _From, State = #state{mode = listener, socket = ListenSocket}) ->
gen_tcp:close(ListenSocket),
{stop, normal, ok, State};
handle_call({controlling_process, NewHandler}, _From, State) ->
{reply, ok, State#state{handler_pid = NewHandler}};
handle_call(get_port, _From, State = #state{mode = listener, socket = ListenSocket}) ->
case inet:port(ListenSocket) of
{ok, Port} ->
{reply, {ok, Port}, State};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call(get_peername, _From, State = #state{mode = connection, socket = Socket}) ->
case inet:peername(Socket) of
{ok, {IP, Port}} ->
{reply, {ok, {IP, Port}}, State};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call(_Request, _From, State) ->
{reply, {error, unknown_call}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(
{noise, ReceivedEConn, Data},
State = #state{
enoise_conn = StateEConn,
handler_pid = Handler
}
) ->
case ReceivedEConn =:= StateEConn of
false ->
{noreply, State};
true ->
Handler ! {cable_transport, self(), Data},
{noreply, State}
end;
handle_info({tcp_closed, Socket}, State = #state{mode = connection, socket = StateSocket}) ->
case Socket =:= StateSocket of
true ->
io:format(user, "[enoise_cable connection ~p] TCP connection closed by peer~n", [self()]),
{stop, normal, State};
false ->
io:format(
user, "[enoise_cable connection ~p] Received tcp_closed for unknown socket~n", [
self()
]
),
{noreply, State}
end;
handle_info({tcp_error, Socket, Reason}, State = #state{mode = connection, socket = StateSocket}) ->
case Socket =:= StateSocket of
true ->
io:format(user, "[enoise_cable connection ~p] TCP error: ~p~n", [self(), Reason]),
{stop, {tcp_error, Reason}, State};
false ->
io:format(
user, "[enoise_cable connection ~p] Received tcp_error for unknown socket~n", [
self()
]
),
{noreply, State}
end;
handle_info(Info, State = #state{mode = Mode}) ->
io:format(user, "[enoise_cable ~p ~p] Unexpected message: ~p~n", [Mode, self(), Info]),
{noreply, State}.
terminate(_Reason, #state{mode = connection, enoise_conn = EConn}) ->
catch enoise:close(EConn),
ok;
terminate(_Reason, #state{mode = listener, socket = ListenSocket}) ->
catch gen_tcp:close(ListenSocket),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
build_enoise_opts(Opts) ->
KeyPair = proplists:get_value(keypair, Opts),
if
KeyPair =:= undefined ->
erlang:error({missing_required_option, keypair});
true ->
ok
end,
Protocol = proplists:get_value(protocol, Opts, ?DEFAULT_PROTOCOL),
PSK = proplists:get_value(psk, Opts, ?DEFAULT_PSK),
Prologue = proplists:get_value(prologue, Opts, ?DEFAULT_PROLOGUE),
ProtocolRecord = enoise_protocol:from_name(Protocol),
[
{noise, ProtocolRecord},
{s, KeyPair},
{psks, [PSK]},
{prologue, Prologue}
].