%%% Copyright (c) 2019, Sunet. %%% See LICENSE for licensing information. %% Spawn an Erlang port running a proxy app. We use the 'remote' %% program from p11-kit as the proxy app. %% Receive PKCS#11 requests from a p11p_server, forward them to the %% proxy app, wait for a reply. If a reply is received within a %% timeout period, proxy the reply to the requesting p11p_server. If %% the request times out, inform the manager (our parent) and exit. %% Track a subset of the PKCS#11 state in order to handle token %% restarts. We start in state 'started'. While in 'started', we allow %% only a few "opening" calls (Initialize, OpenSession and Login) %% through to the token. Corresponding "closing" calls (Finalize, %% CloseSession and Logout) are sent an immediate OK response without %% forwarding them to the token. Any other call is rejected by %% responding with an error. This should make well behaving P11 %% applications be able to deal with us switching the token under %% their feet. -module(p11p_client). -behaviour(gen_server). %% API. -export([start_link/6]). -export([request/2, % Request from p11p-server. stop/2]). % Manager stopping us. -include("p11p_rpc.hrl"). %% Genserver callbacks. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% Records and types. -type token_state() :: started | initialized | session | loggedin | opact | finalized. -record(state, { token :: string(), % Token name. timeout :: non_neg_integer(), port :: port(), replyto :: pid() | undefined, p11state = started :: token_state(), timer :: reference() | undefined, response :: p11rpc:msg() | undefined, recv_count = 0 :: non_neg_integer(), send_count = 0 :: non_neg_integer() }). %% API. -spec start_link(atom(), string(), pid(), string(), list(), non_neg_integer()) -> {ok, pid()} | {error, term()}. start_link(ServName, TokName, Server, ModPath, ModEnv, Timeout) -> lager:info("~p: starting p11p_client for ~s", [self(), TokName]), gen_server:start_link({local, ServName}, ?MODULE, [TokName, Server, ModPath, ModEnv, Timeout], []). -spec request(pid(), p11rpc_msg()) -> ack | nack | {ok, non_neg_integer()}. request(Client, Request) -> gen_server:call(Client, {request, Request}). %% You should invoke stop/1 instead of gen_server:stop/1 if you're %% uncertain whether we (Pid) are alive or not. An example of when %% that can happen is when the manager receives a server_event about a %% lost P11 app -- if the server process terminated on request from us %% because we timed out on an RPC call, chances are that we have %% already terminated by the time the manager acts on the information %% about the lost app. stop(Pid, Reason) -> gen_server:cast(Pid, {stop, Reason}). %% Genserver callbacks. init([TokName, Server, ModPath, ModEnv, Timeout]) -> ProxyAppBinPath = p11p_config:proxyapp_bin_path(), Port = open_port({spawn_executable, ProxyAppBinPath}, [stream, exit_status, {env, ModEnv}, {args, [ModPath, "-v"]} % FIXME: Remove -v ]), true = is_port(Port), lager:debug("~p: ~s: new proxy app port: ~p", [self(), ProxyAppBinPath, Port]), lager:debug("~p: ~s: module: ~s, env: ~p", [self(), ProxyAppBinPath, ModPath, ModEnv]), {ok, #state{port = Port, token = TokName, replyto = Server, timeout = Timeout}}. handle_call({request, Request}, {FromPid, _Tag}, State = #state{port = Port, send_count = Sent}) -> case case State#state.p11state of started -> case p11p_rpc:req_id(Request) of ?P11_RPC_CALL_C_Logout -> ack; ?P11_RPC_CALL_C_CloseSession -> ack; ?P11_RPC_CALL_C_Finalize -> ack; ?P11_RPC_CALL_C_Initialize -> pass; ?P11_RPC_CALL_C_OpenSession -> pass; ?P11_RPC_CALL_C_Login -> pass; _ -> nack end; _ -> pass end of ack -> {reply, ack, State}; nack -> {reply, nack, State}; pass -> lager:debug("~p: sending request from ~p to prxoy app ~p", [self(), FromPid, Port]), D = p11p_rpc:serialise(Request), Buf = case Sent of 0 -> <>; _ -> D end, {ok, _} = do_send(Port, Buf), {reply, {ok, size(Buf)}, State#state{replyto = FromPid, timer = start_timer(State#state.timeout, Port), send_count = Sent + 1}} end; handle_call(Call, _From, State) -> lager:debug("~p: Unhandled call: ~p~n", [self(), Call]), {reply, unhandled, State}. handle_cast({stop, Reason}, State) -> {stop, Reason, State}; handle_cast(Cast, State) -> lager:debug("~p: unhandled cast: ~p~n", [self(), Cast]), {noreply, State}. %% Receiving the very first octets from proxy app since it was started. handle_info({Port, {data, Data}}, State) when Port == State#state.port, State#state.response == undefined -> case hd(Data) of % First octet is RPC protocol version. ?RPC_VERSION -> NewState = response_in(State, p11p_rpc:new(), tl(Data)), {noreply, NewState}; BadVersion -> lager:info("~p: ~p: invalid RPC version: ~p", [self(), Port, BadVersion]), {noreply, State} end; %% Receiving more data from proxy app. handle_info({Port, {data, Data}}, State) when Port == State#state.port -> NewState = response_in(State, State#state.response, Data), {noreply, NewState}; %% Proxy app timed out. handle_info({timeout, Timer, Port}, State) when Port == State#state.port, Timer == State#state.timer -> lager:info("~p: rpc request for ~s timed out, exiting", [self(), State#state.token]), p11p_manager:client_event(timeout, State#state.token), NewState = State#state{timer = undefined}, {stop, normal, NewState}; handle_info(Info, State) -> lager:debug("~p: Unhandled info: ~p~n", [self(), Info]), {noreply, State}. terminate(Reason, #state{port = Port}) -> lager:debug("~p: client terminating with reason ~p", [self(), Reason]), port_close(Port), ok. code_change(_OldVersion, State, _Extra) -> {ok, State}. %% Private send_request(Port, Buf) -> Rand = rand:uniform(100), Prob = p11p_config:testing_drop_prob(), if Rand =< Prob -> lager:debug("~p: faking unresponsive token (~p) by not sending", [self(), Port]); true -> lager:debug("~p: sending ~B octets to token", [self(), size(Buf)]), true = port_command(Port, Buf) end, {ok, size(Buf)}. response_in(S = #state{replyto = Pid, timer = Timer, recv_count = Recv}, MsgIn, DataIn) -> case p11p_rpc:parse(MsgIn, list_to_binary(DataIn)) of {needmore, Msg} -> S#state{response = Msg}; {done, Msg} -> cancel_timer(Timer), lager:debug("~p: <- ~s", [self(), p11p_rpc:dump(Msg)]), {ok, _BytesSent} = p11p_server:reply(Pid, Msg), %% Saving potential data not consumed by parse/2 in new message. S#state{response = p11p_rpc:new(Msg#p11rpc_msg.buffer), p11state = runstate(S#state.p11state, p11p_rpc:req_id(Msg)), recv_count = Recv + 1} end. start_timer(Timeout, Port) -> %%lager:debug("~p: starting timer", [self()]), erlang:start_timer(Timeout, self(), Port). cancel_timer(Timer) -> %%lager:debug("~p: canceling timer", [self()]), erlang:cancel_timer(Timer, [{async, true}, {info, false}]). -spec runstate(token_state(), non_neg_integer()) -> token_state(). runstate(started, ReqId) -> case ReqId of ?P11_RPC_CALL_C_Initialize -> initialized; _ -> started end; runstate(initialized, ReqId) -> case ReqId of ?P11_RPC_CALL_C_OpenSession -> session; ?P11_RPC_CALL_C_Finalize -> finalized; _ -> initialized end; runstate(session, ReqId) -> case ReqId of ?P11_RPC_CALL_C_Login -> loggedin; ?P11_RPC_CALL_C_CloseSession -> initialized; ?P11_RPC_CALL_C_Finalize -> finalized; _ -> session end; runstate(loggedin, ReqId) -> case ReqId of ?P11_RPC_CALL_C_Logout -> session; ?P11_RPC_CALL_C_CloseSession -> initialized; ?P11_RPC_CALL_C_Finalize -> finalized; _ -> loggedin end.