From 6cefc58a1e4aa0889a98add225f5cd14f92fb30f Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 24 Jan 2024 12:44:04 -0800 Subject: [PATCH 1/9] WebSocket --- pkgs/cronet_http/.idea/.gitignore | 3 + pkgs/cronet_http/.idea/vcs.xml | 6 + pkgs/websocket/.gitignore | 7 + pkgs/websocket/CHANGELOG.md | 3 + pkgs/websocket/README.md | 39 ++++++ pkgs/websocket/analysis_options.yaml | 30 +++++ pkgs/websocket/example/websocket_example.dart | 6 + pkgs/websocket/lib/src/websocket.dart | 6 + pkgs/websocket/lib/websocket.dart | 120 ++++++++++++++++++ pkgs/websocket/pubspec.yaml | 16 +++ pkgs/websocket/test/websocket_test.dart | 16 +++ 11 files changed, 252 insertions(+) create mode 100644 pkgs/cronet_http/.idea/.gitignore create mode 100644 pkgs/cronet_http/.idea/vcs.xml create mode 100644 pkgs/websocket/.gitignore create mode 100644 pkgs/websocket/CHANGELOG.md create mode 100644 pkgs/websocket/README.md create mode 100644 pkgs/websocket/analysis_options.yaml create mode 100644 pkgs/websocket/example/websocket_example.dart create mode 100644 pkgs/websocket/lib/src/websocket.dart create mode 100644 pkgs/websocket/lib/websocket.dart create mode 100644 pkgs/websocket/pubspec.yaml create mode 100644 pkgs/websocket/test/websocket_test.dart 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/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..02766ea7da --- /dev/null +++ b/pkgs/websocket/example/websocket_example.dart @@ -0,0 +1,6 @@ +import 'package:websocket/websocket.dart'; + +void main() { + var awesome = Awesome(); + print('awesome: ${awesome.isAwesome}'); +} 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..e26550efbe --- /dev/null +++ b/pkgs/websocket/lib/websocket.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:io' as io; + +import 'package:cupertino_http/cupertino_http.dart'; + +sealed class WebSocketEvent {} + +class TextDataReceived extends WebSocketEvent { + final String text; + TextDataReceived(this.text); +} + +class BinaryDataReceived extends WebSocketEvent { + final Uint8List data; + BinaryDataReceived(this.data); +} + +class Closed extends WebSocketEvent { + final int? code; + final String? reason; + + Closed([this.code, this.reason]); +} + +abstract interface class WebSocket { + void addString(String s); + void addBytes(Uint8List b); + Future close([int? code, String? reason]); + Stream get events; +} + +class IOWebSocket implements WebSocket { + final io.WebSocket _webSocket; + final _events = StreamController(); + + static Future connect(Uri uri) async { + final webSocket = await io.WebSocket.connect(uri.toString()); + return IOWebSocket._(webSocket); + } + + IOWebSocket._(this._webSocket) { + _webSocket.listen( + (event) {}, + onError: (e, st) {}, + onDone: () {}, + ); + } + + @override + void addBytes(Uint8List b) { + _webSocket.add(b); + } + + @override + void addString(String s) { + _webSocket.add(s); + } + + @override + Future close([int? code, String? reason]) async { + await _webSocket.close(code, reason); + } + + @override + Stream get events => _events.stream; +} + +class CupertinoWebSocket implements WebSocket { + static Future connect(Uri uri) async { + late CupertinoWebSocket webSocket; + final session = URLSession.sessionWithConfiguration( + URLSessionConfiguration.defaultSessionConfiguration(), + onWebSocketTaskClosed: (session, task, closeCode, reason) => + webSocket._closed(closeCode, reason), + ); + final task = session.webSocketTaskWithRequest(URLRequest.fromUrl(uri)) + ..resume(); + webSocket = CupertinoWebSocket._(task); + return webSocket; + } + + final URLSessionWebSocketTask _task; + final _events = StreamController(); + CupertinoWebSocket._(this._task) { + _task.receiveMessage(); + } + + void _closed(int? closeCode, Data? reason) { + _events.add(Closed(closeCode)); // XXX + } + + @override + void addBytes(Uint8List b) { + _task + .sendMessage(URLSessionWebSocketMessage.fromData(Data.fromList(b))) + .then((_) => _, onError: (e, st) => _events.addError(e, st)); + } + + @override + void addString(String s) { + _task + .sendMessage(URLSessionWebSocketMessage.fromString(s)) + .then((_) => _, onError: (e, st) => _events.addError(e, st)); + } + + @override + Future close([int? code, String? reason]) async { + // XXX Wait until all pending writes are done. + if (code != null) { + reason = reason ?? ""; + _task.cancelWithCloseCode(code, Data.fromList(reason.codeUnits)); + } else { + _task.cancel(); + } + } + + @override + Stream get events => _events.stream; +} diff --git a/pkgs/websocket/pubspec.yaml b/pkgs/websocket/pubspec.yaml new file mode 100644 index 0000000000..b103eb7cd7 --- /dev/null +++ b/pkgs/websocket/pubspec.yaml @@ -0,0 +1,16 @@ +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 + +# Add regular dependencies here. +dependencies: + cupertino_http: ^1.3.0 + # path: ^1.8.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/pkgs/websocket/test/websocket_test.dart b/pkgs/websocket/test/websocket_test.dart new file mode 100644 index 0000000000..a77d3c635c --- /dev/null +++ b/pkgs/websocket/test/websocket_test.dart @@ -0,0 +1,16 @@ +import 'package:websocket/websocket.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); + }); + }); +} From f925ef2cb319f7e8b9fd9f5e04cb13f1cc5f2f10 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 30 Jan 2024 13:37:50 -0800 Subject: [PATCH 2/9] First checkin --- .../websocket_conformance_test.dart | 6 + pkgs/cupertino_http/example/pubspec.yaml | 2 + pkgs/cupertino_http/lib/cupertino_http.dart | 1 + pkgs/cupertino_http/lib/src/websocket.dart | 150 +++++++++ pkgs/cupertino_http/pubspec.yaml | 2 + pkgs/web_socket_conformance_tests/.gitignore | 7 + .../web_socket_conformance_tests/CHANGELOG.md | 3 + pkgs/web_socket_conformance_tests/README.md | 39 +++ .../analysis_options.yaml | 30 ++ .../bin/generate_server_wrappers.dart | 51 +++ .../web_socket_conformance_tests_example.dart | 6 + .../lib/src/close_local_server.dart | 41 +++ .../lib/src/close_local_server_vm.dart | 12 + .../lib/src/close_local_server_web.dart | 9 + .../lib/src/close_local_tests.dart | 302 +++++++++++++++++ .../lib/src/close_remote_server.dart | 41 +++ .../lib/src/close_remote_server_vm.dart | 12 + .../lib/src/close_remote_server_web.dart | 9 + .../lib/src/close_remote_tests.dart | 316 ++++++++++++++++++ .../src/disconnect_after_upgrade_server.dart | 48 +++ .../disconnect_after_upgrade_server_vm.dart | 12 + .../disconnect_after_upgrade_server_web.dart | 10 + .../src/disconnect_after_upgrade_tests.dart | 41 +++ .../lib/src/echo_server.dart | 30 ++ .../lib/src/echo_server_vm.dart | 12 + .../lib/src/echo_server_web.dart | 9 + .../lib/src/no_upgrade_server.dart | 33 ++ .../lib/src/no_upgrade_server_vm.dart | 12 + .../lib/src/no_upgrade_server_web.dart | 9 + .../lib/src/no_upgrade_tests.dart | 37 ++ .../lib/src/payload_transfer_tests.dart | 207 ++++++++++++ .../lib/src/peer_protocol_errors_server.dart | 49 +++ .../src/peer_protocol_errors_server_vm.dart | 12 + .../src/peer_protocol_errors_server_web.dart | 9 + .../lib/src/peer_protocol_errors_tests.dart | 44 +++ .../lib/web_socket_conformance_tests.dart | 29 ++ .../web_socket_conformance_tests/pubspec.yaml | 19 ++ .../web_socket_conformance_tests_test.dart} | 2 +- pkgs/websocket/lib/htmlwebsocket.dart | 131 ++++++++ pkgs/websocket/lib/iowebsocket.dart | 90 +++++ pkgs/websocket/lib/websocket.dart | 122 ++----- pkgs/websocket/pubspec.yaml | 9 +- pkgs/websocket/test/htmlwebsocket_test.dart | 9 + pkgs/websocket/test/iowebsocket_test.dart | 9 + 44 files changed, 1938 insertions(+), 95 deletions(-) create mode 100644 pkgs/cupertino_http/example/integration_test/websocket_conformance_test.dart create mode 100644 pkgs/cupertino_http/lib/src/websocket.dart create mode 100644 pkgs/web_socket_conformance_tests/.gitignore create mode 100644 pkgs/web_socket_conformance_tests/CHANGELOG.md create mode 100644 pkgs/web_socket_conformance_tests/README.md create mode 100644 pkgs/web_socket_conformance_tests/analysis_options.yaml create mode 100644 pkgs/web_socket_conformance_tests/bin/generate_server_wrappers.dart create mode 100644 pkgs/web_socket_conformance_tests/example/web_socket_conformance_tests_example.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_local_server.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_local_server_vm.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_local_server_web.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_remote_server_vm.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_remote_server_web.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_vm.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_server_web.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/echo_server.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/echo_server_vm.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/echo_server_web.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_vm.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/no_upgrade_server_web.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/no_upgrade_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_vm.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_server_web.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/lib/web_socket_conformance_tests.dart create mode 100644 pkgs/web_socket_conformance_tests/pubspec.yaml rename pkgs/{websocket/test/websocket_test.dart => web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart} (76%) create mode 100644 pkgs/websocket/lib/htmlwebsocket.dart create mode 100644 pkgs/websocket/lib/iowebsocket.dart create mode 100644 pkgs/websocket/test/htmlwebsocket_test.dart create mode 100644 pkgs/websocket/test/iowebsocket_test.dart 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..6209480506 --- /dev/null +++ b/pkgs/cupertino_http/lib/src/websocket.dart @@ -0,0 +1,150 @@ +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 WebSocket { + 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: handleError); + } + + void handleError(Object e, StackTrace? st) { + print('>> ReceiveMessage error: $e'); + if (e is Error) { + if (e.code == 57) { + // onWebSocketTaskClosed could still be invoked and set the close code. + // But it would be too late. Might need a timer here? +// _receivingController.sink.close(); + return; + } + _events.addError(CupertinoWebSocketException.fromError(e), st); + } else { + _events.addError(e, st); + } + } + + CupertinoWebSocket._(this._task) { + scheduleReceive(); + } + + void _closed(int? closeCode, Data? reason) { + print('closing with $closeCode'); + if (!_events.isClosed) { + final closeReason = reason == null ? null : utf8.decode(reason.bytes); + + _events + ..add(Closed(closeCode, closeReason)) + ..close(); + } + } + + @override + void addBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _task + .sendMessage(URLSessionWebSocketMessage.fromData(Data.fromList(b))) + .then((_) => _, onError: _events.addError); + } + + @override + void addString(String s) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _task + .sendMessage(URLSessionWebSocketMessage.fromString(s)) + .then((_) => _, onError: _events.addError); + } + + @override + Future close([int? code, String? reason]) async { + 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')]); + }); +*/ \ No newline at end of file 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..75503384e9 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart @@ -0,0 +1,302 @@ +// 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_local_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 = 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); + + 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('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/close_remote_server.dart b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart new file mode 100644 index 0000000000..82e095ffcf --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_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); + webSocket.close(4123, 'server closed the connection'); + }, onDone: () { + 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_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..4ad6e28a48 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart @@ -0,0 +1,316 @@ +// 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.addString('Please close'); + expect(await channel.events.toList(), + [Closed(4123, 'server closed the connection')]); + }); + + test('send after close', () async { + final channel = await channelFactory(uri); + + channel.addString('Please close'); + expect(await channel.events.toList(), + [Closed(4123, 'server closed the connection')]); + expect(() => channel.addString('test'), throwsStateError); + + print(await httpServerQueue.next); + print(await httpServerQueue.next); + +/* + 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..4a37dacc4e --- /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.addString('test'); + expect( + (await channel.events.single as Closed).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..274c68053b --- /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..5dda10b569 --- /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.addString(''); + expect(await channel.events.first, TextDataReceived('')); + }); + + test('empty binary request and response', () async { + final channel = await channelFactory(uri); + + channel.addBytes(Uint8List(0)); + expect(await channel.events.first, BinaryDataReceived(Uint8List(0))); + }); + + test('string request and response', () async { + final channel = await channelFactory(uri); + + channel.addString("Hello World!"); + expect(await channel.events.first, TextDataReceived("Hello World!")); + }); + + test('binary request and response', () async { + final channel = await channelFactory(uri); + + channel.addBytes(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.addString("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.addBytes(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..7d6febf8f7 --- /dev/null +++ b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart @@ -0,0 +1,44 @@ +// 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); + channel.addString('test'); + expect( + (await channel.events.single as Closed).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..41a267d6c0 --- /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/websocket/test/websocket_test.dart b/pkgs/web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart similarity index 76% rename from pkgs/websocket/test/websocket_test.dart rename to pkgs/web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart index a77d3c635c..3051b24544 100644 --- a/pkgs/websocket/test/websocket_test.dart +++ b/pkgs/web_socket_conformance_tests/test/web_socket_conformance_tests_test.dart @@ -1,4 +1,4 @@ -import 'package:websocket/websocket.dart'; +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; import 'package:test/test.dart'; void main() { diff --git a/pkgs/websocket/lib/htmlwebsocket.dart b/pkgs/websocket/lib/htmlwebsocket.dart new file mode 100644 index 0000000000..4af1657bbf --- /dev/null +++ b/pkgs/websocket/lib/htmlwebsocket.dart @@ -0,0 +1,131 @@ +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 WebSocket { + 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(Closed(code, reason)) + ..close(); + } + } + + HtmlWebSocket._(this._webSocket); + + // JS: Silently discards data if connection is closed. + @override + void addBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _webSocket.send(b.jsify()!); + } + + @override + void addString(String s) { + if (_events.isClosed) { + throw StateError('WebSocket 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..709aa800c2 --- /dev/null +++ b/pkgs/websocket/lib/iowebsocket.dart @@ -0,0 +1,90 @@ +import 'dart:async'; +import 'dart:io' as io; +import 'dart:typed_data'; +import 'package:websocket/websocket.dart'; + +class IOWebSocket implements WebSocket { + 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(Closed(_webSocket.closeCode, _webSocket.closeReason)); + _events.close(); + } + }, + ); + } + + // JS: Silently discards data if connection is closed. + @override + void addBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _webSocket.add(b); + } + + @override + void addString(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) { + unawaited(_events.close()); + await _webSocket.close(code, reason); + } + } + + @override + Stream get events => _events.stream; +} diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index e26550efbe..cf2b7d5355 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -1,19 +1,34 @@ import 'dart:async'; import 'dart:typed_data'; -import 'dart:io' as io; - -import 'package:cupertino_http/cupertino_http.dart'; sealed class WebSocketEvent {} 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; } class BinaryDataReceived extends WebSocketEvent { final Uint8List data; BinaryDataReceived(this.data); + + // XXX + @override + bool operator ==(Object other) => + other is BinaryDataReceived && other.data.length == data.length; + + @override + int get hashCode => data.hashCode; + + @override + String toString() => 'BinaryDataReceived($data)'; } class Closed extends WebSocketEvent { @@ -21,100 +36,29 @@ class Closed extends WebSocketEvent { final String? reason; Closed([this.code, this.reason]); -} - -abstract interface class WebSocket { - void addString(String s); - void addBytes(Uint8List b); - Future close([int? code, String? reason]); - Stream get events; -} - -class IOWebSocket implements WebSocket { - final io.WebSocket _webSocket; - final _events = StreamController(); - - static Future connect(Uri uri) async { - final webSocket = await io.WebSocket.connect(uri.toString()); - return IOWebSocket._(webSocket); - } - - IOWebSocket._(this._webSocket) { - _webSocket.listen( - (event) {}, - onError: (e, st) {}, - onDone: () {}, - ); - } @override - void addBytes(Uint8List b) { - _webSocket.add(b); - } + bool operator ==(Object other) => + other is Closed && other.code == code && other.reason == reason; @override - void addString(String s) { - _webSocket.add(s); - } + int get hashCode => [code, reason].hashCode; @override - Future close([int? code, String? reason]) async { - await _webSocket.close(code, reason); - } - - @override - Stream get events => _events.stream; + String toString() => 'Closed($code, $reason)'; } -class CupertinoWebSocket implements WebSocket { - static Future connect(Uri uri) async { - late CupertinoWebSocket webSocket; - final session = URLSession.sessionWithConfiguration( - URLSessionConfiguration.defaultSessionConfiguration(), - onWebSocketTaskClosed: (session, task, closeCode, reason) => - webSocket._closed(closeCode, reason), - ); - final task = session.webSocketTaskWithRequest(URLRequest.fromUrl(uri)) - ..resume(); - webSocket = CupertinoWebSocket._(task); - return webSocket; - } - - final URLSessionWebSocketTask _task; - final _events = StreamController(); - CupertinoWebSocket._(this._task) { - _task.receiveMessage(); - } - - void _closed(int? closeCode, Data? reason) { - _events.add(Closed(closeCode)); // XXX - } - - @override - void addBytes(Uint8List b) { - _task - .sendMessage(URLSessionWebSocketMessage.fromData(Data.fromList(b))) - .then((_) => _, onError: (e, st) => _events.addError(e, st)); - } - - @override - void addString(String s) { - _task - .sendMessage(URLSessionWebSocketMessage.fromString(s)) - .then((_) => _, onError: (e, st) => _events.addError(e, st)); - } +class XXXWebSocketException implements Exception { + final String message; + XXXWebSocketException([this.message = ""]); +} - @override - Future close([int? code, String? reason]) async { - // XXX Wait until all pending writes are done. - if (code != null) { - reason = reason ?? ""; - _task.cancelWithCloseCode(code, Data.fromList(reason.codeUnits)); - } else { - _task.cancel(); - } - } +abstract interface class WebSocket { + void addString(String s); + void addBytes(Uint8List b); + Future close([int? code, String? reason]); - @override - Stream get events => _events.stream; + /// Will be closed after disconnect. No events will be received after + /// [Closed]. [Closed] will not appear in [events] if [close] is called. + Stream get events; } diff --git a/pkgs/websocket/pubspec.yaml b/pkgs/websocket/pubspec.yaml index b103eb7cd7..c35aaed8c8 100644 --- a/pkgs/websocket/pubspec.yaml +++ b/pkgs/websocket/pubspec.yaml @@ -6,11 +6,10 @@ version: 1.0.0 environment: sdk: ^3.2.4 -# Add regular dependencies here. -dependencies: - cupertino_http: ^1.3.0 - # path: ^1.8.0 - 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)); +} From 4bf037a9bbbd0594515dd5d7e6be3e97c18c9e38 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 30 Jan 2024 16:30:29 -0800 Subject: [PATCH 3/9] More work --- pkgs/cupertino_http/lib/src/websocket.dart | 37 +++++++++++-------- .../lib/src/close_local_tests.dart | 2 +- .../lib/src/close_remote_server.dart | 3 -- .../lib/src/close_remote_tests.dart | 4 -- .../lib/src/peer_protocol_errors_tests.dart | 11 ++++++ pkgs/websocket/lib/htmlwebsocket.dart | 3 +- pkgs/websocket/lib/websocket.dart | 30 ++++++++++++++- pkgs/websocket/pubspec.yaml | 4 +- 8 files changed, 65 insertions(+), 29 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/websocket.dart b/pkgs/cupertino_http/lib/src/websocket.dart index 6209480506..61ad234fb0 100644 --- a/pkgs/cupertino_http/lib/src/websocket.dart +++ b/pkgs/cupertino_http/lib/src/websocket.dart @@ -66,28 +66,33 @@ class CupertinoWebSocket implements WebSocket { void scheduleReceive() { // print('scheduleReceive'); - _task.receiveMessage().then(handleMessage, onError: handleError); + _task.receiveMessage().then(handleMessage, onError: _closeWithError); } - void handleError(Object e, StackTrace? st) { - print('>> ReceiveMessage error: $e'); + CupertinoWebSocket._(this._task) { + scheduleReceive(); + } + + void _closeWithError(Object e) { + print('closedWithError: $e'); if (e is Error) { - if (e.code == 57) { - // onWebSocketTaskClosed could still be invoked and set the close code. - // But it would be too late. Might need a timer here? -// _receivingController.sink.close(); + if (e.domain == 'NSPOSIXErrorDomain' && e.code == 57) { + // Socket is not connected. + // onWebSocketTaskClosed/onComplete will be invoked and may indicate a + // close code. return; } - _events.addError(CupertinoWebSocketException.fromError(e), st); + 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 { - _events.addError(e, st); + throw UnsupportedError(''); } } - CupertinoWebSocket._(this._task) { - scheduleReceive(); - } - void _closed(int? closeCode, Data? reason) { print('closing with $closeCode'); if (!_events.isClosed) { @@ -106,7 +111,7 @@ class CupertinoWebSocket implements WebSocket { } _task .sendMessage(URLSessionWebSocketMessage.fromData(Data.fromList(b))) - .then((_) => _, onError: _events.addError); + .then((_) => _, onError: _closeWithError); } @override @@ -116,7 +121,7 @@ class CupertinoWebSocket implements WebSocket { } _task .sendMessage(URLSessionWebSocketMessage.fromString(s)) - .then((_) => _, onError: _events.addError); + .then((_) => _, onError: _closeWithError); } @override @@ -147,4 +152,4 @@ class CupertinoWebSocket implements WebSocket { expect(await channel.events.toList(), [Closed(4123, 'server closed the connection')]); }); -*/ \ No newline at end of file +*/ 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 index 75503384e9..be7a81fb76 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart @@ -10,7 +10,7 @@ import 'package:test/test.dart'; import 'package:websocket/websocket.dart'; import 'close_local_server_vm.dart' - if (dart.library.html) 'close_local_server_web.dart'; + if (dart.library.html) 'close_server_web.dart'; /// Tests that the [WebSocketChannel] can correctly transmit and receive text /// and binary payloads. 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 index 82e095ffcf..ae66836d99 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_server.dart @@ -28,9 +28,6 @@ void hybridMain(StreamChannel channel) async { webSocket.listen((event) { channel.sink.add(event); webSocket.close(4123, 'server closed the connection'); - }, onDone: () { - channel.sink.add(webSocket.closeCode); - channel.sink.add(webSocket.closeReason); }); }); 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 index 4ad6e28a48..e99f6c9414 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart @@ -85,10 +85,6 @@ void testRemoteClose( expect(await channel.events.toList(), [Closed(4123, 'server closed the connection')]); expect(() => channel.addString('test'), throwsStateError); - - print(await httpServerQueue.next); - print(await httpServerQueue.next); - /* final closeCode = await httpServerQueue.next as int?; final closeReason = await httpServerQueue.next as String?; 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 index 7d6febf8f7..7c6edf14a8 100644 --- 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 @@ -30,6 +30,17 @@ void testPeerProtocolErrors( tearDownAll(() => httpServerChannel.sink.add(null)); test('bad data after upgrade', () async { + final channel = await channelFactory(uri); + expect( + (await channel.events.single as Closed).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.addString('test'); expect( diff --git a/pkgs/websocket/lib/htmlwebsocket.dart b/pkgs/websocket/lib/htmlwebsocket.dart index 4af1657bbf..74772dc79f 100644 --- a/pkgs/websocket/lib/htmlwebsocket.dart +++ b/pkgs/websocket/lib/htmlwebsocket.dart @@ -81,12 +81,12 @@ class HtmlWebSocket implements WebSocket { HtmlWebSocket._(this._webSocket); - // JS: Silently discards data if connection is closed. @override void addBytes(Uint8List b) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } + // Silently discards the data if the connection is closed. _webSocket.send(b.jsify()!); } @@ -95,6 +95,7 @@ class HtmlWebSocket implements WebSocket { if (_events.isClosed) { throw StateError('WebSocket is closed'); } + // Silently discards the data if the connection is closed. _webSocket.send(s.jsify()!); } diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index cf2b7d5355..59a837ad0f 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -53,12 +53,38 @@ class XXXWebSocketException implements Exception { XXXWebSocketException([this.message = ""]); } +class WebSocketConnectionClosed extends XXXWebSocketException { + WebSocketConnectionClosed([super.message = 'Connection Closed']); +} + abstract interface class WebSocket { + /// Throws [WebSocketConnectionClosed] if the [WebSocket] is closed (either through [close] or by the peer). void addString(String s); + + /// Throws [WebSocketConnectionClosed] if the [WebSocket] is closed (either through [close] or by the peer). void addBytes(Uint8List b); + + /// Closes the WebSocket connection. + /// + /// Set the optional code and reason arguments to send close information + /// to the peer. If they are omitted, the peer will see a 1005 status code + /// with no reason. + /// + /// [events] will be closed. Future close([int? code, String? reason]); - /// Will be closed after disconnect. No events will be received after - /// [Closed]. [Closed] will not appear in [events] if [close] is called. + /// Events received from the peer. + /// + /// If a [Closed] event is received then the [Stream] will be closed. A + /// [Closed] event indicates either that: + /// + /// - A close frame was received from the peer. [Closed.code] and + /// [Closed.reason] will be set by the peer. + /// - A failure occured (e.g. the peer disconnected). [Closed.code] and + /// [Closed.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]. Stream get events; } diff --git a/pkgs/websocket/pubspec.yaml b/pkgs/websocket/pubspec.yaml index c35aaed8c8..0c0c5f1653 100644 --- a/pkgs/websocket/pubspec.yaml +++ b/pkgs/websocket/pubspec.yaml @@ -11,5 +11,5 @@ dev_dependencies: test: ^1.24.0 web_socket_conformance_tests: path: ../web_socket_conformance_tests -dependencies: - web: ^0.4.2 +#dependencies: +# web: ^0.4.2 From 26d0df145079347333941143f1824a3fae439f1b Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 30 Jan 2024 16:59:59 -0800 Subject: [PATCH 4/9] Compare --- pkgs/cupertino_http/lib/src/websocket.dart | 2 +- .../lib/src/close_local_tests.dart | 2 +- .../lib/src/close_remote_tests.dart | 2 +- .../src/disconnect_after_upgrade_tests.dart | 2 +- .../lib/src/no_upgrade_tests.dart | 2 +- .../lib/src/payload_transfer_tests.dart | 2 +- .../lib/src/peer_protocol_errors_tests.dart | 2 +- .../lib/web_socket_conformance_tests.dart | 2 +- pkgs/websocket/lib/htmlwebsocket.dart | 2 +- pkgs/websocket/lib/iowebsocket.dart | 2 +- pkgs/websocket/lib/websocket.dart | 22 ++++++++++++++----- 11 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/websocket.dart b/pkgs/cupertino_http/lib/src/websocket.dart index 61ad234fb0..2f6978e641 100644 --- a/pkgs/cupertino_http/lib/src/websocket.dart +++ b/pkgs/cupertino_http/lib/src/websocket.dart @@ -12,7 +12,7 @@ class CupertinoWebSocketException extends XXXWebSocketException { CupertinoWebSocketException(e.toString()); } -class CupertinoWebSocket implements WebSocket { +class CupertinoWebSocket implements XXXWebSocket { static Future connect(Uri uri) async { final readyCompleter = Completer(); late CupertinoWebSocket webSocket; 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 index be7a81fb76..c11747023d 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart @@ -15,7 +15,7 @@ import 'close_local_server_vm.dart' /// Tests that the [WebSocketChannel] can correctly transmit and receive text /// and binary payloads. void testLocalClose( - Future Function(Uri uri, {Iterable? protocols}) + Future Function(Uri uri, {Iterable? protocols}) channelFactory) { group('local close', () { late Uri uri; 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 index e99f6c9414..ed2564b6ba 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart @@ -15,7 +15,7 @@ import 'close_remote_server_vm.dart' /// Tests that the [WebSocketChannel] can correctly transmit and receive text /// and binary payloads. void testRemoteClose( - Future Function(Uri uri, {Iterable? protocols}) + Future Function(Uri uri, {Iterable? protocols}) channelFactory) { group('remote close', () { late Uri uri; 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 index 4a37dacc4e..c6cd7bd0d0 100644 --- 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 @@ -13,7 +13,7 @@ import 'disconnect_after_upgrade_server_vm.dart' /// Tests that the [WebSocketChannel] can correctly transmit and receive text /// and binary payloads. void testDisconnectAfterUpgrade( - Future Function(Uri uri, {Iterable? protocols}) + Future Function(Uri uri, {Iterable? protocols}) channelFactory) { group('disconnect', () { late final Uri uri; 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 index 274c68053b..e3ff30da04 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/no_upgrade_tests.dart @@ -15,7 +15,7 @@ import 'no_upgrade_server_vm.dart' /// Tests that the [WebSocketChannel] can correctly transmit and receive text /// and binary payloads. void testNoUpgrade( - Future Function(Uri uri, {Iterable? protocols}) + Future Function(Uri uri, {Iterable? protocols}) channelFactory) { group('no upgrade', () { late final Uri uri; 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 index 5dda10b569..df581917c2 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart @@ -14,7 +14,7 @@ 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}) + Future Function(Uri uri, {Iterable? protocols}) channelFactory) { group('payload transfer', () { late final Uri uri; 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 index 7c6edf14a8..27e440687d 100644 --- 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 @@ -15,7 +15,7 @@ import 'peer_protocol_errors_server_vm.dart' /// Tests that the [WebSocketChannel] can correctly transmit and receive text /// and binary payloads. void testPeerProtocolErrors( - Future Function(Uri uri, {Iterable? protocols}) + Future Function(Uri uri, {Iterable? protocols}) channelFactory) { group('protocol errors', () { late final Uri uri; 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 index 41a267d6c0..b51507ca73 100644 --- a/pkgs/web_socket_conformance_tests/lib/web_socket_conformance_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/web_socket_conformance_tests.dart @@ -17,7 +17,7 @@ import 'src/peer_protocol_errors_tests.dart'; /// Runs the entire test suite against the given [WebSocketChannel]. void testAll( - Future Function(Uri uri, {Iterable? protocols}) + Future Function(Uri uri, {Iterable? protocols}) webSocketFactory) { testPayloadTransfer(webSocketFactory); testLocalClose(webSocketFactory); diff --git a/pkgs/websocket/lib/htmlwebsocket.dart b/pkgs/websocket/lib/htmlwebsocket.dart index 74772dc79f..a513353db1 100644 --- a/pkgs/websocket/lib/htmlwebsocket.dart +++ b/pkgs/websocket/lib/htmlwebsocket.dart @@ -5,7 +5,7 @@ import 'package:web/web.dart' as web; import 'package:websocket/websocket.dart'; import 'package:web/helpers.dart' as helpers; -class HtmlWebSocket implements WebSocket { +class HtmlWebSocket implements XXXWebSocket { final helpers.WebSocket _webSocket; final _events = StreamController(); diff --git a/pkgs/websocket/lib/iowebsocket.dart b/pkgs/websocket/lib/iowebsocket.dart index 709aa800c2..ad2ed152de 100644 --- a/pkgs/websocket/lib/iowebsocket.dart +++ b/pkgs/websocket/lib/iowebsocket.dart @@ -3,7 +3,7 @@ import 'dart:io' as io; import 'dart:typed_data'; import 'package:websocket/websocket.dart'; -class IOWebSocket implements WebSocket { +class IOWebSocket implements XXXWebSocket { final io.WebSocket _webSocket; final _events = StreamController(); diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index 59a837ad0f..69f51e0bce 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; sealed class WebSocketEvent {} +/// A received text frame. class TextDataReceived extends WebSocketEvent { final String text; TextDataReceived(this.text); @@ -15,14 +16,21 @@ class TextDataReceived extends WebSocketEvent { int get hashCode => text.hashCode; } +// A received binary frame. class BinaryDataReceived extends WebSocketEvent { final Uint8List data; BinaryDataReceived(this.data); - // XXX @override - bool operator ==(Object other) => - other is BinaryDataReceived && other.data.length == data.length; + 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; @@ -31,6 +39,7 @@ class BinaryDataReceived extends WebSocketEvent { String toString() => 'BinaryDataReceived($data)'; } +/// A received close frame or failure. class Closed extends WebSocketEvent { final int? code; final String? reason; @@ -57,11 +66,12 @@ class WebSocketConnectionClosed extends XXXWebSocketException { WebSocketConnectionClosed([super.message = 'Connection Closed']); } -abstract interface class WebSocket { - /// Throws [WebSocketConnectionClosed] if the [WebSocket] is closed (either through [close] or by the peer). +/// What's a good name for this? `SimpleWebSocket`? 'LCDWebSocket`? +abstract interface class XXXWebSocket { + /// Throws [WebSocketConnectionClosed] if the [XXXWebSocket] is closed (either through [close] or by the peer). void addString(String s); - /// Throws [WebSocketConnectionClosed] if the [WebSocket] is closed (either through [close] or by the peer). + /// Throws [WebSocketConnectionClosed] if the [XXXWebSocket] is closed (either through [close] or by the peer). void addBytes(Uint8List b); /// Closes the WebSocket connection. From a6833bc201d4e5ee181282eef74e22332cc86cab Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 31 Jan 2024 15:03:13 -0800 Subject: [PATCH 5/9] Submit --- pkgs/cupertino_http/lib/src/websocket.dart | 6 +- .../lib/src/close_local_tests.dart | 25 ++----- .../lib/src/close_remote_tests.dart | 10 +-- .../src/disconnect_after_upgrade_tests.dart | 4 +- .../lib/src/payload_transfer_tests.dart | 12 +-- .../lib/src/peer_protocol_errors_tests.dart | 6 +- pkgs/websocket/lib/htmlwebsocket.dart | 6 +- pkgs/websocket/lib/iowebsocket.dart | 13 +++- pkgs/websocket/lib/websocket.dart | 73 +++++++++++++------ 9 files changed, 89 insertions(+), 66 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/websocket.dart b/pkgs/cupertino_http/lib/src/websocket.dart index 2f6978e641..7191d4488c 100644 --- a/pkgs/cupertino_http/lib/src/websocket.dart +++ b/pkgs/cupertino_http/lib/src/websocket.dart @@ -99,13 +99,13 @@ class CupertinoWebSocket implements XXXWebSocket { final closeReason = reason == null ? null : utf8.decode(reason.bytes); _events - ..add(Closed(closeCode, closeReason)) + ..add(CloseReceived(closeCode, closeReason)) ..close(); } } @override - void addBytes(Uint8List b) { + void sendBytes(Uint8List b) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } @@ -115,7 +115,7 @@ class CupertinoWebSocket implements XXXWebSocket { } @override - void addString(String s) { + void sendText(String s) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } 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 index c11747023d..149f94e02f 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart @@ -44,32 +44,21 @@ void testLocalClose( // 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'); + final channel = await channelFactory(uri); + await expectLater( + () => channel.close(1004), throwsA(isA())); }); 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)); + final channel = await channelFactory(uri); + await expectLater(() => channel.close(3000, 'Boom'.padLeft(1000)), + throwsA(isA())); }); -*/ + test('with code and reason', () async { final channel = await channelFactory(uri); 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 index ed2564b6ba..daade10793 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart @@ -73,18 +73,18 @@ void testRemoteClose( test('with code and reason', () async { final channel = await channelFactory(uri); - channel.addString('Please close'); + channel.sendText('Please close'); expect(await channel.events.toList(), - [Closed(4123, 'server closed the connection')]); + [CloseReceived(4123, 'server closed the connection')]); }); test('send after close', () async { final channel = await channelFactory(uri); - channel.addString('Please close'); + channel.sendText('Please close'); expect(await channel.events.toList(), - [Closed(4123, 'server closed the connection')]); - expect(() => channel.addString('test'), throwsStateError); + [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?; 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 index c6cd7bd0d0..f114205bf6 100644 --- 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 @@ -29,9 +29,9 @@ void testDisconnectAfterUpgrade( test('disconnect after upgrade', () async { final channel = await channelFactory(uri); - channel.addString('test'); + channel.sendText('test'); expect( - (await channel.events.single as Closed).code, + (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/payload_transfer_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart index df581917c2..70245459ce 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart @@ -40,28 +40,28 @@ void testPayloadTransfer( test('empty string request and response', () async { final channel = await channelFactory(uri); - channel.addString(''); + channel.sendText(''); expect(await channel.events.first, TextDataReceived('')); }); test('empty binary request and response', () async { final channel = await channelFactory(uri); - channel.addBytes(Uint8List(0)); + channel.sendBytes(Uint8List(0)); expect(await channel.events.first, BinaryDataReceived(Uint8List(0))); }); test('string request and response', () async { final channel = await channelFactory(uri); - channel.addString("Hello World!"); + channel.sendText("Hello World!"); expect(await channel.events.first, TextDataReceived("Hello World!")); }); test('binary request and response', () async { final channel = await channelFactory(uri); - channel.addBytes(Uint8List.fromList([1, 2, 3, 4, 5])); + channel.sendBytes(Uint8List.fromList([1, 2, 3, 4, 5])); expect(await channel.events.first, BinaryDataReceived(Uint8List.fromList([1, 2, 3, 4, 5]))); }); @@ -69,7 +69,7 @@ void testPayloadTransfer( test('large string request and response', () async { final channel = await channelFactory(uri); - channel.addString("Hello World!" * 1000); + channel.sendText("Hello World!" * 1000); expect( await channel.events.first, TextDataReceived("Hello World!" * 1000)); }); @@ -77,7 +77,7 @@ void testPayloadTransfer( test('large binary request and response - XXX', () async { final channel = await channelFactory(uri); - channel.addBytes(Uint8List.fromList([1, 2, 3, 4, 5])); + channel.sendBytes(Uint8List.fromList([1, 2, 3, 4, 5])); expect(await channel.events.first, BinaryDataReceived(Uint8List.fromList([1, 2, 3, 4, 5]))); }); 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 index 27e440687d..fd6abd7e2a 100644 --- 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 @@ -32,7 +32,7 @@ void testPeerProtocolErrors( test('bad data after upgrade', () async { final channel = await channelFactory(uri); expect( - (await channel.events.single as Closed).code, + (await channel.events.single as CloseReceived).code, anyOf([ 1002, // protocol error 1005, // closed no status @@ -42,9 +42,9 @@ void testPeerProtocolErrors( test('bad data after upgrade with write', () async { final channel = await channelFactory(uri); - channel.addString('test'); + channel.sendText('test'); expect( - (await channel.events.single as Closed).code, + (await channel.events.single as CloseReceived).code, anyOf([ 1002, // protocol error 1005, // closed no status diff --git a/pkgs/websocket/lib/htmlwebsocket.dart b/pkgs/websocket/lib/htmlwebsocket.dart index a513353db1..17ab4b1ba8 100644 --- a/pkgs/websocket/lib/htmlwebsocket.dart +++ b/pkgs/websocket/lib/htmlwebsocket.dart @@ -74,7 +74,7 @@ class HtmlWebSocket implements XXXWebSocket { print('closing with $code, $reason'); if (!_events.isClosed) { _events - ..add(Closed(code, reason)) + ..add(CloseReceived(code, reason)) ..close(); } } @@ -82,7 +82,7 @@ class HtmlWebSocket implements XXXWebSocket { HtmlWebSocket._(this._webSocket); @override - void addBytes(Uint8List b) { + void sendBytes(Uint8List b) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } @@ -91,7 +91,7 @@ class HtmlWebSocket implements XXXWebSocket { } @override - void addString(String s) { + void sendText(String s) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } diff --git a/pkgs/websocket/lib/iowebsocket.dart b/pkgs/websocket/lib/iowebsocket.dart index ad2ed152de..9c5baa4814 100644 --- a/pkgs/websocket/lib/iowebsocket.dart +++ b/pkgs/websocket/lib/iowebsocket.dart @@ -39,7 +39,8 @@ class IOWebSocket implements XXXWebSocket { onDone: () { print('onDone'); if (!_events.isClosed) { - _events.add(Closed(_webSocket.closeCode, _webSocket.closeReason)); + _events + .add(CloseReceived(_webSocket.closeCode, _webSocket.closeReason)); _events.close(); } }, @@ -48,7 +49,7 @@ class IOWebSocket implements XXXWebSocket { // JS: Silently discards data if connection is closed. @override - void addBytes(Uint8List b) { + void sendBytes(Uint8List b) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } @@ -56,7 +57,7 @@ class IOWebSocket implements XXXWebSocket { } @override - void addString(String s) { + void sendText(String s) { if (_events.isClosed) { throw StateError('WebSocket is closed'); } @@ -81,7 +82,11 @@ class IOWebSocket implements XXXWebSocket { Future close([int? code, String? reason]) async { if (!_events.isClosed) { unawaited(_events.close()); - await _webSocket.close(code, reason); + try { + await _webSocket.close(code, reason); + } on io.WebSocketException catch (e) { + throw XXXWebSocketException(e.message); + } } } diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index 69f51e0bce..59def858e4 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -3,8 +3,10 @@ import 'dart:typed_data'; sealed class WebSocketEvent {} -/// A received text frame. -class TextDataReceived extends WebSocketEvent { +/// Text data received by the peer. +/// +/// See [XXXWebSocket.events]. +final class TextDataReceived extends WebSocketEvent { final String text; TextDataReceived(this.text); @@ -16,8 +18,10 @@ class TextDataReceived extends WebSocketEvent { int get hashCode => text.hashCode; } -// A received binary frame. -class BinaryDataReceived extends WebSocketEvent { +/// Binary data received by the peer. +/// +/// See [XXXWebSocket.events]. +final class BinaryDataReceived extends WebSocketEvent { final Uint8List data; BinaryDataReceived(this.data); @@ -39,22 +43,25 @@ class BinaryDataReceived extends WebSocketEvent { String toString() => 'BinaryDataReceived($data)'; } -/// A received close frame or failure. -class Closed extends WebSocketEvent { +/// A close notification sent from the peer or a failure indication. +/// +/// See [XXXWebSocket.events]. +final class CloseReceived extends WebSocketEvent { + /// See [RFC-6455 7.4](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4) final int? code; final String? reason; - Closed([this.code, this.reason]); + CloseReceived([this.code, this.reason]); @override bool operator ==(Object other) => - other is Closed && other.code == code && other.reason == reason; + other is CloseReceived && other.code == code && other.reason == reason; @override int get hashCode => [code, reason].hashCode; @override - String toString() => 'Closed($code, $reason)'; + String toString() => 'CloseReceived($code, $reason)'; } class XXXWebSocketException implements Exception { @@ -62,17 +69,29 @@ class XXXWebSocketException implements Exception { XXXWebSocketException([this.message = ""]); } -class WebSocketConnectionClosed extends XXXWebSocketException { - WebSocketConnectionClosed([super.message = 'Connection Closed']); +/// Thrown if [XXXWebSocket.sendText] or [XXXWebSocket.sendBytes] is called +/// when the [XXXWebSocket] is closed. +class XXXWebSocketConnectionClosed extends XXXWebSocketException { + XXXWebSocketConnectionClosed([super.message = 'Connection Closed']); } /// What's a good name for this? `SimpleWebSocket`? 'LCDWebSocket`? abstract interface class XXXWebSocket { - /// Throws [WebSocketConnectionClosed] if the [XXXWebSocket] is closed (either through [close] or by the peer). - void addString(String s); - - /// Throws [WebSocketConnectionClosed] if the [XXXWebSocket] is closed (either through [close] or by the peer). - void addBytes(Uint8List b); + /// Say something about not guaranteeing delivery. + /// + /// Throws [XXXWebSocketConnectionClosed] if the [XXXWebSocket] is closed + /// (either through [close] or by the peer). Alternatively, we could just throw + /// the data away - that's what JavaScript does. Probably that is better + /// so every call to [sendText], [sendBytes] and [close] doesn't need to be + /// surrounded in a try block. + void sendText(String s); + + /// Say something about not guaranteeing delivery. + /// + /// Throws [XXXWebSocketConnectionClosed] if the [XXXWebSocket] is closed + /// (either through [close] or by the peer). Alternatively, we could just throw + /// the data away - that's what JavaScript does. + void sendBytes(Uint8List b); /// Closes the WebSocket connection. /// @@ -80,18 +99,28 @@ abstract interface class XXXWebSocket { /// to the peer. If they are omitted, the peer will see a 1005 status code /// with no reason. /// + /// If [code] is not in the range 3000-4999 then an [ArgumentError] + /// will be thrown. + /// + /// If [reason] is longer than 123 bytes when encoded as UTF-8 then + /// [ArgumentError] will be thrown. + /// /// [events] will be closed. + /// + /// Throws [XXXWebSocketConnectionClosed] if the connection is already closed + /// (including by the peer). Alternatively, we could just throw the close + /// away. Future close([int? code, String? reason]); /// Events received from the peer. /// - /// If a [Closed] event is received then the [Stream] will be closed. A - /// [Closed] event indicates either that: + /// 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. [Closed.code] and - /// [Closed.reason] will be set by the peer. - /// - A failure occured (e.g. the peer disconnected). [Closed.code] and - /// [Closed.reason] will be a failure code defined by + /// - 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). /// From 824ffcfafbb8f65caecb376b221224c037d6e61a Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 1 Feb 2024 08:05:06 -0800 Subject: [PATCH 6/9] Update --- .../lib/src/close_local_tests.dart | 252 +++--------------- pkgs/websocket/lib/iowebsocket.dart | 27 +- pkgs/websocket/lib/websocket.dart | 6 +- 3 files changed, 65 insertions(+), 220 deletions(-) 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 index 149f94e02f..e9769ca47d 100644 --- a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart +++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart @@ -49,243 +49,77 @@ void testLocalClose( // If reasonBytes is longer than 123 bytes, then throw a "SyntaxError" DOMException. final channel = await channelFactory(uri); - await expectLater( - () => channel.close(1004), throwsA(isA())); + await expectLater(() => channel.close(1004), throwsA(isA())); }); test('too long close reason', () async { final channel = await channelFactory(uri); - await expectLater(() => channel.close(3000, 'Boom'.padLeft(1000)), - throwsA(isA())); + await expectLater(() => channel.close(3000, 'a'.padLeft(124)), + throwsA(isA())); }); - test('with code and reason', () async { + test('close', () async { final channel = await channelFactory(uri); - await channel.close(3000, 'Client initiated closure'); + await channel.close(); final closeCode = await httpServerQueue.next as int?; final closeReason = await httpServerQueue.next as String?; - expect(closeCode, 3000); - expect(closeReason, 'Client initiated closure'); + expect(closeCode, 1005); + expect(closeReason, ''); expect(await channel.events.isEmpty, true); }); -/* - 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); + test('with code 3000', () async { + final channel = await channelFactory(uri); - expect(sinkDoneComplete, false); - expect(sinkDoneOnError, false); - expect(streamOnData, false); - expect(streamOnDone, false); - expect(streamOnError, false); + await channel.close(3000); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; - 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'); + expect(closeCode, 3000); + expect(closeReason, ''); + expect(await channel.events.isEmpty, true); }); - 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); + test('with code 4999', () async { + final channel = await channelFactory(uri); - 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(); + await channel.close(4999); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; - expect(sinkDoneComplete, true); - expect(sinkDoneOnError, false); - expect(streamOnData, false); - expect(streamOnDone, true); - expect(streamOnError, false); + expect(closeCode, 4999); + expect(closeReason, ''); + expect(await channel.events.isEmpty, true); }); - test('server initiated', () async { - final channel = channelFactory(uri); + test('with code and reason', () async { + final channel = await channelFactory(uri); - var sinkDoneComplete = false; - var sinkDoneOnError = false; - var streamOnData = false; - var streamOnDone = false; - var streamOnError = false; + await channel.close(3000, 'Client initiated closure'); + final closeCode = await httpServerQueue.next as int?; + final closeReason = await httpServerQueue.next as String?; - channel.sink.done.then((_) { - sinkDoneComplete = true; - }, onError: (_) { - sinkDoneOnError = true; - }); + expect(closeCode, 3000); + expect(closeReason, 'Client initiated closure'); + expect(await channel.events.isEmpty, true); + }); - final streamListen = channel.stream.listen((_) { - streamOnData = true; - }, onError: (_) { - streamOnError = true; - }).asFuture(); + test('close after close', () async { + final channel = await channelFactory(uri); - await expectLater(channel.ready, completes); - await streamListen; + await channel.close(3000, 'Client initiated closure'); - expect(channel.closeCode, 4123); - expect(channel.closeReason, 'server closed the connection'); - channel.sink.add('add after connection closed'); - await channel.sink.close(); + 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(sinkDoneComplete, true); - expect(sinkDoneOnError, false); - expect(streamOnData, false); - expect(streamOnError, false); + expect(closeCode, 3000); + expect(closeReason, 'Client initiated closure'); + expect(await channel.events.isEmpty, true); }); - */ }); } diff --git a/pkgs/websocket/lib/iowebsocket.dart b/pkgs/websocket/lib/iowebsocket.dart index 9c5baa4814..fb8ac5a78b 100644 --- a/pkgs/websocket/lib/iowebsocket.dart +++ b/pkgs/websocket/lib/iowebsocket.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'dart:typed_data'; import 'package:websocket/websocket.dart'; @@ -79,14 +80,24 @@ class IOWebSocket implements XXXWebSocket { // 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()); - try { - await _webSocket.close(code, reason); - } on io.WebSocketException catch (e) { - throw XXXWebSocketException(e.message); - } + Future close([int? code, String reason = '']) async { + if (_events.isClosed) { + throw XXXWebSocketConnectionClosed(); + } + + if (code != null) { + RangeError.checkValueInInterval(code, 3000, 4999, 'code'); + } + if (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); } } diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index 59def858e4..0d21ae7632 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -99,7 +99,7 @@ abstract interface class XXXWebSocket { /// to the peer. If they are omitted, the peer will see a 1005 status code /// with no reason. /// - /// If [code] is not in the range 3000-4999 then an [ArgumentError] + /// If [code] is not in the range 3000-4999 then an [RangeError] /// will be thrown. /// /// If [reason] is longer than 123 bytes when encoded as UTF-8 then @@ -109,8 +109,8 @@ abstract interface class XXXWebSocket { /// /// Throws [XXXWebSocketConnectionClosed] if the connection is already closed /// (including by the peer). Alternatively, we could just throw the close - /// away. - Future close([int? code, String? reason]); + /// away. Same as the reasoning for [sendText]. + Future close([int? code, String reason = '']); /// Events received from the peer. /// From 8b7b9325637212f80161294cdb2df68cdd96558f Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 5 Feb 2024 18:13:42 -0800 Subject: [PATCH 7/9] More docs --- pkgs/websocket/example/websocket_example.dart | 6 +- pkgs/websocket/lib/websocket.dart | 57 ++++++++++--------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/pkgs/websocket/example/websocket_example.dart b/pkgs/websocket/example/websocket_example.dart index 02766ea7da..f88b524d39 100644 --- a/pkgs/websocket/example/websocket_example.dart +++ b/pkgs/websocket/example/websocket_example.dart @@ -1,6 +1,4 @@ +import 'package:websocket/iowebsocket.dart'; import 'package:websocket/websocket.dart'; -void main() { - var awesome = Awesome(); - print('awesome: ${awesome.isAwesome}'); -} +void main() {} diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index 0d21ae7632..d77737fc4b 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'dart:typed_data'; +/// An event received by the peer through the [WebSocket]. sealed class WebSocketEvent {} -/// Text data received by the peer. +/// Text data received by the peer through the [WebSocket]. /// /// See [XXXWebSocket.events]. final class TextDataReceived extends WebSocketEvent { @@ -18,7 +19,7 @@ final class TextDataReceived extends WebSocketEvent { int get hashCode => text.hashCode; } -/// Binary data received by the peer. +/// Binary data received by the peer through the [WebSocket]. /// /// See [XXXWebSocket.events]. final class BinaryDataReceived extends WebSocketEvent { @@ -43,7 +44,8 @@ final class BinaryDataReceived extends WebSocketEvent { String toString() => 'BinaryDataReceived($data)'; } -/// A close notification sent from the peer or a failure indication. +/// A close notification (Close frame) sent from the peer through the +/// [WebSocket] or a failure indication. /// /// See [XXXWebSocket.events]. final class CloseReceived extends WebSocketEvent { @@ -69,50 +71,51 @@ class XXXWebSocketException implements Exception { XXXWebSocketException([this.message = ""]); } -/// Thrown if [XXXWebSocket.sendText] or [XXXWebSocket.sendBytes] is called -/// when the [XXXWebSocket] is closed. +/// 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']); } -/// What's a good name for this? `SimpleWebSocket`? 'LCDWebSocket`? +/// 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 { - /// Say something about not guaranteeing delivery. + /// Sends text data to the connected peer. /// /// Throws [XXXWebSocketConnectionClosed] if the [XXXWebSocket] is closed - /// (either through [close] or by the peer). Alternatively, we could just throw - /// the data away - that's what JavaScript does. Probably that is better - /// so every call to [sendText], [sendBytes] and [close] doesn't need to be - /// surrounded in a try block. + /// (either through [close] or by the peer). void sendText(String s); - /// Say something about not guaranteeing delivery. + /// Sends binary data to the connected peer. /// /// Throws [XXXWebSocketConnectionClosed] if the [XXXWebSocket] is closed - /// (either through [close] or by the peer). Alternatively, we could just throw - /// the data away - that's what JavaScript does. + /// (either through [close] or by the peer). void sendBytes(Uint8List b); - /// Closes the WebSocket connection. + /// Closes the WebSocket connection and the [events] `Stream`. /// - /// Set the optional code and reason arguments to send close information - /// to the peer. If they are omitted, the peer will see a 1005 status code - /// with no reason. + /// 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 receive an empty reason string. /// - /// If [code] is not in the range 3000-4999 then an [RangeError] - /// will be thrown. + /// Throws a [RangeError] if [code] is not in the range 3000-4999. /// - /// If [reason] is longer than 123 bytes when encoded as UTF-8 then - /// [ArgumentError] will be thrown. - /// - /// [events] will be closed. + /// 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). Alternatively, we could just throw the close - /// away. Same as the reasoning for [sendText]. + /// (including by the peer). Future close([int? code, String reason = '']); - /// Events received from the peer. + /// 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: From 9de26bb18de4adeeca42c88d92bbe9a2b5cc9246 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 6 Feb 2024 11:25:56 -0800 Subject: [PATCH 8/9] More docs --- pkgs/websocket/lib/websocket.dart | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index d77737fc4b..d617d87dbe 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:typed_data'; -/// An event received by the peer through the [WebSocket]. +/// An event received from the peer through the [XXXWebSocket]. sealed class WebSocketEvent {} -/// Text data received by the peer through the [WebSocket]. +/// Text data received from the peer through the [XXXWebSocket]. /// /// See [XXXWebSocket.events]. final class TextDataReceived extends WebSocketEvent { @@ -19,7 +19,7 @@ final class TextDataReceived extends WebSocketEvent { int get hashCode => text.hashCode; } -/// Binary data received by the peer through the [WebSocket]. +/// Binary data received from the peer through the [XXXWebSocket]. /// /// See [XXXWebSocket.events]. final class BinaryDataReceived extends WebSocketEvent { @@ -44,16 +44,23 @@ final class BinaryDataReceived extends WebSocketEvent { String toString() => 'BinaryDataReceived($data)'; } -/// A close notification (Close frame) sent from the peer through the -/// [WebSocket] or a failure indication. +/// 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; - final String? reason; - CloseReceived([this.code, this.reason]); + /// 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) => @@ -128,5 +135,9 @@ abstract interface class XXXWebSocket { /// (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; } From 7b30f614cfda972960f0a48017587a64d61c1781 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 6 Feb 2024 13:30:23 -0800 Subject: [PATCH 9/9] More --- pkgs/cupertino_http/lib/src/websocket.dart | 14 +++++++++++++- pkgs/websocket/lib/iowebsocket.dart | 8 ++++---- pkgs/websocket/lib/websocket.dart | 4 ++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/websocket.dart b/pkgs/cupertino_http/lib/src/websocket.dart index 7191d4488c..2674badf64 100644 --- a/pkgs/cupertino_http/lib/src/websocket.dart +++ b/pkgs/cupertino_http/lib/src/websocket.dart @@ -96,7 +96,7 @@ class CupertinoWebSocket implements XXXWebSocket { void _closed(int? closeCode, Data? reason) { print('closing with $closeCode'); if (!_events.isClosed) { - final closeReason = reason == null ? null : utf8.decode(reason.bytes); + final closeReason = reason == null ? '' : utf8.decode(reason.bytes); _events ..add(CloseReceived(closeCode, closeReason)) @@ -126,6 +126,18 @@ class CupertinoWebSocket implements XXXWebSocket { @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()); diff --git a/pkgs/websocket/lib/iowebsocket.dart b/pkgs/websocket/lib/iowebsocket.dart index fb8ac5a78b..50939bdb96 100644 --- a/pkgs/websocket/lib/iowebsocket.dart +++ b/pkgs/websocket/lib/iowebsocket.dart @@ -40,8 +40,8 @@ class IOWebSocket implements XXXWebSocket { onDone: () { print('onDone'); if (!_events.isClosed) { - _events - .add(CloseReceived(_webSocket.closeCode, _webSocket.closeReason)); + _events.add(CloseReceived( + _webSocket.closeCode, _webSocket.closeReason ?? "")); _events.close(); } }, @@ -80,7 +80,7 @@ class IOWebSocket implements XXXWebSocket { // endpoint that has already sent a Close frame will continue to process // data. @override - Future close([int? code, String reason = '']) async { + Future close([int? code, String? reason]) async { if (_events.isClosed) { throw XXXWebSocketConnectionClosed(); } @@ -88,7 +88,7 @@ class IOWebSocket implements XXXWebSocket { if (code != null) { RangeError.checkValueInInterval(code, 3000, 4999, 'code'); } - if (utf8.encode(reason).length > 123) { + if (reason != null && utf8.encode(reason).length > 123) { throw ArgumentError.value(reason, "reason", "reason must be <= 123 bytes long when encoded as UTF-8"); } diff --git a/pkgs/websocket/lib/websocket.dart b/pkgs/websocket/lib/websocket.dart index d617d87dbe..8c725fbea4 100644 --- a/pkgs/websocket/lib/websocket.dart +++ b/pkgs/websocket/lib/websocket.dart @@ -108,7 +108,7 @@ abstract interface class XXXWebSocket { /// 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 receive an empty reason string. + /// is set then the peer will not receive a reason string. /// /// Throws a [RangeError] if [code] is not in the range 3000-4999. /// @@ -117,7 +117,7 @@ abstract interface class XXXWebSocket { /// /// Throws [XXXWebSocketConnectionClosed] if the connection is already closed /// (including by the peer). - Future close([int? code, String reason = '']); + Future close([int? code, String? reason]); /// A [Stream] of [WebSocketEvent] received from the peer. ///