// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of "dart:_http";

/// WebSocket status codes used when closing a WebSocket connection.
abstract class WebSocketStatus {
  static const int normalClosure = 1000;
  static const int goingAway = 1001;
  static const int protocolError = 1002;
  static const int unsupportedData = 1003;
  static const int reserved1004 = 1004;
  static const int noStatusReceived = 1005;
  static const int abnormalClosure = 1006;
  static const int invalidFramePayloadData = 1007;
  static const int policyViolation = 1008;
  static const int messageTooBig = 1009;
  static const int missingMandatoryExtension = 1010;
  static const int internalServerError = 1011;
  static const int reserved1015 = 1015;
}

/// Options controlling compression in a [WebSocket].
///
/// A [CompressionOptions] instance can be passed to [WebSocket.connect], or
/// used in other similar places where [WebSocket] compression is configured.
///
/// In most cases the default [compressionDefault] is sufficient, but in some
/// situations, it might be desirable to use different compression parameters,
/// for example to preserve memory on small devices.
class CompressionOptions {
  /// Default [WebSocket] compression configuration.
  ///
  /// Enables compression with default window sizes and no reuse. This is the
  /// default options used by [WebSocket.connect] if no [CompressionOptions] is
  /// supplied.
  ///
  /// * `clientNoContextTakeover`: false
  /// * `serverNoContextTakeover`: false
  /// * `clientMaxWindowBits`: null (default maximal window size of 15 bits)
  /// * `serverMaxWindowBits`: null (default maximal window size of 15 bits)
  static const CompressionOptions compressionDefault = CompressionOptions();

  /// No-compression configuration.
  ///
  /// Disables compression when used as compression configuration for a
  /// [WebSocket].
  static const CompressionOptions compressionOff = CompressionOptions(
    enabled: false,
  );

  /// Whether the client will reuse its compression instances.
  final bool clientNoContextTakeover;

  /// Whether the server will reuse its compression instances.
  final bool serverNoContextTakeover;

  /// The maximal window size bit count requested by the client.
  ///
  /// The windows size for the compression is always a power of two, so the
  /// number of bits precisely determines the window size.
  ///
  /// If set to `null`, the client has no preference, and the compression can
  /// use up to its default maximum window size of 15 bits depending on the
  /// server's preference.
  final int? clientMaxWindowBits;

  /// The maximal window size bit count requested by the server.
  ///
  /// The windows size for the compression is always a power of two, so the
  /// number of bits precisely determines the window size.
  ///
  /// If set to `null`, the server has no preference, and the compression can
  /// use up to its default maximum window size of 15 bits depending on the
  /// client's preference.
  final int? serverMaxWindowBits;

  /// Whether WebSocket compression is enabled.
  ///
  /// If not enabled, the remaining fields have no effect, and the
  /// [compressionOff] instance can, and should, be reused instead of creating a
  /// new instance with compression disabled.
  final bool enabled;

  const CompressionOptions({
    this.clientNoContextTakeover = false,
    this.serverNoContextTakeover = false,
    this.clientMaxWindowBits,
    this.serverMaxWindowBits,
    this.enabled = true,
  });

  /// Parses list of requested server headers to return server compression
  /// response headers.
  ///
  /// Uses [serverMaxWindowBits] value if set, otherwise will attempt to use
  /// value from headers. Defaults to [WebSocket.DEFAULT_WINDOW_BITS]. Returns a
  /// [_CompressionMaxWindowBits] object which contains the response headers and
  /// negotiated max window bits.
  _CompressionMaxWindowBits _createServerResponseHeader(
    HeaderValue? requested,
  ) {
    var info = _CompressionMaxWindowBits("", 0);

    String? part = requested?.parameters[_serverMaxWindowBits];
    if (part != null) {
      if (part.length >= 2 && part.startsWith('0')) {
        throw ArgumentError("Illegal 0 padding on value.");
      } else {
        int mwb =
            serverMaxWindowBits ??
            int.tryParse(part) ??
            _WebSocketImpl.DEFAULT_WINDOW_BITS;
        info.headerValue = "; server_max_window_bits=$mwb";
        info.maxWindowBits = mwb;
      }
    } else {
      info.headerValue = "";
      info.maxWindowBits = _WebSocketImpl.DEFAULT_WINDOW_BITS;
    }
    return info;
  }

  /// Returns default values for client compression request headers.
  String _createClientRequestHeader(HeaderValue? requested, int size) {
    var info = "";

    // If responding to a valid request, specify size
    if (requested != null) {
      info = "; client_max_window_bits=$size";
    } else {
      // Client request. Specify default
      if (clientMaxWindowBits == null) {
        info = "; client_max_window_bits";
      } else {
        info = "; client_max_window_bits=$clientMaxWindowBits";
      }
      if (serverMaxWindowBits != null) {
        info += "; server_max_window_bits=$serverMaxWindowBits";
      }
    }

    return info;
  }

  /// Create a Compression Header.
  ///
  /// If [requested] is null or contains client request headers, returns Client
  /// compression request headers with default settings for
  /// `client_max_window_bits` header value.  If [requested] contains server
  /// response headers this method returns a Server compression response header
  /// negotiating the max window bits for both client and server as requested
  /// `server_max_window_bits` value.  This method returns a
  /// [_CompressionMaxWindowBits] object with the response headers and
  /// negotiated `maxWindowBits` value.
  _CompressionMaxWindowBits _createHeader([HeaderValue? requested]) {
    var info = _CompressionMaxWindowBits("", 0);
    if (!enabled) {
      return info;
    }

    info.headerValue = _WebSocketImpl.PER_MESSAGE_DEFLATE;

    if (clientNoContextTakeover &&
        (requested == null ||
            (requested.parameters.containsKey(_clientNoContextTakeover)))) {
      info.headerValue += "; client_no_context_takeover";
    }

    if (serverNoContextTakeover &&
        (requested == null ||
            (requested.parameters.containsKey(_serverNoContextTakeover)))) {
      info.headerValue += "; server_no_context_takeover";
    }

    var headerList = _createServerResponseHeader(requested);
    info.headerValue += headerList.headerValue;
    info.maxWindowBits = headerList.maxWindowBits;

    info.headerValue += _createClientRequestHeader(
      requested,
      info.maxWindowBits,
    );

    return info;
  }
}

/// The [WebSocketTransformer] provides the ability to upgrade a
/// [HttpRequest] to a [WebSocket] connection. It supports both
/// upgrading a single [HttpRequest] and upgrading a stream of
/// [HttpRequest]s.
///
/// To upgrade a single [HttpRequest] use the static [upgrade] method.
///
///     HttpServer server;
///     server.listen((request) {
///       if (...) {
///         WebSocketTransformer.upgrade(request).then((websocket) {
///           ...
///         });
///       } else {
///         // Do normal HTTP request processing.
///       }
///     });
///
/// To transform a stream of [HttpRequest] events as it implements a
/// stream transformer that transforms a stream of HttpRequest into a
/// stream of WebSockets by upgrading each HttpRequest from the HTTP or
/// HTTPS server, to the WebSocket protocol.
///
///     server.transform(new WebSocketTransformer()).listen((webSocket) => ...);
///
/// This transformer strives to implement WebSockets as specified by RFC6455.
abstract class WebSocketTransformer
    implements StreamTransformer<HttpRequest, WebSocket> {
  /// Create a new [WebSocketTransformer].
  ///
  /// If [protocolSelector] is provided, [protocolSelector] will be called to
  /// select what protocol to use, if any were provided by the client.
  /// [protocolSelector] is should return either a [String] or a [Future]
  /// completing with a [String]. The [String] must exist in the list of
  /// protocols.
  ///
  /// If [compression] is provided, the [WebSocket] created will be configured
  /// to negotiate with the specified [CompressionOptions]. If none is specified
  /// then the [WebSocket] will be created with the default [CompressionOptions].
  factory WebSocketTransformer({
    /*String|Future<String>*/ Function(List<String> protocols)?
    protocolSelector,
    CompressionOptions compression = CompressionOptions.compressionDefault,
  }) {
    return _WebSocketTransformerImpl(protocolSelector, compression);
  }

  /// Upgrades an [HttpRequest] to a [WebSocket] connection. If the
  /// request is not a valid WebSocket upgrade request an HTTP response
  /// with status code 500 will be returned. Otherwise the returned
  /// future will complete with the [WebSocket] when the upgrade process
  /// is complete.
  ///
  /// If [protocolSelector] is provided, [protocolSelector] will be called to
  /// select what protocol to use, if any were provided by the client.
  /// [protocolSelector] is should return either a [String] or a [Future]
  /// completing with a [String]. The [String] must exist in the list of
  /// protocols.
  ///
  /// If [compression] is provided, the [WebSocket] created will be configured
  /// to negotiate with the specified [CompressionOptions]. If none is specified
  /// then the [WebSocket] will be created with the default [CompressionOptions].
  static Future<WebSocket> upgrade(
    HttpRequest request, {
    Function(List<String> protocols)? protocolSelector,
    CompressionOptions compression = CompressionOptions.compressionDefault,
  }) {
    return _WebSocketTransformerImpl._upgrade(
      request,
      protocolSelector,
      compression,
    );
  }

  /// Checks whether the request is a valid WebSocket upgrade request.
  static bool isUpgradeRequest(HttpRequest request) {
    return _WebSocketTransformerImpl._isUpgradeRequest(request);
  }
}

/// A two-way HTTP communication object for client or server applications.
///
/// The stream exposes the messages received. A text message will be of type
/// `String` and a binary message will be of type `List<int>`.
abstract class WebSocket
    implements
        Stream<dynamic /*String|List<int>*/>,
        StreamSink<dynamic /*String|List<int>*/> {
  /// Possible states of the connection.
  static const int connecting = 0;
  static const int open = 1;
  static const int closing = 2;
  static const int closed = 3;

  /// The interval between ping signals.
  ///
  /// A ping message is sent every [pingInterval], starting at the first
  /// [pingInterval] after a new value has been assigned or a pong message has
  /// been received. If a ping message is not answered by a pong message from the
  /// peer, the `WebSocket` is assumed disconnected and the connection is closed
  /// with a [WebSocketStatus.goingAway] close code. When a ping signal is sent,
  /// the pong message must be received within [pingInterval].
  ///
  /// There are never two outstanding pings at any given time, and the next ping
  /// timer starts when the pong is received.
  ///
  /// Set the [pingInterval] to `null` to disable sending ping messages.
  ///
  /// The default value is `null`.
  Duration? pingInterval;

  /// Create a new WebSocket connection. The URL supplied in [url]
  /// must use the scheme `ws` or `wss`.
  ///
  /// The [protocols] argument is specifying the subprotocols the
  /// client is willing to speak.
  ///
  /// The [headers] argument is specifying additional HTTP headers for
  /// setting up the connection. This would typically be the `Origin`
  /// header and potentially cookies. The keys of the map are the header
  /// fields and the values are either String or List<String>.
  ///
  /// If [headers] is provided, there are a number of headers
  /// which are controlled by the WebSocket connection process. These
  /// headers are:
  ///
  ///   - `connection`
  ///   - `sec-websocket-key`
  ///   - `sec-websocket-protocol`
  ///   - `sec-websocket-version`
  ///   - `upgrade`
  ///
  /// If any of these are passed in the `headers` map they will be ignored.
  ///
  /// If the `url` contains user information this will be passed as basic
  /// authentication when setting up the connection.
  static Future<WebSocket> connect(
    String url, {
    Iterable<String>? protocols,
    Map<String, dynamic>? headers,
    CompressionOptions compression = CompressionOptions.compressionDefault,
    HttpClient? customClient,
  }) => _WebSocketImpl.connect(
    url,
    protocols,
    headers,
    compression: compression,
    customClient: customClient,
  );

  @Deprecated(
    'This constructor will be removed in Dart 2.0. Use `implements`'
    ' instead of `extends` if implementing this abstract class.',
  )
  WebSocket();

  /// Creates a WebSocket from an already-upgraded socket.
  ///
  /// The initial WebSocket handshake must have occurred prior to this call. A
  /// WebSocket client can automatically perform the handshake using
  /// [WebSocket.connect], while a server can do so using
  /// [WebSocketTransformer.upgrade]. To manually upgrade an [HttpRequest],
  /// [HttpResponse.detachSocket] may be called.
  ///
  /// [protocol] should be the protocol negotiated by this handshake, if any.
  ///
  /// [serverSide] must be passed explicitly. If it's `false`, the WebSocket will
  /// act as the client and mask the messages it sends. If it's `true`, it will
  /// act as the server and will not mask its messages.
  ///
  /// If [compression] is provided, the [WebSocket] created will be configured
  /// to negotiate with the specified [CompressionOptions]. If none is specified
  /// then the [WebSocket] will be created with the default [CompressionOptions].
  factory WebSocket.fromUpgradedSocket(
    Socket socket, {
    String? protocol,
    bool? serverSide,
    CompressionOptions compression = CompressionOptions.compressionDefault,
  }) {
    if (serverSide == null) {
      throw ArgumentError(
        "The serverSide argument must be passed "
        "explicitly to WebSocket.fromUpgradedSocket.",
      );
    }
    return _WebSocketImpl._fromSocket(
      socket,
      protocol,
      compression,
      serverSide,
    );
  }

  /// Returns the current state of the connection.
  int get readyState;

  /// The extensions property is initially the empty string. After the
  /// WebSocket connection is established this string reflects the
  /// extensions used by the server.
  String get extensions;

  /// The protocol property is initially the empty string. After the
  /// WebSocket connection is established the value is the subprotocol
  /// selected by the server. If no subprotocol is negotiated the
  /// value will remain [:null:].
  String? get protocol;

  /// The close code set when the WebSocket connection is closed. If
  /// there is no close code available this property will be [:null:]
  int? get closeCode;

  /// The close reason set when the WebSocket connection is closed. If
  /// there is no close reason available this property will be [:null:]
  String? get closeReason;

  /// Closes the WebSocket connection. Set the optional [code] and [reason]
  /// arguments to send close information to the remote peer. If they are
  /// omitted, the peer will see [WebSocketStatus.noStatusReceived] code
  /// with no reason.
  Future close([int? code, String? reason]);

  /// Sends data on the WebSocket connection. The data in [data] must
  /// be either a `String`, or a `List<int>` holding bytes.
  void add(/*String|List<int>*/ data);

  /// Sends data from a stream on WebSocket connection. Each data event from
  /// [stream] will be send as a single WebSocket frame. The data from [stream]
  /// must be either `String`s, or `List<int>`s holding bytes.
  Future addStream(Stream stream);

  /// Sends a text message with the text represented by [bytes].
  ///
  /// The [bytes] should be valid UTF-8 encoded Unicode characters. If they are
  /// not, the receiving end will close the connection.
  void addUtf8Text(List<int> bytes);

  /// Gets the user agent used for WebSocket connections.
  static String? get userAgent => _WebSocketImpl.userAgent;

  /// Sets the user agent to use for WebSocket connections.
  static set userAgent(String? userAgent) {
    _WebSocketImpl.userAgent = userAgent;
  }
}

class WebSocketException implements IOException {
  final String message;
  final int? httpStatusCode;

  const WebSocketException([this.message = "", this.httpStatusCode]);

  String toString() {
    if (httpStatusCode != null) {
      return "WebSocketException: $message, HTTP status code: $httpStatusCode";
    }
    return "WebSocketException: $message";
  }
}
