Files
sdk/runtime/bin/websocket_impl.dart
T
sgjesse@google.com 2eb76caab4 Add support for websocket PING and PONG frames
The handling of control frames have been refactored to uniformly
hanlde CLOSE, PING and PONG frames.

Currently this is not tested as I have bot found a good way to test it
yet. It has been somewhat tested with a browser client forcing ping
messages from the server and processing the pong responses from the
browser.

R=ager@google.com, ajohnsen@google.com

BUG=none
TEST=none

Review URL: https://chromiumcodereview.appspot.com//10392195

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@7844 260f80e4-7a28-3924-810f-c04153c831b5
2012-05-22 10:34:47 +00:00

891 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.
final String _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
class _WebSocketMessageType {
static final int NONE = 0;
static final int BINARY = 1;
static final int TEXT = 2;
}
class _WebSocketOpcode {
static final int CONTINUATION = 0;
static final int TEXT = 1;
static final int BINARY = 2;
static final int RESERVED_3 = 3;
static final int RESERVED_4 = 4;
static final int RESERVED_5 = 5;
static final int RESERVED_6 = 6;
static final int RESERVED_7 = 7;
static final int CLOSE = 8;
static final int PING = 9;
static final int PONG = 10;
static final int RESERVED_B = 11;
static final int RESERVED_C = 12;
static final int RESERVED_D = 13;
static final int RESERVED_E = 14;
static final 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 final int START = 0;
static final int LEN_FIRST = 1;
static final int LEN_REST = 2;
static final int MASK = 3;
static final int PAYLOAD = 4;
static final int CLOSED = 5;
static final 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");
break;
}
_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");
break;
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");
break;
}
}
// Hack - as we always do index++ below.
index--;
}
// Move to the next byte.
index++;
}
} catch (var e) {
_reportError(e);
}
}
/**
* Indicate that the underlying communication channel has been closed.
*/
void closed() {
if (_state == START || _state == CLOSED || _state == FAILURE) return;
_reportError(new WebSocketException("Protocol error $_state"));
_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(null, null);
_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;
String reason;
if (_controlPayload.length > 0) {
if (_controlPayload.length == 1) {
throw new WebSocketException("Protocol error");
}
status = _controlPayload[0] << 8 | _controlPayload[1];
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;
}
void _reportError(e) {
// Report the error through the error callback if any. Otherwise
// throw the error.
if (onError != null) {
onError(e);
_state = FAILURE;
} else {
throw e;
}
}
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;
Function onError;
}
class _WebSocketConnectionBase {
void _socketConnected(Socket socket) {
_socket = socket;
_socket.onError = (e) {
_reportError(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;
processor.onError = _onWebSocketError;
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 {
_reportError(new WebSocketException("Unexpected close"));
}
_socket.close();
};
}
void set onMessage(void callback(Object message)) {
_onMessage = callback;
}
void set onClosed(void callback(int status, String reason)) {
_onClosed = callback;
}
void set onError(void callback(e)) {
_onError = 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 (_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.contents());
}
}
_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 {
close(status);
}
}
_onWebSocketError(e) {
_reportError(e);
_socket.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);
}
}
void _reportError(e) {
if (_onError != null) {
_onError(e);
} else {
throw e;
}
}
Socket _socket;
Timer _closeTimer;
int _hash;
Function _onMessage;
Function _onClosed;
Function _onError;
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) => _reportError(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() {
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 = (Math.random() * 0x100000000).toInt();
intToBigEndianBytes(r, nonce, i * 4);
}
_nonce = _Base64._encode(nonce);
_hash = (Math.random() * 0x100000000).toInt();
}
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 (_onerror != null) _onerror("Failed web socket connection");
};
_wsconn.onError = (e) {
if (_onerror != null) _onerror(e);
};
}
int get readyState() => _readyState;
int get bufferedAmount() => 0;
void set onopen(Function callback) {
_onopen = callback;
}
void set onerror(Function callback) {
_onerror = 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 _onerror;
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;
}