423 lines
11 KiB
JavaScript
423 lines
11 KiB
JavaScript
var stream = require('stream');
|
|
var util = require('util');
|
|
var timers = require('timers');
|
|
var http = require('http');
|
|
|
|
var debug = util.debuglog('net');
|
|
|
|
var proxy = {
|
|
protocol: (window.location.protocol == 'https:') ? 'wss' : 'ws',
|
|
hostname: window.location.hostname,
|
|
port: window.location.port,
|
|
path: '/api/vm/net'
|
|
};
|
|
function getProxy() {
|
|
return proxy;
|
|
}
|
|
function getProxyHost() {
|
|
var host = getProxy().hostname;
|
|
if (getProxy().port) {
|
|
host += ':'+getProxy().port;
|
|
}
|
|
return host;
|
|
}
|
|
function getProxyOrigin() {
|
|
return getProxy().protocol + '://' + getProxyHost();
|
|
}
|
|
exports.setProxy = function (options) {
|
|
options = options || {};
|
|
|
|
if (options.protocol) {
|
|
proxy.protocol = options.protocol;
|
|
}
|
|
if (options.hostname) {
|
|
proxy.hostname = options.hostname;
|
|
}
|
|
if (options.port) {
|
|
proxy.port = options.port;
|
|
}
|
|
if (options.path) {
|
|
proxy.path = options.path;
|
|
}
|
|
};
|
|
|
|
exports.createServer = function () {
|
|
throw new Error('Cannot create server in a browser');
|
|
};
|
|
|
|
exports.connect = exports.createConnection = function (/* options, connectListener */) {
|
|
var args = normalizeConnectArgs(arguments);
|
|
debug('createConnection', args);
|
|
var s = new Socket(args[0]);
|
|
return Socket.prototype.connect.apply(s, args);
|
|
};
|
|
|
|
function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; }
|
|
|
|
function isPipeName(s) {
|
|
return util.isString(s) && toNumber(s) === false;
|
|
}
|
|
|
|
// Returns an array [options] or [options, cb]
|
|
// It is the same as the argument of Socket.prototype.connect().
|
|
function normalizeConnectArgs(args) {
|
|
var options = {};
|
|
if (util.isObject(args[0])) {
|
|
// connect(options, [cb])
|
|
options = args[0];
|
|
} else if (isPipeName(args[0])) {
|
|
// connect(path, [cb]);
|
|
options.path = args[0];
|
|
} else {
|
|
// connect(port, [host], [cb])
|
|
options.port = args[0];
|
|
if (util.isString(args[1])) {
|
|
options.host = args[1];
|
|
}
|
|
}
|
|
var cb = args[args.length - 1];
|
|
return util.isFunction(cb) ? [options, cb] : [options];
|
|
}
|
|
exports._normalizeConnectArgs = normalizeConnectArgs;
|
|
|
|
function Socket(options) {
|
|
if (!(this instanceof Socket)) return new Socket(options);
|
|
|
|
this._connecting = false;
|
|
this._host = null;
|
|
|
|
if (util.isNumber(options))
|
|
options = { fd: options }; // Legacy interface.
|
|
else if (util.isUndefined(options))
|
|
options = {};
|
|
|
|
stream.Duplex.call(this, options);
|
|
|
|
// these will be set once there is a connection
|
|
this.readable = this.writable = false;
|
|
|
|
// handle strings directly
|
|
this._writableState.decodeStrings = false;
|
|
|
|
// default to *not* allowing half open sockets
|
|
this.allowHalfOpen = options && options.allowHalfOpen || false;
|
|
}
|
|
util.inherits(Socket, stream.Duplex);
|
|
|
|
exports.Socket = Socket;
|
|
exports.Stream = Socket; // Legacy naming.
|
|
|
|
Socket.prototype.listen = function () {
|
|
throw new Error('Cannot listen in a browser');
|
|
};
|
|
|
|
Socket.prototype.setTimeout = function (msecs, callback) {
|
|
if (msecs > 0 && isFinite(msecs)) {
|
|
timers.enroll(this, msecs);
|
|
//timers._unrefActive(this);
|
|
if (callback) {
|
|
this.once('timeout', callback);
|
|
}
|
|
} else if (msecs === 0) {
|
|
timers.unenroll(this);
|
|
if (callback) {
|
|
this.removeListener('timeout', callback);
|
|
}
|
|
}
|
|
};
|
|
|
|
Socket.prototype._onTimeout = function () {
|
|
debug('_onTimeout');
|
|
this.emit('timeout');
|
|
};
|
|
|
|
Socket.prototype.setNoDelay = function (enable) {};
|
|
Socket.prototype.setKeepAlive = function (setting, msecs) {};
|
|
|
|
Socket.prototype.address = function () {
|
|
return {
|
|
address: this.remoteAddress,
|
|
port: this.remotePort,
|
|
family: this.remoteFamily
|
|
};
|
|
};
|
|
|
|
Object.defineProperty(Socket.prototype, 'readyState', {
|
|
get: function() {
|
|
if (this._connecting) {
|
|
return 'opening';
|
|
} else if (this.readable && this.writable) {
|
|
return 'open';
|
|
} else if (this.readable && !this.writable) {
|
|
return 'readOnly';
|
|
} else if (!this.readable && this.writable) {
|
|
return 'writeOnly';
|
|
} else {
|
|
return 'closed';
|
|
}
|
|
}
|
|
});
|
|
|
|
Socket.prototype.bufferSize = undefined;
|
|
|
|
Socket.prototype._read = function () {};
|
|
|
|
Socket.prototype.end = function(data, encoding) {
|
|
stream.Duplex.prototype.end.call(this, data, encoding);
|
|
this.writable = false;
|
|
|
|
if (this._ws) {
|
|
this._ws.close();
|
|
}
|
|
|
|
// just in case we're waiting for an EOF.
|
|
if (this.readable && !this._readableState.endEmitted)
|
|
this.read(0);
|
|
else
|
|
maybeDestroy(this);
|
|
};
|
|
|
|
// Call whenever we set writable=false or readable=false
|
|
function maybeDestroy(socket) {
|
|
if (!socket.readable &&
|
|
!socket.writable &&
|
|
!socket.destroyed &&
|
|
!socket._connecting &&
|
|
!socket._writableState.length) {
|
|
socket.destroy();
|
|
}
|
|
}
|
|
|
|
Socket.prototype.destroySoon = function() {
|
|
if (this.writable)
|
|
this.end();
|
|
if (this._writableState.finished)
|
|
this.destroy();
|
|
else
|
|
this.once('finish', this.destroy);
|
|
};
|
|
|
|
Socket.prototype.destroy = function(exception) {
|
|
debug('destroy', exception);
|
|
|
|
if (this.destroyed) {
|
|
return;
|
|
}
|
|
|
|
this._connecting = false;
|
|
|
|
this.readable = this.writable = false;
|
|
|
|
timers.unenroll(this);
|
|
|
|
debug('close');
|
|
|
|
this.destroyed = true;
|
|
};
|
|
|
|
Socket.prototype.remoteAddress = null;
|
|
Socket.prototype.remoteFamily = null;
|
|
Socket.prototype.remotePort = null;
|
|
|
|
// Used for servers only - not here
|
|
Socket.prototype.localAddress = null;
|
|
Socket.prototype.localPort = null;
|
|
|
|
Socket.prototype.bytesRead = 0;
|
|
Socket.prototype.bytesWritten = 0;
|
|
|
|
Socket.prototype._write = function (data, encoding, cb) {
|
|
var self = this;
|
|
cb = cb || function () {};
|
|
|
|
// If we are still connecting, then buffer this for later.
|
|
// The Writable logic will buffer up any more writes while
|
|
// waiting for this one to be done.
|
|
if (this._connecting) {
|
|
this._pendingData = data;
|
|
this._pendingEncoding = encoding;
|
|
this.once('connect', function() {
|
|
this._write(data, encoding, cb);
|
|
});
|
|
return;
|
|
}
|
|
this._pendingData = null;
|
|
this._pendingEncoding = '';
|
|
|
|
if (encoding == 'binary' && typeof data == 'string') { //TODO: maybe apply this for all string inputs?
|
|
// Setting encoding is very important for binary data - otherwise the data gets modified
|
|
data = new Buffer(data, encoding);
|
|
}
|
|
|
|
// Send the data
|
|
this._ws.send(data);
|
|
|
|
process.nextTick(function () {
|
|
//console.log('[tcp] sent: ', data.toString(), data.length);
|
|
self.bytesWritten += data.length;
|
|
cb();
|
|
});
|
|
};
|
|
|
|
Socket.prototype.write = function(chunk, encoding, cb) {
|
|
if (!util.isString(chunk) && !util.isBuffer(chunk))
|
|
throw new TypeError('invalid data');
|
|
return stream.Duplex.prototype.write.apply(this, arguments);
|
|
};
|
|
|
|
Socket.prototype.connect = function(options, cb) {
|
|
var self = this;
|
|
|
|
if (!util.isObject(options)) {
|
|
// Old API:
|
|
// connect(port, [host], [cb])
|
|
// connect(path, [cb]);
|
|
var args = normalizeConnectArgs(arguments);
|
|
return Socket.prototype.connect.apply(this, args);
|
|
}
|
|
|
|
cb = cb || function () {};
|
|
|
|
if (this.write !== Socket.prototype.write)
|
|
this.write = Socket.prototype.write;
|
|
|
|
if (options.path) {
|
|
throw new Error('options.path not supported in the browser');
|
|
}
|
|
|
|
self._connecting = true;
|
|
self.writable = true;
|
|
self._host = options.host;
|
|
|
|
var req = http.request({
|
|
hostname: getProxy().hostname,
|
|
port: getProxy().port,
|
|
path: getProxy().path + '/connect',
|
|
method: 'POST'
|
|
}, function (res) {
|
|
var json = '';
|
|
res.on('data', function (buf) {
|
|
json += buf;
|
|
});
|
|
res.on('end', function () {
|
|
var data = null;
|
|
try {
|
|
data = JSON.parse(json);
|
|
} catch (e) {
|
|
data = {
|
|
code: res.statusCode,
|
|
error: json
|
|
};
|
|
}
|
|
|
|
if (data.error) {
|
|
self.emit('error', 'Cannot open TCP connection ['+res.statusCode+']: '+data.error);
|
|
self.destroy();
|
|
return;
|
|
}
|
|
|
|
self.remoteAddress = data.remote.address;
|
|
self.remoteFamily = data.remote.family;
|
|
self.remotePort = data.remote.port;
|
|
|
|
self._connectWebSocket(data.token, function (err) {
|
|
if (err) {
|
|
cb(err);
|
|
return;
|
|
}
|
|
|
|
cb();
|
|
});
|
|
});
|
|
});
|
|
|
|
req.setHeader('Content-Type', 'application/json');
|
|
req.write(JSON.stringify(options));
|
|
req.end();
|
|
|
|
return this;
|
|
};
|
|
|
|
Socket.prototype._connectWebSocket = function (token, cb) {
|
|
var self = this;
|
|
|
|
if (self._ws) {
|
|
process.nextTick(function () {
|
|
cb();
|
|
});
|
|
return;
|
|
}
|
|
|
|
this._ws = new WebSocket(getProxyOrigin() + getProxy().path + '/socket?token='+token);
|
|
this._handleWebsocket();
|
|
|
|
if (cb) {
|
|
self.on('connect', cb);
|
|
}
|
|
};
|
|
|
|
Socket.prototype._handleWebsocket = function () {
|
|
var self = this;
|
|
|
|
this._ws.addEventListener('open', function () {
|
|
//console.log('TCP OK');
|
|
|
|
self._connecting = false;
|
|
self.readable = true;
|
|
|
|
self.emit('connect');
|
|
|
|
self.read(0);
|
|
});
|
|
this._ws.addEventListener('error', function (e) {
|
|
// `e` doesn't contain anything useful (https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications#Connection_errors)
|
|
console.warn('TCP error', e);
|
|
self.emit('error', 'An error occured with the WebSocket');
|
|
});
|
|
this._ws.addEventListener('message', function (e) {
|
|
var contents = e.data;
|
|
|
|
var gotBuffer = function (buffer) {
|
|
//console.log('[tcp] received: ' + buffer.toString(), buffer.length);
|
|
self.bytesRead += buffer.length;
|
|
self.push(buffer);
|
|
};
|
|
|
|
if (typeof contents == 'string') {
|
|
var buffer = new Buffer(contents);
|
|
gotBuffer(buffer);
|
|
} else if (window.Blob && contents instanceof Blob) {
|
|
var fileReader = new FileReader();
|
|
fileReader.addEventListener('load', function (e) {
|
|
var buf = fileReader.result;
|
|
var arr = new Uint8Array(buf);
|
|
gotBuffer(new Buffer(arr));
|
|
});
|
|
fileReader.readAsArrayBuffer(contents);
|
|
} else {
|
|
console.warn('Cannot read TCP stream: unsupported message type', contents);
|
|
}
|
|
});
|
|
this._ws.addEventListener('close', function () {
|
|
if (self.readyState == 'open') {
|
|
//console.log('TCP closed');
|
|
self.destroy();
|
|
}
|
|
});
|
|
};
|
|
|
|
exports.isIP = function (input) {
|
|
if (exports.isIPv4(input)) {
|
|
return 4;
|
|
} else if (exports.isIPv6(input)) {
|
|
return 6;
|
|
} else {
|
|
return 0;
|
|
}
|
|
};
|
|
exports.isIPv4 = function(input) {
|
|
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(input);
|
|
};
|
|
exports.isIPv6 = function(input) {
|
|
return /^(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))$/.test(input);
|
|
};
|