UNPKG

@heroku/socksv5

Version:

SOCKS protocol version 5 server and client implementations for node.js

255 lines (218 loc) 7.07 kB
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; };