/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef Http3Session_H_
#define Http3Session_H_

#include "HttpTrafficAnalyzer.h"
#include "mozilla/Array.h"
#include "mozilla/WeakPtr.h"
#include "mozilla/net/NeqoHttp3Conn.h"
#include "nsAHttpConnection.h"
#include "nsDeque.h"
#include "nsISupportsImpl.h"
#include "nsITimer.h"
#include "nsIUDPSocket.h"
#include "nsRefPtrHashtable.h"
#include "nsTHashMap.h"
#include "nsWeakReference.h"

/*
 * WebTransport
 *
 * Http3Session and the underlying neqo code support multiplexing of multiple
 * WebTransport and multiplexing WebTransport sessions with regular HTTP
 * traffic. Whether WebTransport sessions are polled, will be controlled by the
 * nsHttpConnectionMgr.
 *
 * WebTransport support is negotiated using HTTP/3 setting. Before the settings
 * are available all WebTransport transactions are queued in
 * mWaitingForWebTransportNegotiation. The information on whether WebTransport
 * is supported is received via an HTTP/3 event. The event is
 * Http3Event::Tag::WebTransport  with the value
 * WebTransportEventExternal::Tag::Negotiated  that can be true or false. If
 * the WebTransport feature has been negotiated, queued transactions will be
 * activated otherwise they will be canceled(i.e. WebTransportNegotiationDone).
 *
 * The lifetime of a WebTransport session
 *
 * A WebTransport lifetime consists of 2 parts:
 * - WebTransport session setup
 * - WebTransport session active time
 *
 * WebTransport session setup:
 * A WebTransport session uses a regular HTTP request for negotiation.
 * Therefore when a new WebTransport is started a nsHttpChannel and the
 * corresponding nsHttpTransaction are created. The nsHttpTransaction is
 * dispatched to a Http3Session and after the
 * WebTransportEventExternal::Tag::Negotiated event it behaves almost the same
 * as a regular transaction, e.g. it is added to mStreamTransactionHash and
 * mStreamIdHash. For activating the session NeqoHttp3Conn::CreateWebTransport
 * is  called instead of NeqoHttp3Conn::Fetch(this is called for the regular
 * HTTP requests). In this phase, the WebTransport session is canceled in the
 * same way a regular request is canceled, by canceling the corresponding
 * nsHttpChannel. If HTTP/3 connection is closed in this phase the
 * corresponding nsHttpTransaction is canceled and this may cause the
 * transaction to be restarted (this is the existing restart logic) or the
 * error is propagated to the nsHttpChannel and its listener(via OnStartRequest
 * and OnStopRequest as the regular HTTP request).
 * The phase ends when a connection breaks or when the event
 * Http3Event::Tag::WebTransport with the value
 * WebTransportEventExternal::Tag::Session is received. The parameter
 * aData(from NeqoHttp3Conn::GetEvent) contain the HTTP head of the response.
 * The headers may be:
 * - failed code, i.e. anything except 200. In this case, the nsHttpTransaction
 *   behaves the same as a normal HTTP request and the nsHttpChannel listener
 *   will be informed via OnStartRequest and OnStopRequest calls.
 * - success code, i.e. 200. The code will be propagated to the
 *   nsHttpTransaction. The transaction will parse the header and call
 *   Http3Session::GetWebTransportSession. The function transfers WebTransport
 *   session into the next phase:
 *   - Removes nsHttpTransaction from mStreamTransactionHash.
 *   - Adds the stream to mWebTransportSessions
 *   - The nsHttpTransaction supplies Http3WebTransportSession to the
 *     WebTransportSessionProxy and vice versa.
 *     TODO remove this circular referencing.
 *
 * WebTransport session active time:
 * During this phase the following actions are possible:
 * - Cancelling a WebTransport session by the application:
 *   The application calls Http3WebTransportSession::CloseSession. This
 *   transfers Http3WebTransportSession into the CLOSE_PENDING state and calls
 *   Http3Session::ConnectSlowConsumer to add itself to the “ready for reading
 *   queue”. Consequently, the Http3Session will call
 *   Http3WebTransportSession::WriteSegments and
 *   Http3Session::CloseWebTransport will be called to send the closing signal
 *   to the peer. After this, the Http3WebTransportSession is in the state DONE
 *   and it will be removed from the Http3Session(the CloseStream function
 *   takes care of this).
 * - The peer sending a session closing signal:
 *   Http3Session will receive a Http3Event::Tag::WebTransport event with value
 *   WebTransportEventExternal::Tag::SessionClosed. The
 *   Http3WebTransportSession::OnSessionClosed function for the corresponding
 *   stream wil be called. The function will inform the corresponding
 *   WebTransportSessionProxy by calling OnSessionClosed function. The
 *   Http3WebTransportSession is in the state DONE and will be removed from the
 *   Http3Session(the CloseStream function takes care of this).
 */

namespace mozilla::net {

class HttpConnectionUDP;
class Http3StreamBase;
class QuicSocketControl;
class Http3WebTransportSession;
class Http3WebTransportStream;
class nsHttpConnection;

// IID for the Http3Session interface
#define NS_HTTP3SESSION_IID \
  {0x8fc82aaf, 0xc4ef, 0x46ed, {0x89, 0x41, 0x93, 0x95, 0x8f, 0xac, 0x4f, 0x21}}

enum class EchExtensionStatus {
  kNotPresent,  // No ECH Extension was sent
  kGREASE,      // A GREASE ECH Extension was sent
  kReal         // A 'real' ECH Extension was sent
};

// An abstract layer for testing.
class Http3SessionBase {
 public:
  NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING

  virtual nsresult TryActivating(const nsACString& aMethod,
                                 const nsACString& aScheme,
                                 const nsACString& aAuthorityHeader,
                                 const nsACString& aPath,
                                 const nsACString& aHeaders,
                                 uint64_t* aStreamId,
                                 Http3StreamBase* aStream) = 0;
  virtual void CloseSendingSide(uint64_t aStreamId) = 0;
  virtual void SendHTTPDatagram(uint64_t aStreamId, nsTArray<uint8_t>& aData,
                                uint64_t aTrackingId) = 0;
  virtual nsresult SendPriorityUpdateFrame(uint64_t aStreamId,
                                           uint8_t aPriorityUrgency,
                                           bool aPriorityIncremental) = 0;
  virtual void ConnectSlowConsumer(Http3StreamBase* stream) = 0;

  virtual nsresult SendRequestBody(uint64_t aStreamId, const char* buf,
                                   uint32_t count, uint32_t* countRead) = 0;
  virtual nsresult ReadResponseData(uint64_t aStreamId, char* aBuf,
                                    uint32_t aCount, uint32_t* aCountWritten,
                                    bool* aFin) = 0;
  virtual void FinishTunnelSetup(nsAHttpTransaction* aTransaction) = 0;

  virtual void CloseStream(Http3StreamBase* aStream, nsresult aResult) {}

  // For WebTransport
  virtual void CloseWebTransportConn() = 0;
  virtual void StreamHasDataToWrite(Http3StreamBase* aStream) = 0;
  virtual nsresult CloseWebTransport(uint64_t aSessionId, uint32_t aError,
                                     const nsACString& aMessage) = 0;
  virtual void SendDatagram(Http3WebTransportSession* aSession,
                            nsTArray<uint8_t>& aData, uint64_t aTrackingId) = 0;
  virtual uint64_t MaxDatagramSize(uint64_t aSessionId) = 0;
  virtual nsresult TryActivatingWebTransportStream(
      uint64_t* aStreamId, Http3StreamBase* aStream) = 0;
  virtual void ResetWebTransportStream(Http3WebTransportStream* aStream,
                                       uint64_t aErrorCode) = 0;
  virtual void StreamStopSending(Http3WebTransportStream* aStream,
                                 uint8_t aErrorCode) = 0;
  virtual void SetSendOrder(Http3StreamBase* aStream,
                            Maybe<int64_t> aSendOrder) = 0;
};

class Http3Session final : public Http3SessionBase,
                           public nsAHttpTransaction,
                           public nsAHttpConnection {
 public:
  NS_INLINE_DECL_STATIC_IID(NS_HTTP3SESSION_IID)

  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_NSAHTTPTRANSACTION
  NS_DECL_NSAHTTPCONNECTION(mConnection)

  class OnQuicTimeout final : public nsITimerCallback, public nsINamed {
   public:
    NS_DECL_THREADSAFE_ISUPPORTS
    NS_DECL_NSITIMERCALLBACK
    NS_DECL_NSINAMED

    explicit OnQuicTimeout(HttpConnectionUDP* aConnection);

   private:
    ~OnQuicTimeout() = default;
    RefPtr<HttpConnectionUDP> mConnection;
  };

  Http3Session();
  nsresult Init(const nsHttpConnectionInfo* aConnInfo, nsINetAddr* selfAddr,
                nsINetAddr* peerAddr, HttpConnectionUDP* udpConn,
                uint32_t aProviderFlags, nsIInterfaceRequestor* callbacks,
                nsIUDPSocket* socket, bool aIsTunnel = false);

  bool IsConnected() const { return mState == CONNECTED; }
  bool CanSendData() const {
    return (mState == CONNECTED) || (mState == ZERORTT);
  }
  bool IsClosing() const { return (mState == CLOSING || mState == CLOSED); }
  bool IsClosed() const { return mState == CLOSED; }

  bool AddStream(nsAHttpTransaction* aHttpTransaction, int32_t aPriority,
                 nsIInterfaceRequestor* aCallbacks);

  // Swap the transaction backing an existing stream. Used by the HE /
  // 0-RTT adopt path: the HappyEyeballsTransaction shim was the key
  // under which AddStream registered the stream; after the real
  // nsHttpTransaction adopts it, we need both mStreamTransactionHash
  // and the stream's own mTransaction to point at the real txn so
  // CloseTransaction(real_txn) can find the stream. No-op if aOld
  // isn't in the hash.
  void SwapTransaction(nsAHttpTransaction* aOld, nsAHttpTransaction* aNew);

  bool CanReuse();

  // The following functions are used by Http3Stream and
  // Http3WebTransportSession:
  nsresult TryActivating(const nsACString& aMethod, const nsACString& aScheme,
                         const nsACString& aAuthorityHeader,
                         const nsACString& aPath, const nsACString& aHeaders,
                         uint64_t* aStreamId,
                         Http3StreamBase* aStream) override;
  // The folowing functions are used by Http3Stream:
  void CloseSendingSide(uint64_t aStreamId) override;
  nsresult SendRequestBody(uint64_t aStreamId, const char* buf, uint32_t count,
                           uint32_t* countRead) override;
  nsresult ReadResponseHeaders(uint64_t aStreamId,
                               nsTArray<uint8_t>& aResponseHeaders, bool* aFin);
  nsresult ReadResponseData(uint64_t aStreamId, char* aBuf, uint32_t aCount,
                            uint32_t* aCountWritten, bool* aFin) override;

  // The folowing functions are used by Http3WebTransportSession:
  nsresult CloseWebTransport(uint64_t aSessionId, uint32_t aError,
                             const nsACString& aMessage) override;
  nsresult CreateWebTransportStream(uint64_t aSessionId,
                                    WebTransportStreamType aStreamType,
                                    uint64_t* aStreamId);
  void CloseStream(Http3StreamBase* aStream, nsresult aResult) override;
  void CloseStreamInternal(Http3StreamBase* aStream, nsresult aResult);

  void SetCleanShutdown(bool aCleanShutdown) {
    mCleanShutdown = aCleanShutdown;
  }

  bool TestJoinConnection(const nsACString& hostname, int32_t port);
  bool JoinConnection(const nsACString& hostname, int32_t port);

  void TransactionHasDataToWrite(nsAHttpTransaction* caller) override;
  void TransactionHasDataToRecv(nsAHttpTransaction* caller) override;
  [[nodiscard]] nsresult GetTransactionTLSSocketControl(
      nsITLSSocketControl**) override;

  // This function will be called by QuicSocketControl when the certificate
  // verification is done.
  void Authenticated(int32_t aError, bool aServCertHashesSucceeded = false);

  nsresult ProcessOutputAndEvents(nsIUDPSocket* socket);

  void ReportHttp3Connection();

  int64_t GetBytesWritten() { return mTotalBytesWritten; }
  int64_t BytesRead() { return mTotalBytesRead; }

  nsresult SendData(nsIUDPSocket* socket);
  nsresult RecvData(nsIUDPSocket* socket);

  void DoSetEchConfig(const nsACString& aEchConfig);

  nsresult SendPriorityUpdateFrame(uint64_t aStreamId, uint8_t aPriorityUrgency,
                                   bool aPriorityIncremental) override;

  void ConnectSlowConsumer(Http3StreamBase* stream) override;

  nsresult TryActivatingWebTransportStream(uint64_t* aStreamId,
                                           Http3StreamBase* aStream) override;
  void CloseWebTransportStream(Http3WebTransportStream* aStream,
                               nsresult aResult);
  void StreamHasDataToWrite(Http3StreamBase* aStream) override;
  void ResetWebTransportStream(Http3WebTransportStream* aStream,
                               uint64_t aErrorCode) override;
  void StreamStopSending(Http3WebTransportStream* aStream,
                         uint8_t aErrorCode) override;

  void SendDatagram(Http3WebTransportSession* aSession,
                    nsTArray<uint8_t>& aData, uint64_t aTrackingId) override;
  void SendHTTPDatagram(uint64_t aStreamId, nsTArray<uint8_t>& aData,
                        uint64_t aTrackingId) override;

  uint64_t MaxDatagramSize(uint64_t aSessionId) override;

  void SetSendOrder(Http3StreamBase* aStream,
                    Maybe<int64_t> aSendOrder) override;

  void CloseWebTransportConn() override;

  void FinishTunnelSetup(nsAHttpTransaction* aTransaction) override;

  Http3Stats GetStats();

  // For connect-udp
  already_AddRefed<HttpConnectionUDP> CreateTunnelStream(
      nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks);
  // For HTTP CONNECT
  already_AddRefed<nsHttpConnection> CreateTunnelStream(
      nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks,
      PRIntervalTime aRtt, bool aIsExtendedCONNECT);
  void SetIsInTunnel() { mIsInTunnel = true; }

  void SetDontExclude() { mDontExclude = true; }

 private:
  ~Http3Session();

  void CloseInternal(bool aCallNeqoClose);
  void Shutdown();

  bool RealJoinConnection(const nsACString& hostname, int32_t port,
                          bool justKidding);

  nsresult ProcessOutput(nsIUDPSocket* socket);
  nsresult ProcessInput(nsIUDPSocket* socket);
  nsresult ProcessEvents();

  nsresult ProcessTransactionRead(uint64_t stream_id);
  nsresult ProcessTransactionRead(Http3StreamBase* stream);
  nsresult ProcessSlowConsumers();

  void SetupTimer(uint64_t aTimeout);

  enum ResetType {
    RESET,
    STOP_SENDING,
  };
  void ResetOrStopSendingRecvd(uint64_t aStreamId, uint64_t aError,
                               ResetType aType);

  void QueueStream(Http3StreamBase* stream);
  void RemoveStreamFromQueues(Http3StreamBase*);
  void ProcessPending();

  void CallCertVerification(Maybe<nsCString> aEchPublicName);
  void SetSecInfo();

#ifndef ANDROID
  void EchOutcomeTelemetry();
#endif

  void StreamReadyToWrite(Http3StreamBase* aStream);
  void MaybeResumeSend();

  void CloseConnectionTelemetry(CloseError& aError, bool aClosing);
  void Finish0Rtt(bool aRestart);

  enum ZeroRttOutcome {
    NOT_USED,
    USED_SUCCEEDED,
    USED_REJECTED,
    USED_CONN_ERROR,
    USED_CONN_CLOSED_BY_NECKO
  };
  void ZeroRttTelemetry(ZeroRttOutcome aOutcome);

  RefPtr<NeqoHttp3Conn> mHttp3Connection;
  RefPtr<nsAHttpConnection> mConnection;
  // We need an extra map to store the mapping of WebTransportSession and
  // WebTransportStreams to handle the case that a stream is already removed
  // from mStreamIdHash and we still need the WebTransportSession.
  nsTHashMap<nsUint64HashKey, uint64_t> mWebTransportStreamToSessionMap;
  nsRefPtrHashtable<nsUint64HashKey, Http3StreamBase> mStreamIdHash;
  nsRefPtrHashtable<nsPtrHashKey<nsAHttpTransaction>, Http3StreamBase>
      mStreamTransactionHash;

  nsRefPtrDeque<Http3StreamBase> mReadyForWrite;

  nsTArray<RefPtr<Http3StreamBase>> mSlowConsumersReadyForRead;
  nsRefPtrDeque<Http3StreamBase> mQueuedStreams;

  enum State {
    INITIALIZING,
    ZERORTT,
    CONNECTED,
    CLOSING,
    CLOSED
  } mState{INITIALIZING};

  bool mAuthenticationStarted{false};
  bool mCleanShutdown{false};
  bool mGoawayReceived{false};
  bool mShouldClose{false};
  bool mIsClosedByNeqo{false};
  bool mHttp3ConnectionReported = false;
  // mError is neqo error (a protocol error) and that may mean that we will
  // send some packets after that.
  nsresult mError{NS_OK};
  // This is a socket error, there is no poioint in sending anything on that
  // socket.
  nsresult mSocketError{NS_OK};
  bool mBeforeConnectedError{false};
  uint64_t mCurrentBrowserId;

  // True if this http3 session uses NSPR for UDP IO.
  bool mUseNSPRForIO{true};

  RefPtr<HttpConnectionUDP> mUdpConn;

  nsCOMPtr<nsITimer> mTimer;
  RefPtr<OnQuicTimeout> mTimerCallback;

  nsTHashMap<nsCStringHashKey, bool> mJoinConnectionCache;

  RefPtr<QuicSocketControl> mSocketControl;

  uint64_t mTransactionCount = 0;

  // The stream(s) that we are getting 0RTT data from.
  nsTArray<WeakPtr<Http3StreamBase>> m0RTTStreams;
  // The stream(s) that are not able to send 0RTT data. We need to
  // remember them put them into mReadyForWrite queue when 0RTT finishes.
  nsTArray<WeakPtr<Http3StreamBase>> mCannotDo0RTTStreams;

  // The following variables are needed for telemetry.
  TimeStamp mConnectionIdleStart;
  TimeStamp mConnectionIdleEnd;
  Maybe<uint64_t> mFirstStreamIdReuseIdleConnection;
  TimeStamp mTimerShouldTrigger;
  TimeStamp mZeroRttStarted;
  TimeStamp mLastTRRResponseTime;  // Time of the last successful TRR response
  uint64_t mBlockedByStreamLimitCount = 0;
  uint64_t mTransactionsBlockedByStreamLimitCount = 0;
  uint64_t mTransactionsSenderBlockedByFlowControlCount = 0;

  // NS_NET_STATUS_CONNECTED_TO event will be created by the Http3Session.
  // We want to  propagate it to the first transaction.
  RefPtr<nsHttpTransaction> mFirstHttpTransaction;

  RefPtr<nsHttpConnectionInfo> mConnInfo;

  int64_t mTotalBytesRead = 0;     // total data read
  int64_t mTotalBytesWritten = 0;  // total data read
  PRIntervalTime mLastWriteTime = 0;
  nsCString mServer;

  // Records whether we sent an ECH Extension and whether it was a GREASE Xtn
  EchExtensionStatus mEchExtensionStatus = EchExtensionStatus::kNotPresent;

  // Records whether the handshake finished successfully and we established a
  // a connection.
  bool mHandshakeSucceeded = false;

  nsCOMPtr<nsINetAddr> mNetAddr;

  enum class ExtendedConnectKind : uint8_t {
    WebTransport = 0,
    ConnectUDP,
    // add more extended CONNECT protocols here
  };

  enum ExtendedConnectNegotiation { DISABLED, NEGOTIATING, FAILED, SUCCEEDED };

  struct ExtendedConnectState {
    ExtendedConnectNegotiation mStatus = DISABLED;
    nsTArray<WeakPtr<Http3StreamBase>> mWaiters;
  };

  Array<ExtendedConnectState, 2> mExtConnect{ExtendedConnectState{},
                                             ExtendedConnectState{}};

  // convenience accessors
  ExtendedConnectState& ExtState(ExtendedConnectKind aKind) {
    return mExtConnect[static_cast<size_t>(aKind)];
  }

  bool DeferIfNegotiating(ExtendedConnectKind aKind, Http3StreamBase* aStream);
  // 1795854 implement the case when WebTransport is not supported.
  // Also, implement the case when the  HTTP/3 session fails before settings
  // are exchanged.
  void FinishNegotiation(ExtendedConnectKind aKind, bool aSuccess);

  inline bool HasNoActiveStreams() const {
    return mStreamTransactionHash.Count() == 0 &&
           mWebTransportSessions.IsEmpty() && mWebTransportStreams.IsEmpty() &&
           mTunnelStreams.IsEmpty();
  }

  nsTArray<RefPtr<Http3StreamBase>> mWebTransportSessions;
  nsTArray<RefPtr<Http3StreamBase>> mWebTransportStreams;
  nsTArray<RefPtr<Http3StreamBase>> mTunnelStreams;

  bool mHasWebTransportSession = false;
  // When true, we don't add this connection info into the Http/3 excluded list.
  bool mDontExclude = false;
  // True if any stream accepted Do0RTT() during the ZERORTT phase.  When the
  // session closes with mBeforeConnectedError we suppress ExcludeHttp3: the
  // PSK ticket is single-use so the retry does a full handshake and the H3
  // server itself should still be reachable.
  bool mHad0RttStream = false;
  // The lifetime of the UDP socket is managed by the HttpConnectionUDP. This
  // is only used in Http3Session::ProcessOutput. Using raw pointer here to
  // improve performance.
  nsIUDPSocket* mSocket;
  uint32_t mTrrStreams = 0;
  bool mIsInTunnel = false;
};

}  // namespace mozilla::net

#endif  // Http3Session_H_
