Fix a library name conflict in pkg/http_parser.
R=rnystrom@google.com
BUG=19117
Review URL: https://codereview.chromium.org//311663002
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/http_parser@36902 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..328e779
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,16 @@
+## 0.0.2+3
+
+* Fix a library name conflict.
+
+## 0.0.2+2
+
+* Fixes for HTTP date formatting.
+
+## 0.0.2+1
+
+* Minor code refactoring.
+
+## 0.0.2
+
+* Added `CompatibleWebSocket`, for platform- and API-independent support for the
+ WebSocket API.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..edfa71a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+`http_parser` is a platform-independent package for parsing and serializing
+various HTTP-related formats. It's designed to be usable on both the browser and
+the server, and thus avoids referencing any types from `dart:io` or `dart:html`.
diff --git a/lib/http_parser.dart b/lib/http_parser.dart
new file mode 100644
index 0000000..0aa9cea
--- /dev/null
+++ b/lib/http_parser.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2014, 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.
+
+library http_parser;
+
+export 'src/http_date.dart';
+export 'src/media_type.dart';
+export 'src/web_socket.dart';
diff --git a/lib/src/bytes_builder.dart b/lib/src/bytes_builder.dart
new file mode 100644
index 0000000..446e175
--- /dev/null
+++ b/lib/src/bytes_builder.dart
@@ -0,0 +1,214 @@
+// 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.
+
+// This is a copy of "dart:io"'s BytesBuilder implementation, from
+// sdk/lib/io/bytes_builder.dart. It's copied here to make it available to
+// non-"dart:io" applications (issue 18348).
+//
+// Because it's copied directly, there are no modifications from the original.
+library http_parser.bytes_builder;
+
+import 'dart:math';
+import 'dart:typed_data';
+
+/**
+ * Builds a list of bytes, allowing bytes and lists of bytes to be added at the
+ * end.
+ *
+ * Used to efficiently collect bytes and lists of bytes.
+ */
+abstract class BytesBuilder {
+ /**
+ * Construct a new empty [BytesBuilder].
+ *
+ * If [copy] is true, the data is always copied when added to the list. If
+ * it [copy] is false, the data is only copied if needed. That means that if
+ * the lists are changed after added to the [BytesBuilder], it may effect the
+ * output. Default is `true`.
+ */
+ factory BytesBuilder({bool copy: true}) {
+ if (copy) {
+ return new _CopyingBytesBuilder();
+ } else {
+ return new _BytesBuilder();
+ }
+ }
+
+ /**
+ * Appends [bytes] to the current contents of the builder.
+ *
+ * Each value of [bytes] will be bit-representation truncated to the range
+ * 0 .. 255.
+ */
+ void add(List<int> bytes);
+
+ /**
+ * Append [byte] to the current contents of the builder.
+ *
+ * The [byte] will be bit-representation truncated to the range 0 .. 255.
+ */
+ void addByte(int byte);
+
+ /**
+ * Returns the contents of `this` and clears `this`.
+ *
+ * The list returned is a view of the the internal buffer, limited to the
+ * [length].
+ */
+ List<int> takeBytes();
+
+ /**
+ * Returns a copy of the current contents of the builder.
+ *
+ * Leaves the contents of the builder intact.
+ */
+ List<int> toBytes();
+
+ /**
+ * The number of bytes in the builder.
+ */
+ int get length;
+
+ /**
+ * Returns `true` if the buffer is empty.
+ */
+ bool get isEmpty;
+
+ /**
+ * Returns `true` if the buffer is not empty.
+ */
+ bool get isNotEmpty;
+
+ /**
+ * Clear the contents of the builder.
+ */
+ void clear();
+}
+
+
+class _CopyingBytesBuilder implements BytesBuilder {
+ // Start with 1024 bytes.
+ static const int _INIT_SIZE = 1024;
+
+ int _length = 0;
+ Uint8List _buffer;
+
+ void add(List<int> bytes) {
+ int bytesLength = bytes.length;
+ if (bytesLength == 0) return;
+ int required = _length + bytesLength;
+ if (_buffer == null) {
+ int size = _pow2roundup(required);
+ size = max(size, _INIT_SIZE);
+ _buffer = new Uint8List(size);
+ } else if (_buffer.length < required) {
+ // We will create a list in the range of 2-4 times larger than
+ // required.
+ int size = _pow2roundup(required) * 2;
+ var newBuffer = new Uint8List(size);
+ newBuffer.setRange(0, _buffer.length, _buffer);
+ _buffer = newBuffer;
+ }
+ assert(_buffer.length >= required);
+ if (bytes is Uint8List) {
+ _buffer.setRange(_length, required, bytes);
+ } else {
+ for (int i = 0; i < bytesLength; i++) {
+ _buffer[_length + i] = bytes[i];
+ }
+ }
+ _length = required;
+ }
+
+ void addByte(int byte) => add([byte]);
+
+ List<int> takeBytes() {
+ if (_buffer == null) return new Uint8List(0);
+ var buffer = new Uint8List.view(_buffer.buffer, 0, _length);
+ clear();
+ return buffer;
+ }
+
+ List<int> toBytes() {
+ if (_buffer == null) return new Uint8List(0);
+ return new Uint8List.fromList(
+ new Uint8List.view(_buffer.buffer, 0, _length));
+ }
+
+ int get length => _length;
+
+ bool get isEmpty => _length == 0;
+
+ bool get isNotEmpty => _length != 0;
+
+ void clear() {
+ _length = 0;
+ _buffer = null;
+ }
+
+ int _pow2roundup(int x) {
+ --x;
+ x |= x >> 1;
+ x |= x >> 2;
+ x |= x >> 4;
+ x |= x >> 8;
+ x |= x >> 16;
+ return x + 1;
+ }
+}
+
+
+class _BytesBuilder implements BytesBuilder {
+ int _length = 0;
+ final List _chunks = [];
+
+ void add(List<int> bytes) {
+ if (bytes is! Uint8List) {
+ bytes = new Uint8List.fromList(bytes);
+ }
+ _chunks.add(bytes);
+ _length += bytes.length;
+ }
+
+ void addByte(int byte) => add([byte]);
+
+ List<int> takeBytes() {
+ if (_chunks.length == 0) return new Uint8List(0);
+ if (_chunks.length == 1) {
+ var buffer = _chunks.single;
+ clear();
+ return buffer;
+ }
+ var buffer = new Uint8List(_length);
+ int offset = 0;
+ for (var chunk in _chunks) {
+ buffer.setRange(offset, offset + chunk.length, chunk);
+ offset += chunk.length;
+ }
+ clear();
+ return buffer;
+ }
+
+ List<int> toBytes() {
+ if (_chunks.length == 0) return new Uint8List(0);
+ var buffer = new Uint8List(_length);
+ int offset = 0;
+ for (var chunk in _chunks) {
+ buffer.setRange(offset, offset + chunk.length, chunk);
+ offset += chunk.length;
+ }
+ return buffer;
+ }
+
+ int get length => _length;
+
+ bool get isEmpty => _length == 0;
+
+ bool get isNotEmpty => _length != 0;
+
+ void clear() {
+ _length = 0;
+ _chunks.clear();
+ }
+}
diff --git a/lib/src/http_date.dart b/lib/src/http_date.dart
new file mode 100644
index 0000000..0626c72
--- /dev/null
+++ b/lib/src/http_date.dart
@@ -0,0 +1,152 @@
+// Copyright (c) 2014, 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.
+
+library http_parser.http_date;
+
+import 'package:string_scanner/string_scanner.dart';
+
+const _WEEKDAYS = const ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+const _MONTHS = const ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
+ "Sep", "Oct", "Nov", "Dec"];
+
+final _shortWeekdayRegExp = new RegExp(r"Mon|Tue|Wed|Thu|Fri|Sat|Sun");
+final _longWeekdayRegExp =
+ new RegExp(r"Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday");
+final _monthRegExp =
+ new RegExp(r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec");
+final _digitRegExp = new RegExp(r"\d+");
+
+/// Return a HTTP-formatted string representation of [date].
+///
+/// This follows [RFC 822](http://tools.ietf.org/html/rfc822) as updated by [RFC
+/// 1123](http://tools.ietf.org/html/rfc1123).
+String formatHttpDate(DateTime date) {
+ date = date.toUtc();
+ var buffer = new StringBuffer()
+ ..write(_WEEKDAYS[date.weekday - 1])
+ ..write(", ")
+ ..write(date.day <= 9 ? "0" : "")
+ ..write(date.day.toString())
+ ..write(" ")
+ ..write(_MONTHS[date.month - 1])
+ ..write(" ")
+ ..write(date.year.toString())
+ ..write(date.hour <= 9 ? " 0" : " ")
+ ..write(date.hour.toString())
+ ..write(date.minute <= 9 ? ":0" : ":")
+ ..write(date.minute.toString())
+ ..write(date.second <= 9 ? ":0" : ":")
+ ..write(date.second.toString())
+ ..write(" GMT");
+ return buffer.toString();
+}
+
+/// Parses an HTTP-formatted date into a UTC [DateTime].
+///
+/// This follows [RFC
+/// 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3). It will
+/// throw a [FormatException] if [date] is invalid.
+DateTime parseHttpDate(String date) {
+ try {
+ var scanner = new StringScanner(date);
+
+ if (scanner.scan(_longWeekdayRegExp)) {
+ // RFC 850 starts with a long weekday.
+ scanner.expect(", ");
+ var day = _parseInt(scanner, 2);
+ scanner.expect("-");
+ var month = _parseMonth(scanner);
+ scanner.expect("-");
+ var year = 1900 + _parseInt(scanner, 2);
+ scanner.expect(" ");
+ var time = _parseTime(scanner);
+ scanner.expect(" GMT");
+ scanner.expectDone();
+
+ return _makeDateTime(year, month, day, time);
+ }
+
+ // RFC 1123 and asctime both start with a short weekday.
+ scanner.expect(_shortWeekdayRegExp);
+ if (scanner.scan(", ")) {
+ // RFC 1123 follows the weekday with a comma.
+ var day = _parseInt(scanner, 2);
+ scanner.expect(" ");
+ var month = _parseMonth(scanner);
+ scanner.expect(" ");
+ var year = _parseInt(scanner, 4);
+ scanner.expect(" ");
+ var time = _parseTime(scanner);
+ scanner.expect(" GMT");
+ scanner.expectDone();
+
+ return _makeDateTime(year, month, day, time);
+ }
+
+ // asctime follows the weekday with a space.
+ scanner.expect(" ");
+ var month = _parseMonth(scanner);
+ scanner.expect(" ");
+ var day = scanner.scan(" ") ?
+ _parseInt(scanner, 1) :
+ _parseInt(scanner, 2);
+ scanner.expect(" ");
+ var time = _parseTime(scanner);
+ scanner.expect(" ");
+ var year = _parseInt(scanner, 4);
+ scanner.expectDone();
+
+ return _makeDateTime(year, month, day, time);
+ } on FormatException catch (error) {
+ throw new FormatException('Invalid HTTP date "$date": ${error.message}');
+ }
+}
+
+/// Parses a short-form month name to a form accepted by [DateTime].
+int _parseMonth(StringScanner scanner) {
+ scanner.expect(_monthRegExp);
+ // DateTime uses 1-indexed months.
+ return _MONTHS.indexOf(scanner.lastMatch[0]) + 1;
+}
+
+/// Parses an int an enforces that it has exactly [digits] digits.
+int _parseInt(StringScanner scanner, int digits) {
+ scanner.expect(_digitRegExp);
+ if (scanner.lastMatch[0].length != digits) {
+ scanner.error("expected a $digits-digit number.");
+ }
+
+ return int.parse(scanner.lastMatch[0]);
+}
+
+/// Parses an timestamp of the form "HH:MM:SS" on a 24-hour clock.
+DateTime _parseTime(StringScanner scanner) {
+ var hours = _parseInt(scanner, 2);
+ if (hours >= 24) scanner.error("hours may not be greater than 24.");
+ scanner.expect(':');
+
+ var minutes = _parseInt(scanner, 2);
+ if (minutes >= 60) scanner.error("minutes may not be greater than 60.");
+ scanner.expect(':');
+
+ var seconds = _parseInt(scanner, 2);
+ if (seconds >= 60) scanner.error("seconds may not be greater than 60.");
+
+ return new DateTime(1, 1, 1, hours, minutes, seconds);
+}
+
+/// Returns a UTC [DateTime] from the given components.
+///
+/// Validates that [day] is a valid day for [month]. If it's not, throws a
+/// [FormatException].
+DateTime _makeDateTime(int year, int month, int day, DateTime time) {
+ var dateTime = new DateTime.utc(
+ year, month, day, time.hour, time.minute, time.second);
+
+ // If [day] was too large, it will cause [month] to overflow.
+ if (dateTime.month != month) {
+ throw new FormatException("invalid day '$day' for month '$month'.");
+ }
+ return dateTime;
+}
diff --git a/lib/src/media_type.dart b/lib/src/media_type.dart
new file mode 100644
index 0000000..7a1ff23
--- /dev/null
+++ b/lib/src/media_type.dart
@@ -0,0 +1,161 @@
+// Copyright (c) 2014, 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.
+
+library http_parser.media_type;
+
+import 'package:collection/collection.dart';
+import 'package:string_scanner/string_scanner.dart';
+
+// All of the following regular expressions come from section 2.2 of the HTTP
+// spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
+final _lws = new RegExp(r"(?:\r\n)?[ \t]+");
+final _token = new RegExp(r'[^()<>@,;:"\\/[\]?={} \t\x00-\x1F\x7F]+');
+final _quotedString = new RegExp(r'"(?:[^"\x00-\x1F\x7F]|\\.)*"');
+final _quotedPair = new RegExp(r'\\(.)');
+
+/// A regular expression matching any number of [_lws] productions in a row.
+final _whitespace = new RegExp("(?:${_lws.pattern})*");
+
+/// A regular expression matching a character that is not a valid HTTP token.
+final _nonToken = new RegExp(r'[()<>@,;:"\\/\[\]?={} \t\x00-\x1F\x7F]');
+
+/// A regular expression matching a character that needs to be backslash-escaped
+/// in a quoted string.
+final _escapedChar = new RegExp(r'["\x00-\x1F\x7F]');
+
+/// A class representing an HTTP media type, as used in Accept and Content-Type
+/// headers.
+///
+/// This is immutable; new instances can be created based on an old instance by
+/// calling [change].
+class MediaType {
+ /// The primary identifier of the MIME type.
+ final String type;
+
+ /// The secondary identifier of the MIME type.
+ final String subtype;
+
+ /// The parameters to the media type.
+ ///
+ /// This map is immutable.
+ final Map<String, String> parameters;
+
+ /// The media type's MIME type.
+ String get mimeType => "$type/$subtype";
+
+ /// Parses a media type.
+ ///
+ /// This will throw a FormatError if the media type is invalid.
+ factory MediaType.parse(String mediaType) {
+ // This parsing is based on sections 3.6 and 3.7 of the HTTP spec:
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html.
+ try {
+ var scanner = new StringScanner(mediaType);
+ scanner.scan(_whitespace);
+ scanner.expect(_token);
+ var type = scanner.lastMatch[0];
+ scanner.expect('/');
+ scanner.expect(_token);
+ var subtype = scanner.lastMatch[0];
+ scanner.scan(_whitespace);
+
+ var parameters = {};
+ while (scanner.scan(';')) {
+ scanner.scan(_whitespace);
+ scanner.expect(_token);
+ var attribute = scanner.lastMatch[0];
+ scanner.expect('=');
+
+ var value;
+ if (scanner.scan(_token)) {
+ value = scanner.lastMatch[0];
+ } else {
+ scanner.expect(_quotedString);
+ var quotedString = scanner.lastMatch[0];
+ value = quotedString.substring(1, quotedString.length - 1).
+ replaceAllMapped(_quotedPair, (match) => match[1]);
+ }
+
+ scanner.scan(_whitespace);
+ parameters[attribute] = value;
+ }
+
+ scanner.expectDone();
+ return new MediaType(type, subtype, parameters);
+ } on FormatException catch (error) {
+ throw new FormatException(
+ 'Invalid media type "$mediaType": ${error.message}');
+ }
+ }
+
+ MediaType(this.type, this.subtype, [Map<String, String> parameters])
+ : this.parameters = new UnmodifiableMapView(
+ parameters == null ? {} : new Map.from(parameters));
+
+ /// Returns a copy of this [MediaType] with some fields altered.
+ ///
+ /// [type] and [subtype] alter the corresponding fields. [mimeType] is parsed
+ /// and alters both the [type] and [subtype] fields; it cannot be passed along
+ /// with [type] or [subtype].
+ ///
+ /// [parameters] overwrites and adds to the corresponding field. If
+ /// [clearParameters] is passed, it replaces the corresponding field entirely
+ /// instead.
+ MediaType change({String type, String subtype, String mimeType,
+ Map<String, String> parameters, bool clearParameters: false}) {
+ if (mimeType != null) {
+ if (type != null) {
+ throw new ArgumentError("You may not pass both [type] and [mimeType].");
+ } else if (subtype != null) {
+ throw new ArgumentError("You may not pass both [subtype] and "
+ "[mimeType].");
+ }
+
+ var segments = mimeType.split('/');
+ if (segments.length != 2) {
+ throw new FormatException('Invalid mime type "$mimeType".');
+ }
+
+ type = segments[0];
+ subtype = segments[1];
+ }
+
+ if (type == null) type = this.type;
+ if (subtype == null) subtype = this.subtype;
+ if (parameters == null) parameters = {};
+
+ if (!clearParameters) {
+ var newParameters = parameters;
+ parameters = new Map.from(this.parameters);
+ parameters.addAll(newParameters);
+ }
+
+ return new MediaType(type, subtype, parameters);
+ }
+
+ /// Converts the media type to a string.
+ ///
+ /// This will produce a valid HTTP media type.
+ String toString() {
+ var buffer = new StringBuffer()
+ ..write(type)
+ ..write("/")
+ ..write(subtype);
+
+ parameters.forEach((attribute, value) {
+ buffer.write("; $attribute=");
+ if (_nonToken.hasMatch(value)) {
+ buffer
+ ..write('"')
+ ..write(value.replaceAllMapped(
+ _escapedChar, (match) => "\\" + match[0]))
+ ..write('"');
+ } else {
+ buffer.write(value);
+ }
+ });
+
+ return buffer.toString();
+ }
+}
diff --git a/lib/src/web_socket.dart b/lib/src/web_socket.dart
new file mode 100644
index 0000000..77c9d2b
--- /dev/null
+++ b/lib/src/web_socket.dart
@@ -0,0 +1,936 @@
+// Copyright (c) 2014, 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.
+
+library http_parser.web_socket;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:crypto/crypto.dart';
+
+import 'bytes_builder.dart';
+
+/// An implementation of the WebSocket protocol that's not specific to "dart:io"
+/// or to any particular HTTP API.
+///
+/// Because this is HTTP-API-agnostic, it doesn't handle the initial [WebSocket
+/// handshake][]. This needs to be handled manually by the user of the code.
+/// Once that's been done, [new CompatibleWebSocket] can be called with the
+/// underlying socket and it will handle the remainder of the protocol.
+///
+/// [WebSocket handshake]: https://tools.ietf.org/html/rfc6455#section-4
+abstract class CompatibleWebSocket implements Stream, StreamSink {
+ /// The interval for sending ping signals.
+ ///
+ /// 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.GOING_AWAY] 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.
+ ///
+ /// By default, the [pingInterval] is `null`, indicating that ping messages
+ /// are disabled.
+ Duration pingInterval;
+
+ /// The [close code][] set when the WebSocket connection is closed.
+ ///
+ /// [close code]: https://tools.ietf.org/html/rfc6455#section-7.1.5
+ ///
+ /// Before the connection has been closed, this will be `null`.
+ int get closeCode;
+
+ /// The [close reason][] set when the WebSocket connection is closed.
+ ///
+ /// [close reason]: https://tools.ietf.org/html/rfc6455#section-7.1.6
+ ///
+ /// Before the connection has been closed, this will be `null`.
+ String get closeReason;
+
+ /// Signs a `Sec-WebSocket-Key` header sent by a WebSocket client as part of
+ /// the [initial handshake].
+ ///
+ /// The return value should be sent back to the client in a
+ /// `Sec-WebSocket-Accept` header.
+ ///
+ /// [initial handshake]: https://tools.ietf.org/html/rfc6455#section-4.2.2
+ static String signKey(String key) {
+ var hash = new SHA1();
+ // We use [codeUnits] here rather than UTF-8-decoding the string because
+ // [key] is expected to be base64 encoded, and so will be pure ASCII.
+ hash.add((key + _webSocketGUID).codeUnits);
+ return CryptoUtils.bytesToBase64(hash.close());
+ }
+
+ /// Creates a new WebSocket handling messaging across an existing socket.
+ ///
+ /// Because this is HTTP-API-agnostic, the initial [WebSocket handshake][]
+ /// must have already been completed on the socket before this is called.
+ ///
+ /// If [stream] is also a [StreamSink] (for example, if it's a "dart:io"
+ /// `Socket`), it will be used for both sending and receiving data. Otherwise,
+ /// it will be used for receiving data and [sink] will be used for sending it.
+ ///
+ /// If this is a WebSocket server, [serverSide] should be `true` (the
+ /// default); if it's a client, [serverSide] should be `false`.
+ ///
+ /// [WebSocket handshake]: https://tools.ietf.org/html/rfc6455#section-4
+ factory CompatibleWebSocket(Stream<List<int>> stream,
+ {StreamSink<List<int>> sink, bool serverSide: true}) {
+ if (sink == null) {
+ if (stream is! StreamSink) {
+ throw new ArgumentError("If stream isn't also a StreamSink, sink must "
+ "be passed explicitly.");
+ }
+ sink = stream as StreamSink;
+ }
+
+ return new _WebSocketImpl._fromSocket(stream, sink, serverSide);
+ }
+
+ /// Closes the web socket connection.
+ ///
+ /// [closeCode] and [closeReason] are the [close code][] and [reason][] sent
+ /// to the remote peer, respectively. If they are omitted, the peer will see
+ /// a "no status received" code with no reason.
+ ///
+ /// [close code]: https://tools.ietf.org/html/rfc6455#section-7.1.5
+ /// [reason]: https://tools.ietf.org/html/rfc6455#section-7.1.6
+ Future close([int closeCode, String closeReason]);
+}
+
+/// An exception thrown by [CompatibleWebSocket].
+class CompatibleWebSocketException implements Exception {
+ final String message;
+
+ CompatibleWebSocketException([this.message]);
+
+ String toString() => message == null
+ ? "CompatibleWebSocketException" :
+ "CompatibleWebSocketException: $message";
+}
+
+// The following code is copied from sdk/lib/io/websocket_impl.dart. The
+// "dart:io" implementation isn't used directly both to support non-"dart:io"
+// applications, and because it's incompatible with non-"dart:io" HTTP requests
+// (issue 18172).
+//
+// Because it's copied directly, only modifications necessary to support the
+// desired public API and to remove "dart:io" dependencies have been made.
+
+/**
+ * Web socket status codes used when closing a web socket connection.
+ */
+abstract class _WebSocketStatus {
+ static const int NORMAL_CLOSURE = 1000;
+ static const int GOING_AWAY = 1001;
+ static const int PROTOCOL_ERROR = 1002;
+ static const int UNSUPPORTED_DATA = 1003;
+ static const int RESERVED_1004 = 1004;
+ static const int NO_STATUS_RECEIVED = 1005;
+ static const int ABNORMAL_CLOSURE = 1006;
+ static const int INVALID_FRAME_PAYLOAD_DATA = 1007;
+ static const int POLICY_VIOLATION = 1008;
+ static const int MESSAGE_TOO_BIG = 1009;
+ static const int MISSING_MANDATORY_EXTENSION = 1010;
+ static const int INTERNAL_SERVER_ERROR = 1011;
+ static const int RESERVED_1015 = 1015;
+}
+
+abstract class _WebSocketState {
+ static const int CONNECTING = 0;
+ static const int OPEN = 1;
+ static const int CLOSING = 2;
+ static const int CLOSED = 3;
+}
+
+const String _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+final _random = new Random();
+
+// Matches _WebSocketOpcode.
+class _WebSocketMessageType {
+ static const int NONE = 0;
+ static const int TEXT = 1;
+ static const int BINARY = 2;
+}
+
+
+class _WebSocketOpcode {
+ static const int CONTINUATION = 0;
+ static const int TEXT = 1;
+ static const int BINARY = 2;
+ static const int RESERVED_3 = 3;
+ static const int RESERVED_4 = 4;
+ static const int RESERVED_5 = 5;
+ static const int RESERVED_6 = 6;
+ static const int RESERVED_7 = 7;
+ static const int CLOSE = 8;
+ static const int PING = 9;
+ static const int PONG = 10;
+ static const int RESERVED_B = 11;
+ static const int RESERVED_C = 12;
+ static const int RESERVED_D = 13;
+ static const int RESERVED_E = 14;
+ static const int RESERVED_F = 15;
+}
+
+/**
+ * The web socket protocol transformer handles the protocol byte stream
+ * which is supplied through the [:handleData:]. As the protocol is processed,
+ * it'll output frame data as either a List<int> or String.
+ *
+ * Important infomation about usage: Be sure you use cancelOnError, so the
+ * socket will be closed when the processer encounter an error. Not using it
+ * will lead to undefined behaviour.
+ */
+// TODO(ajohnsen): make this transformer reusable?
+class _WebSocketProtocolTransformer implements StreamTransformer, EventSink {
+ static const int START = 0;
+ static const int LEN_FIRST = 1;
+ static const int LEN_REST = 2;
+ static const int MASK = 3;
+ static const int PAYLOAD = 4;
+ static const int CLOSED = 5;
+ static const int FAILURE = 6;
+
+ int _state = START;
+ bool _fin = false;
+ int _opcode = -1;
+ int _len = -1;
+ bool _masked = false;
+ int _remainingLenBytes = -1;
+ int _remainingMaskingKeyBytes = 4;
+ int _remainingPayloadBytes = -1;
+ int _unmaskingIndex = 0;
+ int _currentMessageType = _WebSocketMessageType.NONE;
+ int closeCode = _WebSocketStatus.NO_STATUS_RECEIVED;
+ String closeReason = "";
+
+ EventSink _eventSink;
+
+ final bool _serverSide;
+ final List _maskingBytes = new List(4);
+ final BytesBuilder _payload = new BytesBuilder(copy: false);
+
+ _WebSocketProtocolTransformer([this._serverSide = false]);
+
+ Stream bind(Stream stream) {
+ return new Stream.eventTransformed(
+ stream,
+ (EventSink eventSink) {
+ if (_eventSink != null) {
+ throw new StateError("WebSocket transformer already used.");
+ }
+ _eventSink = eventSink;
+ return this;
+ });
+ }
+
+ void addError(Object error, [StackTrace stackTrace]) =>
+ _eventSink.addError(error, stackTrace);
+
+ void close() => _eventSink.close();
+
+ /**
+ * Process data received from the underlying communication channel.
+ */
+ void add(Uint8List buffer) {
+ int count = buffer.length;
+ int index = 0;
+ int lastIndex = count;
+ if (_state == CLOSED) {
+ throw new CompatibleWebSocketException("Data on closed connection");
+ }
+ if (_state == FAILURE) {
+ throw new CompatibleWebSocketException("Data on failed connection");
+ }
+ while ((index < lastIndex) && _state != CLOSED && _state != FAILURE) {
+ int byte = buffer[index];
+ if (_state <= LEN_REST) {
+ if (_state == START) {
+ _fin = (byte & 0x80) != 0;
+ if ((byte & 0x70) != 0) {
+ // The RSV1, RSV2 bits RSV3 must be all zero.
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ _opcode = (byte & 0xF);
+ if (_opcode <= _WebSocketOpcode.BINARY) {
+ if (_opcode == _WebSocketOpcode.CONTINUATION) {
+ if (_currentMessageType == _WebSocketMessageType.NONE) {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ } else {
+ assert(_opcode == _WebSocketOpcode.TEXT ||
+ _opcode == _WebSocketOpcode.BINARY);
+ if (_currentMessageType != _WebSocketMessageType.NONE) {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ _currentMessageType = _opcode;
+ }
+ } else if (_opcode >= _WebSocketOpcode.CLOSE &&
+ _opcode <= _WebSocketOpcode.PONG) {
+ // Control frames cannot be fragmented.
+ if (!_fin) throw new CompatibleWebSocketException("Protocol error");
+ } else {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ _state = LEN_FIRST;
+ } else if (_state == LEN_FIRST) {
+ _masked = (byte & 0x80) != 0;
+ _len = byte & 0x7F;
+ if (_isControlFrame() && _len > 125) {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ if (_len == 126) {
+ _len = 0;
+ _remainingLenBytes = 2;
+ _state = LEN_REST;
+ } else if (_len == 127) {
+ _len = 0;
+ _remainingLenBytes = 8;
+ _state = LEN_REST;
+ } else {
+ assert(_len < 126);
+ _lengthDone();
+ }
+ } else {
+ assert(_state == LEN_REST);
+ _len = _len << 8 | byte;
+ _remainingLenBytes--;
+ if (_remainingLenBytes == 0) {
+ _lengthDone();
+ }
+ }
+ } else {
+ if (_state == MASK) {
+ _maskingBytes[4 - _remainingMaskingKeyBytes--] = byte;
+ if (_remainingMaskingKeyBytes == 0) {
+ _maskDone();
+ }
+ } else {
+ assert(_state == PAYLOAD);
+ // The payload is not handled one byte at a time but in blocks.
+ int payloadLength = min(lastIndex - index, _remainingPayloadBytes);
+ _remainingPayloadBytes -= payloadLength;
+ // Unmask payload if masked.
+ if (_masked) {
+ _unmask(index, payloadLength, buffer);
+ }
+ // Control frame and data frame share _payloads.
+ _payload.add(
+ new Uint8List.view(buffer.buffer, index, payloadLength));
+ index += payloadLength;
+ if (_isControlFrame()) {
+ if (_remainingPayloadBytes == 0) _controlFrameEnd();
+ } else {
+ if (_currentMessageType != _WebSocketMessageType.TEXT &&
+ _currentMessageType != _WebSocketMessageType.BINARY) {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ if (_remainingPayloadBytes == 0) _messageFrameEnd();
+ }
+
+ // Hack - as we always do index++ below.
+ index--;
+ }
+ }
+
+ // Move to the next byte.
+ index++;
+ }
+ }
+
+ void _unmask(int index, int length, Uint8List buffer) {
+ const int BLOCK_SIZE = 16;
+ // Skip Int32x4-version if message is small.
+ if (length >= BLOCK_SIZE) {
+ // Start by aligning to 16 bytes.
+ final int startOffset = BLOCK_SIZE - (index & 15);
+ final int end = index + startOffset;
+ for (int i = index; i < end; i++) {
+ buffer[i] ^= _maskingBytes[_unmaskingIndex++ & 3];
+ }
+ index += startOffset;
+ length -= startOffset;
+ final int blockCount = length ~/ BLOCK_SIZE;
+ if (blockCount > 0) {
+ // Create mask block.
+ int mask = 0;
+ for (int i = 3; i >= 0; i--) {
+ mask = (mask << 8) | _maskingBytes[(_unmaskingIndex + i) & 3];
+ }
+ Int32x4 blockMask = new Int32x4(mask, mask, mask, mask);
+ Int32x4List blockBuffer = new Int32x4List.view(
+ buffer.buffer, index, blockCount);
+ for (int i = 0; i < blockBuffer.length; i++) {
+ blockBuffer[i] ^= blockMask;
+ }
+ final int bytes = blockCount * BLOCK_SIZE;
+ index += bytes;
+ length -= bytes;
+ }
+ }
+ // Handle end.
+ final int end = index + length;
+ for (int i = index; i < end; i++) {
+ buffer[i] ^= _maskingBytes[_unmaskingIndex++ & 3];
+ }
+ }
+
+ void _lengthDone() {
+ if (_masked) {
+ if (!_serverSide) {
+ throw new CompatibleWebSocketException(
+ "Received masked frame from server");
+ }
+ _state = MASK;
+ } else {
+ if (_serverSide) {
+ throw new CompatibleWebSocketException(
+ "Received unmasked frame from client");
+ }
+ _remainingPayloadBytes = _len;
+ _startPayload();
+ }
+ }
+
+ void _maskDone() {
+ _remainingPayloadBytes = _len;
+ _startPayload();
+ }
+
+ void _startPayload() {
+ // If there is no actual payload perform perform callbacks without
+ // going through the PAYLOAD state.
+ if (_remainingPayloadBytes == 0) {
+ if (_isControlFrame()) {
+ switch (_opcode) {
+ case _WebSocketOpcode.CLOSE:
+ _state = CLOSED;
+ _eventSink.close();
+ break;
+ case _WebSocketOpcode.PING:
+ _eventSink.add(new _WebSocketPing());
+ break;
+ case _WebSocketOpcode.PONG:
+ _eventSink.add(new _WebSocketPong());
+ break;
+ }
+ _prepareForNextFrame();
+ } else {
+ _messageFrameEnd();
+ }
+ } else {
+ _state = PAYLOAD;
+ }
+ }
+
+ void _messageFrameEnd() {
+ if (_fin) {
+ switch (_currentMessageType) {
+ case _WebSocketMessageType.TEXT:
+ _eventSink.add(UTF8.decode(_payload.takeBytes()));
+ break;
+ case _WebSocketMessageType.BINARY:
+ _eventSink.add(_payload.takeBytes());
+ break;
+ }
+ _currentMessageType = _WebSocketMessageType.NONE;
+ }
+ _prepareForNextFrame();
+ }
+
+ void _controlFrameEnd() {
+ switch (_opcode) {
+ case _WebSocketOpcode.CLOSE:
+ closeCode = _WebSocketStatus.NO_STATUS_RECEIVED;
+ var payload = _payload.takeBytes();
+ if (payload.length > 0) {
+ if (payload.length == 1) {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ closeCode = payload[0] << 8 | payload[1];
+ if (closeCode == _WebSocketStatus.NO_STATUS_RECEIVED) {
+ throw new CompatibleWebSocketException("Protocol error");
+ }
+ if (payload.length > 2) {
+ closeReason = UTF8.decode(payload.sublist(2));
+ }
+ }
+ _state = CLOSED;
+ _eventSink.close();
+ break;
+
+ case _WebSocketOpcode.PING:
+ _eventSink.add(new _WebSocketPing(_payload.takeBytes()));
+ break;
+
+ case _WebSocketOpcode.PONG:
+ _eventSink.add(new _WebSocketPong(_payload.takeBytes()));
+ break;
+ }
+ _prepareForNextFrame();
+ }
+
+ bool _isControlFrame() {
+ return _opcode == _WebSocketOpcode.CLOSE ||
+ _opcode == _WebSocketOpcode.PING ||
+ _opcode == _WebSocketOpcode.PONG;
+ }
+
+ void _prepareForNextFrame() {
+ if (_state != CLOSED && _state != FAILURE) _state = START;
+ _fin = false;
+ _opcode = -1;
+ _len = -1;
+ _remainingLenBytes = -1;
+ _remainingMaskingKeyBytes = 4;
+ _remainingPayloadBytes = -1;
+ _unmaskingIndex = 0;
+ }
+}
+
+
+class _WebSocketPing {
+ final List<int> payload;
+ _WebSocketPing([this.payload = null]);
+}
+
+
+class _WebSocketPong {
+ final List<int> payload;
+ _WebSocketPong([this.payload = null]);
+}
+
+// TODO(ajohnsen): Make this transformer reusable.
+class _WebSocketOutgoingTransformer implements StreamTransformer, EventSink {
+ final _WebSocketImpl webSocket;
+ EventSink _eventSink;
+
+ _WebSocketOutgoingTransformer(this.webSocket);
+
+ Stream bind(Stream stream) {
+ return new Stream.eventTransformed(
+ stream,
+ (EventSink eventSink) {
+ if (_eventSink != null) {
+ throw new StateError("WebSocket transformer already used");
+ }
+ _eventSink = eventSink;
+ return this;
+ });
+ }
+
+ void add(message) {
+ if (message is _WebSocketPong) {
+ addFrame(_WebSocketOpcode.PONG, message.payload);
+ return;
+ }
+ if (message is _WebSocketPing) {
+ addFrame(_WebSocketOpcode.PING, message.payload);
+ return;
+ }
+ List<int> data;
+ int opcode;
+ if (message != null) {
+ if (message is String) {
+ opcode = _WebSocketOpcode.TEXT;
+ data = UTF8.encode(message);
+ } else {
+ if (message is !List<int>) {
+ throw new ArgumentError(message);
+ }
+ opcode = _WebSocketOpcode.BINARY;
+ data = message;
+ }
+ } else {
+ opcode = _WebSocketOpcode.TEXT;
+ }
+ addFrame(opcode, data);
+ }
+
+ void addError(Object error, [StackTrace stackTrace]) =>
+ _eventSink.addError(error, stackTrace);
+
+ void close() {
+ int code = webSocket._outCloseCode;
+ String reason = webSocket._outCloseReason;
+ List<int> data;
+ if (code != null) {
+ data = new List<int>();
+ data.add((code >> 8) & 0xFF);
+ data.add(code & 0xFF);
+ if (reason != null) {
+ data.addAll(UTF8.encode(reason));
+ }
+ }
+ addFrame(_WebSocketOpcode.CLOSE, data);
+ _eventSink.close();
+ }
+
+ void addFrame(int opcode, List<int> data) =>
+ createFrame(opcode, data, webSocket._serverSide).forEach(_eventSink.add);
+
+ static Iterable createFrame(int opcode, List<int> data, bool serverSide) {
+ bool mask = !serverSide; // Masking not implemented for server.
+ int dataLength = data == null ? 0 : data.length;
+ // Determine the header size.
+ int headerSize = (mask) ? 6 : 2;
+ if (dataLength > 65535) {
+ headerSize += 8;
+ } else if (dataLength > 125) {
+ headerSize += 2;
+ }
+ Uint8List header = new Uint8List(headerSize);
+ int index = 0;
+ // Set FIN and opcode.
+ header[index++] = 0x80 | opcode;
+ // Determine size and position of length field.
+ int lengthBytes = 1;
+ int firstLengthByte = 1;
+ if (dataLength > 65535) {
+ header[index++] = 127;
+ lengthBytes = 8;
+ } else if (dataLength > 125) {
+ header[index++] = 126;
+ lengthBytes = 2;
+ }
+ // Write the length in network byte order into the header.
+ for (int i = 0; i < lengthBytes; i++) {
+ header[index++] = dataLength >> (((lengthBytes - 1) - i) * 8) & 0xFF;
+ }
+ if (mask) {
+ header[1] |= 1 << 7;
+ var maskBytes = [_random.nextInt(256), _random.nextInt(256),
+ _random.nextInt(256), _random.nextInt(256)];
+ header.setRange(index, index + 4, maskBytes);
+ index += 4;
+ if (data != null) {
+ Uint8List list;
+ // If this is a text message just do the masking inside the
+ // encoded data.
+ if (opcode == _WebSocketOpcode.TEXT && data is Uint8List) {
+ list = data;
+ } else {
+ if (data is Uint8List) {
+ list = new Uint8List.fromList(data);
+ } else {
+ list = new Uint8List(data.length);
+ for (int i = 0; i < data.length; i++) {
+ if (data[i] < 0 || 255 < data[i]) {
+ throw new ArgumentError(
+ "List element is not a byte value "
+ "(value ${data[i]} at index $i)");
+ }
+ list[i] = data[i];
+ }
+ }
+ }
+ const int BLOCK_SIZE = 16;
+ int blockCount = list.length ~/ BLOCK_SIZE;
+ if (blockCount > 0) {
+ // Create mask block.
+ int mask = 0;
+ for (int i = 3; i >= 0; i--) {
+ mask = (mask << 8) | maskBytes[i];
+ }
+ Int32x4 blockMask = new Int32x4(mask, mask, mask, mask);
+ Int32x4List blockBuffer = new Int32x4List.view(
+ list.buffer, 0, blockCount);
+ for (int i = 0; i < blockBuffer.length; i++) {
+ blockBuffer[i] ^= blockMask;
+ }
+ }
+ // Handle end.
+ for (int i = blockCount * BLOCK_SIZE; i < list.length; i++) {
+ list[i] ^= maskBytes[i & 3];
+ }
+ data = list;
+ }
+ }
+ assert(index == headerSize);
+ if (data == null) {
+ return [header];
+ } else {
+ return [header, data];
+ }
+ }
+}
+
+
+class _WebSocketConsumer implements StreamConsumer {
+ final _WebSocketImpl webSocket;
+ final StreamSink<List<int>> sink;
+ StreamController _controller;
+ StreamSubscription _subscription;
+ bool _issuedPause = false;
+ bool _closed = false;
+ Completer _closeCompleter = new Completer();
+ Completer _completer;
+
+ _WebSocketConsumer(this.webSocket, this.sink);
+
+ void _onListen() {
+ if (_subscription != null) {
+ _subscription.cancel();
+ }
+ }
+
+ void _onPause() {
+ if (_subscription != null) {
+ _subscription.pause();
+ } else {
+ _issuedPause = true;
+ }
+ }
+
+ void _onResume() {
+ if (_subscription != null) {
+ _subscription.resume();
+ } else {
+ _issuedPause = false;
+ }
+ }
+
+ void _cancel() {
+ if (_subscription != null) {
+ var subscription = _subscription;
+ _subscription = null;
+ subscription.cancel();
+ }
+ }
+
+ _ensureController() {
+ if (_controller != null) return;
+ _controller = new StreamController(sync: true,
+ onPause: _onPause,
+ onResume: _onResume,
+ onCancel: _onListen);
+ var stream = _controller.stream.transform(
+ new _WebSocketOutgoingTransformer(webSocket));
+ sink.addStream(stream)
+ .then((_) {
+ _done();
+ _closeCompleter.complete(webSocket);
+ }, onError: (error, StackTrace stackTrace) {
+ _closed = true;
+ _cancel();
+ if (error is ArgumentError) {
+ if (!_done(error, stackTrace)) {
+ _closeCompleter.completeError(error, stackTrace);
+ }
+ } else {
+ _done();
+ _closeCompleter.complete(webSocket);
+ }
+ });
+ }
+
+ bool _done([error, StackTrace stackTrace]) {
+ if (_completer == null) return false;
+ if (error != null) {
+ _completer.completeError(error, stackTrace);
+ } else {
+ _completer.complete(webSocket);
+ }
+ _completer = null;
+ return true;
+ }
+
+ Future addStream(var stream) {
+ if (_closed) {
+ stream.listen(null).cancel();
+ return new Future.value(webSocket);
+ }
+ _ensureController();
+ _completer = new Completer();
+ _subscription = stream.listen(
+ (data) {
+ _controller.add(data);
+ },
+ onDone: _done,
+ onError: _done,
+ cancelOnError: true);
+ if (_issuedPause) {
+ _subscription.pause();
+ _issuedPause = false;
+ }
+ return _completer.future;
+ }
+
+ Future close() {
+ _ensureController();
+ Future closeSocket() {
+ return sink.close().catchError((_) {}).then((_) => webSocket);
+ }
+ _controller.close();
+ return _closeCompleter.future.then((_) => closeSocket());
+ }
+
+ void add(data) {
+ if (_closed) return;
+ _ensureController();
+ _controller.add(data);
+ }
+
+ void closeSocket() {
+ _closed = true;
+ _cancel();
+ close();
+ }
+}
+
+
+class _WebSocketImpl extends Stream implements CompatibleWebSocket {
+ StreamController _controller;
+ StreamSubscription _subscription;
+ StreamController _sink;
+
+ final bool _serverSide;
+ int _readyState = _WebSocketState.CONNECTING;
+ bool _writeClosed = false;
+ int _closeCode;
+ String _closeReason;
+ Duration _pingInterval;
+ Timer _pingTimer;
+ _WebSocketConsumer _consumer;
+
+ int _outCloseCode;
+ String _outCloseReason;
+ Timer _closeTimer;
+
+ _WebSocketImpl._fromSocket(Stream<List<int>> stream,
+ StreamSink<List<int>> sink, [this._serverSide = false]) {
+ _consumer = new _WebSocketConsumer(this, sink);
+ _sink = new StreamController();
+ _sink.stream.pipe(_consumer);
+ _readyState = _WebSocketState.OPEN;
+
+ var transformer = new _WebSocketProtocolTransformer(_serverSide);
+ _subscription = stream.transform(transformer).listen(
+ (data) {
+ if (data is _WebSocketPing) {
+ if (!_writeClosed) _consumer.add(new _WebSocketPong(data.payload));
+ } else if (data is _WebSocketPong) {
+ // Simply set pingInterval, as it'll cancel any timers.
+ pingInterval = _pingInterval;
+ } else {
+ _controller.add(data);
+ }
+ },
+ onError: (error) {
+ if (_closeTimer != null) _closeTimer.cancel();
+ if (error is FormatException) {
+ _close(_WebSocketStatus.INVALID_FRAME_PAYLOAD_DATA);
+ } else {
+ _close(_WebSocketStatus.PROTOCOL_ERROR);
+ }
+ _controller.close();
+ },
+ onDone: () {
+ if (_closeTimer != null) _closeTimer.cancel();
+ if (_readyState == _WebSocketState.OPEN) {
+ _readyState = _WebSocketState.CLOSING;
+ if (!_isReservedStatusCode(transformer.closeCode)) {
+ _close(transformer.closeCode);
+ } else {
+ _close();
+ }
+ _readyState = _WebSocketState.CLOSED;
+ }
+ _closeCode = transformer.closeCode;
+ _closeReason = transformer.closeReason;
+ _controller.close();
+ },
+ cancelOnError: true);
+ _subscription.pause();
+ _controller = new StreamController(sync: true,
+ onListen: _subscription.resume,
+ onPause: _subscription.pause,
+ onResume: _subscription.resume);
+ }
+
+ StreamSubscription listen(void onData(message),
+ {Function onError,
+ void onDone(),
+ bool cancelOnError}) {
+ return _controller.stream.listen(onData,
+ onError: onError,
+ onDone: onDone,
+ cancelOnError: cancelOnError);
+ }
+
+ Duration get pingInterval => _pingInterval;
+
+ void set pingInterval(Duration interval) {
+ if (_writeClosed) return;
+ if (_pingTimer != null) _pingTimer.cancel();
+ _pingInterval = interval;
+
+ if (_pingInterval == null) return;
+
+ _pingTimer = new Timer(_pingInterval, () {
+ if (_writeClosed) return;
+ _consumer.add(new _WebSocketPing());
+ _pingTimer = new Timer(_pingInterval, () {
+ // No pong received.
+ _close(_WebSocketStatus.GOING_AWAY);
+ });
+ });
+ }
+
+ int get closeCode => _closeCode;
+ String get closeReason => _closeReason;
+
+ void add(data) => _sink.add(data);
+ void addError(error, [StackTrace stackTrace]) =>
+ _sink.addError(error, stackTrace);
+ Future addStream(Stream stream) => _sink.addStream(stream);
+ Future get done => _sink.done;
+
+ Future close([int code, String reason]) {
+ if (_isReservedStatusCode(code)) {
+ throw new CompatibleWebSocketException("Reserved status code $code");
+ }
+ if (_outCloseCode == null) {
+ _outCloseCode = code;
+ _outCloseReason = reason;
+ }
+ if (_closeTimer == null && !_controller.isClosed) {
+ // When closing the web-socket, we no longer accept data.
+ _closeTimer = new Timer(const Duration(seconds: 5), () {
+ _subscription.cancel();
+ _controller.close();
+ });
+ }
+ return _sink.close();
+ }
+
+ void _close([int code, String reason]) {
+ if (_writeClosed) return;
+ if (_outCloseCode == null) {
+ _outCloseCode = code;
+ _outCloseReason = reason;
+ }
+ _writeClosed = true;
+ _consumer.closeSocket();
+ }
+
+ static bool _isReservedStatusCode(int code) {
+ return code != null &&
+ (code < _WebSocketStatus.NORMAL_CLOSURE ||
+ code == _WebSocketStatus.RESERVED_1004 ||
+ code == _WebSocketStatus.NO_STATUS_RECEIVED ||
+ code == _WebSocketStatus.ABNORMAL_CLOSURE ||
+ (code > _WebSocketStatus.INTERNAL_SERVER_ERROR &&
+ code < _WebSocketStatus.RESERVED_1015) ||
+ (code >= _WebSocketStatus.RESERVED_1015 &&
+ code < 3000));
+ }
+}
+
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..af1a192
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,14 @@
+name: http_parser
+version: 0.0.2+3
+author: "Dart Team <misc@dartlang.org>"
+homepage: http://www.dartlang.org
+description: >
+ A platform-independent package for parsing and serializing HTTP formats.
+dependencies:
+ crypto: ">=0.9.0 <0.10.0"
+ collection: ">=0.9.1 <0.10.0"
+ string_scanner: ">=0.0.0 <0.1.0"
+dev_dependencies:
+ unittest: ">=0.10.0 <0.11.0"
+environment:
+ sdk: ">=1.2.0 <2.0.0"
diff --git a/test/http_date_test.dart b/test/http_date_test.dart
new file mode 100644
index 0000000..303d6d7
--- /dev/null
+++ b/test/http_date_test.dart
@@ -0,0 +1,347 @@
+// Copyright (c) 2014, 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.
+
+library http_parser.http_date_test;
+
+import 'package:http_parser/http_parser.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+ group('format', () {
+ test('many values with 9', () {
+ var date = new DateTime.utc(2014, 9, 9, 9, 9, 9);
+ var formatted = formatHttpDate(date);
+
+ expect(formatted, 'Tue, 09 Sep 2014 09:09:09 GMT');
+ var parsed = parseHttpDate(formatted);
+
+ expect(parsed, date);
+ });
+
+ test('end of year', () {
+ var date = new DateTime.utc(1999, 12, 31, 23, 59, 59);
+ var formatted = formatHttpDate(date);
+
+ expect(formatted, 'Fri, 31 Dec 1999 23:59:59 GMT');
+ var parsed = parseHttpDate(formatted);
+
+ expect(parsed, date);
+ });
+
+ test('start of year', () {
+ var date = new DateTime.utc(2000, 1, 1, 0, 0, 0);
+ var formatted = formatHttpDate(date);
+
+ expect(formatted, 'Sat, 01 Jan 2000 00:00:00 GMT');
+ var parsed = parseHttpDate(formatted);
+
+ expect(parsed, date);
+ });
+ });
+
+ group("parse", () {
+ group("RFC 1123", () {
+ test("parses the example date", () {
+ var date = parseHttpDate("Sun, 06 Nov 1994 08:49:37 GMT");
+ expect(date.day, equals(6));
+ expect(date.month, equals(DateTime.NOVEMBER));
+ expect(date.year, equals(1994));
+ expect(date.hour, equals(8));
+ expect(date.minute, equals(49));
+ expect(date.second, equals(37));
+ expect(date.timeZoneName, equals("UTC"));
+ });
+
+ test("whitespace is required", () {
+ expect(() => parseHttpDate("Sun,06 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 199408:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:37GMT"),
+ throwsFormatException);
+ });
+
+ test("exactly one space is required", () {
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+ });
+
+ test("requires precise number lengths", () {
+ expect(() => parseHttpDate("Sun, 6 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 8:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:9:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:7 GMT"),
+ throwsFormatException);
+ });
+
+ test("requires reasonable numbers", () {
+ expect(() => parseHttpDate("Sun, 00 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 31 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 32 Aug 1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 24:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:60:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun, 06 Nov 1994 08:49:60 GMT"),
+ throwsFormatException);
+ });
+
+ test("only allows short weekday names", () {
+ expect(() => parseHttpDate("Sunday, 6 Nov 1994 08:49:37 GMT"),
+ throwsFormatException);
+ });
+
+ test("only allows short month names", () {
+ expect(() => parseHttpDate("Sun, 6 November 1994 08:49:37 GMT"),
+ throwsFormatException);
+ });
+
+ test("only allows GMT", () {
+ expect(() => parseHttpDate("Sun, 6 Nov 1994 08:49:37 PST"),
+ throwsFormatException);
+ });
+
+ test("disallows trailing whitespace", () {
+ expect(() => parseHttpDate("Sun, 6 Nov 1994 08:49:37 GMT "),
+ throwsFormatException);
+ });
+ });
+
+ group("RFC 850", () {
+ test("parses the example date", () {
+ var date = parseHttpDate("Sunday, 06-Nov-94 08:49:37 GMT");
+ expect(date.day, equals(6));
+ expect(date.month, equals(DateTime.NOVEMBER));
+ expect(date.year, equals(1994));
+ expect(date.hour, equals(8));
+ expect(date.minute, equals(49));
+ expect(date.second, equals(37));
+ expect(date.timeZoneName, equals("UTC"));
+ });
+
+ test("whitespace is required", () {
+ expect(() => parseHttpDate("Sunday,06-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-9408:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:49:37GMT"),
+ throwsFormatException);
+ });
+
+ test("exactly one space is required", () {
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+ });
+
+ test("requires precise number lengths", () {
+ expect(() => parseHttpDate("Sunday, 6-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-1994 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 8:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:9:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:49:7 GMT"),
+ throwsFormatException);
+ });
+
+ test("requires reasonable numbers", () {
+ expect(() => parseHttpDate("Sunday, 00-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 31-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 32-Aug-94 08:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 24:49:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:60:37 GMT"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sunday, 06-Nov-94 08:49:60 GMT"),
+ throwsFormatException);
+ });
+
+ test("only allows long weekday names", () {
+ expect(() => parseHttpDate("Sun, 6-Nov-94 08:49:37 GMT"),
+ throwsFormatException);
+ });
+
+ test("only allows short month names", () {
+ expect(() => parseHttpDate("Sunday, 6-November-94 08:49:37 GMT"),
+ throwsFormatException);
+ });
+
+ test("only allows GMT", () {
+ expect(() => parseHttpDate("Sunday, 6-Nov-94 08:49:37 PST"),
+ throwsFormatException);
+ });
+
+ test("disallows trailing whitespace", () {
+ expect(() => parseHttpDate("Sunday, 6-Nov-94 08:49:37 GMT "),
+ throwsFormatException);
+ });
+ });
+
+ group("asctime()", () {
+ test("parses the example date", () {
+ var date = parseHttpDate("Sun Nov 6 08:49:37 1994");
+ expect(date.day, equals(6));
+ expect(date.month, equals(DateTime.NOVEMBER));
+ expect(date.year, equals(1994));
+ expect(date.hour, equals(8));
+ expect(date.minute, equals(49));
+ expect(date.second, equals(37));
+ expect(date.timeZoneName, equals("UTC"));
+ });
+
+ test("parses a date with a two-digit day", () {
+ var date = parseHttpDate("Sun Nov 16 08:49:37 1994");
+ expect(date.day, equals(16));
+ expect(date.month, equals(DateTime.NOVEMBER));
+ expect(date.year, equals(1994));
+ expect(date.hour, equals(8));
+ expect(date.minute, equals(49));
+ expect(date.second, equals(37));
+ expect(date.timeZoneName, equals("UTC"));
+ });
+
+ test("whitespace is required", () {
+ expect(() => parseHttpDate("SunNov 6 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov6 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 608:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:371994"),
+ throwsFormatException);
+ });
+
+ test("the right amount of whitespace is required", () {
+ expect(() => parseHttpDate("Sun Nov 6 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:37 1994"),
+ throwsFormatException);
+ });
+
+ test("requires precise number lengths", () {
+ expect(() => parseHttpDate("Sun Nov 016 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 8:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:9:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:7 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:37 94"),
+ throwsFormatException);
+ });
+
+ test("requires reasonable numbers", () {
+ expect(() => parseHttpDate("Sun Nov 0 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 31 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Aug 32 08:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 24:49:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:60:37 1994"),
+ throwsFormatException);
+
+ expect(() => parseHttpDate("Sun Nov 6 08:49:60 1994"),
+ throwsFormatException);
+ });
+
+ test("only allows short weekday names", () {
+ expect(() => parseHttpDate("Sunday Nov 0 08:49:37 1994"),
+ throwsFormatException);
+ });
+
+ test("only allows short month names", () {
+ expect(() => parseHttpDate("Sun November 0 08:49:37 1994"),
+ throwsFormatException);
+ });
+
+ test("disallows trailing whitespace", () {
+ expect(() => parseHttpDate("Sun November 0 08:49:37 1994 "),
+ throwsFormatException);
+ });
+ });
+ });
+}
diff --git a/test/media_type_test.dart b/test/media_type_test.dart
new file mode 100644
index 0000000..14affbb
--- /dev/null
+++ b/test/media_type_test.dart
@@ -0,0 +1,163 @@
+// Copyright (c) 2014, 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.
+
+library http_parser.media_type_test;
+
+import 'package:http_parser/http_parser.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+ group("parse", () {
+ test("parses a simple MIME type", () {
+ var type = new MediaType.parse("text/plain");
+ expect(type.type, equals("text"));
+ expect(type.subtype, equals("plain"));
+ });
+
+ test("allows leading whitespace", () {
+ expect(new MediaType.parse(" text/plain").mimeType, equals("text/plain"));
+ expect(new MediaType.parse("\ttext/plain").mimeType,
+ equals("text/plain"));
+ });
+
+ test("allows trailing whitespace", () {
+ expect(new MediaType.parse("text/plain ").mimeType, equals("text/plain"));
+ expect(new MediaType.parse("text/plain\t").mimeType,
+ equals("text/plain"));
+ });
+
+ test("disallows separators in the MIME type", () {
+ expect(() => new MediaType.parse("te(xt/plain"), throwsFormatException);
+ expect(() => new MediaType.parse("text/pla=in"), throwsFormatException);
+ });
+
+ test("disallows whitespace around the slash", () {
+ expect(() => new MediaType.parse("text /plain"), throwsFormatException);
+ expect(() => new MediaType.parse("text/ plain"), throwsFormatException);
+ });
+
+ test("parses parameters", () {
+ var type = new MediaType.parse("text/plain;foo=bar;baz=bang");
+ expect(type.mimeType, equals("text/plain"));
+ expect(type.parameters, equals({"foo": "bar", "baz": "bang"}));
+ });
+
+ test("allows whitespace around the semicolon", () {
+ var type = new MediaType.parse("text/plain ; foo=bar ; baz=bang");
+ expect(type.mimeType, equals("text/plain"));
+ expect(type.parameters, equals({"foo": "bar", "baz": "bang"}));
+ });
+
+ test("disallows whitespace around the equals", () {
+ expect(() => new MediaType.parse("text/plain; foo =bar"),
+ throwsFormatException);
+ expect(() => new MediaType.parse("text/plain; foo= bar"),
+ throwsFormatException);
+ });
+
+ test("disallows separators in the parameters", () {
+ expect(() => new MediaType.parse("text/plain; fo:o=bar"),
+ throwsFormatException);
+ expect(() => new MediaType.parse("text/plain; foo=b@ar"),
+ throwsFormatException);
+ });
+
+ test("parses quoted parameters", () {
+ var type = new MediaType.parse(
+ 'text/plain; foo="bar space"; baz="bang\\\\escape"');
+ expect(type.mimeType, equals("text/plain"));
+ expect(type.parameters, equals({
+ "foo": "bar space",
+ "baz": "bang\\escape"
+ }));
+ });
+ });
+
+ group("change", () {
+ var type;
+ setUp(() {
+ type = new MediaType.parse("text/plain; foo=bar; baz=bang");
+ });
+
+ test("uses the existing fields by default", () {
+ var newType = type.change();
+ expect(newType.type, equals("text"));
+ expect(newType.subtype, equals("plain"));
+ expect(newType.parameters, equals({"foo": "bar", "baz": "bang"}));
+ });
+
+ test("[type] overrides the existing type", () {
+ expect(type.change(type: "new").type, equals("new"));
+ });
+
+ test("[subtype] overrides the existing subtype", () {
+ expect(type.change(subtype: "new").subtype, equals("new"));
+ });
+
+ test("[mimeType] overrides the existing type and subtype", () {
+ var newType = type.change(mimeType: "image/png");
+ expect(newType.type, equals("image"));
+ expect(newType.subtype, equals("png"));
+ });
+
+ test("[parameters] overrides and adds to existing parameters", () {
+ expect(type.change(parameters: {
+ "foo": "zap",
+ "qux": "fblthp"
+ }).parameters, equals({
+ "foo": "zap",
+ "baz": "bang",
+ "qux": "fblthp"
+ }));
+ });
+
+ test("[clearParameters] removes existing parameters", () {
+ expect(type.change(clearParameters: true).parameters, isEmpty);
+ });
+
+ test("[clearParameters] with [parameters] removes before adding", () {
+ var newType = type.change(
+ parameters: {"foo": "zap"},
+ clearParameters: true);
+ expect(newType.parameters, equals({"foo": "zap"}));
+ });
+
+ test("[type] with [mimeType] is illegal", () {
+ expect(() => type.change(type: "new", mimeType: "image/png"),
+ throwsArgumentError);
+ });
+
+ test("[subtype] with [mimeType] is illegal", () {
+ expect(() => type.change(subtype: "new", mimeType: "image/png"),
+ throwsArgumentError);
+ });
+ });
+
+ group("toString", () {
+ test("serializes a simple MIME type", () {
+ expect(new MediaType("text", "plain").toString(), equals("text/plain"));
+ });
+
+ test("serializes a token parameter as a token", () {
+ expect(new MediaType("text", "plain", {"foo": "bar"}).toString(),
+ equals("text/plain; foo=bar"));
+ });
+
+ test("serializes a non-token parameter as a quoted string", () {
+ expect(new MediaType("text", "plain", {"foo": "bar baz"}).toString(),
+ equals('text/plain; foo="bar baz"'));
+ });
+
+ test("escapes a quoted string as necessary", () {
+ expect(new MediaType("text", "plain", {"foo": 'bar"\x7Fbaz'}).toString(),
+ equals('text/plain; foo="bar\\"\\\x7Fbaz"'));
+ });
+
+ test("serializes multiple parameters", () {
+ expect(new MediaType("text", "plain", {
+ "foo": "bar", "baz": "bang"
+ }).toString(), equals("text/plain; foo=bar; baz=bang"));
+ });
+ });
+}
diff --git a/test/web_socket_test.dart b/test/web_socket_test.dart
new file mode 100644
index 0000000..45b84d9
--- /dev/null
+++ b/test/web_socket_test.dart
@@ -0,0 +1,95 @@
+// Copyright (c) 2014, 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.
+
+library http_parser.web_socket_test;
+
+import 'dart:io';
+
+import 'package:http_parser/http_parser.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+ test("a client can communicate with a WebSocket server", () {
+ return HttpServer.bind("localhost", 0).then((server) {
+ server.transform(new WebSocketTransformer()).listen((webSocket) {
+ webSocket.add("hello!");
+ webSocket.first.then((request) {
+ expect(request, equals("ping"));
+ webSocket.add("pong");
+ webSocket.close();
+ });
+ });
+
+ var client = new HttpClient();
+ return client.openUrl("GET", Uri.parse("http://localhost:${server.port}"))
+ .then((request) {
+ request.headers
+ ..set("Connection", "Upgrade")
+ ..set("Upgrade", "websocket")
+ ..set("Sec-WebSocket-Key", "x3JJHMbDL1EzLkh9GBhXDw==")
+ ..set("Sec-WebSocket-Version", "13");
+ return request.close();
+ }).then((response) => response.detachSocket()).then((socket) {
+ var webSocket = new CompatibleWebSocket(socket, serverSide: false);
+
+ var n = 0;
+ return webSocket.listen((message) {
+ if (n == 0) {
+ expect(message, equals("hello!"));
+ webSocket.add("ping");
+ } else if (n == 1) {
+ expect(message, equals("pong"));
+ webSocket.close();
+ server.close();
+ } else {
+ fail("Only expected two messages.");
+ }
+ n++;
+ }).asFuture();
+ });
+ });
+ });
+
+ test("a server can communicate with a WebSocket client", () {
+ return HttpServer.bind("localhost", 0).then((server) {
+ server.listen((request) {
+ var response = request.response;
+ response.statusCode = 101;
+ response.headers
+ ..set("Connection", "Upgrade")
+ ..set("Upgrade", "websocket")
+ ..set("Sec-WebSocket-Accept", CompatibleWebSocket.signKey(
+ request.headers.value('Sec-WebSocket-Key')));
+ response.contentLength = 0;
+ response.detachSocket().then((socket) {
+ var webSocket = new CompatibleWebSocket(socket);
+ webSocket.add("hello!");
+ webSocket.first.then((request) {
+ expect(request, equals("ping"));
+ webSocket.add("pong");
+ webSocket.close();
+ });
+ });
+ });
+
+ return WebSocket.connect('ws://localhost:${server.port}')
+ .then((webSocket) {
+ var n = 0;
+ return webSocket.listen((message) {
+ if (n == 0) {
+ expect(message, equals("hello!"));
+ webSocket.add("ping");
+ } else if (n == 1) {
+ expect(message, equals("pong"));
+ webSocket.close();
+ server.close();
+ } else {
+ fail("Only expected two messages.");
+ }
+ n++;
+ }).asFuture();
+ });
+ });
+ });
+}