/**
|
|
* Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
**/
|
|
|
|
var http = function() {
|
|
|
|
if (!chrome.sockets || !chrome.sockets.tcpServer)
|
|
return {};
|
|
|
|
// Wrap chrome.sockets.tcp socketId with a Promise API.
|
|
var PSocket = (function() {
|
|
// chrome.sockets.tcp uses a global listener for incoming data so
|
|
// use a map to dispatch to the proper instance.
|
|
var socketMap = {};
|
|
chrome.sockets.tcp.onReceive.addListener(function(info) {
|
|
var pSocket = socketMap[info.socketId];
|
|
if (pSocket) {
|
|
if (pSocket.handlers) {
|
|
// Fulfil the pending read.
|
|
pSocket.handlers.resolve(info.data);
|
|
delete pSocket.handlers;
|
|
}
|
|
else {
|
|
// No pending read so put data on the queue.
|
|
pSocket.readQueue.push(info);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Read errors also use a global listener.
|
|
chrome.sockets.tcp.onReceiveError.addListener(function(info) {
|
|
var pSocket = socketMap[info.socketId];
|
|
if (pSocket) {
|
|
if (pSocket.handlers) {
|
|
// Reject the pending read.
|
|
pSocket.handlers.reject(new Error('chrome.sockets.tcp error ' + info.resultCode));
|
|
delete pSocket.handlers;
|
|
}
|
|
else {
|
|
// No pending read so put data on the queue.
|
|
pSocket.readQueue.push(info);
|
|
}
|
|
}
|
|
});
|
|
|
|
// PSocket constructor.
|
|
return function(socketId) {
|
|
this.socketId = socketId;
|
|
this.readQueue = [];
|
|
|
|
// Register this instance for incoming data processing.
|
|
socketMap[socketId] = this;
|
|
chrome.sockets.tcp.setPaused(socketId, false);
|
|
};
|
|
})();
|
|
|
|
// Returns a Promise<ArrayBuffer> with read data.
|
|
PSocket.prototype.read = function() {
|
|
var that = this;
|
|
if (this.readQueue.length) {
|
|
// Return data from the queue.
|
|
var info = this.readQueue.shift();
|
|
if (!info.resultCode)
|
|
return Promise.resolve(info.data);
|
|
else
|
|
return Promise.reject(new Error('chrome.sockets.tcp error ' + info.resultCode));
|
|
}
|
|
else {
|
|
// The queue is empty so install handlers.
|
|
return new Promise(function(resolve, reject) {
|
|
that.handlers = { resolve: resolve, reject: reject };
|
|
});
|
|
}
|
|
};
|
|
|
|
// Returns a Promise<integer> with the number of bytes written.
|
|
PSocket.prototype.write = function(data) {
|
|
var that = this;
|
|
return new Promise(function(resolve, reject) {
|
|
chrome.sockets.tcp.send(that.socketId, data, function(info) {
|
|
if (info && info.resultCode >= 0)
|
|
resolve(info.bytesSent);
|
|
else
|
|
reject(new Error('chrome sockets.tcp error ' + (info && info.resultCode)));
|
|
});
|
|
});
|
|
};
|
|
|
|
// Returns a Promise.
|
|
PSocket.prototype.close = function() {
|
|
var that = this;
|
|
return new Promise(function(resolve, reject) {
|
|
chrome.sockets.tcp.disconnect(that.socketId, function() {
|
|
chrome.sockets.tcp.close(that.socketId, resolve);
|
|
});
|
|
});
|
|
};
|
|
|
|
// Http response code strings.
|
|
var responseMap = {
|
|
200: 'OK',
|
|
301: 'Moved Permanently',
|
|
304: 'Not Modified',
|
|
400: 'Bad Request',
|
|
401: 'Unauthorized',
|
|
403: 'Forbidden',
|
|
404: 'Not Found',
|
|
413: 'Request Entity Too Large',
|
|
414: 'Request-URI Too Long',
|
|
500: 'Internal Server Error'};
|
|
|
|
/**
|
|
* Convert from an ArrayBuffer to a string.
|
|
* @param {ArrayBuffer} buffer The array buffer to convert.
|
|
* @return {string} The textual representation of the array.
|
|
*/
|
|
var arrayBufferToString = function(buffer) {
|
|
var array = new Uint8Array(buffer);
|
|
var str = '';
|
|
for (var i = 0; i < array.length; ++i) {
|
|
str += String.fromCharCode(array[i]);
|
|
}
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* Convert from an UTF-8 array to UTF-8 string.
|
|
* @param {array} UTF-8 array
|
|
* @return {string} UTF-8 string
|
|
*/
|
|
var ary2utf8 = (function() {
|
|
|
|
var patterns = [
|
|
{pattern: '0xxxxxxx', bytes: 1},
|
|
{pattern: '110xxxxx', bytes: 2},
|
|
{pattern: '1110xxxx', bytes: 3},
|
|
{pattern: '11110xxx', bytes: 4},
|
|
{pattern: '111110xx', bytes: 5},
|
|
{pattern: '1111110x', bytes: 6}
|
|
];
|
|
patterns.forEach(function(item) {
|
|
item.header = item.pattern.replace(/[^10]/g, '');
|
|
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
|
|
item.pattern01 = parseInt(item.pattern01, 2);
|
|
item.mask_length = item.header.length;
|
|
item.data_length = 8 - item.header.length;
|
|
var mask = '';
|
|
for (var i = 0, len = item.mask_length; i < len; i++) {
|
|
mask += '1';
|
|
}
|
|
for (var i = 0, len = item.data_length; i < len; i++) {
|
|
mask += '0';
|
|
}
|
|
item.mask = mask;
|
|
item.mask = parseInt(item.mask, 2);
|
|
});
|
|
|
|
return function(ary) {
|
|
var codes = [];
|
|
var cur = 0;
|
|
while(cur < ary.length) {
|
|
var first = ary[cur];
|
|
var pattern = null;
|
|
for (var i = 0, len = patterns.length; i < len; i++) {
|
|
if ((first & patterns[i].mask) == patterns[i].pattern01) {
|
|
pattern = patterns[i];
|
|
break;
|
|
}
|
|
}
|
|
if (pattern == null) {
|
|
throw 'utf-8 decode error';
|
|
}
|
|
var rest = ary.slice(cur + 1, cur + pattern.bytes);
|
|
cur += pattern.bytes;
|
|
var code = '';
|
|
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
|
|
for (var i = 0, len = rest.length; i < len; i++) {
|
|
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
|
|
}
|
|
codes.push(parseInt(code, 2));
|
|
}
|
|
return String.fromCharCode.apply(null, codes);
|
|
};
|
|
|
|
})();
|
|
|
|
/**
|
|
* Convert from an UTF-8 string to UTF-8 array.
|
|
* @param {string} UTF-8 string
|
|
* @return {array} UTF-8 array
|
|
*/
|
|
var utf82ary = (function() {
|
|
|
|
var patterns = [
|
|
{pattern: '0xxxxxxx', bytes: 1},
|
|
{pattern: '110xxxxx', bytes: 2},
|
|
{pattern: '1110xxxx', bytes: 3},
|
|
{pattern: '11110xxx', bytes: 4},
|
|
{pattern: '111110xx', bytes: 5},
|
|
{pattern: '1111110x', bytes: 6}
|
|
];
|
|
patterns.forEach(function(item) {
|
|
item.header = item.pattern.replace(/[^10]/g, '');
|
|
item.mask_length = item.header.length;
|
|
item.data_length = 8 - item.header.length;
|
|
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
|
|
});
|
|
|
|
var code2utf8array = function(code) {
|
|
var pattern = null;
|
|
var code01 = code.toString(2);
|
|
for (var i = 0, len = patterns.length; i < len; i++) {
|
|
if (code01.length <= patterns[i].max_bit_length) {
|
|
pattern = patterns[i];
|
|
break;
|
|
}
|
|
}
|
|
if (pattern == null) {
|
|
throw 'utf-8 encode error';
|
|
}
|
|
var ary = [];
|
|
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
|
|
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
|
|
code01 = code01.slice(0, -6);
|
|
}
|
|
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
|
|
return ary;
|
|
};
|
|
|
|
return function(str) {
|
|
var codes = [];
|
|
for (var i = 0, len = str.length; i < len; i++) {
|
|
var code = str.charCodeAt(i);
|
|
Array.prototype.push.apply(codes, code2utf8array(code));
|
|
}
|
|
return codes;
|
|
};
|
|
|
|
})();
|
|
|
|
/**
|
|
* Convert a string to an ArrayBuffer.
|
|
* @param {string} string The string to convert.
|
|
* @return {ArrayBuffer} An array buffer whose bytes correspond to the string.
|
|
*/
|
|
var stringToArrayBuffer = function(string) {
|
|
var buffer = new ArrayBuffer(string.length);
|
|
var bufferView = new Uint8Array(buffer);
|
|
for (var i = 0; i < string.length; i++) {
|
|
bufferView[i] = string.charCodeAt(i);
|
|
}
|
|
return buffer;
|
|
};
|
|
|
|
/**
|
|
* An event source can dispatch events. These are dispatched to all of the
|
|
* functions listening for that event type with arguments.
|
|
* @constructor
|
|
*/
|
|
function EventSource() {
|
|
this.listeners_ = {};
|
|
};
|
|
|
|
EventSource.prototype = {
|
|
/**
|
|
* Add |callback| as a listener for |type| events.
|
|
* @param {string} type The type of the event.
|
|
* @param {function(Object|undefined): boolean} callback The function to call
|
|
* when this event type is dispatched. Arguments depend on the event
|
|
* source and type. The function returns whether the event was "handled"
|
|
* which will prevent delivery to the rest of the listeners.
|
|
*/
|
|
addEventListener: function(type, callback) {
|
|
if (!this.listeners_[type])
|
|
this.listeners_[type] = [];
|
|
this.listeners_[type].push(callback);
|
|
},
|
|
|
|
/**
|
|
* Remove |callback| as a listener for |type| events.
|
|
* @param {string} type The type of the event.
|
|
* @param {function(Object|undefined): boolean} callback The callback
|
|
* function to remove from the event listeners for events having type
|
|
* |type|.
|
|
*/
|
|
removeEventListener: function(type, callback) {
|
|
if (!this.listeners_[type])
|
|
return;
|
|
for (var i = this.listeners_[type].length - 1; i >= 0; i--) {
|
|
if (this.listeners_[type][i] == callback) {
|
|
this.listeners_[type].splice(i, 1);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Dispatch an event to all listeners for events of type |type|.
|
|
* @param {type} type The type of the event being dispatched.
|
|
* @param {...Object} var_args The arguments to pass when calling the
|
|
* callback function.
|
|
* @return {boolean} Returns true if the event was handled.
|
|
*/
|
|
dispatchEvent: function(type, var_args) {
|
|
if (!this.listeners_[type])
|
|
return false;
|
|
for (var i = 0; i < this.listeners_[type].length; i++) {
|
|
if (this.listeners_[type][i].apply(
|
|
/* this */ null,
|
|
/* var_args */ Array.prototype.slice.call(arguments, 1))) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* HttpServer provides a lightweight Http web server. Currently it only
|
|
* supports GET requests and upgrading to other protocols (i.e. WebSockets).
|
|
* @constructor
|
|
*/
|
|
function HttpServer() {
|
|
EventSource.apply(this);
|
|
this.readyState_ = 0;
|
|
}
|
|
|
|
HttpServer.prototype = {
|
|
__proto__: EventSource.prototype,
|
|
|
|
/**
|
|
* Listen for connections on |port| using the interface |host|.
|
|
* @param {number} port The port to listen for incoming connections on.
|
|
* @param {string=} opt_host The host interface to listen for connections on.
|
|
* This will default to 0.0.0.0 if not specified which will listen on
|
|
* all interfaces.
|
|
*/
|
|
listen: function(port, opt_host) {
|
|
var t = this;
|
|
chrome.sockets.tcpServer.create(function(socketInfo) {
|
|
chrome.sockets.tcpServer.onAccept.addListener(function(acceptInfo) {
|
|
if (acceptInfo.socketId === socketInfo.socketId)
|
|
t.readRequestFromSocket_(new PSocket(acceptInfo.clientSocketId));
|
|
});
|
|
this.socketId = socketInfo.socketId;
|
|
chrome.runtime.sendMessage({socketId: this.socketId});
|
|
chrome.sockets.tcpServer.listen(
|
|
socketInfo.socketId,
|
|
opt_host || '0.0.0.0',
|
|
port,
|
|
50,
|
|
function(result) {
|
|
if (!result) {
|
|
t.readyState_ = 1;
|
|
}
|
|
else {
|
|
console.log(
|
|
'listen error ' +
|
|
chrome.runtime.lastError.message +
|
|
' (normal if another instance is already serving requests)');
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
close: function() {
|
|
if (this.socketId) {
|
|
chrome.sockets.tcpServer.close(this.socketId);
|
|
this.socketId = 0;
|
|
}
|
|
},
|
|
readRequestFromSocket_: function(pSocket) {
|
|
var t = this;
|
|
var requestData = '';
|
|
var endIndex = 0;
|
|
var onDataRead = function(data) {
|
|
requestData += arrayBufferToString(data).replace(/\r\n/g, '\n');
|
|
// Check for end of request.
|
|
endIndex = requestData.indexOf('\n\n', endIndex);
|
|
if (endIndex == -1) {
|
|
endIndex = requestData.length - 1;
|
|
return pSocket.read().then(onDataRead);
|
|
}
|
|
|
|
var headers = requestData.substring(0, endIndex).split('\n');
|
|
var headerMap = {};
|
|
// headers[0] should be the Request-Line
|
|
var requestLine = headers[0].split(' ');
|
|
headerMap['method'] = requestLine[0];
|
|
headerMap['url'] = requestLine[1];
|
|
headerMap['Http-Version'] = requestLine[2];
|
|
for (var i = 1; i < headers.length; i++) {
|
|
requestLine = headers[i].split(':', 2);
|
|
if (requestLine.length == 2)
|
|
headerMap[requestLine[0]] = requestLine[1].trim();
|
|
}
|
|
var request = new HttpRequest(headerMap, pSocket);
|
|
t.onRequest_(request);
|
|
};
|
|
|
|
pSocket.read().then(onDataRead).catch(function(e) {
|
|
pSocket.close();
|
|
});
|
|
},
|
|
|
|
onRequest_: function(request) {
|
|
var type = request.headers['Upgrade'] ? 'upgrade' : 'request';
|
|
var keepAlive = request.headers['Connection'] == 'keep-alive';
|
|
if (!this.dispatchEvent(type, request))
|
|
request.close();
|
|
else if (keepAlive)
|
|
this.readRequestFromSocket_(request.pSocket_);
|
|
},
|
|
};
|
|
|
|
// MIME types for common extensions.
|
|
var extensionTypes = {
|
|
'css': 'text/css',
|
|
'html': 'text/html',
|
|
'htm': 'text/html',
|
|
'jpg': 'image/jpeg',
|
|
'jpeg': 'image/jpeg',
|
|
'js': 'text/javascript',
|
|
'png': 'image/png',
|
|
'svg': 'image/svg+xml',
|
|
'txt': 'text/plain'};
|
|
|
|
/**
|
|
* Constructs an HttpRequest object which tracks all of the request headers and
|
|
* socket for an active Http request.
|
|
* @param {Object} headers The HTTP request headers.
|
|
* @param {Object} pSocket The socket to use for the response.
|
|
* @constructor
|
|
*/
|
|
function HttpRequest(headers, pSocket) {
|
|
this.version = 'HTTP/1.1';
|
|
this.headers = headers;
|
|
this.responseHeaders_ = {};
|
|
this.headersSent = false;
|
|
this.pSocket_ = pSocket;
|
|
this.writes_ = 0;
|
|
this.bytesRemaining = 0;
|
|
this.finished_ = false;
|
|
this.readyState = 1;
|
|
}
|
|
|
|
HttpRequest.prototype = {
|
|
__proto__: EventSource.prototype,
|
|
|
|
/**
|
|
* Closes the Http request.
|
|
*/
|
|
close: function() {
|
|
// The socket for keep alive connections will be re-used by the server.
|
|
// Just stop referencing or using the socket in this HttpRequest.
|
|
if (this.headers['Connection'] != 'keep-alive')
|
|
pSocket.close();
|
|
|
|
this.pSocket_ = null;
|
|
this.readyState = 3;
|
|
},
|
|
|
|
/**
|
|
* Write the provided headers as a response to the request.
|
|
* @param {int} responseCode The HTTP status code to respond with.
|
|
* @param {Object} responseHeaders The response headers describing the
|
|
* response.
|
|
*/
|
|
writeHead: function(responseCode, responseHeaders) {
|
|
var headerString = this.version + ' ' + responseCode + ' ' +
|
|
(responseMap[responseCode] || 'Unknown');
|
|
this.responseHeaders_ = responseHeaders;
|
|
if (this.headers['Connection'] == 'keep-alive')
|
|
responseHeaders['Connection'] = 'keep-alive';
|
|
if (!responseHeaders['Content-Length'] && responseHeaders['Connection'] == 'keep-alive')
|
|
responseHeaders['Transfer-Encoding'] = 'chunked';
|
|
for (var i in responseHeaders) {
|
|
headerString += '\r\n' + i + ': ' + responseHeaders[i];
|
|
}
|
|
headerString += '\r\n\r\n';
|
|
this.write_(stringToArrayBuffer(headerString));
|
|
},
|
|
|
|
/**
|
|
* Writes data to the response stream.
|
|
* @param {string|ArrayBuffer} data The data to write to the stream.
|
|
*/
|
|
write: function(data) {
|
|
if (this.responseHeaders_['Transfer-Encoding'] == 'chunked') {
|
|
var newline = '\r\n';
|
|
var byteLength = (data instanceof ArrayBuffer) ? data.byteLength : data.length;
|
|
var chunkLength = byteLength.toString(16).toUpperCase() + newline;
|
|
var buffer = new ArrayBuffer(chunkLength.length + byteLength + newline.length);
|
|
var bufferView = new Uint8Array(buffer);
|
|
for (var i = 0; i < chunkLength.length; i++)
|
|
bufferView[i] = chunkLength.charCodeAt(i);
|
|
if (data instanceof ArrayBuffer) {
|
|
bufferView.set(new Uint8Array(data), chunkLength.length);
|
|
} else {
|
|
for (var i = 0; i < data.length; i++)
|
|
bufferView[chunkLength.length + i] = data.charCodeAt(i);
|
|
}
|
|
for (var i = 0; i < newline.length; i++)
|
|
bufferView[chunkLength.length + byteLength + i] = newline.charCodeAt(i);
|
|
data = buffer;
|
|
} else if (!(data instanceof ArrayBuffer)) {
|
|
data = stringToArrayBuffer(data);
|
|
}
|
|
this.write_(data);
|
|
},
|
|
|
|
/**
|
|
* Finishes the HTTP response writing |data| before closing.
|
|
* @param {string|ArrayBuffer=} opt_data Optional data to write to the stream
|
|
* before closing it.
|
|
*/
|
|
end: function(opt_data) {
|
|
if (opt_data)
|
|
this.write(opt_data);
|
|
if (this.responseHeaders_['Transfer-Encoding'] == 'chunked')
|
|
this.write('');
|
|
this.finished_ = true;
|
|
this.checkFinished_();
|
|
},
|
|
|
|
/**
|
|
* Automatically serve the given |url| request.
|
|
* @param {string} url The URL to fetch the file to be served from. This is
|
|
* retrieved via an XmlHttpRequest and served as the response to the
|
|
* request.
|
|
*/
|
|
serveUrl: function(url) {
|
|
var t = this;
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onloadend = function() {
|
|
var type = 'text/plain';
|
|
if (this.getResponseHeader('Content-Type')) {
|
|
type = this.getResponseHeader('Content-Type');
|
|
} else if (url.indexOf('.') != -1) {
|
|
var extension = url.substr(url.indexOf('.') + 1);
|
|
type = extensionTypes[extension] || type;
|
|
}
|
|
console.log('Served ' + url);
|
|
var contentLength = this.getResponseHeader('Content-Length');
|
|
if (xhr.status == 200)
|
|
contentLength = (this.response && this.response.byteLength) || 0;
|
|
t.writeHead(this.status, {
|
|
'Content-Type': type,
|
|
'Content-Length': contentLength});
|
|
t.end(this.response);
|
|
};
|
|
xhr.open('GET', url, true);
|
|
xhr.responseType = 'arraybuffer';
|
|
xhr.send();
|
|
},
|
|
|
|
write_: function(array) {
|
|
var t = this;
|
|
this.bytesRemaining += array.byteLength;
|
|
this.pSocket_.write(array).then(function(bytesWritten) {
|
|
t.bytesRemaining -= bytesWritten;
|
|
t.checkFinished_();
|
|
}).catch(function(e) {
|
|
console.error(e.message);
|
|
return;
|
|
});
|
|
},
|
|
|
|
checkFinished_: function() {
|
|
if (!this.finished_ || this.bytesRemaining > 0)
|
|
return;
|
|
this.close();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Constructs a server which is capable of accepting WebSocket connections.
|
|
* @param {HttpServer} httpServer The Http Server to listen and handle
|
|
* WebSocket upgrade requests on.
|
|
* @constructor
|
|
*/
|
|
function WebSocketServer(httpServer) {
|
|
EventSource.apply(this);
|
|
httpServer.addEventListener('upgrade', this.upgradeToWebSocket_.bind(this));
|
|
}
|
|
|
|
WebSocketServer.prototype = {
|
|
__proto__: EventSource.prototype,
|
|
|
|
upgradeToWebSocket_: function(request) {
|
|
if (request.headers['Upgrade'] != 'websocket' ||
|
|
!request.headers['Sec-WebSocket-Key']) {
|
|
return false;
|
|
}
|
|
|
|
if (this.dispatchEvent('request', new WebSocketRequest(request))) {
|
|
if (request.pSocket_)
|
|
request.reject();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Constructs a WebSocket request object from an Http request. This invalidates
|
|
* the Http request's socket and offers accept and reject methods for accepting
|
|
* and rejecting the WebSocket upgrade request.
|
|
* @param {HttpRequest} httpRequest The HTTP request to upgrade.
|
|
*/
|
|
function WebSocketRequest(httpRequest) {
|
|
// We'll assume control of the socket for this request.
|
|
HttpRequest.apply(this, [httpRequest.headers, httpRequest.pSocket_]);
|
|
httpRequest.pSocket_ = null;
|
|
}
|
|
|
|
WebSocketRequest.prototype = {
|
|
__proto__: HttpRequest.prototype,
|
|
|
|
/**
|
|
* Accepts the WebSocket request.
|
|
* @return {WebSocketServerSocket} The websocket for the accepted request.
|
|
*/
|
|
accept: function() {
|
|
// Construct WebSocket response key.
|
|
var clientKey = this.headers['Sec-WebSocket-Key'];
|
|
var toArray = function(str) {
|
|
var a = [];
|
|
for (var i = 0; i < str.length; i++) {
|
|
a.push(str.charCodeAt(i));
|
|
}
|
|
return a;
|
|
}
|
|
var toString = function(a) {
|
|
var str = '';
|
|
for (var i = 0; i < a.length; i++) {
|
|
str += String.fromCharCode(a[i]);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
// Magic string used for websocket connection key hashing:
|
|
// http://en.wikipedia.org/wiki/WebSocket
|
|
var magicStr = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
|
|
// clientKey is base64 encoded key.
|
|
clientKey += magicStr;
|
|
var sha1 = new Sha1();
|
|
sha1.reset();
|
|
sha1.update(toArray(clientKey));
|
|
var responseKey = btoa(toString(sha1.digest()));
|
|
var responseHeader = {
|
|
'Upgrade': 'websocket',
|
|
'Connection': 'Upgrade',
|
|
'Sec-WebSocket-Accept': responseKey};
|
|
if (this.headers['Sec-WebSocket-Protocol'])
|
|
responseHeader['Sec-WebSocket-Protocol'] = this.headers['Sec-WebSocket-Protocol'];
|
|
this.writeHead(101, responseHeader);
|
|
var socket = new WebSocketServerSocket(this.pSocket_);
|
|
// Detach the socket so that we don't use it anymore.
|
|
this.pSocket_ = 0;
|
|
return socket;
|
|
},
|
|
|
|
/**
|
|
* Rejects the WebSocket request, closing the connection.
|
|
*/
|
|
reject: function() {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs a WebSocketServerSocket using the given socketId. This should be
|
|
* a socket which has already been upgraded from an Http request.
|
|
* @param {number} socketId The socket id with an active websocket connection.
|
|
*/
|
|
function WebSocketServerSocket(pSocket) {
|
|
this.pSocket_ = pSocket;
|
|
this.readyState = 1;
|
|
EventSource.apply(this);
|
|
this.readFromSocket_();
|
|
}
|
|
|
|
WebSocketServerSocket.prototype = {
|
|
__proto__: EventSource.prototype,
|
|
|
|
/**
|
|
* Send |data| on the WebSocket.
|
|
* @param {string|Array.<number>|ArrayBuffer} data The data to send over the WebSocket.
|
|
*/
|
|
send: function(data) {
|
|
// WebSocket must specify opcode when send frame.
|
|
// The opcode for data frame is 1(text) or 2(binary).
|
|
if (typeof data == 'string' || data instanceof String) {
|
|
this.sendFrame_(1, data);
|
|
} else {
|
|
this.sendFrame_(2, data);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Begin closing the WebSocket. Note that the WebSocket protocol uses a
|
|
* handshake to close the connection, so this call will begin the closing
|
|
* process.
|
|
*/
|
|
close: function() {
|
|
if (this.readyState === 1) {
|
|
this.sendFrame_(8);
|
|
this.readyState = 2;
|
|
}
|
|
},
|
|
|
|
readFromSocket_: function() {
|
|
var t = this;
|
|
var data = [];
|
|
var message = '';
|
|
var fragmentedOp = 0;
|
|
var fragmentedMessages = [];
|
|
|
|
var onDataRead = function(dataBuffer) {
|
|
var a = new Uint8Array(dataBuffer);
|
|
for (var i = 0; i < a.length; i++)
|
|
data.push(a[i]);
|
|
|
|
while (data.length) {
|
|
var length_code = -1;
|
|
var data_start = 6;
|
|
var mask;
|
|
var fin = (data[0] & 128) >> 7;
|
|
var op = data[0] & 15;
|
|
|
|
if (data.length > 1)
|
|
length_code = data[1] & 127;
|
|
if (length_code > 125) {
|
|
if ((length_code == 126 && data.length > 7) ||
|
|
(length_code == 127 && data.length > 14)) {
|
|
if (length_code == 126) {
|
|
length_code = data[2] * 256 + data[3];
|
|
mask = data.slice(4, 8);
|
|
data_start = 8;
|
|
} else if (length_code == 127) {
|
|
length_code = 0;
|
|
for (var i = 0; i < 8; i++) {
|
|
length_code = length_code * 256 + data[2 + i];
|
|
}
|
|
mask = data.slice(10, 14);
|
|
data_start = 14;
|
|
}
|
|
} else {
|
|
length_code = -1; // Insufficient data to compute length
|
|
}
|
|
} else {
|
|
if (data.length > 5)
|
|
mask = data.slice(2, 6);
|
|
}
|
|
|
|
if (length_code > -1 && data.length >= data_start + length_code) {
|
|
var decoded = data.slice(data_start, data_start + length_code).map(function(byte, index) {
|
|
return byte ^ mask[index % 4];
|
|
});
|
|
if (op == 1) {
|
|
decoded = ary2utf8(decoded);
|
|
}
|
|
data = data.slice(data_start + length_code);
|
|
if (fin && op > 0) {
|
|
// Unfragmented message.
|
|
if (!t.onFrame_(op, decoded))
|
|
return;
|
|
} else {
|
|
// Fragmented message.
|
|
fragmentedOp = fragmentedOp || op;
|
|
fragmentedMessages.push(decoded);
|
|
if (fin) {
|
|
var joinMessage = null;
|
|
if (op == 1) {
|
|
joinMessage = fragmentedMessagess.join('');
|
|
} else {
|
|
joinMessage = fragmentedMessages.reduce(function(pre, cur) {
|
|
return Array.prototype.push.apply(pre, cur);
|
|
}, []);
|
|
}
|
|
if (!t.onFrame_(fragmentedOp, joinMessage))
|
|
return;
|
|
fragmentedOp = 0;
|
|
fragmentedMessages = [];
|
|
}
|
|
}
|
|
} else {
|
|
break; // Insufficient data, wait for more.
|
|
}
|
|
}
|
|
|
|
return t.pSocket_.read().then(onDataRead);
|
|
};
|
|
|
|
this.pSocket_.read().then(function(data) {
|
|
return onDataRead(data);
|
|
}).catch(function(e) {
|
|
t.close_();
|
|
});
|
|
},
|
|
|
|
onFrame_: function(op, data) {
|
|
if (op == 1 || op == 2) {
|
|
if (typeof data == 'string' || data instanceof String) {
|
|
// Don't do anything.
|
|
} else if (Array.isArray(data)) {
|
|
data = new Uint8Array(data).buffer;
|
|
} else if (data instanceof ArrayBuffer) {
|
|
// Don't do anything.
|
|
} else {
|
|
data = data.buffer;
|
|
}
|
|
this.dispatchEvent('message', {'data': data});
|
|
} else if (op == 8) {
|
|
// A close message must be confirmed before the websocket is closed.
|
|
if (this.readyState === 1) {
|
|
this.sendFrame_(8);
|
|
this.readyState = 2;
|
|
} else {
|
|
this.close_();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
sendFrame_: function(op, data) {
|
|
var t = this;
|
|
var WebsocketFrameData = function(op, data) {
|
|
var ary = data;
|
|
if (typeof data == 'string' || data instanceof String) {
|
|
ary = utf82ary(data);
|
|
}
|
|
if (Array.isArray(ary)) {
|
|
ary = new Uint8Array(ary);
|
|
}
|
|
if (ary instanceof ArrayBuffer) {
|
|
ary = new Uint8Array(ary);
|
|
}
|
|
ary = new Uint8Array(ary.buffer);
|
|
var length = ary.length;
|
|
if (ary.length > 65535)
|
|
length += 10;
|
|
else if (ary.length > 125)
|
|
length += 4;
|
|
else
|
|
length += 2;
|
|
var lengthBytes = 0;
|
|
var buffer = new ArrayBuffer(length);
|
|
var bv = new Uint8Array(buffer);
|
|
bv[0] = 128 | (op & 15); // Fin and type text.
|
|
bv[1] = ary.length > 65535 ? 127 :
|
|
(ary.length > 125 ? 126 : ary.length);
|
|
if (ary.length > 65535)
|
|
lengthBytes = 8;
|
|
else if (ary.length > 125)
|
|
lengthBytes = 2;
|
|
var len = ary.length;
|
|
for (var i = lengthBytes - 1; i >= 0; i--) {
|
|
bv[2 + i] = len & 255;
|
|
len = len >> 8;
|
|
}
|
|
var dataStart = lengthBytes + 2;
|
|
for (var i = 0; i < ary.length; i++) {
|
|
bv[dataStart + i] = ary[i];
|
|
}
|
|
return buffer;
|
|
}
|
|
var array = WebsocketFrameData(op, data || '');
|
|
this.pSocket_.write(array).then(function(bytesWritten) {
|
|
if (bytesWritten !== array.byteLength)
|
|
throw new Error('insufficient write');
|
|
}).catch(function(e) {
|
|
t.close_();
|
|
});
|
|
},
|
|
|
|
close_: function() {
|
|
if (this.readyState !== 3) {
|
|
this.pSocket_.close();
|
|
this.readyState = 3;
|
|
this.dispatchEvent('close');
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
'Server': HttpServer,
|
|
'WebSocketServer': WebSocketServer,
|
|
};
|
|
}();
|