node-http-proxy/vendor/websocket.js

636 lines
20 KiB
JavaScript

/*
* Copyright (c) 2010, Peter Griess <pg@std.in>
* https://github.com/pgriess/node-websocket-client
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of node-websocket-client nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
var assert = require('assert');
var buffer = require('buffer');
var crypto = require('crypto');
var events = require('events');
var http = require('http');
var https = require('https');
var net = require('net');
var urllib = require('url');
var sys = require('sys');
var FRAME_NO = 0;
var FRAME_LO = 1;
var FRAME_HI = 2;
// Values for readyState as per the W3C spec
var CONNECTING = 0;
var OPEN = 1;
var CLOSING = 2;
var CLOSED = 3;
var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
var debug = (debugLevel & 0x4) ?
function() { sys.error.apply(this, arguments); } :
function() { };
// Generate a Sec-WebSocket-* value
var createSecretKey = function() {
// How many spaces will we be inserting?
var numSpaces = 1 + Math.floor(Math.random() * 12);
assert.ok(1 <= numSpaces && numSpaces <= 12);
// What is the numerical value of our key?
var keyVal = (Math.floor(
Math.random() * (4294967295 / numSpaces)
) * numSpaces);
// Our string starts with a string representation of our key
var s = keyVal.toString();
// Insert 'numChars' worth of noise in the character ranges
// [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters)
var numChars = 1 + Math.floor(Math.random() * 12);
assert.ok(1 <= numChars && numChars <= 12);
for (var i = 0; i < numChars; i++) {
var pos = Math.floor(Math.random() * s.length + 1);
var c = Math.floor(Math.random() * (14 + 68));
c = (c <= 14) ?
String.fromCharCode(c + 0x21) :
String.fromCharCode((c - 14) + 0x3a);
s = s.substring(0, pos) + c + s.substring(pos, s.length);
}
// We shoudln't have any spaces in our value until we insert them
assert.equal(s.indexOf(' '), -1);
// Insert 'numSpaces' worth of spaces
for (var i = 0; i < numSpaces; i++) {
var pos = Math.floor(Math.random() * (s.length - 1)) + 1;
s = s.substring(0, pos) + ' ' + s.substring(pos, s.length);
}
assert.notEqual(s.charAt(0), ' ');
assert.notEqual(s.charAt(s.length), ' ');
return s;
};
// Generate a challenge sequence
var createChallenge = function() {
var c = '';
for (var i = 0; i < 8; i++) {
c += String.fromCharCode(Math.floor(Math.random() * 255));
}
return c;
};
// Get the value of a secret key string
//
// This strips non-digit values and divides the result by the number of
// spaces found.
var secretKeyValue = function(sk) {
var ns = 0;
var v = 0;
for (var i = 0; i < sk.length; i++) {
var cc = sk.charCodeAt(i);
if (cc == 0x20) {
ns++;
} else if (0x30 <= cc && cc <= 0x39) {
v = v * 10 + cc - 0x30;
}
}
return Math.floor(v / ns);
}
// Get the to-be-hashed value of a secret key string
//
// This takes the result of secretKeyValue() and encodes it in a big-endian
// byte string
var secretKeyHashValue = function(sk) {
var skv = secretKeyValue(sk);
var hv = '';
hv += String.fromCharCode((skv >> 24) & 0xff);
hv += String.fromCharCode((skv >> 16) & 0xff);
hv += String.fromCharCode((skv >> 8) & 0xff);
hv += String.fromCharCode((skv >> 0) & 0xff);
return hv;
};
// Compute the secret key signature based on two secret key strings and some
// handshaking data.
var computeSecretKeySignature = function(s1, s2, hs) {
assert.equal(hs.length, 8);
var hash = crypto.createHash('md5');
hash.update(secretKeyHashValue(s1));
hash.update(secretKeyHashValue(s2));
hash.update(hs);
return hash.digest('binary');
};
// Return a hex representation of the given binary string; used for debugging
var str2hex = function(str) {
var hexChars = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'
];
var out = '';
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
out += hexChars[(c & 0xf0) >>> 4];
out += hexChars[c & 0x0f];
out += ' ';
}
return out.trim();
};
// Set a constant on the given object
var setConstant = function(obj, name, value) {
Object.defineProperty(obj, name, {
get : function() {
return value;
}
});
};
// WebSocket object
//
// This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/
//
// N.B. Arguments are parsed in the anonymous function at the bottom of the
// constructor.
var WebSocket = function(url, proto, opts) {
events.EventEmitter.call(this);
// Retain a reference to our object
var self = this;
// State of our end of the connection
var readyState = CONNECTING;
// Whether or not the server has sent a close handshake
var serverClosed = false;
// Our underlying net.Stream instance
var stream = undefined;
opts = opts || {
origin : 'http://www.example.com'
};
// Frame parsing functions
//
// These read data from the given buffer starting at the given offset,
// looking for the end of the current frame. If found, the current frame is
// emitted and the function returns. Only a single frame is processed at a
// time.
//
// The number of bytes read to complete a frame is returned, which the
// caller is to use to advance along its buffer. If 0 is returned, no
// completed frame bytes were found, and the caller should probably enqueue
// the buffer as a continuation of the current message. If a complete frame
// is read, the function is responsible for resting 'frameType'.
// Framing data
var frameType = FRAME_NO;
var bufs = [];
var bufsBytes = 0;
// Frame-parsing functions
var frameFuncs = [
// FRAME_NO
function(buf, off) {
if (buf[off] & 0x80) {
frameType = FRAME_HI;
} else {
frameType = FRAME_LO;
}
return 1;
},
// FRAME_LO
function(buf, off) {
debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')');
// Find the first instance of 0xff, our terminating byte
for (var i = off; i < buf.length && buf[i] != 0xff; i++)
;
// We didn't find a terminating byte
if (i >= buf.length) {
return 0;
}
// We found a terminating byte; collect all bytes into a single buffer
// and emit it
var mb = null;
if (bufs.length == 0) {
mb = buf.slice(off, i);
} else {
mb = new buffer.Buffer(bufsBytes + i);
var mbOff = 0;
bufs.forEach(function(b) {
b.copy(mb, mbOff, 0, b.length);
mbOff += b.length;
});
assert.equal(mbOff, bufsBytes);
// Don't call Buffer.copy() if we're coping 0 bytes. Rather
// than being a no-op, this will trigger a range violation on
// the destination.
if (i > 0) {
buf.copy(mb, mbOff, off, i);
}
// We consumed all of the buffers that we'd been saving; clear
// things out
bufs = [];
bufsBytes = 0;
}
process.nextTick(function() {
var b = mb;
return function() {
var m = b.toString('utf8');
self.emit('data', b);
self.emit('message', m); // wss compat
if (self.onmessage) {
self.onmessage({data: m});
}
};
}());
frameType = FRAME_NO;
return i - off + 1;
},
// FRAME_HI
function(buf, off) {
debug('frame_hi(' + sys.inspect(buf) + ', ' + off + ')');
if (buf[off] !== 0) {
throw new Error('High-byte framing not supported.');
}
serverClosed = true;
return 1;
}
];
// Handle data coming from our socket
var dataListener = function(buf) {
if (buf.length <= 0 || serverClosed) {
return;
}
debug('dataListener(' + sys.inspect(buf) + ')');
var off = 0;
var consumed = 0;
do {
if (frameType < 0 || frameFuncs.length <= frameType) {
throw new Error('Unexpected frame type: ' + frameType);
}
assert.equal(bufs.length === 0, bufsBytes === 0);
assert.ok(off < buf.length);
consumed = frameFuncs[frameType](buf, off);
off += consumed;
} while (!serverClosed && consumed > 0 && off < buf.length);
if (serverClosed) {
serverCloseHandler();
}
if (consumed == 0) {
bufs.push(buf.slice(off, buf.length));
bufsBytes += buf.length - off;
}
};
// Handle incoming file descriptors
var fdListener = function(fd) {
self.emit('fd', fd);
};
// Handle errors from any source (HTTP client, stream, etc)
var errorListener = function(e) {
process.nextTick(function() {
self.emit('wserror', e);
if (self.onerror) {
self.onerror(e);
}
});
};
// Finish the closing process; destroy the socket and tell the application
// that we've closed.
var finishClose = function() {
readyState = CLOSED;
if (stream) {
stream.end();
stream.destroy();
stream = undefined;
}
process.nextTick(function() {
self.emit('close');
if (self.onclose) {
self.onclose();
}
});
};
// Send a close frame to the server
var sendClose = function() {
assert.equal(OPEN, readyState);
readyState = CLOSING;
stream.write('\xff\x00', 'binary');
};
// Handle a close packet sent from the server
var serverCloseHandler = function() {
assert.ok(serverClosed);
assert.ok(readyState === OPEN || readyState === CLOSING);
bufs = [];
bufsBytes = 0;
// Handle state transitions asynchronously so that we don't change
// readyState before the application has had a chance to process data
// events which are already in the delivery pipeline. For example, a
// 'data' event could be delivered with a readyState of CLOSING if we
// received both frames in the same packet.
process.nextTick(function() {
if (readyState === OPEN) {
sendClose();
}
finishClose();
});
};
// External API
self.close = function(timeout) {
if (readyState === CONNECTING) {
// If we're still in the process of connecting, the server is not
// in a position to understand our close frame. Just nuke the
// connection and call it a day.
finishClose();
} else if (readyState === OPEN) {
sendClose();
if (timeout) {
setTimeout(finishClose, timeout * 1000);
}
}
};
self.send = function(str, fd) {
if (readyState != OPEN) {
return;
}
stream.write('\x00', 'binary');
stream.write(str, 'utf8', fd);
stream.write('\xff', 'binary');
};
// wss compat
self.write = self.send;
setConstant(self, 'url', url);
Object.defineProperty(self, 'readyState', {
get : function() {
return readyState;
}
});
// Connect and perform handshaking with the server
(function() {
// Parse constructor arguments
if (!url) {
throw new Error('Url and must be specified.');
}
// Secrets used for handshaking
var key1 = createSecretKey();
var key2 = createSecretKey();
var challenge = createChallenge();
debug(
'key1=\'' + str2hex(key1) + '\'; ' +
'key2=\'' + str2hex(key2) + '\'; ' +
'challenge=\'' + str2hex(challenge) + '\''
);
var httpHeaders = {
'Connection' : 'Upgrade',
'Upgrade' : 'WebSocket',
'Sec-WebSocket-Key1' : key1,
'Sec-WebSocket-Key2' : key2
};
if (opts.origin) {
httpHeaders['Origin'] = opts.origin;
}
if (proto) {
httpHeaders['Sec-WebSocket-Protocol'] = proto;
}
var httpPath = '/';
// Create the HTTP client that we'll use for handshaking. We'll cannabalize
// its socket via the 'upgrade' event and leave it to rot.
//
// N.B. The ws+unix:// scheme makes use of the implementation detail
// that http.Client passes its constructor arguments through,
// un-inspected to net.Stream.connect(). The latter accepts a
// string as its first argument to connect to a UNIX socket.
var protocol, agent, port, u = urllib.parse(url);
if (u.protocol === 'ws:' || u.protocol === 'wss:') {
protocol = u.protocol === 'ws:' ? http : https;
port = u.protocol === 'ws:' ? 80 : 443;
agent = u.protocol === new protocol.Agent({
host: u.hostname,
port: u.port || port
});
httpPath = (u.pathname || '/') + (u.search || '');
httpHeaders.Host = u.hostname + (u.port ? (":" + u.port) : "");
}
else if (urlScheme === 'ws+unix') {
throw new Error('ws+unix is not implemented');
// var sockPath = url.substring('ws+unix://'.length, url.length);
// httpClient = http.createClient(sockPath);
// httpHeaders.Host = 'localhost';
}
else {
throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
}
var httpReq = protocol.request({
host: u.hostname,
method: 'GET',
agent: agent,
port: u.port,
path: httpPath,
headers: httpHeaders
});
httpReq.on('error', function (e) {
errorListener(e);
});
httpReq.on('upgrade', (function() {
var data = undefined;
return function(res, s, head) {
stream = s;
//
// Emit the `wsupgrade` event to inspect the raw
// arguments returned from the websocket request.
//
self.emit('wsupgrade', httpHeaders, res, s, head);
stream.on('data', function(d) {
if (d.length <= 0) {
return;
}
if (!data) {
data = d;
} else {
var data2 = new buffer.Buffer(data.length + d.length);
data.copy(data2, 0, 0, data.length);
d.copy(data2, data.length, 0, d.length);
data = data2;
}
if (data.length >= 16) {
var expected = computeSecretKeySignature(key1, key2, challenge);
var actual = data.slice(0, 16).toString('binary');
// Handshaking fails; we're donezo
if (actual != expected) {
debug(
'expected=\'' + str2hex(expected) + '\'; ' +
'actual=\'' + str2hex(actual) + '\''
);
process.nextTick(function() {
// N.B. Emit 'wserror' here, as 'error' is a reserved word in the
// EventEmitter world, and gets thrown.
self.emit(
'wserror',
new Error('Invalid handshake from server:' +
'expected \'' + str2hex(expected) + '\', ' +
'actual \'' + str2hex(actual) + '\''
)
);
if (self.onerror) {
self.onerror();
}
finishClose();
});
}
//
// Un-register our data handler and add the one to be used
// for the normal, non-handshaking case. If we have extra
// data left over, manually fire off the handler on
// whatever remains.
//
stream.removeAllListeners('data');
stream.on('data', dataListener);
readyState = OPEN;
process.nextTick(function() {
self.emit('open');
if (self.onopen) {
self.onopen();
}
});
// Consume any leftover data
if (data.length > 16) {
stream.emit('data', data.slice(16, data.length));
}
}
});
stream.on('fd', fdListener);
stream.on('error', errorListener);
stream.on('close', function() {
errorListener(new Error('Stream closed unexpectedly.'));
});
stream.emit('data', head);
};
})());
httpReq.write(challenge, 'binary');
httpReq.end();
})();
};
sys.inherits(WebSocket, events.EventEmitter);
exports.WebSocket = WebSocket;
// Add some constants to the WebSocket object
setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING);
setConstant(WebSocket.prototype, 'OPEN', OPEN);
setConstant(WebSocket.prototype, 'CLOSING', CLOSING);
setConstant(WebSocket.prototype, 'CLOSED', CLOSED);
// vim:ts=4 sw=4 et