// 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: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 createServer({int backlog: 0}) => secure ? HttpServer.bindSecure(HOST_NAME, 0, serverContext, backlog: backlog) : HttpServer.bind(HOST_NAME, 0, backlog: backlog); Future createClient(int port) => // TODO(whesse): Add client context argument to WebSocket.connect WebSocket.connect('${secure ? "wss" : "ws"}://$HOST_NAME:$port/'); Future 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.scheme == "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(); }