%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2016-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

%%----------------------------------------------------------------------
%% Purpose: Manages ssl sessions and trusted certifacates
%% (Note: See the document internal_doc/pem_and_cert_cache.md for additional
%% information)
%%----------------------------------------------------------------------

-module(ssl_pem_cache).
-moduledoc false.
-behaviour(gen_server).

%% Internal application API
-export([start_link/1, 
	 start_link_dist/1,
	 name/1,
	 insert/2,
         insert/3,
	 clear/0]).

% Spawn export
-export([init_pem_cache_validator/1]).

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

-include("ssl_handshake.hrl").
-include("ssl_internal.hrl").
-include_lib("kernel/include/file.hrl").

-record(state, {
	  pem_cache,                  
	  last_pem_check             :: integer(),
	  clear            :: integer()
	 }).

-define(CLEAR_PEM_CACHE, 120000).

%%====================================================================
%% API
%%====================================================================

%%--------------------------------------------------------------------
-spec name(normal | dist) -> atom().
%%
%% Description: Returns the registered name of the ssl cache process
%% in the operation modes 'normal' and 'dist'.
%%--------------------------------------------------------------------
name(normal) ->
    ?MODULE;
name(dist) ->
    list_to_atom(atom_to_list(?MODULE) ++ "_dist").

%%--------------------------------------------------------------------
-spec start_link(list()) -> {ok, pid()} | ignore | {error, term()}.
%%
%% Description: Starts the ssl pem cache handler 
%%--------------------------------------------------------------------
start_link(_) ->
    CacheName = name(normal),
    gen_server:start_link({local, CacheName}, 
			  ?MODULE, [CacheName], []).

%%--------------------------------------------------------------------
-spec start_link_dist(list()) -> {ok, pid()} | ignore | {error, term()}.
%%
%% Description: Starts a special instance of the ssl manager to
%% be used by the erlang distribution. Note disables soft upgrade!
%%--------------------------------------------------------------------
start_link_dist(_) ->
    DistCacheName = name(dist),
    gen_server:start_link({local, DistCacheName}, 
			  ?MODULE, [DistCacheName], []).


%%--------------------------------------------------------------------
-spec insert(binary(), term()) -> ok | {error, ssl:reason()}.
%%		    
%% Description: Cache a pem file and return its content.
%%--------------------------------------------------------------------
insert(File, Content) ->    
    case bypass_cache() of
	true ->
	    ok;
	false ->
	    cast({cache_pem, File, Content}),
            ok
    end.
%%--------------------------------------------------------------------
-spec insert(Name ::atom(), binary(), term()) -> ok | {error, ssl:reason()}.
%%
%% Description: Cache a pem file and return its content.
%%--------------------------------------------------------------------
insert(Name, File, Content) ->
    case bypass_cache() of
	true ->
	    ok;
	false ->
	    cast(Name, {cache_pem, File, Content}),
            ok
    end.


%%--------------------------------------------------------------------
-spec clear() -> ok.
%%
%% Description: Clear the PEM cache
%%--------------------------------------------------------------------
clear() ->
    %% Not supported for distribution at the moement, should it be?
    put(ssl_pem_cache, name(normal)),
    call(unconditionally_clear_pem_cache).

-spec invalidate_pem(File::binary()) -> ok.
invalidate_pem(File) ->
    cast({invalidate_pem, File}).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%%--------------------------------------------------------------------
-spec init(list()) -> {ok, #state{}}.
%% Possible return values not used now. 
%% |  {ok, #state{}, timeout()} | ignore | {stop, term()}.		  
%%
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([Name]) ->
    put(ssl_pem_cache, Name),
    process_flag(trap_exit, true),
    PemCache = ssl_pkix_db:create_pem_cache(Name),
    Interval = pem_check_interval(),
    erlang:send_after(Interval, self(), clear_pem_cache),
    erlang:system_time(second),
    {ok, #state{pem_cache = PemCache,
		last_pem_check =  erlang:convert_time_unit(os:system_time(), native, second),
		clear = Interval 	
	       }}.

%%--------------------------------------------------------------------
-spec handle_call(term(), gen_server:from(), #state{}) -> {reply, Reply::term(), #state{}}.
%% Possible return values not used now.  
%%					      {reply, term(), #state{}, timeout()} |
%%					      {noreply, #state{}} |
%%					      {noreply, #state{}, timeout()} |
%%					      {stop, reason(), term(), #state{}} |
%%					      {stop, reason(), #state{}}.
%%
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call({unconditionally_clear_pem_cache, _},_, 
	    #state{pem_cache = PemCache} = State) ->
    ssl_pkix_db:clear(PemCache),
    Result = ssl_manager:refresh_trusted_db(ssl_manager_type()),
    {reply, Result,  State}.

%%--------------------------------------------------------------------
-spec  handle_cast(term(), #state{}) -> {noreply, #state{}}.
%% Possible return values not used now.  
%%				      | {noreply, #state{}, timeout()} |
%%				       {stop, reason(), #state{}}.
%%
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast({cache_pem, File, Content}, #state{pem_cache = Db} = State) ->
    ssl_pkix_db:insert(File, Content, Db), 
    {noreply, State};

handle_cast({invalidate_pem, File}, #state{pem_cache = Db} = State) ->
    ssl_pkix_db:remove(File, Db),
    ssl_manager:refresh_trusted_db(ssl_manager_type(), File),
    {noreply, State}.


%%--------------------------------------------------------------------
-spec handle_info(term(), #state{}) -> {noreply, #state{}}.
%% Possible return values not used now.
%%				      |{noreply, #state{}, timeout()} |
%%				      {stop, reason(), #state{}}.
%%
%% Description: Handling all non call/cast messages
%%-------------------------------------------------------------------
handle_info(clear_pem_cache, #state{pem_cache = PemCache,
				    clear = Interval,
				    last_pem_check = CheckPoint} = State) ->
    NewCheckPoint = erlang:convert_time_unit(os:system_time(), native, second),
    start_pem_cache_validator(PemCache, CheckPoint),
    erlang:send_after(Interval, self(), clear_pem_cache),
    {noreply, State#state{last_pem_check = NewCheckPoint}};

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

%%--------------------------------------------------------------------
-spec terminate(ssl:reason(), #state{}) -> ok.
%%		       
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, #state{}) ->
    ok.

%%--------------------------------------------------------------------
-spec code_change(term(), #state{}, list()) -> {ok, #state{}}.			 
%%
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
call(Msg) ->
    gen_server:call(get(ssl_pem_cache), {Msg, self()}, infinity).

cast(Msg) ->
    cast(get(ssl_pem_cache), Msg).

cast(Name, Msg) ->
    gen_server:cast(Name, Msg).

start_pem_cache_validator(PemCache, CheckPoint) ->
    spawn_link(?MODULE, init_pem_cache_validator, 
	       [[get(ssl_pem_cache), PemCache, CheckPoint]]).

init_pem_cache_validator([CacheName, PemCache, CheckPoint]) ->
    put(ssl_pem_cache, CacheName),
    ssl_pkix_db:foldl(fun pem_cache_validate/2,
		      CheckPoint, PemCache).

pem_cache_validate({File, _}, CheckPoint) ->
    case file:read_file_info(File, [{time, posix}]) of
	{ok, #file_info{mtime = Time}} when Time < CheckPoint ->
	    ok;
	_  ->
	    invalidate_pem(File)
    end,
    CheckPoint.

pem_check_interval() ->
    case application:get_env(ssl, ssl_pem_cache_clean) of
	{ok, Interval} when is_integer(Interval) ->
	    Interval;
	_  ->
	    ?CLEAR_PEM_CACHE
    end.

bypass_cache() ->
    case application:get_env(ssl, bypass_pem_cache) of
	{ok, Bool} when is_boolean(Bool) ->
	    Bool;
	_ ->
	    false
    end.

ssl_manager_type() ->
    case get(ssl_pem_cache) of
        ?MODULE ->
            normal;
        _ ->
            dist
    end.
