%%% Copyright (c) 2019, Sunet. %%% See LICENSE for licensing information. -module(p11p_config). -behaviour(gen_server). %%% API %%% -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([start_link/0]). -export([modules_for_token/1, module_path/1, module_env/1, nameof/1, proxyapp_bin_path/0, socket_dir/0, socket_path_has_pid/0, testing_drop_prob/0, tokens/0, token_balance/1, token_retries/1, token_timeout/1]). %%% Records and types %%% -record(p11module, { name :: string(), path :: string(), env :: [{string(), string()}] }). -type p11module() :: #p11module{}. -record(token, { name :: string(), timeout :: non_neg_integer(), failover :: non_neg_integer(), % How many failover attempts. balance :: [integer()], modules = #{} :: #{string() => p11module()} }). -type token() :: #token{}. -record(state, { socket_dir :: string(), socket_path_has_pid :: boolean(), proxyapp_bin_path :: string(), testing_drop_prob :: non_neg_integer(), tokens :: #{string() => token()} }). %%% Genserver callbacks %%% init(_Args) -> case application:get_env(p11p, config_file) of {ok, ConfigFile} -> {ok, init_state(ConfigFile)}; _ -> {ok, init_state()} end. handle_call(proxyapp_bin_path, _From, S = #state{proxyapp_bin_path = Path}) -> {reply, Path, S}; handle_call(socket_dir, _From, S = #state{socket_dir = Dir}) -> {reply, Dir, S}; handle_call(socket_path_has_pid, _From, S = #state{socket_path_has_pid = B}) -> {reply, B, S}; handle_call(testing_drop_prob, _From, S = #state{testing_drop_prob = P}) -> {reply, P, S}; handle_call(tokens, _From, State = #state{tokens = Tokens}) -> {reply, maps:values(Tokens), State}; handle_call({modules_for_token, TokName}, _, S = #state{tokens = Tokens}) -> #{TokName := Token} = Tokens, {reply, maps:values(Token#token.modules), S}; handle_call({token_balance, TokName}, _, State = #state{tokens = Tokens}) -> #{TokName := Token} = Tokens, {reply, Token#token.balance, State}; handle_call({token_retries, TokName}, _, State = #state{tokens = Tokens}) -> #{TokName := Token} = Tokens, {reply, Token#token.failover, State}; handle_call({token_timeout, TokName}, _, State = #state{tokens = Tokens}) -> #{TokName := Token} = Tokens, {reply, Token#token.timeout, State}; handle_call(Request, _From, State) -> lager:warning("Unhandled call: ~p", [Request]), {reply, unhandled, State}. handle_cast(Message, State) -> lager:warning("Unhandled cast: ~p", [Message]), {noreply, State}. handle_info(Info, State) -> lager:warning("Unhandled info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVersion, State, _Extra) -> {ok, State}. %%% External functions %%% start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). proxyapp_bin_path() -> gen_server:call(?MODULE, proxyapp_bin_path). socket_dir() -> gen_server:call(?MODULE, socket_dir). socket_path_has_pid() -> gen_server:call(?MODULE, socket_path_has_pid). testing_drop_prob() -> gen_server:call(?MODULE, testing_drop_prob). -spec tokens() -> [token()]. tokens() -> gen_server:call(?MODULE, tokens). -spec token_balance(string()) -> [integer()]. token_balance(TokName) -> gen_server:call(?MODULE, {token_balance, TokName}). -spec token_retries(string()) -> non_neg_integer(). token_retries(TokName) -> gen_server:call(?MODULE, {token_retries, TokName}). -spec token_timeout(string()) -> non_neg_integer(). token_timeout(TokName) -> gen_server:call(?MODULE, {token_timeout, TokName}). -spec modules_for_token(string()) -> [p11module()]. modules_for_token(TokName) -> gen_server:call(?MODULE, {modules_for_token, TokName}). -spec module_path(p11module()) -> string(). module_path(Module) -> Module#p11module.path. -spec module_env(p11module()) -> []. module_env(Module) -> Module#p11module.env. nameof(#token{name = Name}) -> Name; nameof(#p11module{name = Name}) -> Name; nameof(List) -> [nameof(E) || E <- List]. %%% Private functions %%% -define(PROXYAPP_DEFAULT, "/usr/lib/x86_64-linux-gnu/p11-kit/p11-kit-remote"). init_state() -> #state{ socket_dir = application:get_env(p11p, socket_dir, default_socket_dir()), socket_path_has_pid = application:get_env(p11p, socket_path_has_pid, true), proxyapp_bin_path = application:get_env(p11p, proxyapp_bin_path, ?PROXYAPP_DEFAULT), testing_drop_prob = application:get_env(p11p, testing_drop_prob, 0), tokens = conf_tokens(application:get_env(p11p, vtokens, []))}. init_state(Filename) -> {ok, Config} = p11p_config_file:load_config(Filename), #state{ socket_dir = p11p_config_file:get(Config, string, "socket_dir", default_socket_dir()), socket_path_has_pid = p11p_config_file:get(Config, bool, "socket_path_has_pid", true), proxyapp_bin_path = p11p_config_file:get(Config, string, "proxyapp_bin_path", ?PROXYAPP_DEFAULT), testing_drop_prob = p11p_config_file:get(Config, integer, "testing_drop_prob", 0), tokens = conf_tokens(p11p_config_file:get(Config, section, "vtokens", []))}. conf_tokens(L) -> conf_tokens(L, #{}). conf_tokens([], Acc) -> Acc; conf_tokens([H = {Name, _}|T], Acc) -> conf_tokens(T, Acc#{Name => new_token(H)}). -spec new_token({string(), [tuple()]}) -> token(). new_token({Name, Settings}) -> Modules = conf_modules(proplists:get_value(modules, Settings)), #token{ name = Name, timeout = proplists:get_value(timeout, Settings, 25000), failover = proplists:get_value(failover, Settings, maps:size(Modules) - 1), balance = lists:map(fun(N) -> case N of 0 -> -1; _ -> N end end, balance(proplists:get_value(balance, Settings, []), maps:size(Modules))), modules = Modules }. balance([], NModules) -> [0 || _ <- lists:seq(1, NModules)]; balance(List, NModules) -> List ++ [0 || _ <- lists:seq(1, NModules - length(List))]. conf_modules(L) -> conf_modules(L, #{}). conf_modules([], Acc) -> Acc; conf_modules([{Name, Path}|T], Acc) -> conf_modules(T, Acc#{Name => new_module(Name, Path, [])}); conf_modules([{Name, Path, Env}|T], Acc) -> conf_modules(T, Acc#{Name => new_module(Name, Path, Env)}). new_module(Name, Path, Env) -> #p11module{ name = Name, path = Path, env = Env }. geteuid() -> %% TODO: Maybe find a POSIX library instead of invoking a shell? list_to_integer(string:strip(os:cmd("/usr/bin/id -u"), right, $\n)). default_socket_dir() -> "/run/user/" ++ integer_to_list(geteuid()) ++ "/p11p". %%% Unit tests %%% -include_lib("eunit/include/eunit.hrl"). tokens_init_test_() -> {setup, fun() -> conf_tokens( [ {"vtoken0", [{balance, [3]}, {modules, [{"bogusmod0_0", "/path/to/bogusmod0_0"}, {"bogusmod0_1", "/path/to/bogusmod0_1"} ]}]}, {"vtoken1", [{timeout, 12000}, {failover, 3}, {modules, [{"bogusmod1_0", "/path/to/bogusmod1_0"}, {"bogusmod1_1", "/path/to/bogusmod1_1", [{"MYENV", "myenv"}]} ]}]} ]) end, fun(_) -> ok end, fun(Conf) -> [?_assertEqual( #{"vtoken0" => {token,"vtoken0", 25000, 1, [3,-1], #{"bogusmod0_0" => {p11module,"bogusmod0_0", "/path/to/bogusmod0_0", []}, "bogusmod0_1" => {p11module,"bogusmod0_1", "/path/to/bogusmod0_1", []}}}, "vtoken1" => {token,"vtoken1", 12000, 3, [-1,-1], #{"bogusmod1_0" => {p11module,"bogusmod1_0", "/path/to/bogusmod1_0", []}, "bogusmod1_1" => {p11module,"bogusmod1_1", "/path/to/bogusmod1_1", [{"MYENV", "myenv"}]}}} }, Conf)] end}.