|  | // 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 | 
|  |  | 
|  | import "dart:async"; | 
|  | import "dart:convert"; | 
|  | import "dart:io"; | 
|  | import "dart:math"; | 
|  | import "dart:typed_data"; | 
|  |  | 
|  | import "package:crypto/crypto.dart"; | 
|  | import "package:expect/async_helper.dart"; | 
|  | import "package:expect/expect.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(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.compressionDefault, | 
|  | CompressionOptions clientOpts = CompressionOptions.compressionDefault, | 
|  | 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")!; | 
|  |  | 
|  | 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(false).runTests(); | 
|  | // TODO(whesse): Make WebSocket.connect() take an optional context: parameter. | 
|  | // new SecurityConfiguration(true).runTests(); | 
|  | } |