%%% @doc %%% Minimal Stream Protocol: Connection Worker %%% @end -module(msp_router). -vsn("0.1.0"). -behavior(gen_server). -author("Jarvis Carroll "). -copyright("Jarvis Carroll "). -license("MIT"). -export([begin_routing/4, add_channel/3]). %% gen_server -export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -include("$zx_include/zx_logger.hrl"). %%% Type and Record Definitions -type endpoint() :: {inet:ip_address(), inet:port_number()}. % msp_channel server. -type channel() :: pid(). -record(s, {socket :: gen_udp:socket(), peer :: endpoint(), side :: 0 | 1, connections = #{} :: #{integer() => channel()}}). -type state() :: none | #s{}. %%% Interface % msp does not negotiate new connections, but once you have a % socket, and a host that knows you will be talking MSP, then a % new msp_router can be initialized. begin_routing(Router, OurSock, {TheirIP, TheirPort}, OurSide) -> % Transfer the socket to the gen_server. If it is % active then a bunch of messages will be received % at once, but that is fine. case gen_udp:controlling_process(OurSock, Router) of ok -> gen_server:cast(Router, {begin_routing, OurSock, {TheirIP, TheirPort}, OurSide}); {error, Reason} -> {error, Reason} end. add_channel(Router, ChanID, ChanPID) -> gen_server:cast(Router, {add_channel, ChanID, ChanPID}). %%% gen_server -spec start_link() -> Result when Result :: {ok, pid()} | {error, Reason :: term()}. start_link() -> gen_server:start_link(?MODULE, none, []). % TODO: SWP? SWP of SWPs? init(none) -> {ok, none}. handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp", [From, Unexpected]), {reply, undef, State}. handle_cast({begin_routing, Sock, Peer, Side}, none) -> State = do_begin_routing(Sock, Peer, Side), {noreply, State}; handle_cast({add_channel, ChanID, ChanPID}, State) -> NewState = do_add_channel(ChanID, ChanPID, State), {noreply, NewState}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp", [Unexpected]), {noreply, State}. handle_info({udp, Sock, IP, Port, Packet}, State = #s{socket = Sock, peer = {IP, Port}}) -> io:format("Got packet: ~p~n", [Packet]), do_dispatch(State, Packet), {noreply, State}; handle_info(Unexpected, State) -> ok = io:format("Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. -spec code_change(OldVersion, State, Extra) -> {ok, NewState} | {error, Reason} when OldVersion :: Version | {old, Version}, Version :: term(), State :: state(), Extra :: term(), NewState :: term(), Reason :: term(). code_change(_, State, _) -> {ok, State}. terminate(_, _) -> ok. %%% Doer Functions do_begin_routing(Sock, Peer, Side) -> ok = inet:setopts(Sock, [{active, once}, {mode, binary}]), State = #s{socket = Sock, peer = Peer, side = Side}, State. do_add_channel(ChanID, ChanPID, State = #s{connections = Conns}) -> NewConns = maps:put(ChanID, ChanPID, Conns), State#s{connections = NewConns}. do_dispatch(#s{socket = Sock, peer = Peer, connections = Conns}, <>) -> ok = inet:setopts(Sock, [{active, once}]), case maps:find(ID, Conns) of {ok, Conn} -> erlang:send(Conn, {msp_fragment, Packet}); error -> {ok, OurPort} = inet:port(Sock), {TheirIP, TheirPort} = Peer, msp_channel_man:dispatch_fallback(OurPort, TheirIP, TheirPort, ID, Packet) end; do_dispatch(_, <<>>) -> % Empty datagram? ok.