diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index b374dd9907..aa1ecd3aa1 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:cupertino_http/src/utils.dart'; import 'package:web_socket/web_socket.dart'; import 'cupertino_api.dart'; @@ -206,11 +207,10 @@ class CupertinoWebSocket implements WebSocket { if (_events.isClosed) { throw WebSocketConnectionClosed(); } + + checkCloseCodeRfc(code); + checkCloseReason(reason); - if (code != null && code != 1000 && !(code >= 3000 && code <= 4999)) { - throw ArgumentError('Invalid argument: $code, close code must be 1000 or ' - 'in the range 3000-4999'); - } 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/cupertino_http/lib/src/utils.dart b/pkgs/cupertino_http/lib/src/utils.dart index 7bc87aa87c..aad01d4fd1 100644 --- a/pkgs/cupertino_http/lib/src/utils.dart +++ b/pkgs/cupertino_http/lib/src/utils.dart @@ -2,6 +2,7 @@ // 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:convert'; import 'dart:ffi'; import 'dart:io'; @@ -90,3 +91,27 @@ ncb.NSArray stringIterableToNSArray(Iterable strings) { ncb.NSURL uriToNSURL(Uri uri) => ncb.NSURL .URLWithString_(linkedLibs, uri.toString().toNSString(linkedLibs))!; + + +/// Throw if the given close code is not valid according to RFC 6455. +/// See https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4 +void checkCloseCodeRfc(int? code) { + const reservedCloseCodes = [1004, 1005, 1006]; + if (code != null && + !(code >= 1000 && code <= 1011 && !reservedCloseCodes.contains(code)) && + !(code >= 3000 && code <= 4999)) { + throw ArgumentError( + 'Invalid argument: $code, close code must be in the range 1000-1011 or ' + 'in the range 3000-4999, and cannot be one of reserved codes ' + '(${reservedCloseCodes.join(', ')})', + ); + } +} + +/// Throw if the given close reason is not valid. +void checkCloseReason(String? reason) { + 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/web_socket/lib/src/browser_web_socket.dart b/pkgs/web_socket/lib/src/browser_web_socket.dart index fc66628b7a..f18b332b56 100644 --- a/pkgs/web_socket/lib/src/browser_web_socket.dart +++ b/pkgs/web_socket/lib/src/browser_web_socket.dart @@ -125,7 +125,7 @@ class BrowserWebSocket implements WebSocket { throw WebSocketConnectionClosed(); } - checkCloseCode(code); + checkCloseCodeWeb(code); checkCloseReason(reason); unawaited(_events.close()); diff --git a/pkgs/web_socket/lib/src/fake_web_socket.dart b/pkgs/web_socket/lib/src/fake_web_socket.dart index 1e4d07198a..c74e743502 100644 --- a/pkgs/web_socket/lib/src/fake_web_socket.dart +++ b/pkgs/web_socket/lib/src/fake_web_socket.dart @@ -23,7 +23,7 @@ class FakeWebSocket implements WebSocket { throw WebSocketConnectionClosed(); } - checkCloseCode(code); + checkCloseCodeWeb(code); checkCloseReason(reason); unawaited(_events.close()); diff --git a/pkgs/web_socket/lib/src/io_web_socket.dart b/pkgs/web_socket/lib/src/io_web_socket.dart index 7ea084bec3..f6bfe4a952 100644 --- a/pkgs/web_socket/lib/src/io_web_socket.dart +++ b/pkgs/web_socket/lib/src/io_web_socket.dart @@ -105,7 +105,7 @@ class IOWebSocket implements WebSocket { throw WebSocketConnectionClosed(); } - checkCloseCode(code); + checkCloseCodeRfc(code); checkCloseReason(reason); unawaited(_events.close()); diff --git a/pkgs/web_socket/lib/src/utils.dart b/pkgs/web_socket/lib/src/utils.dart index 7331840f62..19f8939d22 100644 --- a/pkgs/web_socket/lib/src/utils.dart +++ b/pkgs/web_socket/lib/src/utils.dart @@ -4,14 +4,37 @@ import 'dart:convert'; -/// Throw if the given close code is not valid. -void checkCloseCode(int? code) { +/// Throw if the given close code is not valid according to WHATWG spec. +/// +/// This is more suitable for clients running in web browsers. +/// +/// See https://websockets.spec.whatwg.org/#dom-websocket-close. +void checkCloseCodeWeb(int? code) { if (code != null && code != 1000 && !(code >= 3000 && code <= 4999)) { throw ArgumentError('Invalid argument: $code, close code must be 1000 or ' 'in the range 3000-4999'); } } +/// Throw if the given close code is not valid according to RFC 6455. +/// +/// This is more suitable for clients running in native environments, possibly +/// as a server endpoint. +/// +/// See https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4 +void checkCloseCodeRfc(int? code) { + const reservedCloseCodes = [1004, 1005, 1006]; + if (code != null && + !(code >= 1000 && code <= 1011 && !reservedCloseCodes.contains(code)) && + !(code >= 3000 && code <= 4999)) { + throw ArgumentError( + 'Invalid argument: $code, close code must be in the range 1000-1011 or ' + 'in the range 3000-4999, and cannot be one of reserved codes ' + '(${reservedCloseCodes.join(', ')})', + ); + } +} + /// Throw if the given close reason is not valid. void checkCloseReason(String? reason) { if (reason != null && utf8.encode(reason).length > 123) { diff --git a/pkgs/web_socket/lib/src/web_socket.dart b/pkgs/web_socket/lib/src/web_socket.dart index 255c6e6c6d..d938ad1561 100644 --- a/pkgs/web_socket/lib/src/web_socket.dart +++ b/pkgs/web_socket/lib/src/web_socket.dart @@ -150,7 +150,10 @@ abstract interface class WebSocket { /// [code] is set then the peer will see a 1005 status code. If no [reason] /// is set then the peer will not receive a reason string. /// - /// Throws an [ArgumentError] if [code] is not 1000 or in the range 3000-4999. + /// Throws an [ArgumentError] if [code] is not accepted by the implementation. + /// All implementations must accept [code] values of 1000, and in the range + /// 3000-4999. Some implementations may accept additional values, but must not + /// accept reserved codes 1004, 1005, 1006, and 1015. /// /// Throws an [ArgumentError] if [reason] is longer than 123 bytes when /// encoded as UTF-8 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 cf39f95eb3..f069da87b1 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 @@ -55,11 +55,16 @@ void testCloseLocal( httpServerChannel.sink.add(null); }); - test('reserved close code: 1004', () async { - final channel = await channelFactory(uri); - await expectLater( - () => channel.close(1004), throwsA(isA())); - }); + /// Codes 1004-1006 and 1015 cannot be used in close() calls. + for (final reservedCode in [1004, 1005, 1006, 1015]) { + test('reserved close code: $reservedCode', () async { + final channel = await channelFactory(uri); + await expectLater( + () => channel.close(reservedCode), + throwsA(isA()), + ); + }); + } test('reserved close code: 2999', () async { final channel = await channelFactory(uri);