%% A remote manager is a genserver for coordination of remotes for all %% tokens. %% Provide a lookup service for servers in need of a remote to send %% requests to, by keeping track of which module is current for a %% given vtoken and spawn a p11p_remote genserver "on demand". %% %% Provide a client event and a server event API for servers and %% remotes, respectively, where events like "remote timing out" and %% "p11 client hung up" can be reported. %% %% Keep track of (successful) p11 requests which might cause state %% changes in the token, like "logins". When switching token under the %% feet of the p11 client, replay whatever is needed to the new %% token. This goes for the p11-kit RPC protocol version octet too. %% Certain state changing p11 requests cannot be replayed, like %% "generate new key". Any such (successful) request invalidates all %% other remotes for the given token. -module(p11p_remote_manager). -behaviour(gen_server). %% API. -export([start_link/0]). -export([remote_for_token/1, client_event/2]). % For servers. -export([server_event/2]). % For remotes. %% Genserver callbacks. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% Records and types. -record(remote, { tokname :: string(), servid :: atom(), modpath :: string(), pid :: pid() | undefined }). -record(token, { remotes :: [#remote{}], % Active remote in hd(). replay = <<>> :: binary() % FIXME: seems unfeasable, remove }). -record(state, { tokens :: #{string() => #token{}} }). %% API implementation. -spec start_link() -> {ok, pid()} | {error, term()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec remote_for_token(string()) -> pid(). remote_for_token(TokName) -> gen_server:call(?MODULE, {remote_for_token, TokName}). client_event(Event, Args) -> gen_server:cast(?MODULE, {client_event, Event, Args}). server_event(Event, Args) -> gen_server:cast(?MODULE, {server_event, Event, Args}). %% Genserver callbacks. init([]) -> {ok, #state{tokens = init_tokens(p11p_config:tokens())}}. handle_call({remote_for_token, TokName}, _From, #state{tokens = Tokens} = State) -> #{TokName := Token} = Tokens, Remotes = Token#token.remotes, #remote{tokname = TokName, servid = ServId, modpath = ModPath, pid = Pid} = Remote = hd(Remotes), case Pid of undefined -> {ok, NewPid} = p11p_remote:start_link(ServId, TokName, ModPath), NewRemote = Remote#remote{pid = NewPid}, NewToken = Token#token{remotes = [NewRemote | tl(Remotes)]}, NewState = State#state{tokens = Tokens#{TokName := NewToken}}, {reply, NewPid, NewState}; _ -> {reply, Pid, State} end; handle_call(Call, _From, State) -> lager:debug("Unhandled call: ~p~n", [Call]), {reply, unhandled, State}. handle_cast({server_event, timeout, [TokName, Server]}, #state{tokens = Tokens} = State) -> lager:debug("~p: ~s: timed out, stopping ~p", [self(), TokName, Server]), gen_server:stop(Server), % Hang up on p11 client. %% TODO: do some code dedup with remote_for_token? #{TokName := Token} = Tokens, Remotes = Token#token.remotes, Remote = hd(Remotes), NewRemote = Remote#remote{pid = undefined}, NewToken = Token#token{remotes = tl(Remotes) ++ [NewRemote]}, NewState = State#state{tokens = Tokens#{TokName := NewToken}}, lager:debug("~p: ~s: updated token: ~p", [self(), TokName, NewToken]), {noreply, NewState}; handle_cast({client_event, client_gone, [TokName, Pid]}, #state{tokens = Tokens} = State) -> lager:debug("~p: asking remote ~p to stop", [self(), Pid]), p11p_remote:stop(Pid, normal), #{TokName := Token} = Tokens, Remotes = Token#token.remotes, NewRemotes = [case E#remote.pid of Pid -> E#remote{pid = undefined}; _ -> E end || E <- Remotes], NewToken = Token#token{remotes = NewRemotes}, NewState = State#state{tokens = Tokens#{TokName := NewToken}}, {noreply, NewState}; handle_cast(Cast, State) -> lager:debug("Unhandled cast: ~p~n", [Cast]), {noreply, State}. handle_info({Port, {exit_status, Status}}, State) -> %% FIXME: do we need to be trapping exits explicitly? lager:info("~p: process ~p exited with ~p", [self(), Port, Status]), {noreply, State}; handle_info(Info, State) -> lager:debug("~p: Unhandled info: ~p~n", [self(), Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVersion, State, _Extra) -> {ok, State}. %% Private functions -spec init_tokens([p11p_config:token()]) -> #{string() => #token{}}. init_tokens(ConfTokens) -> init_tokens(ConfTokens, #{}). init_tokens([], Acc)-> lager:debug("~p: created tokens from config: ~p", [self(), Acc]), Acc; init_tokens([H|T], Acc)-> TokName = p11p_config:nameof(H), init_tokens(T, Acc#{TokName => new_token(TokName, H)}). new_token(TokName, ConfToken) -> #token{remotes = remotes(TokName, p11p_config:modules_for_token(p11p_config:nameof(ConfToken)))}. remotes(TokName, ConfModules) -> remotes(TokName, ConfModules, []). remotes(_, [], Acc) -> Acc; remotes(TokName, [H|T], Acc) -> ModName = p11p_config:nameof(H), ServName = "p11p_remote:" ++ TokName ++ ":" ++ ModName, ModPath = p11p_config:module_path(H), remotes(TokName, T, [#remote{ tokname = TokName, servid = list_to_atom(ServName), modpath = ModPath, pid = undefined } | Acc]).