Files
sdk/runtime/bin/websocket_impl.dart
T
sgjesse@google.com b0790fa96d Never send reserved status codes in a close frame
When receiving a close frame with no status code the reported status
code is 1005. If the close is echoed as part of the close handshake
the echoed close frame must also have no status code and NOT status
code 1005.

The description of status code 1005 from the spec:

1005 is a reserved value and MUST NOT be set as a status code in a
Close control frame by an endpoint.  It is designated for use in
applications expecting a status code to indicate that no status
code was actually present.

R=ajohnsen@google.com

BUG=dart:5209

Review URL: https://codereview.chromium.org//10946007

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@12482 260f80e4-7a28-3924-810f-c04153c831b5
2012-09-18 10:27:47 +00:00

874 lines
25 KiB
Dart

// Copyright (c) 2012, 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.
const String _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
class _WebSocketMessageType {
static const int NONE = 0;
static const int BINARY = 1;
static const int TEXT = 2;
}
class _WebSocketOpcode {
static const int CONTINUATION = 0;
static const int TEXT = 1;
static const int BINARY = 2;
static const int RESERVED_3 = 3;
static const int RESERVED_4 = 4;
static const int RESERVED_5 = 5;
static const int RESERVED_6 = 6;
static const int RESERVED_7 = 7;
static const int CLOSE = 8;
static const int PING = 9;
static const int PONG = 10;
static const int RESERVED_B = 11;
static const int RESERVED_C = 12;
static const int RESERVED_D = 13;
static const int RESERVED_E = 14;
static const int RESERVED_F = 15;
}
/**
* The web socket protocol processor handles the protocol byte stream
* which is supplied through the [:update:] and [:closed:]
* methods. As the protocol is processed the following callbacks are
* called:
*
* [:onMessageStart:]
* [:onMessageData:]
* [:onMessageEnd:]
* [:onClosed:]
* [:onError:]
*
*/
class _WebSocketProtocolProcessor {
static const int START = 0;
static const int LEN_FIRST = 1;
static const int LEN_REST = 2;
static const int MASK = 3;
static const int PAYLOAD = 4;
static const int CLOSED = 5;
static const int FAILURE = 6;
_WebSocketProtocolProcessor() {
_prepareForNextFrame();
_currentMessageType = _WebSocketMessageType.NONE;
}
/**
* Process data received from the underlying communication channel.
*/
void update(List<int> buffer, int offset, int count) {
int index = offset;
int lastIndex = offset + count;
try {
if (_state == CLOSED) {
throw new WebSocketException("Data on closed connection");
}
if (_state == FAILURE) {
throw new WebSocketException("Data on failed connection");
}
while ((index < lastIndex) && _state != CLOSED && _state != FAILURE) {
int byte = buffer[index];
switch (_state) {
case START:
_fin = (byte & 0x80) != 0;
_opcode = (byte & 0xF);
switch (_opcode) {
case _WebSocketOpcode.CONTINUATION:
if (_currentMessageType == _WebSocketMessageType.NONE) {
throw new WebSocketException("Protocol error");
}
break;
case _WebSocketOpcode.TEXT:
if (_currentMessageType != _WebSocketMessageType.NONE) {
throw new WebSocketException("Protocol error");
}
_currentMessageType = _WebSocketMessageType.TEXT;
if (onMessageStart !== null) {
onMessageStart(_WebSocketMessageType.TEXT);
}
break;
case _WebSocketOpcode.BINARY:
if (_currentMessageType != _WebSocketMessageType.NONE) {
throw new WebSocketException("Protocol error");
}
_currentMessageType = _WebSocketMessageType.BINARY;
if (onMessageStart !== null) {
onMessageStart(_WebSocketMessageType.BINARY);
}
break;
case _WebSocketOpcode.CLOSE:
case _WebSocketOpcode.PING:
case _WebSocketOpcode.PONG:
// Control frames cannot be fragmented.
if (!_fin) throw new WebSocketException("Protocol error");
break;
default:
throw new WebSocketException("Protocol error");
}
_state = LEN_FIRST;
break;
case LEN_FIRST:
_masked = (byte & 0x80) != 0;
_len = byte & 0x7F;
if (_isControlFrame() && _len > 126) {
throw new WebSocketException("Protocol error");
}
if (_len < 126) {
_lengthDone();
} else if (_len == 126) {
_len = 0;
_remainingLenBytes = 2;
_state = LEN_REST;
} else if (_len == 127) {
_len = 0;
_remainingLenBytes = 8;
_state = LEN_REST;
}
break;
case LEN_REST:
_len = _len << 8 | byte;
_remainingLenBytes--;
if (_remainingLenBytes == 0) {
_lengthDone();
}
break;
case MASK:
_maskingKey = _maskingKey << 8 | byte;
_remainingMaskingKeyBytes--;
if (_remainingMaskingKeyBytes == 0) {
_maskDone();
}
break;
case PAYLOAD:
// The payload is not handled one byte at a time but in blocks.
int payload;
if (lastIndex - index <= _remainingPayloadBytes) {
payload = lastIndex - index;
} else {
payload = _remainingPayloadBytes;
}
_remainingPayloadBytes -= payload;
// Unmask payload if masked.
if (_masked) {
for (int i = 0; i < payload; i++) {
int maskingByte =
((_maskingKey >> ((3 - _unmaskingIndex) * 8)) & 0xFF);
buffer[index + i] = buffer[index + i] ^ maskingByte;
_unmaskingIndex = (_unmaskingIndex + 1) % 4;
}
}
if (_isControlFrame()) {
if (payload > 0) {
// Allocate a buffer for collecting the control frame
// payload if any.
if (_controlPayload == null) {
_controlPayload = new List<int>();
}
_controlPayload.addAll(buffer.getRange(index, payload));
index += payload;
}
if (_remainingPayloadBytes == 0) {
_controlFrameEnd();
}
} else {
switch (_currentMessageType) {
case _WebSocketMessageType.NONE:
throw new WebSocketException("Protocol error");
case _WebSocketMessageType.TEXT:
case _WebSocketMessageType.BINARY:
if (onMessageData !== null) {
onMessageData(buffer, index, payload);
}
index += payload;
if (_remainingPayloadBytes == 0) {
_messageFrameEnd();
}
break;
default:
throw new WebSocketException("Protocol error");
}
}
// Hack - as we always do index++ below.
index--;
}
// Move to the next byte.
index++;
}
} catch (e) {
if (onClosed !== null) onClosed(WebSocketStatus.PROTOCOL_ERROR,
"Protocol error");
_state = FAILURE;
}
}
/**
* Indicate that the underlying communication channel has been closed.
*/
void closed() {
if (_state == START || _state == CLOSED || _state == FAILURE) return;
if (onClosed !== null) onClosed(WebSocketStatus.ABNORMAL_CLOSURE,
"Connection closed unexpectedly");
_state = CLOSED;
}
void _lengthDone() {
if (_masked) {
_state = MASK;
_remainingMaskingKeyBytes = 4;
} else {
_remainingPayloadBytes = _len;
_startPayload();
}
}
void _maskDone() {
_remainingPayloadBytes = _len;
_startPayload();
}
void _startPayload() {
// If there is no actual payload perform perform callbacks without
// going through the PAYLOAD state.
if (_remainingPayloadBytes == 0) {
if (_isControlFrame()) {
switch (_opcode) {
case _WebSocketOpcode.CLOSE:
if (onClosed !== null) onClosed(1005, "");
_state = CLOSED;
break;
case _WebSocketOpcode.PING:
if (onPing !== null) onPing(null);
break;
case _WebSocketOpcode.PONG:
if (onPong !== null) onPong(null);
break;
}
_prepareForNextFrame();
} else {
_messageFrameEnd();
}
} else {
_state = PAYLOAD;
}
}
void _messageFrameEnd() {
if (_fin) {
if (onMessageEnd !== null) onMessageEnd();
_currentMessageType = _WebSocketMessageType.NONE;
}
_prepareForNextFrame();
}
void _controlFrameEnd() {
switch (_opcode) {
case _WebSocketOpcode.CLOSE:
int status = WebSocketStatus.NO_STATUS_RECEIVED;
String reason = "";
if (_controlPayload.length > 0) {
if (_controlPayload.length == 1) {
throw new WebSocketException("Protocol error");
}
status = _controlPayload[0] << 8 | _controlPayload[1];
if (status == WebSocketStatus.NO_STATUS_RECEIVED) {
throw new WebSocketException("Protocol error");
}
if (_controlPayload.length > 2) {
var decoder = _StringDecoders.decoder(Encoding.UTF_8);
decoder.write(
_controlPayload.getRange(2, _controlPayload.length - 2));
reason = decoder.decoded();
}
}
if (onClosed !== null) onClosed(status, reason);
_state = CLOSED;
break;
case _WebSocketOpcode.PING:
if (onPing !== null) onPing(_controlPayload);
break;
case _WebSocketOpcode.PONG:
if (onPong !== null) onPong(_controlPayload);
break;
}
_prepareForNextFrame();
}
bool _isControlFrame() {
return _opcode == _WebSocketOpcode.CLOSE ||
_opcode == _WebSocketOpcode.PING ||
_opcode == _WebSocketOpcode.PONG;
}
void _prepareForNextFrame() {
if (_state != CLOSED && _state != FAILURE) _state = START;
_fin = null;
_opcode = null;
_len = null;
_masked = null;
_maskingKey = 0;
_remainingLenBytes = null;
_remainingMaskingKeyBytes = null;
_remainingPayloadBytes = null;
_unmaskingIndex = 0;
_controlPayload = null;
}
int _state;
bool _fin;
int _opcode;
int _len;
bool _masked;
int _maskingKey;
int _remainingLenBytes;
int _remainingMaskingKeyBytes;
int _remainingPayloadBytes;
int _unmaskingIndex;
int _currentMessageType;
List<int> _controlPayload;
Function onMessageStart;
Function onMessageData;
Function onMessageEnd;
Function onPing;
Function onPong;
Function onClosed;
}
class _WebSocketConnectionBase {
void _socketConnected(Socket socket) {
_socket = socket;
_socket.onError = (e) => _socket.close();
}
void _startProcessing(List<int> unparsedData) {
_WebSocketProtocolProcessor processor = new _WebSocketProtocolProcessor();
processor.onMessageStart = _onWebSocketMessageStart;
processor.onMessageData = _onWebSocketMessageData;
processor.onMessageEnd = _onWebSocketMessageEnd;
processor.onPing = _onWebSocketPing;
processor.onPong = _onWebSocketPong;
processor.onClosed = _onWebSocketClosed;
if (unparsedData !== null) {
processor.update(unparsedData, 0, unparsedData.length);
}
_socket.onData = () {
int available = _socket.available();
List<int> data = new List<int>(available);
int read = _socket.readList(data, 0, available);
processor.update(data, 0, read);
};
_socket.onClosed = () {
processor.closed();
if (_closeSent) {
// Got socket close in response to close frame. Don't treat
// that as an error.
if (_closeTimer !== null) _closeTimer.cancel();
} else {
if (_onClosed !== null) _onClosed(WebSocketStatus.ABNORMAL_CLOSURE,
"Unexpected close");
}
_socket.close();
};
}
void set onMessage(void callback(Object message)) {
_onMessage = callback;
}
void set onClosed(void callback(int status, String reason)) {
_onClosed = callback;
}
send(message) {
if (_closeSent) {
throw new WebSocketException("Connection closed");
}
List<int> data;
int opcode;
if (message !== null) {
if (message is String) {
opcode = _WebSocketOpcode.TEXT;
data = _StringEncoders.encoder(Encoding.UTF_8).encodeString(message);
} else {
if (message is !List<int>) {
throw new IllegalArgumentException(message);
}
opcode = _WebSocketOpcode.BINARY;
data = message;
}
} else {
opcode = _WebSocketOpcode.TEXT;
}
_sendFrame(opcode, data);
}
close([int status, String reason]) {
if (status == WebSocketStatus.RESERVED_1004 ||
status == WebSocketStatus.NO_STATUS_RECEIVED ||
status == WebSocketStatus.RESERVED_1015) {
throw new WebSocketException("Reserved status code $status");
}
if (_closeSent) return;
List<int> data;
if (status !== null) {
data = new List<int>();
data.add((status >> 8) & 0xFF);
data.add(status & 0xFF);
if (reason !== null) {
data.addAll(
_StringEncoders.encoder(Encoding.UTF_8).encodeString(reason));
}
}
_sendFrame(_WebSocketOpcode.CLOSE, data);
if (_closeReceived) {
// Close the socket when the close frame has been sent - if it
// does not take too long.
_socket.outputStream.close();
_socket.outputStream.onClosed = () {
if (_closeTimer !== null) _closeTimer.cancel();
_socket.close();
};
_closeTimer = new Timer(5000, (t) {
_socket.close();
});
} else {
// Half close the socket and expect a close frame in response
// before closing the socket. If a close frame does not arrive
// within a reasonable amount of time just close the socket.
_socket.outputStream.close();
_closeTimer = new Timer(5000, (t) {
_socket.close();
});
}
_closeSent = true;
}
int hashCode() => _hash;
_onWebSocketMessageStart(int type) {
_currentMessageType = type;
if (_currentMessageType == _WebSocketMessageType.TEXT) {
_decoder = _StringDecoders.decoder(Encoding.UTF_8);
} else {
_outputStream = new ListOutputStream();
}
}
_onWebSocketMessageData(List<int> buffer, int offset, int count) {
if (_currentMessageType == _WebSocketMessageType.TEXT) {
_decoder.write(buffer.getRange(offset, count));
} else {
_outputStream.write(buffer.getRange(offset, count));
}
}
_onWebSocketMessageEnd() {
if (_onMessage !== null) {
if (_currentMessageType == _WebSocketMessageType.TEXT) {
_onMessage(_decoder.decoded());
} else {
_onMessage(_outputStream.read());
}
}
_decoder = null;
_outputStream = null;
}
_onWebSocketPing(List<int> payload) {
_sendFrame(_WebSocketOpcode.PONG, payload);
}
_onWebSocketPong(List<int> payload) {
// Currently pong messages are ignored.
}
_onWebSocketClosed(int status, String reason) {
_closeReceived = true;
if (_onClosed !== null) _onClosed(status, reason);
if (_closeSent) {
// Got close frame in response to close frame. Now close the socket.
if (_closeTimer !== null) _closeTimer.cancel();
_socket.close();
} else {
if (status != WebSocketStatus.NO_STATUS_RECEIVED) {
close(status);
} else {
close();
}
}
}
_sendFrame(int opcode, [List<int> data]) {
bool mask = false; // Masking not implemented for server.
int dataLength = data == null ? 0 : data.length;
// Determine the header size.
int headerSize = (mask) ? 6 : 2;
if (dataLength > 65535) {
headerSize += 8;
} else if (dataLength > 125) {
headerSize += 2;
}
List<int> header = new List<int>(headerSize);
int index = 0;
// Set FIN and opcode.
header[index++] = 0x80 | opcode;
// Determine size and position of length field.
int lengthBytes = 1;
int firstLengthByte = 1;
if (dataLength > 65535) {
header[index++] = 127;
lengthBytes = 8;
} else if (dataLength > 125) {
header[index++] = 126;
lengthBytes = 2;
}
// Write the length in network byte order into the header.
for (int i = 0; i < lengthBytes; i++) {
header[index++] = dataLength >> (((lengthBytes - 1) - i) * 8) & 0xFF;
}
assert(index == headerSize);
_socket.outputStream.write(header);
if (data !== null) {
_socket.outputStream.write(data);
}
}
Socket _socket;
Timer _closeTimer;
int _hash;
Function _onMessage;
Function _onClosed;
int _currentMessageType = _WebSocketMessageType.NONE;
_StringDecoder _decoder;
ListOutputStream _outputStream;
bool _closeReceived = false;
bool _closeSent = false;
}
class _WebSocketConnection
extends _WebSocketConnectionBase implements WebSocketConnection {
_WebSocketConnection(DetachedSocket detached) {
_hash = detached.socket.hashCode();
_socketConnected(detached.socket);
_startProcessing(detached.unparsedData);
}
}
class _WebSocketHandler implements WebSocketHandler {
void onRequest(HttpRequest request, HttpResponse response) {
// Check that this is a web socket upgrade.
if (!_isWebSocketUpgrade(request)) {
response.statusCode = HttpStatus.BAD_REQUEST;
response.outputStream.close();
return;
}
// Send the upgrade response.
response.statusCode = HttpStatus.SWITCHING_PROTOCOLS;
response.headers.add(HttpHeaders.CONNECTION, "Upgrade");
response.headers.add(HttpHeaders.UPGRADE, "websocket");
String key = request.headers.value("Sec-WebSocket-Key");
SHA1 sha1 = new SHA1();
sha1.update("$key$_webSocketGUID".charCodes());
String accept = _Base64._encode(sha1.digest());
response.headers.add("Sec-WebSocket-Accept", accept);
response.contentLength = 0;
// Upgrade the connection and get the underlying socket.
WebSocketConnection conn =
new _WebSocketConnection(response.detachSocket());
if (_onOpen !== null) _onOpen(conn);
}
void set onOpen(callback(WebSocketConnection connection)) {
_onOpen = callback;
}
bool _isWebSocketUpgrade(HttpRequest request) {
if (request.method != "GET") {
return false;
}
if (request.headers[HttpHeaders.CONNECTION] == null) {
return false;
}
bool isUpgrade = false;
request.headers[HttpHeaders.CONNECTION].forEach((String value) {
if (value.toLowerCase() == "upgrade") isUpgrade = true;
});
if (!isUpgrade) return false;
String upgrade = request.headers.value(HttpHeaders.UPGRADE);
if (upgrade == null || upgrade.toLowerCase() != "websocket") {
return false;
}
String version = request.headers.value("Sec-WebSocket-Version");
if (version == null || version != "13") {
return false;
}
String key = request.headers.value("Sec-WebSocket-Key");
if (key == null) {
return false;
}
return true;
}
Function _onOpen;
}
class _WebSocketClientConnection
extends _WebSocketConnectionBase implements WebSocketClientConnection {
_WebSocketClientConnection(HttpClientConnection this._conn,
[List<String> protocols]) {
_conn.onRequest = _onHttpClientRequest;
_conn.onResponse = _onHttpClientResponse;
_conn.onError = (e) {
if (_onClosed !== null) {
_onClosed(WebSocketStatus.ABNORMAL_CLOSURE, "$e");
}
};
// Generate the nonce now as it is also used to set the hash code.
_generateNonceAndHash();
}
void set onRequest(void callback(HttpClientRequest request)) {
_onRequest = callback;
}
void set onOpen(void callback()) {
_onOpen = callback;
}
void set onNoUpgrade(void callback(HttpClientResponse request)) {
_onNoUpgrade = callback;
}
void _onHttpClientRequest(HttpClientRequest request) {
if (_onRequest !== null) {
_onRequest(request);
}
// Setup the initial handshake.
request.headers.add(HttpHeaders.CONNECTION, "upgrade");
request.headers.set(HttpHeaders.UPGRADE, "websocket");
request.headers.set("Sec-WebSocket-Key", _nonce);
request.headers.set("Sec-WebSocket-Version", "13");
request.contentLength = 0;
request.outputStream.close();
}
void _onHttpClientResponse(HttpClientResponse response) {
if (response.statusCode != HttpStatus.SWITCHING_PROTOCOLS) {
if (_onNoUpgrade !== null) {
_onNoUpgrade(response);
} else {
_conn.detachSocket().socket.close();
throw new WebSocketException("Protocol upgrade refused");
}
return;
}
if (!_isWebSocketUpgrade(response)) {
_conn.detachSocket().socket.close();
throw new WebSocketException("Protocol upgrade failed");
return;
}
// Connection upgrade successful.
DetachedSocket detached = _conn.detachSocket();
_socketConnected(detached.socket);
if (_onOpen !== null) _onOpen();
_startProcessing(detached.unparsedData);
}
void _generateNonceAndHash() {
Random random = new Random();
assert(_nonce == null);
void intToBigEndianBytes(int value, List<int> bytes, int offset) {
bytes[offset] = (value >> 24) & 0xFF;
bytes[offset + 1] = (value >> 16) & 0xFF;
bytes[offset + 2] = (value >> 8) & 0xFF;
bytes[offset + 3] = value & 0xFF;
}
// Generate 16 random bytes. Use the last four bytes for the hash code.
List<int> nonce = new List<int>(16);
for (int i = 0; i < 4; i++) {
int r = random.nextInt(0x100000000);
intToBigEndianBytes(r, nonce, i * 4);
}
_nonce = _Base64._encode(nonce);
_hash = random.nextInt(0x100000000);
}
bool _isWebSocketUpgrade(HttpClientResponse response) {
if (response.headers[HttpHeaders.CONNECTION] == null) {
return false;
}
bool isUpgrade = false;
response.headers[HttpHeaders.CONNECTION].forEach((String value) {
if (value.toLowerCase() == "upgrade") isUpgrade = true;
});
if (!isUpgrade) return false;
String upgrade = response.headers.value(HttpHeaders.UPGRADE);
if (upgrade == null || upgrade.toLowerCase() != "websocket") {
return false;
}
String accept = response.headers.value("Sec-WebSocket-Accept");
if (accept == null) {
return false;
}
SHA1 sha1 = new SHA1();
sha1.update("$_nonce$_webSocketGUID".charCodes());
List<int> expectedAccept = sha1.digest();
List<int> receivedAccept = _Base64._decode(accept);
if (expectedAccept.length != receivedAccept.length) return false;
for (int i = 0; i < expectedAccept.length; i++) {
if (expectedAccept[i] != receivedAccept[i]) return false;
}
return true;
}
Function _onRequest;
Function _onOpen;
Function _onNoUpgrade;
HttpClientConnection _conn;
String _nonce;
}
class _WebSocket implements WebSocket {
_WebSocket(String url, [protocols]) {
Uri uri = new Uri.fromString(url);
if (uri.scheme != "ws") {
throw new WebSocketException("Unsupported URL scheme ${uri.scheme}");
}
if (uri.userInfo != "") {
throw new WebSocketException("Unsupported user info ${uri.userInfo}");
}
int port = uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : uri.port;
String path;
if (uri.query != "") {
if (uri.fragment != "") {
path = "${uri.path}?${uri.query}#${uri.fragment}";
} else {
path = "${uri.path}?${uri.query}";
}
} else {
path = uri.path;
}
HttpClient client = new HttpClient();
HttpClientConnection conn = client.open("GET", uri.domain, port, path);
if (protocols is String) protocols = [protocols];
_wsconn = new WebSocketClientConnection(conn, protocols);
_wsconn.onOpen = () {
// HTTP client not needed after socket have been detached.
client.shutdown();
client = null;
_readyState = WebSocket.OPEN;
if (_onopen !== null) _onopen();
};
_wsconn.onMessage = (message) {
if (_onmessage !== null) {
_onmessage(new _WebSocketMessageEvent(message));
}
};
_wsconn.onClosed = (status, reason) {
_readyState = WebSocket.CLOSED;
if (_onclose !== null) {
_onclose(new _WebSocketCloseEvent(true, status, reason));
}
};
_wsconn.onNoUpgrade = (response) {
if (_onclose !== null) {
_onclose(
new _WebSocketCloseEvent(true,
WebSocketStatus.ABNORMAL_CLOSURE,
"Connection not upgraded"));
}
};
}
int get readyState => _readyState;
int get bufferedAmount => 0;
void set onopen(Function callback) {
_onopen = callback;
}
void set onerror(Function callback) {}
void set onclose(Function callback) {
_onclose = callback;
}
String get extensions => null;
String get protocol => null;
void close(int code, String reason) {
if (_readyState < WebSocket.CLOSING) _readyState = WebSocket.CLOSING;
_wsconn.close(code, reason);
}
void set onmessage(Function callback) {
_onmessage = callback;
}
void send(data) {
_wsconn.send(data);
}
WebSocketClientConnection _wsconn;
int _readyState = WebSocket.CONNECTING;
Function _onopen;
Function _onclose;
Function _onmessage;
}
class _WebSocketMessageEvent implements MessageEvent {
_WebSocketMessageEvent(this._data);
get data => _data;
var _data;
}
class _WebSocketCloseEvent implements CloseEvent {
_WebSocketCloseEvent(this._wasClean, this._code, this._reason);
bool get wasClean => _wasClean;
int get code => _code;
String get reason => _reason;
bool _wasClean;
int _code;
String _reason;
}