a323c55162
Change-Id: I4d492a63a41880ef8cd69321372a4853825b9efd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426321 Reviewed-by: Liam Appelbe <liama@google.com> Auto-Submit: Bob Nystrom <rnystrom@google.com> Commit-Queue: Liam Appelbe <liama@google.com>
466 lines
14 KiB
Dart
466 lines
14 KiB
Dart
// 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();
|
|
}
|