blob: c62a5219d6579c66f866d36129bc55c713ef954a [file] [log] [blame]
// 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();
}