// 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 { /// 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*/ Function(List 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 upgrade( HttpRequest request, { Function(List 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`. abstract class WebSocket implements Stream*/>, StreamSink*/> { /// 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. /// /// 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 connect( String url, { Iterable? protocols, Map? 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` holding bytes. void add(/*String|List*/ 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`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 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"; } }