Skip to main content

src/vpn_admin.erl

%%%-------------------------------------------------------------------
%% @doc Read-only administration facade.
%%%-------------------------------------------------------------------
-module(vpn_admin).

-export([dashboard/0,
         summary/0,
         summary_view/0,
         summary_json/0,
         summary_json_pretty/0,
         certificate_view/1,
         extract_cn/1,
         overview/0,
         peer_counts/0]).

dashboard() ->
    #{status => vpn_manager:status(),
      certificates => vpn_manager:certificates()}.

summary() ->
    Status = vpn_manager:status(),
    Certificates = vpn_manager:certificates(),
    Counts = peer_counts(),
    #{counts => Counts#{certificates => length(Certificates)},
      peers => [summary_peer(PeerId, PeerStatus, Certificates)
                || {PeerId, PeerStatus} <- maps:to_list(maps:get(peers, Status, #{}))]}.

summary_view() ->
    Summary = summary(),
    #{counts => maps:get(counts, Summary, #{}),
      peers => [peer_view(Peer) || Peer <- maps:get(peers, Summary, [])]}.

summary_json() ->
    try
        jiffy:encode(summary_view())
    catch
        _:Reason ->
            jiffy:encode(#{error => <<"summary_generation_failed">>,
                           reason => iolist_to_binary(io_lib:format("~p", [Reason]))})
    end.

summary_json_pretty() ->
    summary_json().

overview() ->
    Counts = peer_counts(),
    #{configured_peers => maps:get(configured, Counts),
      running_peers => maps:get(running, Counts),
      stopped_peers => maps:get(stopped, Counts),
      certificates => length(vpn_manager:certificates())}.

peer_counts() ->
    Configured = length(vpn_manager:list_peers()),
    Running = length(vpn_manager:running_peers()),
    #{configured => Configured,
      running => Running,
      stopped => Configured - Running}.

summary_peer(PeerId, PeerStatus, Certificates) ->
    Config = maps:get(config, PeerStatus, #{}),
    Stats = maps:get(stats, PeerStatus, #{}),
    LinkStats = maps:get(link, Stats, #{}),
    Certificate = certificate_for_peer(PeerId, Certificates),
    #{id => PeerId,
      running => maps:get(running, PeerStatus, false),
      mode => maps:get(mode, Config, undefined),
      ip => maps:get(ip, Config, undefined),
      remote_peer_id => maps:get(remote_peer_id, Config, undefined),
      crypto_failures => maps:get(crypto_failures, LinkStats, 0),
      frames_rejected => maps:get(frames_rejected, LinkStats, 0),
      certificate => compact_certificate(Certificate)}.

certificate_for_peer(PeerId, Certificates) ->
    case [Certificate || #{peer_id := Id} = Certificate <- Certificates,
                         Id =:= PeerId] of
        [Certificate | _] ->
            Certificate;
        [] ->
            #{}
    end.

compact_certificate(Certificate) ->
    maps:with(
        [subject,
            issuer,
            trusted,
            key_match,
            not_before,
            not_after],
        Certificate).

peer_view(Peer) ->
    #{id => json_value(maps:get(id, Peer, undefined)),
      running => maps:get(running, Peer, false),
      mode => json_value(maps:get(mode, Peer, undefined)),
      ip => json_value(maps:get(ip, Peer, undefined)),
      remote_peer_id => json_value(maps:get(remote_peer_id, Peer, undefined)),
      crypto_failures => maps:get(crypto_failures, Peer, 0),
      frames_rejected => maps:get(frames_rejected, Peer, 0),
      certificate => certificate_view(maps:get(certificate, Peer, #{}))}.

certificate_view(Certificate) ->
    #{subject_cn => extract_cn(maps:get(subject, Certificate, undefined)),
      issuer_cn => extract_cn(maps:get(issuer, Certificate, undefined)),
      trusted => maps:get(trusted, Certificate, false),
      key_match => maps:get(key_match, Certificate, false),
      not_before => time_value(maps:get(not_before, Certificate, undefined)),
      not_after => time_value(maps:get(not_after, Certificate, undefined))}.

extract_cn({rdnSequence, RDNs}) ->
    extract_cn_from_rdns(RDNs);
extract_cn({subject, PeerId}) ->
    json_value(PeerId);
extract_cn({issuer, PeerId}) ->
    json_value(PeerId);
extract_cn(Value) ->
    json_value(Value).

extract_cn_from_rdns([[{'AttributeTypeAndValue', {2,5,4,3}, Value} | _] | _]) ->
    directory_string_value(Value);
extract_cn_from_rdns([_ | Rest]) ->
    extract_cn_from_rdns(Rest);
extract_cn_from_rdns([]) ->
    null.

directory_string_value({utf8String, Value}) ->
    json_value(Value);
directory_string_value({printableString, Value}) ->
    json_value(Value);
directory_string_value({teletexString, Value}) ->
    json_value(Value);
directory_string_value({bmpString, Value}) ->
    json_value(Value);
directory_string_value(<<12, Len:8, Value:Len/binary, _/binary>>) ->
    Value;
directory_string_value(Value) ->
    json_value(Value).

time_value({utcTime, Value}) ->
    json_value(Value);
time_value({generalTime, Value}) ->
    json_value(Value);
time_value(Value) ->
    json_value(Value).

json_value(undefined) ->
    null;
json_value(null) ->
    null;
json_value(Value) when is_binary(Value); is_boolean(Value); is_integer(Value); is_float(Value) ->
    Value;
json_value(Value) when is_atom(Value) ->
    atom_to_binary(Value, utf8);
json_value(Value) when is_list(Value) ->
    unicode:characters_to_binary(Value);
json_value(Value) when is_map(Value) ->
    maps:map(fun(_Key, MapValue) -> json_value(MapValue) end, Value);
json_value(_Value) ->
    null.