# frozen_string_literal: true

# Copyright (C) 2020 MongoDB Inc.
#
# 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.

module Mongo
  class Socket
    # This module caches OCSP responses for their indicated validity time.
    #
    # The key is the CertificateId used for the OCSP request.
    # The value is an OcspVerifier::Response.
    #
    # @api private
    module OcspCache
      module_function def set(cert_id, response)
        delete(cert_id)
        responses << response
      end

      # Retrieves a cached SingleResponse for the specified CertificateId.
      #
      # This method may return expired responses if they are revoked.
      # Such responses were valid when they were first received.
      #
      # This method may also return responses that are valid but that may
      # expire by the time caller uses them. The caller should not perform
      # update time checks on the returned response.
      #
      # @return [ OpenSSL::OCSP::SingleResponse ] The previously
      #   retrieved response.
      module_function def get(cert_id)
        resp = responses.detect do |resp|
          resp.certid.cmp(cert_id)
        end
        # Only expire responses with good status.
        # Once a certificate is revoked, it should stay revoked forever,
        # hence we should be able to cache revoked responses indefinitely.
        if resp && resp.cert_status == OpenSSL::OCSP::V_CERTSTATUS_GOOD &&
           resp.next_update < Time.now
          responses.delete(resp)
          resp = nil
        end

        # If we have connected to a server and cached the OCSP response for it,
        # and then never connect to that server again, the cached OCSP response
        # is going to remain in memory indefinitely. Periodically remove all
        # expired OCSP responses, not just the ones matching the certificate id
        # we are querying by.
        if rand < 0.01
          responses.delete_if do |resp|
            resp.next_update < Time.now
          end
        end

        resp
      end

      module_function def delete(cert_id)
        responses.delete_if do |resp|
          resp.certid.cmp(cert_id)
        end
      end

      # Clears the driver's OCSP response cache.
      #
      # @note Use Mongo.clear_ocsp_cache from applications instead of invoking
      #   this method directly.
      module_function def clear
        responses.replace([])
      end

      private

      LOCK = Mutex.new

      module_function def responses
        LOCK.synchronize do
          @responses ||= []
        end
      end
    end
  end
end
