@heroku/socksv5
Version:
SOCKS protocol version 5 server and client implementations for node.js
255 lines (218 loc) • 7.07 kB
JavaScript
var net = require('net'),
normalizeConnectArgs = normalizeConnectArgs,
dns = require('dns'),
util = require('util'),
inherits = util.inherits,
EventEmitter = require('events').EventEmitter;
function normalizeConnectArgs(args) {
var options = {};
if (args.length === 0) {
return [options];
} else if (args[0] !== null && typeof args[0] === 'object') {
// connect(options, [cb])
options = args[0];
} else if (typeof args[0] === 'string' && !(Number(args[0]) >= 0)) {
// connect(path, [cb]);
options.path = args[0];
} else {
// connect(port, [host], [cb])
options.port = args[0];
if (args.length > 1 && typeof args[1] === 'string') {
options.host = args[1];
}
}
var cb = args[args.length - 1];
return typeof cb === 'function' ? [options, cb] : [options];
}
var Parser = require('./client.parser'),
ipbytes = require('./utils').ipbytes;
var CMD = require('./constants').CMD,
ATYP = require('./constants').ATYP;
function Client(options) {
if (!(this instanceof Client))
return new Client(options);
var self = this;
EventEmitter.call(this);
this._hadError = false;
this._ready = false;
this._sock = new net.Socket();
this._sock.on('connect', function() {
self._onConnect();
}).on('error', function(err) {
if (!self._hadError && !self._ready) {
self._hadError = true;
self.emit('error', err);
}
}).on('close', function(had_err) {
self.emit('close', self._hadError || had_err);
});
this._parser = undefined;
this._proxyaddr = options && options.proxyHost;
this._proxyport = options && options.proxyPort;
if (typeof this._proxyaddr !== 'string')
this._proxyaddr = 'localhost';
else if (typeof this._proxyport !== 'number')
this._proxyport = 1080;
this._dstaddr = undefined;
this._dstport = undefined;
this._localDNS = (options && typeof options.localDNS === 'boolean'
? options.localDNS
: true);
this._strictLocalDNS = (options && typeof options.strictLocalDNS === 'boolean'
? options.strictLocalDNS
: true);
this._auths = [];
if (options && Array.isArray(options.auths)) {
for (var i = 0, len = options.auths.length; i < len; ++i)
this.useAuth(options.auths[i]);
}
}
inherits(Client, EventEmitter);
Client.prototype._onConnect = function() {
var self = this,
parser = this._parser,
socket = this._sock;
var auths = self._auths,
alen = auths.length,
authsbuf = new Buffer(2 + alen);
authsbuf[0] = 0x05;
authsbuf[1] = alen;
for (var a = 0, p = 2; a < alen; ++a, ++p)
authsbuf[p] = auths[a].METHOD;
socket.write(authsbuf);
parser.on('method', function(method) {
alen = auths.length;
for (var i = 0; i < alen; ++i) {
if (auths[i].METHOD === method) {
auths[i].client(socket, function(result) {
if (result === true) {
parser.authed = true;
parser.start();
self._sendRequest();
} else {
self._hadError = true;
if (util.isError(result))
self.emit('error', result);
else {
var err = new Error('Authentication failed');
err.code = 'EAUTHFAILED';
self.emit('error', err);
}
socket.end();
}
});
self._sock.resume();
return;
}
}
var err = new Error('Authentication method mismatch');
err.code = 'EAUTHNOTSUPPORT';
self._hadError = true;
self.emit('error', err);
socket.end();
}).on('error', function(err) {
self._hadError = true;
self.emit('error', err);
if (socket.writable)
socket.end();
}).on('reply', function(repInfo) {
self._ready = true;
self.emit('connect', self._sock);
self._sock.resume();
});
};
Client.prototype._sendRequest = function() {
var self = this,
iptype = net.isIP(this._dstaddr);
var addrlen = (iptype === 0
? Buffer.byteLength(self._dstaddr)
: (iptype === 4 ? 4 : 16)),
reqbuf = new Buffer(6 + (iptype === 0 ? 1 : 0) + addrlen),
p;
reqbuf[0] = 0x05;
reqbuf[1] = CMD.CONNECT;
reqbuf[2] = 0x00;
if (iptype > 0) {
var addrbytes = ipbytes(self._dstaddr);
reqbuf[3] = (iptype === 4 ? ATYP.IPv4 : ATYP.IPv6);
p = 4;
for (var i = 0; i < addrlen; ++i, ++p)
reqbuf[p] = addrbytes[i];
} else {
reqbuf[3] = ATYP.NAME;
reqbuf[4] = addrlen;
reqbuf.write(self._dstaddr, 5, addrlen);
p = 5 + addrlen;
}
reqbuf.writeUInt16BE(self._dstport, p, true);
self._sock.write(reqbuf);
};
Client.prototype.useAuth = function(auth) {
if (typeof auth !== 'object'
|| typeof auth.client !== 'function'
|| auth.client.length !== 2)
throw new Error('Invalid authentication handler');
else if (this._auths.length >= 255)
throw new Error('Too many authentication handlers (limited to 255).');
this._auths.push(auth);
return this;
};
Client.prototype.connect = function(options, cb) {
var self = this;
if (this._auths.length === 0)
throw new Error('Missing client authentication method(s)');
if (typeof options !== 'object') {
// Old API:
// connect(port, [host], [cb])
// connect(path, [cb]);
var args = normalizeConnectArgs(arguments);
return Client.prototype.connect.apply(this, args);
}
if (!options.port)
throw new Error('Can only connect to TCP hosts');
if (typeof cb === 'function')
this.once('connect', cb);
this._dstaddr = options.host || 'localhost';
this._dstport = +options.port;
if (typeof options.localDNS === 'boolean')
this._localDNS = options.localDNS;
if (typeof options.strictLocalDNS === 'boolean')
this._strictLocalDNS = options.strictLocalDNS;
if (typeof options.proxyHost === 'string')
this._proxyhost = options.proxyHost;
if (typeof options.proxyPort === 'string')
this._proxyport = options.proxyPort;
if (this._parser)
this._parser.stop();
this._parser = new Parser(this._sock);
this._hadError = this._ready = false;
var realOptions = {
host: this._proxyhost,
port: this._proxyport,
localAddress: options.localAddress // TODO: remove?
};
if (net.isIP(this._dstaddr) === 0 && this._localDNS) {
dns.lookup(this._dstaddr, function(err, addr) {
if (err && self._strictLocalDNS) {
self._hadError = true;
self.emit('error', err);
self.emit('close', true);
return;
}
if (addr)
self._dstaddr = addr;
self._sock.connect(realOptions);
});
} else
this._sock.connect(realOptions);
return this;
};
exports.Client = Client;
exports.connect = exports.createConnection = function() {
var args = normalizeConnectArgs(arguments),
client = new Client(args[0]);
process.nextTick(function() {
Client.prototype.connect.apply(client, args);
});
return client;
};