~cypheon/ecertmon

ref: c41253492b6d9ba83cec931602aaa355908bdaed ecertmon/src/cert_scanner.erl -rw-r--r-- 3.6 KiB
c4125349 — Johann Rudloff Allow a certain threshold of errors before reporting invalid cert 8 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
-module(cert_scanner).
-behaviour(gen_server).

-include_lib("public_key/include/public_key.hrl"). 

%% API.
-export([start_link/1]).

%% gen_server.
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).

-record(state, {
          hostname,
          port,
          timer,
          validity,
          % count of errors since last success
          errorCount,
          % total count of errors
          totalErrors
}).

%% API.

-spec start_link({string(), number()}) -> {ok, pid()}.
start_link({Hostname, Port}) ->
  gen_server:start_link(?MODULE, [{Hostname, Port}], []).

%% gen_server.

init([{Hostname, Port}]) ->
  {ok, ScanInterval} = application:get_env(scan_interval),
  logger:info("Cert Scanner initialized for ~s:~p~n", [Hostname, Port]),
  erlang:send_after(300, self(), scan_trigger),
  Timer = timer:send_interval(ScanInterval, self(), scan_trigger),
  {ok, #state{
          hostname = Hostname,
          port = Port,
          timer = Timer,
          validity = unknown,
          errorCount = 0,
          totalErrors = 0
         }}.

handle_call({get_status}, _From, State) ->
  {reply, [
           State#state.validity,
           {errors, State#state.totalErrors}
          ], State};
handle_call(_Request, _From, State) ->
  {reply, ignored, State}.

handle_cast(_Msg, State) ->
  {noreply, State}.

handle_info(scan_trigger, State) ->
  do_scan(State);

handle_info(_Info, State) ->
  {noreply, State}.

terminate(_Reason, _State) ->
  ok.

code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%% logic
do_scan(State) ->
  Hostname = State#state.hostname,
  case ssl:connect(State#state.hostname, State#state.port, [], 5000) of
  {ok, Socket} ->
      logger:debug("connection established: ~s:~p~n", [Hostname,
                                                       State#state.port]),
      {ok, PeerCertBytes} = ssl:peercert(Socket),
      ok = ssl:close(Socket),

      PeerCert = public_key:pkix_decode_cert(PeerCertBytes, otp),
      Validity = (PeerCert#'OTPCertificate'.tbsCertificate)
      #'OTPTBSCertificate'.validity,
      NotAfterStr = Validity#'Validity'.notAfter,
      NotAfterSeconds = pubkey_cert:time_str_2_gregorian_sec(NotAfterStr),
      NotAfter = calendar:gregorian_seconds_to_datetime(NotAfterSeconds),
      NotAfterEpoch = datetime_to_epoch(NotAfter),
      NowSeconds = calendar:datetime_to_gregorian_seconds(
                     calendar:universal_time()),
      RemainingDays = (NotAfterSeconds - NowSeconds) / (24 * 3600),
      logger:debug("peer cert ~s valid until: ~p (unix epoch: ~p, remaining days: ~p)~n",
                   [Hostname, NotAfter, NotAfterEpoch, RemainingDays]),
      {noreply, State#state{validity = {valid, NotAfterEpoch}, errorCount = 0}};
    {error, Reason} ->
      logger:warning("failed to connect to ~p: ~p", [Hostname, Reason]),
      {ok, MaxErrorCount} = application:get_env(max_error_count),

      % if max error count is not reached, we keep the old validity
      ReportedValidity = if
                           State#state.errorCount < MaxErrorCount ->
                             State#state.validity;
                           true -> {error, Reason}
                         end,
      {noreply, State#state{
                  validity = ReportedValidity,
                  errorCount = State#state.errorCount + 1,
                  totalErrors = State#state.totalErrors + 1
      }}
  end.

%% utilities

datetime_to_epoch(DateTime) ->
  calendar:datetime_to_gregorian_seconds(DateTime) -
  calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}).