/**
 * TLS Session Manger base class implementations
 * (C) 2011-2023 Jack Lloyd
 *     2022-2023 René Meusel - Rohde & Schwarz Cybersecurity
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */

#include <botan/tls_session_manager.h>

#include <botan/assert.h>
#include <botan/rng.h>
#include <botan/tls_callbacks.h>
#include <botan/tls_policy.h>
#include <botan/tls_session.h>
#include <algorithm>

#if defined(BOTAN_HAS_TLS_13)
   #include <botan/tls_psk_identity_13.h>
#endif

namespace Botan::TLS {

Session_Manager::Session_Manager(const std::shared_ptr<RandomNumberGenerator>& rng) : m_rng(rng) {
   BOTAN_ASSERT_NONNULL(m_rng);
}

std::optional<Session_Handle> Session_Manager::establish(const Session& session,
                                                         const std::optional<Session_ID>& id,
                                                         bool tls12_no_ticket) {
   // Establishing a session does not require locking at this level as
   // concurrent TLS instances on a server will create unique sessions.

   // By default, the session manager does not emit session tickets anyway
   BOTAN_UNUSED(tls12_no_ticket);
   BOTAN_ASSERT(session.side() == Connection_Side::Server, "Client tried to establish a session");

   Session_Handle handle(id.value_or(m_rng->random_vec<Session_ID>(32)));
   store(session, handle);
   return handle;
}

std::optional<Session> Session_Manager::retrieve(const Session_Handle& handle,
                                                 Callbacks& callbacks,
                                                 const Policy& policy) {
   // Retrieving a session for a given handle does not require locking on this
   // level. Concurrent threads might handle the removal of an expired ticket
   // more than once, but removing an already removed ticket is a harmless NOOP.

   auto session = retrieve_one(handle);
   if(!session.has_value()) {
      return std::nullopt;
   }

   // A value of '0' means: No policy restrictions.
   const std::chrono::seconds policy_lifetime =
      (policy.session_ticket_lifetime().count() > 0) ? policy.session_ticket_lifetime() : std::chrono::seconds::max();

   // RFC 5077 3.3 -- "Old Session Tickets"
   //    A server MAY treat a ticket as valid for a shorter or longer period of
   //    time than what is stated in the ticket_lifetime_hint.
   //
   // RFC 5246 F.1.4 -- TLS 1.2
   //    If either party suspects that the session may have been compromised, or
   //    that certificates may have expired or been revoked, it should force a
   //    full handshake.  An upper limit of 24 hours is suggested for session ID
   //    lifetimes.
   //
   // RFC 8446 4.6.1 -- TLS 1.3
   //    A server MAY treat a ticket as valid for a shorter period of time than
   //    what is stated in the ticket_lifetime.
   //
   // Note: This disregards what is stored in the session (e.g. "lifetime_hint")
   //       and only takes the local policy into account. The lifetime stored in
   //       the sessions was taken from the same policy anyways and changes by
   //       the application should have an immediate effect.
   const auto ticket_age =
      std::chrono::duration_cast<std::chrono::seconds>(callbacks.tls_current_timestamp() - session->start_time());
   const bool expired = ticket_age > policy_lifetime;

   if(expired) {
      remove(handle);
      return std::nullopt;
   } else {
      return session;
   }
}

std::vector<Session_with_Handle> Session_Manager::find_and_filter(const Server_Information& info,
                                                                  Callbacks& callbacks,
                                                                  const Policy& policy) {
   // A value of '0' means: No policy restrictions. Session ticket lifetimes as
   // communicated by the server apply regardless.
   const std::chrono::seconds policy_lifetime =
      (policy.session_ticket_lifetime().count() > 0) ? policy.session_ticket_lifetime() : std::chrono::seconds::max();

   const size_t max_sessions_hint = std::max(policy.maximum_session_tickets_per_client_hello(), size_t(1));
   const auto now = callbacks.tls_current_timestamp();

   // An arbitrary number of loop iterations to perform before giving up
   // to avoid a potential endless loop with a misbehaving session manager.
   constexpr unsigned int max_attempts = 10;
   std::vector<Session_with_Handle> sessions_and_handles;

   // Query the session manager implementation for new sessions until at least
   // one session passes the filter or no more sessions are found.
   for(unsigned int attempt = 0; attempt < max_attempts && sessions_and_handles.empty(); ++attempt) {
      sessions_and_handles = find_some(info, max_sessions_hint);

      // ... underlying implementation didn't find anything. Early exit.
      if(sessions_and_handles.empty()) {
         break;
      }

      std::erase_if(sessions_and_handles, [&](const auto& session) {
         const auto age = std::chrono::duration_cast<std::chrono::seconds>(now - session.session.start_time());

         // RFC 5077 3.3 -- "Old Session Tickets"
         //    The ticket_lifetime_hint field contains a hint from the
         //    server about how long the ticket should be stored. [...]
         //    A client SHOULD delete the ticket and associated state when
         //    the time expires. It MAY delete the ticket earlier based on
         //    local policy.
         //
         //    A value [in ticket_lifetime_hint] of zero is reserved to indicate
         //    that the lifetime of the ticket is unspecified.
         //
         // RFC 5246 F.1.4 -- TLS 1.2
         //    If either party suspects that the session may have been
         //    compromised, or that certificates may have expired or been
         //    revoked, it should force a full handshake.  An upper limit of
         //    24 hours is suggested for session ID lifetimes.
         //
         // RFC 8446 4.2.11.1 -- TLS 1.3
         //    The client's view of the age of a ticket is the time since the
         //    receipt of the NewSessionTicket message.  Clients MUST NOT
         //    attempt to use tickets which have ages greater than the
         //    "ticket_lifetime" value which was provided with the ticket.
         //
         // RFC 8446 4.6.1 -- TLS 1.3
         //    Clients MUST NOT cache tickets for longer than 7 days,
         //    regardless of the ticket_lifetime, and MAY delete tickets
         //    earlier based on local policy.
         //
         // Note: TLS 1.3 tickets with a lifetime longer than 7 days are
         //       rejected during parsing with an "Illegal Parameter" alert.
         //       Other suggestions are left to the application via
         //       Policy::session_ticket_lifetime(). Session lifetimes as
         //       communicated by the server via the "lifetime_hint" are
         //       obeyed regardless of the policy setting.
         const auto session_lifetime_hint = session.session.lifetime_hint();

         const bool is_rfc5077_unspecified =
            (session_lifetime_hint.count() == 0 && session.session.version().is_pre_tls_13());
         const auto effective_hint = is_rfc5077_unspecified ? std::chrono::seconds::max() : session_lifetime_hint;
         const bool expired = age > std::min(policy_lifetime, effective_hint);

         if(expired) {
            remove(session.handle);
         }

         return expired;
      });
   }

   return sessions_and_handles;
}

std::vector<Session_with_Handle> Session_Manager::find(const Server_Information& info,
                                                       Callbacks& callbacks,
                                                       const Policy& policy) {
   auto allow_reusing_tickets = policy.reuse_session_tickets();

   // Session_Manager::find() must be an atomic getter if ticket reuse is not
   // allowed. I.e. each ticket handed to concurrently requesting threads must
   // be unique. In that case we must hold a lock while retrieving a ticket.
   // Otherwise, no locking is required on this level.
   std::optional<lock_guard_type<recursive_mutex_type>> lk;
   if(!allow_reusing_tickets) {
      lk.emplace(mutex());
   }

   auto sessions_and_handles = find_and_filter(info, callbacks, policy);

   // std::vector::resize() cannot be used as the vector's members aren't
   // default constructible.
   const auto session_limit = policy.maximum_session_tickets_per_client_hello();
   while(session_limit > 0 && sessions_and_handles.size() > session_limit) {
      sessions_and_handles.pop_back();
   }

   // RFC 8446 Appendix C.4
   //    Clients SHOULD NOT reuse a ticket for multiple connections. Reuse of
   //    a ticket allows passive observers to correlate different connections.
   //
   // When reuse of session tickets is not allowed, remove all tickets to be
   // returned from the implementation's internal storage.
   if(!allow_reusing_tickets) {
      // The lock must be held here, otherwise we cannot guarantee the
      // transactional retrieval of tickets to concurrently requesting clients.
      BOTAN_ASSERT_NOMSG(lk.has_value());
      for(const auto& [session, handle] : sessions_and_handles) {
         if(!session.version().is_pre_tls_13() || !handle.is_id()) {
            remove(handle);
         }
      }
   }

   return sessions_and_handles;
}

#if defined(BOTAN_HAS_TLS_13)

std::optional<std::pair<Session, uint16_t>> Session_Manager::choose_from_offered_tickets(
   const std::vector<PskIdentity>& tickets,
   std::string_view hash_function,
   Callbacks& callbacks,
   const Policy& policy) {
   // Note that the TLS server currently does not ensure that tickets aren't
   // reused. As a result, no locking is required on this level.

   // Limit how many identities we attempt to look up to prevent a client
   // from forcing excessive session store queries.
   const size_t max_attempts = std::min<size_t>(tickets.size(), 5);

   for(size_t i = 0; i < max_attempts; ++i) {
      const auto& ticket = tickets[i];
      auto session = retrieve(Session_Handle(Opaque_Session_Handle(ticket.identity())), callbacks, policy);
      if(session.has_value() && session->ciphersuite().prf_algo() == hash_function &&
         session->version().is_tls_13_or_later()) {
         return std::pair{std::move(session.value()), static_cast<uint16_t>(i)};
      }

      // RFC 8446 4.2.10
      //    For PSKs provisioned via NewSessionTicket, a server MUST validate
      //    that the ticket age for the selected PSK identity [...] is within a
      //    small tolerance of the time since the ticket was issued.  If it is
      //    not, the server SHOULD proceed with the handshake but reject 0-RTT,
      //    and SHOULD NOT take any other action that assumes that this
      //    ClientHello is fresh.
      //
      // TODO: The ticket-age is currently not checked (as 0-RTT is not
      //       implemented) and we simply take the SHOULD at face value.
      //       Instead we could add a policy check letting the user decide.
   }

   return std::nullopt;
}

#endif

}  // namespace Botan::TLS
