| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| // |
| // VMOptions= |
| // VMOptions=--short_socket_read |
| // VMOptions=--short_socket_write |
| // VMOptions=--short_socket_read --short_socket_write |
| |
| // @dart = 2.9 |
| |
| import "dart:async"; |
| import "dart:convert"; |
| import "dart:io"; |
| import "dart:typed_data"; |
| import "dart:math"; |
| |
| import "package:async_helper/async_helper.dart"; |
| import "package:crypto/crypto.dart"; |
| import "package:expect/expect.dart"; |
| import "package:path/path.dart"; |
| |
| const WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; |
| |
| const String HOST_NAME = 'localhost'; |
| |
| String localFile(path) => Platform.script.resolve(path).toFilePath(); |
| |
| SecurityContext serverContext = new SecurityContext() |
| ..useCertificateChain(localFile('certificates/server_chain.pem')) |
| ..usePrivateKey(localFile('certificates/server_key.pem'), |
| password: 'dartdart'); |
| |
| class SecurityConfiguration { |
| final bool secure; |
| |
| SecurityConfiguration({bool this.secure}); |
| |
| Future<HttpServer> createServer({int backlog: 0}) => secure |
| ? HttpServer.bindSecure(HOST_NAME, 0, serverContext, backlog: backlog) |
| : HttpServer.bind(HOST_NAME, 0, backlog: backlog); |
| |
| Future<WebSocket> createClient(int port) => |
| // TODO(whesse): Add client context argument to WebSocket.connect |
| WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/'); |
| |
| Future<HttpClientResponse> createWebsocket(String url, String headerValue) { |
| HttpClient _httpClient = new HttpClient(); |
| Uri uri = Uri.parse(url); |
| |
| Random random = new Random(); |
| // Generate 16 random bytes. |
| Uint8List nonceData = new Uint8List(16); |
| for (int i = 0; i < 16; i++) { |
| nonceData[i] = random.nextInt(256); |
| } |
| String nonce = base64.encode(nonceData); |
| |
| uri = new Uri( |
| scheme: uri.isScheme("wss") ? "https" : "http", |
| userInfo: uri.userInfo, |
| host: uri.host, |
| port: uri.port, |
| path: uri.path, |
| query: uri.query, |
| fragment: uri.fragment); |
| return _httpClient.openUrl("GET", uri).then((request) { |
| if (uri.userInfo != null && !uri.userInfo.isEmpty) { |
| // If the URL contains user information use that for basic |
| // authorization. |
| String auth = base64.encode(utf8.encode(uri.userInfo)); |
| request.headers.set(HttpHeaders.authorizationHeader, "Basic $auth"); |
| } |
| // Setup the initial handshake. |
| request.headers |
| ..set(HttpHeaders.connectionHeader, "Upgrade") |
| ..set(HttpHeaders.upgradeHeader, "websocket") |
| ..set("Sec-WebSocket-Key", nonce) |
| ..set("Cache-Control", "no-cache") |
| ..set("Sec-WebSocket-Version", "13") |
| ..set("Sec-WebSocket-Extensions", headerValue); |
| |
| return request.close(); |
| }); |
| } |
| |
| void testCompressionSupport( |
| {server: false, client: false, contextTakeover: false}) { |
| asyncStart(); |
| |
| var clientOptions = new CompressionOptions( |
| enabled: client, |
| serverNoContextTakeover: contextTakeover, |
| clientNoContextTakeover: contextTakeover); |
| var serverOptions = new CompressionOptions( |
| enabled: server, |
| serverNoContextTakeover: contextTakeover, |
| clientNoContextTakeover: contextTakeover); |
| |
| createServer().then((server) { |
| server.listen((request) { |
| Expect.isTrue(WebSocketTransformer.isUpgradeRequest(request)); |
| WebSocketTransformer.upgrade(request, compression: serverOptions) |
| .then((webSocket) { |
| webSocket.listen((message) { |
| Expect.equals("Hello World", message); |
| |
| webSocket.add(message); |
| webSocket.close(); |
| }); |
| webSocket.add("Hello World"); |
| }); |
| }); |
| |
| var url = '${secure ? "wss" : "ws"}://$HOST_NAME:${server.port}/'; |
| WebSocket.connect(url, compression: clientOptions).then((websocket) { |
| var future = websocket.listen((message) { |
| Expect.equals("Hello World", message); |
| }).asFuture(); |
| websocket.add("Hello World"); |
| return future; |
| }).then((_) { |
| server.close(); |
| asyncEnd(); |
| }); |
| }); |
| } |
| |
| void testContextSupport( |
| {CompressionOptions serverOpts, |
| CompressionOptions clientOpts, |
| int messages}) { |
| asyncStart(); |
| |
| createServer().then((server) { |
| server.listen((request) { |
| Expect.isTrue(WebSocketTransformer.isUpgradeRequest(request)); |
| WebSocketTransformer.upgrade(request, compression: serverOpts) |
| .then((webSocket) { |
| webSocket.listen((message) { |
| Expect.equals("Hello World", message); |
| webSocket.add(message); |
| }); |
| }); |
| }); |
| |
| var url = '${secure ? "wss" : "ws"}://$HOST_NAME:${server.port}/'; |
| WebSocket.connect(url, compression: clientOpts).then((websocket) { |
| var i = 1; |
| websocket.listen((message) { |
| Expect.equals("Hello World", message); |
| if (i == messages) { |
| websocket.close(); |
| return; |
| } |
| websocket.add("Hello World"); |
| i++; |
| }, onDone: () { |
| server.close(); |
| asyncEnd(); |
| }); |
| websocket.add("Hello World"); |
| }); |
| }); |
| } |
| |
| void testCompressionHeaders() { |
| asyncStart(); |
| createServer().then((server) { |
| server.listen((request) { |
| Expect.equals( |
| 'Upgrade', request.headers.value(HttpHeaders.connectionHeader)); |
| Expect.equals( |
| 'websocket', request.headers.value(HttpHeaders.upgradeHeader)); |
| |
| 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) |
| ..headers.add( |
| "Sec-WebSocket-Extensions", |
| "permessage-deflate;" |
| // Test quoted values and space padded = |
| 'server_max_window_bits="10"; client_max_window_bits = 12' |
| 'client_no_context_takeover; server_no_context_takeover'); |
| request.response.contentLength = 0; |
| request.response.detachSocket().then((socket) { |
| return new WebSocket.fromUpgradedSocket(socket, serverSide: true); |
| }).then((websocket) { |
| websocket.add("Hello"); |
| websocket.close(); |
| asyncEnd(); |
| }); |
| }); |
| |
| var url = '${secure ? "wss" : "ws"}://$HOST_NAME:${server.port}/'; |
| |
| WebSocket.connect(url).then((websocket) { |
| return websocket.listen((message) { |
| Expect.equals("Hello", message); |
| websocket.close(); |
| }).asFuture(); |
| }).then((_) => server.close()); |
| }); |
| } |
| |
| void testReturnHeaders(String headerValue, String expected, |
| {CompressionOptions serverCompression: |
| CompressionOptions.compressionDefault}) { |
| asyncStart(); |
| createServer().then((server) { |
| server.listen((request) { |
| // Stuff |
| Expect.isTrue(WebSocketTransformer.isUpgradeRequest(request)); |
| WebSocketTransformer.upgrade(request, compression: serverCompression) |
| .then((webSocket) { |
| webSocket.listen((message) { |
| Expect.equals("Hello World", message); |
| |
| webSocket.add(message); |
| webSocket.close(); |
| }); |
| }); |
| }); |
| |
| var url = '${secure ? "wss" : "ws"}://$HOST_NAME:${server.port}/'; |
| createWebsocket(url, headerValue).then((HttpClientResponse response) { |
| Expect.equals(response.statusCode, HttpStatus.switchingProtocols); |
| print(response.headers.value('Sec-WebSocket-Extensions')); |
| Expect.equals( |
| response.headers.value("Sec-WebSocket-Extensions"), expected); |
| |
| String accept = response.headers.value("Sec-WebSocket-Accept"); |
| Expect.isNotNull(accept); |
| |
| var protocol = response.headers.value('Sec-WebSocket-Protocol'); |
| return response.detachSocket().then((socket) => |
| new WebSocket.fromUpgradedSocket(socket, |
| protocol: protocol, serverSide: false)); |
| }).then((websocket) { |
| var future = websocket.listen((message) { |
| Expect.equals("Hello", message); |
| websocket.close(); |
| }).asFuture(); |
| websocket.add("Hello World"); |
| return future; |
| }).then((_) { |
| server.close(); |
| asyncEnd(); |
| }); |
| }); // End createServer |
| } |
| |
| void testClientRequestHeaders(CompressionOptions compression) { |
| asyncStart(); |
| createServer().then((server) { |
| server.listen((request) { |
| var extensionHeader = request.headers.value('Sec-WebSocket-Extensions'); |
| var hv = HeaderValue.parse(extensionHeader); |
| Expect.equals(compression.serverNoContextTakeover, |
| hv.parameters.containsKey('server_no_context_takeover')); |
| Expect.equals(compression.clientNoContextTakeover, |
| hv.parameters.containsKey('client_no_context_takeover')); |
| Expect.equals(compression.serverMaxWindowBits?.toString(), |
| hv.parameters['server_max_window_bits']); |
| Expect.equals(compression.clientMaxWindowBits?.toString(), |
| hv.parameters['client_max_window_bits']); |
| |
| WebSocketTransformer.upgrade(request).then((webSocket) { |
| webSocket.listen((message) { |
| Expect.equals('Hello World', message); |
| |
| webSocket.add(message); |
| webSocket.close(); |
| }); |
| }); |
| }); |
| |
| var url = '${secure ? "wss" : "ws"}://$HOST_NAME:${server.port}/'; |
| |
| WebSocket.connect(url, compression: compression).then((websocket) { |
| var future = websocket.listen((message) { |
| Expect.equals('Hello World', message); |
| websocket.close(); |
| }).asFuture(); |
| websocket.add('Hello World'); |
| return future; |
| }).then((_) { |
| server.close(); |
| asyncEnd(); |
| }); |
| }); |
| } |
| |
| void runTests() { |
| // No compression or takeover |
| testCompressionSupport(); |
| // compression no takeover |
| testCompressionSupport(server: true, client: true); |
| // compression and context takeover. |
| testCompressionSupport(server: true, client: true, contextTakeover: true); |
| // Compression on client but not server. No take over |
| testCompressionSupport(client: true); |
| // Compression on server but not client. |
| testCompressionSupport(server: true); |
| |
| // Test Multiple messages with various context takeover configurations. |
| // no context takeover on the server. |
| var serverComp = new CompressionOptions(serverNoContextTakeover: true); |
| testContextSupport( |
| serverOpts: serverComp, clientOpts: serverComp, messages: 5); |
| // no contexttakeover on the client. |
| var clientComp = new CompressionOptions(clientNoContextTakeover: true); |
| testContextSupport( |
| serverOpts: clientComp, clientOpts: clientComp, messages: 5); |
| // no context takeover enabled for both. |
| var compression = new CompressionOptions( |
| serverNoContextTakeover: true, clientNoContextTakeover: true); |
| testContextSupport( |
| serverOpts: compression, clientOpts: compression, messages: 5); |
| // no context take over for opposing configurations. |
| testContextSupport( |
| serverOpts: serverComp, clientOpts: clientComp, messages: 5); |
| testContextSupport( |
| serverOpts: clientComp, clientOpts: serverComp, messages: 5); |
| |
| testCompressionHeaders(); |
| // Chrome headers |
| testReturnHeaders('permessage-deflate; client_max_window_bits', |
| "permessage-deflate; client_max_window_bits=15"); |
| // Firefox headers |
| testReturnHeaders( |
| 'permessage-deflate', "permessage-deflate; client_max_window_bits=15"); |
| // Ensure max_window_bits resize appropriately. |
| testReturnHeaders( |
| 'permessage-deflate; server_max_window_bits=10', |
| "permessage-deflate;" |
| " server_max_window_bits=10;" |
| " client_max_window_bits=10"); |
| // Don't provider context takeover if requested but not enabled. |
| // Default is not enabled. |
| testReturnHeaders( |
| 'permessage-deflate; client_max_window_bits;' |
| 'client_no_context_takeover', |
| 'permessage-deflate; client_max_window_bits=15'); |
| // Enable context Takeover and provide if requested. |
| compression = new CompressionOptions( |
| clientNoContextTakeover: true, serverNoContextTakeover: true); |
| testReturnHeaders( |
| 'permessage-deflate; client_max_window_bits; ' |
| 'client_no_context_takeover', |
| 'permessage-deflate; client_no_context_takeover; ' |
| 'client_max_window_bits=15', |
| serverCompression: compression); |
| // Enable context takeover and don't provide if not requested |
| compression = new CompressionOptions( |
| clientNoContextTakeover: true, serverNoContextTakeover: true); |
| testReturnHeaders('permessage-deflate; client_max_window_bits; ', |
| 'permessage-deflate; client_max_window_bits=15', |
| serverCompression: compression); |
| |
| compression = CompressionOptions.compressionDefault; |
| testClientRequestHeaders(compression); |
| compression = new CompressionOptions( |
| clientNoContextTakeover: true, serverNoContextTakeover: true); |
| testClientRequestHeaders(compression); |
| compression = new CompressionOptions( |
| clientNoContextTakeover: true, |
| serverNoContextTakeover: true, |
| clientMaxWindowBits: 8, |
| serverMaxWindowBits: 8); |
| testClientRequestHeaders(compression); |
| } |
| } |
| |
| main() { |
| new SecurityConfiguration(secure: false).runTests(); |
| // TODO(whesse): Make WebSocket.connect() take an optional context: parameter. |
| // new SecurityConfiguration(secure: true).runTests(); |
| } |