| // 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; |
| |
| @Deprecated("Use normalClosure instead") |
| static const int NORMAL_CLOSURE = normalClosure; |
| @Deprecated("Use goingAway instead") |
| static const int GOING_AWAY = goingAway; |
| @Deprecated("Use protocolError instead") |
| static const int PROTOCOL_ERROR = protocolError; |
| @Deprecated("Use unsupportedData instead") |
| static const int UNSUPPORTED_DATA = unsupportedData; |
| @Deprecated("Use reserved1004 instead") |
| static const int RESERVED_1004 = reserved1004; |
| @Deprecated("Use noStatusReceived instead") |
| static const int NO_STATUS_RECEIVED = noStatusReceived; |
| @Deprecated("Use abnormalClosure instead") |
| static const int ABNORMAL_CLOSURE = abnormalClosure; |
| @Deprecated("Use invalidFramePayloadData instead") |
| static const int INVALID_FRAME_PAYLOAD_DATA = invalidFramePayloadData; |
| @Deprecated("Use policyViolation instead") |
| static const int POLICY_VIOLATION = policyViolation; |
| @Deprecated("Use messageTooBig instead") |
| static const int MESSAGE_TOO_BIG = messageTooBig; |
| @Deprecated("Use missingMandatoryExtension instead") |
| static const int MISSING_MANDATORY_EXTENSION = missingMandatoryExtension; |
| @Deprecated("Use internalServerError instead") |
| static const int INTERNAL_SERVER_ERROR = internalServerError; |
| @Deprecated("Use reserved1015 instead") |
| static const int RESERVED_1015 = reserved1015; |
| } |
| |
| /// 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 = |
| const CompressionOptions(); |
| @Deprecated("Use compressionDefault instead") |
| static const CompressionOptions DEFAULT = compressionDefault; |
| |
| /// No-compression configuration. |
| /// |
| /// Disables compression when used as compression configuration for a |
| /// [WebSocket]. |
| static const CompressionOptions compressionOff = |
| const CompressionOptions(enabled: false); |
| @Deprecated("Use compressionOff instead") |
| static const CompressionOptions OFF = compressionOff; |
| |
| /// 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 = new _CompressionMaxWindowBits("", 0); |
| |
| String? part = requested?.parameters[_serverMaxWindowBits]; |
| if (part != null) { |
| if (part.length >= 2 && part.startsWith('0')) { |
| throw new 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 = new _CompressionMaxWindowBits("", 0); |
| if (!enabled) { |
| return info; |
| } |
| |
| info.headerValue = _WebSocketImpl.PER_MESSAGE_DEFLATE; |
| |
| if (clientNoContextTakeover && |
| (requested == null || |
| (requested != null && |
| requested.parameters.containsKey(_clientNoContextTakeover)))) { |
| info.headerValue += "; client_no_context_takeover"; |
| } |
| |
| if (serverNoContextTakeover && |
| (requested == null || |
| (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>*/ protocolSelector(List<String> protocols)?, |
| CompressionOptions compression = CompressionOptions.compressionDefault}) { |
| return new _WebSocketTransformerImpl(protocolSelector, compression); |
| } |
| |
| /** |
| * Upgrades a [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, |
| {protocolSelector(List<String> protocols)?, |
| 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; |
| |
| @Deprecated("Use connecting instead") |
| static const int CONNECTING = connecting; |
| @Deprecated("Use open instead") |
| static const int OPEN = open; |
| @Deprecated("Use closing instead") |
| static const int CLOSING = closing; |
| @Deprecated("Use closed instead") |
| static const int CLOSED = closed; |
| |
| /** |
| * 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}) => |
| _WebSocketImpl.connect(url, protocols, headers, compression: compression); |
| |
| @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 new ArgumentError("The serverSide argument must be passed " |
| "explicitly to WebSocket.fromUpgradedSocket."); |
| } |
| return new _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; |
| |
| const WebSocketException([this.message = ""]); |
| |
| String toString() => "WebSocketException: $message"; |
| } |