diff --git a/pkgs/cronet_http/.idea/.gitignore b/pkgs/cronet_http/.idea/.gitignore new file mode 100644 index 0000000000..26d33521af --- /dev/null +++ b/pkgs/cronet_http/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/pkgs/cronet_http/.idea/vcs.xml b/pkgs/cronet_http/.idea/vcs.xml new file mode 100644 index 0000000000..b2bdec2d71 --- /dev/null +++ b/pkgs/cronet_http/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pkgs/cupertino_http/example/integration_test/websocket_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/websocket_conformance_test.dart new file mode 100644 index 0000000000..45535395a8 --- /dev/null +++ b/pkgs/cupertino_http/example/integration_test/websocket_conformance_test.dart @@ -0,0 +1,6 @@ +import 'package:cupertino_http/cupertino_http.dart'; +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; + +void main() { + testAll((uri, {protocols}) => CupertinoWebSocket.connect(uri)); +} diff --git a/pkgs/cupertino_http/example/pubspec.yaml b/pkgs/cupertino_http/example/pubspec.yaml index 026cb8a39a..a1a92f7dd6 100644 --- a/pkgs/cupertino_http/example/pubspec.yaml +++ b/pkgs/cupertino_http/example/pubspec.yaml @@ -31,6 +31,8 @@ dev_dependencies: integration_test: sdk: flutter test: ^1.21.1 + web_socket_conformance_tests: + path: ../../web_socket_conformance_tests flutter: uses-material-design: true diff --git a/pkgs/cupertino_http/lib/cupertino_http.dart b/pkgs/cupertino_http/lib/cupertino_http.dart index 243ac81436..6bbf729c2e 100644 --- a/pkgs/cupertino_http/lib/cupertino_http.dart +++ b/pkgs/cupertino_http/lib/cupertino_http.dart @@ -88,3 +88,4 @@ import 'src/cupertino_client.dart'; export 'src/cupertino_api.dart'; export 'src/cupertino_client.dart'; +export 'src/websocket.dart'; diff --git a/pkgs/cupertino_http/lib/src/websocket.dart b/pkgs/cupertino_http/lib/src/websocket.dart new file mode 100644 index 0000000000..2674badf64 --- /dev/null +++ b/pkgs/cupertino_http/lib/src/websocket.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cupertino_http/cupertino_http.dart'; +import 'package:websocket/websocket.dart'; + +class CupertinoWebSocketException extends XXXWebSocketException { + CupertinoWebSocketException([super.message = '']); + + factory CupertinoWebSocketException.fromError(Error e) => + CupertinoWebSocketException(e.toString()); +} + +class CupertinoWebSocket implements XXXWebSocket { + static Future connect(Uri uri) async { + final readyCompleter = Completer(); + late CupertinoWebSocket webSocket; + + final session = URLSession.sessionWithConfiguration( + URLSessionConfiguration.defaultSessionConfiguration(), + onComplete: (session, task, error) { + print('onComplete:'); + if (!readyCompleter.isCompleted) { + if (error != null) { + readyCompleter + .completeError(CupertinoWebSocketException.fromError(error)); + } else { + readyCompleter.complete(); + } + } else { + webSocket._closed(1006, Data.fromList('abnormal close'.codeUnits)); + } + }, onWebSocketTaskOpened: (session, task, protocol) { + print('onWebSocketTaskOpened:'); +// _protocol = protocol; + readyCompleter.complete(); + }, onWebSocketTaskClosed: (session, task, closeCode, reason) { + print('onWebSocketTaskClosed: $closeCode'); + webSocket._closed(closeCode, reason); + }); + print(uri); + final task = session.webSocketTaskWithRequest(URLRequest.fromUrl(uri)) + ..resume(); + await readyCompleter.future; + return webSocket = CupertinoWebSocket._(task); + } + + final URLSessionWebSocketTask _task; + final _events = StreamController(); + + void handleMessage(URLSessionWebSocketMessage value) { + print('handleMessage: $value'); + late WebSocketEvent v; + switch (value.type) { + case URLSessionWebSocketMessageType.urlSessionWebSocketMessageTypeString: + v = TextDataReceived(value.string!); + break; + case URLSessionWebSocketMessageType.urlSessionWebSocketMessageTypeData: + v = BinaryDataReceived(value.data!.bytes); + break; + } + _events.add(v); + scheduleReceive(); + } + + void scheduleReceive() { +// print('scheduleReceive'); + _task.receiveMessage().then(handleMessage, onError: _closeWithError); + } + + CupertinoWebSocket._(this._task) { + scheduleReceive(); + } + + void _closeWithError(Object e) { + print('closedWithError: $e'); + if (e is Error) { + if (e.domain == 'NSPOSIXErrorDomain' && e.code == 57) { + // Socket is not connected. + // onWebSocketTaskClosed/onComplete will be invoked and may indicate a + // close code. + return; + } + var (int code, String? reason) = switch ([e.domain, e.code]) { + ['NSPOSIXErrorDomain', 100] => (1002, e.localizedDescription), + _ => (1006, e.localizedDescription) + }; + _task.cancel(); + _closed(code, reason == null ? null : Data.fromList(reason.codeUnits)); + } else { + throw UnsupportedError(''); + } + } + + void _closed(int? closeCode, Data? reason) { + print('closing with $closeCode'); + if (!_events.isClosed) { + final closeReason = reason == null ? '' : utf8.decode(reason.bytes); + + _events + ..add(CloseReceived(closeCode, closeReason)) + ..close(); + } + } + + @override + void sendBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _task + .sendMessage(URLSessionWebSocketMessage.fromData(Data.fromList(b))) + .then((_) => _, onError: _closeWithError); + } + + @override + void sendText(String s) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _task + .sendMessage(URLSessionWebSocketMessage.fromString(s)) + .then((_) => _, onError: _closeWithError); + } + + @override + Future close([int? code, String? reason]) async { + if (_events.isClosed) { + throw XXXWebSocketConnectionClosed(); + } + + if (code != null) { + RangeError.checkValueInInterval(code, 3000, 4999, 'code'); + } + if (reason != null && utf8.encode(reason).length > 123) { + throw ArgumentError.value(reason, 'reason', + 'reason must be <= 123 bytes long when encoded as UTF-8'); + } + + if (!_events.isClosed) { + unawaited(_events.close()); + + // XXX Wait until all pending writes are done. + print('close($code, $reason)'); + if (code != null) { + reason = reason ?? ''; + _task.cancelWithCloseCode(code, Data.fromList(reason.codeUnits)); + } else { + _task.cancel(); + } + } + } + + @override + Stream get events => _events.stream; +} + +/* + test('with code and reason', () async { + final channel = await channelFactory(uri); + + channel.addString('Please close'); + expect(await channel.events.toList(), + [Closed(4123, 'server closed the connection')]); + }); +*/ diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 644bf1bf4d..d0c899993d 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -15,6 +15,8 @@ dependencies: flutter: sdk: flutter http: ^1.2.0 + websocket: + path: ../websocket dev_dependencies: dart_flutter_team_lints: ^2.0.0 diff --git a/pkgs/web_socket_conformance_tests/.gitignore b/pkgs/web_socket_conformance_tests/.gitignore new file mode 100644 index 0000000000..3cceda5578 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/pkgs/web_socket_conformance_tests/CHANGELOG.md b/pkgs/web_socket_conformance_tests/CHANGELOG.md new file mode 100644 index 0000000000..effe43c82c --- /dev/null +++ b/pkgs/web_socket_conformance_tests/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/pkgs/web_socket_conformance_tests/README.md b/pkgs/web_socket_conformance_tests/README.md new file mode 100644 index 0000000000..8b55e735b5 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/pkgs/web_socket_conformance_tests/analysis_options.yaml b/pkgs/web_socket_conformance_tests/analysis_options.yaml new file mode 100644 index 0000000000..dee8927aaf --- /dev/null +++ b/pkgs/web_socket_conformance_tests/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/pkgs/web_socket_conformance_tests/bin/generate_server_wrappers.dart b/pkgs/web_socket_conformance_tests/bin/generate_server_wrappers.dart new file mode 100644 index 0000000000..7168868db2 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/bin/generate_server_wrappers.dart @@ -0,0 +1,51 @@ +// Copyright (c) 2023, 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. + +/// Generates the '*_server_vm.dart' and '*_server_web.dart' support files. +library; + +import 'dart:core'; +import 'dart:io'; + +import 'package:dart_style/dart_style.dart'; + +const vm = '''// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import ''; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} +'''; + +const web = '''// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'web_socket_conformance_tests/src/')); +'''; + +void main() async { + final files = await Directory('lib/src').list().toList(); + final formatter = DartFormatter(); + + files.where((file) => file.path.endsWith('_server.dart')).forEach((file) { + final vmPath = file.path.replaceAll('_server.dart', '_server_vm.dart'); + File(vmPath).writeAsStringSync(formatter.format(vm.replaceAll( + '', file.uri.pathSegments.last))); + + final webPath = file.path.replaceAll('_server.dart', '_server_web.dart'); + File(webPath).writeAsStringSync(formatter.format(web.replaceAll( + '', file.uri.pathSegments.last))); + }); +} diff --git a/pkgs/web_socket_conformance_tests/example/web_socket_conformance_tests_example.dart b/pkgs/web_socket_conformance_tests/example/web_socket_conformance_tests_example.dart new file mode 100644 index 0000000000..1ecd81f114 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/example/web_socket_conformance_tests_example.dart @@ -0,0 +1,6 @@ +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; + +void main() { + var awesome = Awesome(); + print('awesome: ${awesome.isAwesome}'); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_local_server.dart b/pkgs/web_socket_conformance_tests/lib/src/close_local_server.dart new file mode 100644 index 0000000000..8ca6a79fcc --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_server.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an WebSocket server that echos the payload of the request. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - echoes the request payload +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + late HttpServer server; + + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + final webSocket = await WebSocketTransformer.upgrade( + request, + ); + + webSocket.listen((event) { + channel.sink.add(event); + }, onDone: () { + webSocket.close(4123, 'server closed the connection'); + channel.sink.add(webSocket.closeCode); + channel.sink.add(webSocket.closeReason); + }); + }); + + channel.sink.add(server.port); + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_local_server_vm.dart b/pkgs/web_socket_conformance_tests/lib/src/close_local_server_vm.dart new file mode 100644 index 0000000000..c0d0652326 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_server_vm.dart @@ -0,0 +1,12 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'close_local_server.dart'; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_local_server_web.dart b/pkgs/web_socket_conformance_tests/lib/src/close_local_server_web.dart new file mode 100644 index 0000000000..f7bb3810a6 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_server_web.dart @@ -0,0 +1,9 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'web_socket_conformance_tests/src/close_local_server.dart')); diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart new file mode 100644 index 0000000000..e9769ca47d --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2023, 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. + +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; +import 'package:websocket/websocket.dart'; + +import 'close_local_server_vm.dart' + if (dart.library.html) 'close_server_web.dart'; + +/// Tests that the [WebSocketChannel] can correctly transmit and receive text +/// and binary payloads. +void testLocalClose( + Future Function(Uri uri, {Iterable? protocols}) + channelFactory) { + group('local close', () { + late Uri uri; + late StreamChannel httpServerChannel; + late StreamQueue httpServerQueue; + + setUp(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + uri = Uri.parse('ws://localhost:${await httpServerQueue.next}'); + }); + tearDown(() async { + httpServerChannel.sink.add(null); +// await httpServerQueue.next; + }); +/* + test('connected', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + expect(channel.closeCode, null); + expect(channel.closeReason, null); + }); +*/ + // https://websockets.spec.whatwg.org/#eventdef-websocket-close + // Dart will wait up to 5 seconds to get the close code from the server otherwise + // it will use the local close code. + + test('reserved close code', () async { + // If code is present, but is neither an integer equal to 1000 nor an integer in the range 3000 to 4999, inclusive, throw an "InvalidAccessError" DOMException. + // If reasonBytes is longer than 123 bytes, then throw a "SyntaxError" DOMException. + + final channel = await channelFactory(uri); + await expectLater(() => channel.close(1004), throwsA(isA())); + }); + + test('too long close reason', () async { + final channel = await channelFactory(uri); + await expectLater(() => channel.close(3000, 'a'.padLeft(124)), + throwsA(isA())); + }); + + test('close', () async { + final channel = await channelFactory(uri); + + await channel.close(); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; + + expect(closeCode, 1005); + expect(closeReason, ''); + expect(await channel.events.isEmpty, true); + }); + + test('with code 3000', () async { + final channel = await channelFactory(uri); + + await channel.close(3000); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; + + expect(closeCode, 3000); + expect(closeReason, ''); + expect(await channel.events.isEmpty, true); + }); + + test('with code 4999', () async { + final channel = await channelFactory(uri); + + await channel.close(4999); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; + + expect(closeCode, 4999); + expect(closeReason, ''); + expect(await channel.events.isEmpty, true); + }); + + test('with code and reason', () async { + final channel = await channelFactory(uri); + + await channel.close(3000, 'Client initiated closure'); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; + + expect(closeCode, 3000); + expect(closeReason, 'Client initiated closure'); + expect(await channel.events.isEmpty, true); + }); + + test('close after close', () async { + final channel = await channelFactory(uri); + + await channel.close(3000, 'Client initiated closure'); + + expectLater( + () async => await channel.close(3001, 'Client initiated closure'), + throwsA(isA())); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; + + expect(closeCode, 3000); + expect(closeReason, 'Client initiated closure'); + expect(await channel.events.isEmpty, true); + }); + }); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart new file mode 100644 index 0000000000..ae66836d99 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an WebSocket server that echos the payload of the request. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - echoes the request payload +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + late HttpServer server; + + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + final webSocket = await WebSocketTransformer.upgrade( + request, + ); + + webSocket.listen((event) { + channel.sink.add(event); + webSocket.close(4123, 'server closed the connection'); + }); + }); + + channel.sink.add(server.port); + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_remote_server_vm.dart b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server_vm.dart new file mode 100644 index 0000000000..4cc6dba56e --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server_vm.dart @@ -0,0 +1,12 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'close_remote_server.dart'; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_remote_server_web.dart b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server_web.dart new file mode 100644 index 0000000000..6e832bacac --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server_web.dart @@ -0,0 +1,9 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'web_socket_conformance_tests/src/close_remote_server.dart')); diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart new file mode 100644 index 0000000000..daade10793 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart @@ -0,0 +1,312 @@ +// Copyright (c) 2023, 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. + +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; +import 'package:websocket/websocket.dart'; + +import 'close_remote_server_vm.dart' + if (dart.library.html) 'close_remote_server_web.dart'; + +/// Tests that the [WebSocketChannel] can correctly transmit and receive text +/// and binary payloads. +void testRemoteClose( + Future Function(Uri uri, {Iterable? protocols}) + channelFactory) { + group('remote close', () { + late Uri uri; + late StreamChannel httpServerChannel; + late StreamQueue httpServerQueue; + + setUp(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + uri = Uri.parse('ws://localhost:${await httpServerQueue.next}'); + }); + tearDown(() async { + httpServerChannel.sink.add(null); +// await httpServerQueue.next; + }); +/* + test('connected', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + expect(channel.closeCode, null); + expect(channel.closeReason, null); + }); +*/ + // https://websockets.spec.whatwg.org/#eventdef-websocket-close + // Dart will wait up to 5 seconds to get the close code from the server otherwise + // it will use the local close code. + +/* + test('reserved close code', () async { + // If code is present, but is neither an integer equal to 1000 nor an integer in the range 3000 to 4999, inclusive, throw an "InvalidAccessError" DOMException. + // If reasonBytes is longer than 123 bytes, then throw a "SyntaxError" DOMException. + + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + expect(channel.closeCode, null); + expect(channel.closeReason, null); + // web uncaught // InvalidAccessError + // sync WebSocketException + await channel.sink.close(1004, 'boom'); + }); + + test('too long close reason', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + expect(channel.closeCode, null); + expect(channel.closeReason, null); + // web uncaught // SyntaxError + // vm: passes! + await channel.sink.close(1000, 'Boom'.padLeft(1000)); + }); +*/ + test('with code and reason', () async { + final channel = await channelFactory(uri); + + channel.sendText('Please close'); + expect(await channel.events.toList(), + [CloseReceived(4123, 'server closed the connection')]); + }); + + test('send after close', () async { + final channel = await channelFactory(uri); + + channel.sendText('Please close'); + expect(await channel.events.toList(), + [CloseReceived(4123, 'server closed the connection')]); + expect(() => channel.sendText('test'), throwsStateError); +/* + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; + expect(closeCode, 3000); + expect(closeReason, 'Client initiated closure');*/ + }); +/* + test('cancel', () async { + final channel = + channelFactory(uri.replace(queryParameters: {'sleep': '5'})); + + var sinkDoneComplete = false; + var sinkDoneOnError = false; + var streamOnData = false; + var streamOnDone = false; + var streamOnError = false; + + channel.sink.done.then((_) { + sinkDoneComplete = true; + }, onError: (_) { + sinkDoneOnError = true; + }); + + final streamSubscription = channel.stream.listen((_) { + streamOnData = true; + }, onError: (_) { + streamOnError = true; + }, onDone: () { + streamOnDone = true; + }); + + await expectLater(channel.ready, completes); + // VM: Cancels subscription to the socket, which means that this deadlocks. + await streamSubscription.cancel(); + expect(() => channel.stream.listen((_) {}), throwsStateError); + channel.sink.add('add after stream closed'); + + expect(channel.closeCode, null); + expect(channel.closeReason, null); + + expect(sinkDoneComplete, false); + expect(sinkDoneOnError, false); + expect(streamOnData, false); + expect(streamOnDone, false); + expect(streamOnError, false); + + await channel.sink.done; + expect(await httpServerQueue.next, 'add after stream closed'); + expect(await httpServerQueue.next, null); + expect(await httpServerQueue.next, null); + expect(channel.closeCode, 4123); + expect(channel.closeReason, 'server closed the connection'); + // cancelling should close according to lassa! + }, skip: _isVM); + + test('cancel - client close', () async { + final channel = + channelFactory(uri.replace(queryParameters: {'sleep': '5'})); + + var sinkDoneComplete = false; + var sinkDoneOnError = false; + var streamOnData = false; + var streamOnDone = false; + var streamOnError = false; + + channel.sink.done.then((_) { + sinkDoneComplete = true; + }, onError: (_) { + sinkDoneOnError = true; + }); + + final streamSubscription = channel.stream.listen((_) { + streamOnData = true; + }, onError: (_) { + streamOnError = true; + }, onDone: () { + streamOnDone = true; + }); + + await expectLater(channel.ready, completes); + await streamSubscription.cancel(); + expect(() => channel.stream.listen((_) {}), throwsStateError); + channel.sink.add('add after stream closed'); + + expect(channel.closeCode, null); + expect(channel.closeReason, null); + + expect(sinkDoneComplete, false); + expect(sinkDoneOnError, false); + expect(streamOnData, false); + expect(streamOnDone, false); + expect(streamOnError, false); + + await channel.sink.close(4444, 'client closed the connection'); + expect(await httpServerQueue.next, 'add after stream closed'); + expect(await httpServerQueue.next, 4444); + expect(await httpServerQueue.next, 'client closed the connection'); + expect(channel.closeCode, 4444); + expect(channel.closeReason, 'client closed the connection'); + }); + + test('client initiated', () async { + final channel = channelFactory(uri); + + var sinkDoneComplete = false; + var sinkDoneOnError = false; + var streamOnData = false; + var streamOnDone = false; + var streamOnError = false; + + channel.sink.done.then((_) { + sinkDoneComplete = true; + }, onError: (_) { + sinkDoneOnError = true; + }); + + channel.stream.listen((_) { + streamOnData = true; + }, onError: (_) { + streamOnError = true; + }, onDone: () { + streamOnDone = true; + }); + + await expectLater(channel.ready, completes); + await channel.sink.close(4444, 'client closed the connection'); + expect(channel.closeCode, null); // VM 4123 + expect(channel.closeReason, null); // VM 'server closed the connection' + + expect(await httpServerQueue.next, 4444); // VM 4123 + expect(await httpServerQueue.next, + 'client closed the connection'); // VM 'server closed the connection' + expect(channel.closeCode, 4123); + expect(channel.closeReason, 'server closed the connection'); + expect(() => channel.sink.add('add after connection closed'), + throwsStateError); + + expect(sinkDoneComplete, true); + expect(sinkDoneOnError, false); + expect(streamOnData, false); + expect(streamOnDone, true); + expect(streamOnError, false); + }, skip: _isVM); + + test('client initiated - slow server', () async { + final channel = + channelFactory(uri.replace(queryParameters: {'sleep': '5'})); + + var sinkDoneComplete = false; + var sinkDoneOnError = false; + var streamOnData = false; + var streamOnDone = false; + var streamOnError = false; + + channel.sink.done.then((_) { + sinkDoneComplete = true; + }, onError: (_) { + sinkDoneOnError = true; + }); + + channel.stream.listen((_) { + streamOnData = true; + }, onError: (_) { + streamOnError = true; + }, onDone: () { + streamOnDone = true; + }); + + await expectLater(channel.ready, completes); + await channel.sink.close(4444, 'client closed the connection'); + expect(channel.closeCode, null); + expect(channel.closeReason, null); + + expect(await httpServerQueue.next, 4444); + expect(await httpServerQueue.next, 'client closed the connection'); + expect(channel.closeCode, 4444); // VM: null - sometimes null + expect(channel.closeReason, 'client closed the connection'); // VM: null + expect(() => channel.sink.add('add after connection closed'), + throwsStateError); + await channel.sink.close(); + + expect(sinkDoneComplete, true); + expect(sinkDoneOnError, false); + expect(streamOnData, false); + expect(streamOnDone, true); + expect(streamOnError, false); + }); + + test('server initiated', () async { + final channel = channelFactory(uri); + + var sinkDoneComplete = false; + var sinkDoneOnError = false; + var streamOnData = false; + var streamOnDone = false; + var streamOnError = false; + + channel.sink.done.then((_) { + sinkDoneComplete = true; + }, onError: (_) { + sinkDoneOnError = true; + }); + + final streamListen = channel.stream.listen((_) { + streamOnData = true; + }, onError: (_) { + streamOnError = true; + }).asFuture(); + + await expectLater(channel.ready, completes); + await streamListen; + + expect(channel.closeCode, 4123); + expect(channel.closeReason, 'server closed the connection'); + channel.sink.add('add after connection closed'); + await channel.sink.close(); + + expect(sinkDoneComplete, true); + expect(sinkDoneOnError, false); + expect(streamOnData, false); + expect(streamOnError, false); + }); + */ + }); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server.dart b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server.dart new file mode 100644 index 0000000000..1ab0e19265 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import "package:crypto/crypto.dart"; +import 'package:stream_channel/stream_channel.dart'; + +const WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +/// Starts an WebSocket server that echos the payload of the request. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - echoes the request payload +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + late final HttpServer server; + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + var key = request.headers.value('Sec-WebSocket-Key'); + var digest = sha1.convert("$key$WEB_SOCKET_GUID".codeUnits); + var accept = base64.encode(digest.bytes); + request.response + ..statusCode = HttpStatus.switchingProtocols + ..headers.add(HttpHeaders.connectionHeader, "Upgrade") + ..headers.add(HttpHeaders.upgradeHeader, "websocket") + ..headers.add("Sec-WebSocket-Accept", accept); + request.response.contentLength = 0; +// await request.response.close(); + final socket = await request.response.detachSocket(); +// final websocket = WebSocket.fromUpgradedSocket(socket, serverSide: true); +// websocket.listen((x) => print('server received: $x')); + socket.destroy(); + print('socket is closed'); + }); + + channel.sink.add(server.port); + + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_vm.dart b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_vm.dart new file mode 100644 index 0000000000..0bc7426239 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_vm.dart @@ -0,0 +1,12 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'disconnect_after_upgrade_server.dart'; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_web.dart b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_web.dart new file mode 100644 index 0000000000..9e1a13771f --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_web.dart @@ -0,0 +1,10 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: + 'web_socket_conformance_tests/src/disconnect_after_upgrade_server.dart')); diff --git a/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart new file mode 100644 index 0000000000..f114205bf6 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2023, 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. + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; +import 'package:websocket/websocket.dart'; + +import 'disconnect_after_upgrade_server_vm.dart' + if (dart.library.html) 'disconnect_after_upgrade_server_web.dart'; + +/// Tests that the [WebSocketChannel] can correctly transmit and receive text +/// and binary payloads. +void testDisconnectAfterUpgrade( + Future Function(Uri uri, {Iterable? protocols}) + channelFactory) { + group('disconnect', () { + late final Uri uri; + late final StreamChannel httpServerChannel; + late final StreamQueue httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + uri = Uri.parse('ws://localhost:${await httpServerQueue.next}'); + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('disconnect after upgrade', () async { + final channel = await channelFactory(uri); + channel.sendText('test'); + expect( + (await channel.events.single as CloseReceived).code, + anyOf([ + 1005, // closed no status + 1006, // closed abnormal + ])); + }); + }); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/echo_server.dart b/pkgs/web_socket_conformance_tests/lib/src/echo_server.dart new file mode 100644 index 0000000000..1d7c14db1d --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/echo_server.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an WebSocket server that echos the payload of the request. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - echoes the request payload +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + late HttpServer server; + + server = (await HttpServer.bind('localhost', 0)) + ..transform(WebSocketTransformer()) + .listen((WebSocket webSocket) => webSocket.listen(webSocket.add)); + + channel.sink.add(server.port); + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/echo_server_vm.dart b/pkgs/web_socket_conformance_tests/lib/src/echo_server_vm.dart new file mode 100644 index 0000000000..a589cc0d1c --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/echo_server_vm.dart @@ -0,0 +1,12 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'echo_server.dart'; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/echo_server_web.dart b/pkgs/web_socket_conformance_tests/lib/src/echo_server_web.dart new file mode 100644 index 0000000000..b553554f69 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/echo_server_web.dart @@ -0,0 +1,9 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'web_socket_conformance_tests/src/echo_server.dart')); diff --git a/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server.dart b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server.dart new file mode 100644 index 0000000000..f32c025d24 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import "package:crypto/crypto.dart"; +import 'package:stream_channel/stream_channel.dart'; + +const WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +/// Starts an WebSocket server that echos the payload of the request. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - echoes the request payload +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + final server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + request.response.statusCode = 200; + request.response.close(); + }); + channel.sink.add(server.port); + + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_vm.dart b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_vm.dart new file mode 100644 index 0000000000..7f8cd5cf5a --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_vm.dart @@ -0,0 +1,12 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'no_upgrade_server.dart'; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_web.dart b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_web.dart new file mode 100644 index 0000000000..97409bc34a --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_web.dart @@ -0,0 +1,9 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'web_socket_conformance_tests/src/no_upgrade_server.dart')); diff --git a/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_tests.dart new file mode 100644 index 0000000000..e3ff30da04 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_tests.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2023, 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. + +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; +import 'package:websocket/websocket.dart'; + +import 'no_upgrade_server_vm.dart' + if (dart.library.html) 'no_upgrade_server_web.dart'; + +/// Tests that the [WebSocketChannel] can correctly transmit and receive text +/// and binary payloads. +void testNoUpgrade( + Future Function(Uri uri, {Iterable? protocols}) + channelFactory) { + group('no upgrade', () { + late final Uri uri; + late final StreamChannel httpServerChannel; + late final StreamQueue httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + uri = Uri.parse('ws://localhost:${await httpServerQueue.next}'); + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('close before upgrade', () async { + await expectLater( + () => channelFactory(uri), throwsA(isA())); + }); + }); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart new file mode 100644 index 0000000000..70245459ce --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart @@ -0,0 +1,207 @@ +// Copyright (c) 2023, 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. + +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; +import 'package:websocket/websocket.dart'; + +import 'echo_server_vm.dart' if (dart.library.html) 'echo_server_web.dart'; + +/// Tests that the [WebSocketChannel] can correctly transmit and receive text +/// and binary payloads. +void testPayloadTransfer( + Future Function(Uri uri, {Iterable? protocols}) + channelFactory) { + group('payload transfer', () { + late final Uri uri; + late final StreamChannel httpServerChannel; + late final StreamQueue httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + uri = Uri.parse('ws://localhost:${await httpServerQueue.next}'); + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('close immediately', () async { + final channel = await channelFactory(uri); + + await channel.close(); + print('closed!'); + expect(await channel.events.isEmpty, + true); // Stream can't be listened to at this point. + }); + + test('empty string request and response', () async { + final channel = await channelFactory(uri); + + channel.sendText(''); + expect(await channel.events.first, TextDataReceived('')); + }); + + test('empty binary request and response', () async { + final channel = await channelFactory(uri); + + channel.sendBytes(Uint8List(0)); + expect(await channel.events.first, BinaryDataReceived(Uint8List(0))); + }); + + test('string request and response', () async { + final channel = await channelFactory(uri); + + channel.sendText("Hello World!"); + expect(await channel.events.first, TextDataReceived("Hello World!")); + }); + + test('binary request and response', () async { + final channel = await channelFactory(uri); + + channel.sendBytes(Uint8List.fromList([1, 2, 3, 4, 5])); + expect(await channel.events.first, + BinaryDataReceived(Uint8List.fromList([1, 2, 3, 4, 5]))); + }); + + test('large string request and response', () async { + final channel = await channelFactory(uri); + + channel.sendText("Hello World!" * 1000); + expect( + await channel.events.first, TextDataReceived("Hello World!" * 1000)); + }); + + test('large binary request and response - XXX', () async { + final channel = await channelFactory(uri); + + channel.sendBytes(Uint8List.fromList([1, 2, 3, 4, 5])); + expect(await channel.events.first, + BinaryDataReceived(Uint8List.fromList([1, 2, 3, 4, 5]))); + }); +/* + */ +/* + test('List request and response', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + channel.sink.add([1, 2, 3, 4, 5]); + expect(await channel.stream.first, [1, 2, 3, 4, 5]); + }, skip: _isWeb); + + test('List with >255 value', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + expect(() => channel.sink.add([1, 2, 256, 4, 5]), throwsArgumentError); + }, skip: _isWeb || _isVM); + + test('List with <0 value', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + expect(() => channel.sink.add([1, 2, 256, 4, 5]), throwsArgumentError); + }, skip: _isWeb || _isVM); + + test('Uint8List request and response', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + channel.sink.add(Uint8List.fromList([1, 2, 3, 4, 5])); + expect(await channel.stream.first, [1, 2, 3, 4, 5]); + }); + + test('duration request and response', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + expect(() => channel.sink.add(const Duration(seconds: 5)), + throwsArgumentError); + }, skip: _isWeb || _isVM); + + test('error added to sink', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + expect(() => channel.sink.addError(Exception('what should this do?')), + throwsUnsupportedError); + await channel.sink.close(); + expect(channel.stream.isEmpty, true); + }, skip: _isWeb || _isVM); + + test('add after error', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + expect(() => channel.sink.addError(Exception('what should this do?')), + throwsUnsupportedError); + + channel.sink.add('Hello World!'); + expect(await channel.stream.first, 'Hello World!'); + }, skip: _isWeb || _isVM); + + test('alternative string and binary request and response', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + channel.sink.add('Hello '); + channel.sink.add([1, 2, 3]); + channel.sink.add('World!'); + channel.sink.add([4, 5]); + + expect(await channel.stream.take(4).toList(), [ + 'Hello ', + [1, 2, 3], + 'World!', + [4, 5] + ]); + }, skip: _isWeb); + + test('increasing payload string size', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + final s = StringBuffer('Hello World\n'); + channel.sink.add(s.toString()); + await for (final response in channel.stream) { + expect(response, s.toString()); + if (s.length >= 10000) { + await channel.sink.close(); + break; + } + s.writeln('HelloWorld'); + channel.sink.add(s.toString()); + } + }); + + test('increasing payload binary size', () async { + final channel = channelFactory(uri); + + await expectLater(channel.ready, completes); + + final data = [1, 2, 3, 4, 5]; + channel.sink.add(data); + await for (final response in channel.stream) { + expect(response, data); + if (data.length >= 10000) { + await channel.sink.close(); + break; + } + data.addAll([1, 2, 3, 4, 5]); + channel.sink.add(data); + } + }); + */ + }); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server.dart b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server.dart new file mode 100644 index 0000000000..0a41b357f6 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2023, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import "package:crypto/crypto.dart"; +import 'package:stream_channel/stream_channel.dart'; + +const WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +/// Starts an WebSocket server that echos the payload of the request. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - echoes the request payload +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + late final HttpServer server; + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + var key = request.headers.value('Sec-WebSocket-Key'); + var digest = sha1.convert("$key$WEB_SOCKET_GUID".codeUnits); + var accept = base64.encode(digest.bytes); + request.response + ..statusCode = HttpStatus.switchingProtocols + ..headers.add(HttpHeaders.connectionHeader, "Upgrade") + ..headers.add(HttpHeaders.upgradeHeader, "websocket") + ..headers.add("Sec-WebSocket-Accept", accept); + request.response.contentLength = 0; +// await request.response.close(); + final socket = await request.response.detachSocket(); +// socket.write('\r\n'); +// socket.write('\r\n'); +// final websocket = WebSocket.fromUpgradedSocket(socket, serverSide: true); +// websocket.listen((x) => print('server received: $x')); + socket.write('marry had a little lamb whose fleece was white as snow'); + }); + + channel.sink.add(server.port); + + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_vm.dart b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_vm.dart new file mode 100644 index 0000000000..4996e3b6c2 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_vm.dart @@ -0,0 +1,12 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'peer_protocol_errors_server.dart'; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_web.dart b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_web.dart new file mode 100644 index 0000000000..361b02c30f --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_web.dart @@ -0,0 +1,9 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'web_socket_conformance_tests/src/peer_protocol_errors_server.dart')); diff --git a/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart new file mode 100644 index 0000000000..fd6abd7e2a --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2023, 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. + +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; +import 'package:websocket/websocket.dart'; + +import 'peer_protocol_errors_server_vm.dart' + if (dart.library.html) 'peer_protocol_errors_server_web.dart'; + +/// Tests that the [WebSocketChannel] can correctly transmit and receive text +/// and binary payloads. +void testPeerProtocolErrors( + Future Function(Uri uri, {Iterable? protocols}) + channelFactory) { + group('protocol errors', () { + late final Uri uri; + late final StreamChannel httpServerChannel; + late final StreamQueue httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + uri = Uri.parse('ws://localhost:${await httpServerQueue.next}'); + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('bad data after upgrade', () async { + final channel = await channelFactory(uri); + expect( + (await channel.events.single as CloseReceived).code, + anyOf([ + 1002, // protocol error + 1005, // closed no status + 1006, // closed abnormal + ])); + }); + + test('bad data after upgrade with write', () async { + final channel = await channelFactory(uri); + channel.sendText('test'); + expect( + (await channel.events.single as CloseReceived).code, + anyOf([ + 1002, // protocol error + 1005, // closed no status + 1006, // closed abnormal + ])); + }); + }); +} diff --git a/pkgs/web_socket_conformance_tests/lib/web_socket_conformance_tests.dart b/pkgs/web_socket_conformance_tests/lib/web_socket_conformance_tests.dart new file mode 100644 index 0000000000..b51507ca73 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/web_socket_conformance_tests.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2022, 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. + +import 'package:web_socket_conformance_tests/src/disconnect_after_upgrade_tests.dart'; +import 'package:websocket/websocket.dart'; + +// import 'src/failure_tests.dart'; +// import 'src/close_tests.dart'; +import 'src/close_local_tests.dart'; +import 'src/close_remote_tests.dart'; +import 'src/no_upgrade_tests.dart'; +import 'src/payload_transfer_tests.dart'; +import 'src/peer_protocol_errors_tests.dart'; + +// import 'src/protocol_tests.dart'; + +/// Runs the entire test suite against the given [WebSocketChannel]. +void testAll( + Future Function(Uri uri, {Iterable? protocols}) + webSocketFactory) { + testPayloadTransfer(webSocketFactory); + testLocalClose(webSocketFactory); + testRemoteClose(webSocketFactory); +// testProtocols(channelFactory); + testNoUpgrade(webSocketFactory); + testDisconnectAfterUpgrade(webSocketFactory); + testPeerProtocolErrors(webSocketFactory); +} diff --git a/pkgs/web_socket_conformance_tests/pubspec.yaml b/pkgs/web_socket_conformance_tests/pubspec.yaml new file mode 100644 index 0000000000..7c4cbca4b8 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/pubspec.yaml @@ -0,0 +1,19 @@ +name: web_socket_conformance_tests +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo +publish_to: none + +environment: + sdk: ^3.2.4 + +# Add regular dependencies here. +dependencies: + async: ^2.11.0 + dart_style: ^2.3.4 + stream_channel: ^2.1.2 + test: ^1.24.0 + websocket: + path: ../websocket +dev_dependencies: + lints: ^2.1.0 diff --git a/pkgs/web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart b/pkgs/web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart new file mode 100644 index 0000000000..3051b24544 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart @@ -0,0 +1,16 @@ +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + final awesome = Awesome(); + + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () { + expect(awesome.isAwesome, isTrue); + }); + }); +} diff --git a/pkgs/websocket/.gitignore b/pkgs/websocket/.gitignore new file mode 100644 index 0000000000..3cceda5578 --- /dev/null +++ b/pkgs/websocket/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/pkgs/websocket/CHANGELOG.md b/pkgs/websocket/CHANGELOG.md new file mode 100644 index 0000000000..effe43c82c --- /dev/null +++ b/pkgs/websocket/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/pkgs/websocket/README.md b/pkgs/websocket/README.md new file mode 100644 index 0000000000..8b55e735b5 --- /dev/null +++ b/pkgs/websocket/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/pkgs/websocket/analysis_options.yaml b/pkgs/websocket/analysis_options.yaml new file mode 100644 index 0000000000..dee8927aaf --- /dev/null +++ b/pkgs/websocket/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/pkgs/websocket/example/websocket_example.dart b/pkgs/websocket/example/websocket_example.dart new file mode 100644 index 0000000000..f88b524d39 --- /dev/null +++ b/pkgs/websocket/example/websocket_example.dart @@ -0,0 +1,4 @@ +import 'package:websocket/iowebsocket.dart'; +import 'package:websocket/websocket.dart'; + +void main() {} diff --git a/pkgs/websocket/lib/htmlwebsocket.dart b/pkgs/websocket/lib/htmlwebsocket.dart new file mode 100644 index 0000000000..17ab4b1ba8 --- /dev/null +++ b/pkgs/websocket/lib/htmlwebsocket.dart @@ -0,0 +1,132 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:typed_data'; +import 'package:web/web.dart' as web; +import 'package:websocket/websocket.dart'; +import 'package:web/helpers.dart' as helpers; + +class HtmlWebSocket implements XXXWebSocket { + final helpers.WebSocket _webSocket; + final _events = StreamController(); + + static Future connect(Uri url) async { + final socket = helpers.WebSocket(url.toString()) + ..binaryType = "arraybuffer"; + final htmlSocket = HtmlWebSocket._(socket); + final readyCompleter = Completer(); + + if (socket.readyState == helpers.WebSocket.OPEN) { + readyCompleter.complete(); + } else { + if (socket.readyState == helpers.WebSocket.CLOSING || + socket.readyState == helpers.WebSocket.CLOSED) { + readyCompleter.completeError(XXXWebSocketException( + 'WebSocket state error: ${socket.readyState}')); + } else { + // The socket API guarantees that only a single open event will be + // emitted. + socket.onOpen.first.then((_) { + readyCompleter.complete(); + }); + } + } + + socket.onError.first.then((e) { + print('I GOT A REAL ERROR!: $e'); + // Unfortunately, the underlying WebSocket API doesn't expose any + // specific information about the error itself. + final error = XXXWebSocketException('WebSocket connection failed.'); + if (!readyCompleter.isCompleted) { + readyCompleter.completeError(error); + } else { + htmlSocket._closed(1006, 'error'); + } + }); + + socket.onMessage.listen((e) { + final eventData = e.data!; + late WebSocketEvent data; + if (eventData.typeofEquals('string')) { + data = TextDataReceived((eventData as JSString).toDart); + } else if (eventData.typeofEquals('object') && + (eventData as JSObject).instanceOfString('ArrayBuffer')) { + data = BinaryDataReceived( + (eventData as JSArrayBuffer).toDart.asUint8List()); + } else { + throw Exception('test'); + } + htmlSocket._events.add(data); + }); + + socket.onClose.first.then((event) { + if (!readyCompleter.isCompleted) { + readyCompleter.complete(); + } + + htmlSocket._closed(event.code, event.reason); + }); + + await readyCompleter.future; + return htmlSocket; + } + + void _closed(int? code, String? reason) { + print('closing with $code, $reason'); + if (!_events.isClosed) { + _events + ..add(CloseReceived(code, reason)) + ..close(); + } + } + + HtmlWebSocket._(this._webSocket); + + @override + void sendBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + // Silently discards the data if the connection is closed. + _webSocket.send(b.jsify()!); + } + + @override + void sendText(String s) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + // Silently discards the data if the connection is closed. + _webSocket.send(s.jsify()!); + } + + /// Closes the stream. + /// https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + /// Cannot send more data after this. + + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) It SHOULD do so as soon as practical. An + // endpoint MAY delay sending a Close frame until its current message is + // sent (for instance, if the majority of a fragmented message is + // already sent, an endpoint MAY send the remaining fragments before + // sending a Close frame). However, there is no guarantee that the + // endpoint that has already sent a Close frame will continue to process + // data. + @override + Future close([int? code, String? reason]) async { + if (!_events.isClosed) { + unawaited(_events.close()); + if ((code, reason) case (final closeCode?, final closeReason?)) { + _webSocket.close(closeCode, closeReason); + } else if (code case final closeCode?) { + _webSocket.close(closeCode); + } else { + _webSocket.close(); + } + } + } + + @override + Stream get events => _events.stream; +} diff --git a/pkgs/websocket/lib/iowebsocket.dart b/pkgs/websocket/lib/iowebsocket.dart new file mode 100644 index 0000000000..50939bdb96 --- /dev/null +++ b/pkgs/websocket/lib/iowebsocket.dart @@ -0,0 +1,106 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; +import 'dart:typed_data'; +import 'package:websocket/websocket.dart'; + +class IOWebSocket implements XXXWebSocket { + final io.WebSocket _webSocket; + final _events = StreamController(); + + static Future connect(Uri uri) async { + try { + final webSocket = await io.WebSocket.connect(uri.toString()); + return IOWebSocket._(webSocket); + } on io.WebSocketException catch (e) { + print(e.message); + throw XXXWebSocketException(e.message); + } + } + + IOWebSocket._(this._webSocket) { + _webSocket.listen( + (event) { + print('event: $event'); + switch (event) { + case String e: + _events.add(TextDataReceived(e)); + case List e: + _events.add(BinaryDataReceived(Uint8List.fromList(e))); + } + }, + onError: (e, st) { + final wse = switch (e) { + io.WebSocketException(message: final message) => + XXXWebSocketException(message), + _ => XXXWebSocketException(), + }; + _events.addError(wse, st); + }, + onDone: () { + print('onDone'); + if (!_events.isClosed) { + _events.add(CloseReceived( + _webSocket.closeCode, _webSocket.closeReason ?? "")); + _events.close(); + } + }, + ); + } + + // JS: Silently discards data if connection is closed. + @override + void sendBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _webSocket.add(b); + } + + @override + void sendText(String s) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _webSocket.add(s); + } + + /// Closes the stream. + /// https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + /// Cannot send more data after this. + + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) It SHOULD do so as soon as practical. An + // endpoint MAY delay sending a Close frame until its current message is + // sent (for instance, if the majority of a fragmented message is + // already sent, an endpoint MAY send the remaining fragments before + // sending a Close frame). However, there is no guarantee that the + // endpoint that has already sent a Close frame will continue to process + // data. + @override + Future close([int? code, String? reason]) async { + if (_events.isClosed) { + throw XXXWebSocketConnectionClosed(); + } + + if (code != null) { + RangeError.checkValueInInterval(code, 3000, 4999, 'code'); + } + if (reason != null && utf8.encode(reason).length > 123) { + throw ArgumentError.value(reason, "reason", + "reason must be <= 123 bytes long when encoded as UTF-8"); + } + + unawaited(_events.close()); + try { + await _webSocket.close(code, reason); + } on io.WebSocketException catch (e) { + throw XXXWebSocketException(e.message); + } + } + + @override + Stream get events => _events.stream; +} diff --git a/pkgs/websocket/lib/src/websocket.dart b/pkgs/websocket/lib/src/websocket.dart new file mode 100644 index 0000000000..e8a6f15901 --- /dev/null +++ b/pkgs/websocket/lib/src/websocket.dart @@ -0,0 +1,6 @@ +// TODO: Put public facing types in this file. + +/// Checks if you are awesome. Spoiler: you are. +class Awesome { + bool get isAwesome => true; +} diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart new file mode 100644 index 0000000000..8c725fbea4 --- /dev/null +++ b/pkgs/websocket/lib/websocket.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'dart:typed_data'; + +/// An event received from the peer through the [XXXWebSocket]. +sealed class WebSocketEvent {} + +/// Text data received from the peer through the [XXXWebSocket]. +/// +/// See [XXXWebSocket.events]. +final class TextDataReceived extends WebSocketEvent { + final String text; + TextDataReceived(this.text); + + @override + bool operator ==(Object other) => + other is TextDataReceived && other.text == text; + + @override + int get hashCode => text.hashCode; +} + +/// Binary data received from the peer through the [XXXWebSocket]. +/// +/// See [XXXWebSocket.events]. +final class BinaryDataReceived extends WebSocketEvent { + final Uint8List data; + BinaryDataReceived(this.data); + + @override + bool operator ==(Object other) { + if (other is BinaryDataReceived && other.data.length == data.length) { + for (var i = 0; i < data.length; ++i) { + if (other.data[i] != data[i]) return false; + } + return true; + } + return false; + } + + @override + int get hashCode => data.hashCode; + + @override + String toString() => 'BinaryDataReceived($data)'; +} + +/// A close notification (Close frame) received from the peer through the +/// [XXXWebSocket] or a failure indication. +/// +/// See [XXXWebSocket.events]. +final class CloseReceived extends WebSocketEvent { + /// A numerical code indicating the reason why the WebSocket was closed. + /// + /// See [RFC-6455 7.4](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4) + /// for guidance on how to interpret these codes. + final int? code; + + /// A textual explanation of the reason why the WebSocket was closed. + /// + /// Will be empty if the peer did not specify a reason. + final String reason; + + CloseReceived([this.code, this.reason = ""]); + + @override + bool operator ==(Object other) => + other is CloseReceived && other.code == code && other.reason == reason; + + @override + int get hashCode => [code, reason].hashCode; + + @override + String toString() => 'CloseReceived($code, $reason)'; +} + +class XXXWebSocketException implements Exception { + final String message; + XXXWebSocketException([this.message = ""]); +} + +/// Thrown if [XXXWebSocket.sendText], [XXXWebSocket.sendBytes], or +/// [XXXWebSocket.closed] is called when the [XXXWebSocket] is closed. +class XXXWebSocketConnectionClosed extends XXXWebSocketException { + XXXWebSocketConnectionClosed([super.message = 'Connection Closed']); +} + +/// The interface for WebSocket connections. +/// +/// TODO: insert a usage example. +/// +/// TODO: thank of a better name, ideally not "WebSocket". Maybe +/// "SimpleWebSocket"? +abstract interface class XXXWebSocket { + /// Sends text data to the connected peer. + /// + /// Throws [XXXWebSocketConnectionClosed] if the [XXXWebSocket] is closed + /// (either through [close] or by the peer). + void sendText(String s); + + /// Sends binary data to the connected peer. + /// + /// Throws [XXXWebSocketConnectionClosed] if the [XXXWebSocket] is closed + /// (either through [close] or by the peer). + void sendBytes(Uint8List b); + + /// Closes the WebSocket connection and the [events] `Stream`. + /// + /// Sends a Close frame to the peer. If the optional [code] and [reason] + /// arguments are given, they will be included in the Close frame. If no + /// [code] is set then the peer will see a 1005 status code. If no [reason] + /// is set then the peer will not receive a reason string. + /// + /// Throws a [RangeError] if [code] is not in the range 3000-4999. + /// + /// Throws an [ArgumentError] if [reason] is longer than 123 bytes when + /// encoded as UTF-8 + /// + /// Throws [XXXWebSocketConnectionClosed] if the connection is already closed + /// (including by the peer). + Future close([int? code, String? reason]); + + /// A [Stream] of [WebSocketEvent] received from the peer. + /// + /// Data received by the peer will be delivered as a [TextDataReceived] or + /// [BinaryDataReceived]. + /// + /// If a [CloseReceived] event is received then the [Stream] will be closed. A + /// [CloseReceived] event indicates either that: + /// + /// - A close frame was received from the peer. [CloseReceived.code] and + /// [CloseReceived.reason] will be set by the peer. + /// - A failure occured (e.g. the peer disconnected). [CloseReceived.code] and + /// [CloseReceived.reason] will be a failure code defined by + /// (RFC-6455)[https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1] + /// (e.g. 1006). + /// + /// Errors will never appear in this [Stream]. + /// + /// TODO: we can't use a SynchronousStreamController here, right? It would be + /// cool if we deliver [CloseReceived] **before** the user sees write failures + /// because [events] is closed. + Stream get events; +} diff --git a/pkgs/websocket/pubspec.yaml b/pkgs/websocket/pubspec.yaml new file mode 100644 index 0000000000..0c0c5f1653 --- /dev/null +++ b/pkgs/websocket/pubspec.yaml @@ -0,0 +1,15 @@ +name: websocket +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.2.4 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 + web_socket_conformance_tests: + path: ../web_socket_conformance_tests +#dependencies: +# web: ^0.4.2 diff --git a/pkgs/websocket/test/htmlwebsocket_test.dart b/pkgs/websocket/test/htmlwebsocket_test.dart new file mode 100644 index 0000000000..3edf704591 --- /dev/null +++ b/pkgs/websocket/test/htmlwebsocket_test.dart @@ -0,0 +1,9 @@ +import 'package:websocket/htmlwebsocket.dart'; +import 'package:websocket/websocket.dart'; +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; + +import 'package:test/test.dart'; + +void main() { + testAll((uri, {protocols}) => HtmlWebSocket.connect(uri)); +} diff --git a/pkgs/websocket/test/iowebsocket_test.dart b/pkgs/websocket/test/iowebsocket_test.dart new file mode 100644 index 0000000000..425c1e0e64 --- /dev/null +++ b/pkgs/websocket/test/iowebsocket_test.dart @@ -0,0 +1,9 @@ +import 'package:websocket/iowebsocket.dart'; +import 'package:websocket/websocket.dart'; +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; + +import 'package:test/test.dart'; + +void main() { + testAll((uri, {protocols}) => IOWebSocket.connect(uri)); +}