783 lines
16 KiB
JavaScript
783 lines
16 KiB
JavaScript
/*!
|
|
* socks.js - socks proxy for bcoin
|
|
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* @module net/socks
|
|
*/
|
|
|
|
var assert = require('assert');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var net = require('net');
|
|
var util = require('../utils/util');
|
|
var co = require('../utils/co');
|
|
var IP = require('../utils/ip');
|
|
var StaticWriter = require('../utils/staticwriter');
|
|
var BufferReader = require('../utils/reader');
|
|
|
|
/**
|
|
* SOCKS state machine
|
|
* @constructor
|
|
*/
|
|
|
|
function SOCKS() {
|
|
if (!(this instanceof SOCKS))
|
|
return new SOCKS();
|
|
|
|
EventEmitter.call(this);
|
|
|
|
this.socket = new net.Socket();
|
|
this.state = SOCKS.states.INIT;
|
|
this.target = SOCKS.states.INIT;
|
|
this.destHost = '0.0.0.0';
|
|
this.destPort = 0;
|
|
this.username = '';
|
|
this.password = '';
|
|
this.name = 'localhost';
|
|
this.destroyed = false;
|
|
this.timeout = null;
|
|
this.proxied = false;
|
|
}
|
|
|
|
util.inherits(SOCKS, EventEmitter);
|
|
|
|
SOCKS.states = {
|
|
INIT: 0,
|
|
CONNECT: 1,
|
|
HANDSHAKE: 2,
|
|
AUTH: 3,
|
|
PROXY: 4,
|
|
PROXY_DONE: 5,
|
|
RESOLVE: 6,
|
|
RESOLVE_DONE: 7
|
|
};
|
|
|
|
SOCKS.statesByVal = util.revMap(SOCKS.states);
|
|
|
|
SOCKS.errors = [
|
|
'',
|
|
'General failure',
|
|
'Connection not allowed',
|
|
'Network is unreachable',
|
|
'Host is unreachable',
|
|
'Connection refused',
|
|
'TTL expired',
|
|
'Command not supported',
|
|
'Address type not supported',
|
|
'Unknown proxy error'
|
|
];
|
|
|
|
SOCKS.prototype.error = function error(err) {
|
|
var msg;
|
|
|
|
if (this.destroyed)
|
|
return;
|
|
|
|
if (err instanceof Error) {
|
|
this.emit('error', err);
|
|
this.destroy();
|
|
return;
|
|
}
|
|
|
|
msg = util.fmt.apply(util, arguments);
|
|
this.emit('error', new Error(msg));
|
|
this.destroy();
|
|
};
|
|
|
|
SOCKS.prototype.getError = function getError(code) {
|
|
if (code >= SOCKS.errors.length)
|
|
return SOCKS.errors[9];
|
|
|
|
return SOCKS.errors[code];
|
|
};
|
|
|
|
SOCKS.prototype.destroy = function destroy() {
|
|
if (this.destroyed)
|
|
return;
|
|
|
|
this.destroyed = true;
|
|
this.socket.destroy();
|
|
|
|
this.stopTimeout();
|
|
|
|
if (this.state === this.target)
|
|
return;
|
|
|
|
this.emit('close');
|
|
};
|
|
|
|
SOCKS.prototype.startTimeout = function startTimeout() {
|
|
var self = this;
|
|
this.timeout = setTimeout(function() {
|
|
var state = SOCKS.statesByVal[self.state];
|
|
self.timeout = null;
|
|
self.error('SOCKS request timed out (state=%s).', state);
|
|
}, 8000);
|
|
};
|
|
|
|
SOCKS.prototype.stopTimeout = function stopTimeout() {
|
|
if (this.timeout != null) {
|
|
clearTimeout(this.timeout);
|
|
this.timeout = null;
|
|
}
|
|
};
|
|
|
|
SOCKS.prototype.connect = function connect(port, host) {
|
|
var self = this;
|
|
|
|
assert(typeof port === 'number');
|
|
assert(typeof host === 'string');
|
|
|
|
this.state = SOCKS.states.CONNECT;
|
|
this.socket.connect(port, host);
|
|
|
|
this.socket.on('connect', function() {
|
|
if (self.proxied)
|
|
return;
|
|
self.handleConnect();
|
|
});
|
|
|
|
this.socket.on('data', function(data) {
|
|
if (self.proxied)
|
|
return;
|
|
self.handleData(data);
|
|
});
|
|
|
|
this.socket.on('error', function(err) {
|
|
if (self.proxied)
|
|
return;
|
|
self.handleError(err);
|
|
});
|
|
|
|
this.socket.on('close', function() {
|
|
if (self.proxied)
|
|
return;
|
|
self.handleClose();
|
|
});
|
|
};
|
|
|
|
SOCKS.prototype.open = function open(options) {
|
|
assert(this.state === SOCKS.states.INIT);
|
|
|
|
assert(options);
|
|
|
|
if (options.username != null) {
|
|
assert(typeof options.username === 'string');
|
|
this.username = options.username;
|
|
assert(typeof options.password === 'string',
|
|
'Username must have a password.');
|
|
}
|
|
|
|
if (options.password != null) {
|
|
assert(typeof options.password === 'string');
|
|
this.password = options.password;
|
|
}
|
|
|
|
this.startTimeout();
|
|
this.connect(options.port, options.host);
|
|
};
|
|
|
|
SOCKS.prototype.proxy = function proxy(options) {
|
|
assert(options);
|
|
assert(typeof options.destHost === 'string');
|
|
assert(typeof options.destPort === 'number');
|
|
|
|
this.destHost = options.destHost;
|
|
this.destPort = options.destPort;
|
|
this.target = SOCKS.states.PROXY_DONE;
|
|
|
|
this.open(options);
|
|
};
|
|
|
|
SOCKS.prototype.resolve = function resolve(options) {
|
|
assert(options);
|
|
assert(typeof options.name === 'string');
|
|
|
|
this.name = options.name;
|
|
this.target = SOCKS.states.RESOLVE_DONE;
|
|
|
|
this.open(options);
|
|
};
|
|
|
|
SOCKS.prototype.handleConnect = function handleConnect() {
|
|
assert(this.state === SOCKS.states.CONNECT);
|
|
this.sendHandshake();
|
|
};
|
|
|
|
SOCKS.prototype.handleError = function handleError(err) {
|
|
this.error(err);
|
|
};
|
|
|
|
SOCKS.prototype.handleClose = function handleClose() {
|
|
var state;
|
|
|
|
if (this.state !== this.target) {
|
|
state = SOCKS.statesByVal[this.state];
|
|
this.error('SOCKS request destroyed (state=%s).', state);
|
|
return;
|
|
}
|
|
|
|
this.destroy();
|
|
};
|
|
|
|
SOCKS.prototype.handleData = function handleData(data) {
|
|
switch (this.state) {
|
|
case SOCKS.states.INIT:
|
|
this.error('Data before SOCKS connection.');
|
|
break;
|
|
case SOCKS.states.CONNECT:
|
|
this.error('Data before SOCKS handshake.');
|
|
break;
|
|
case SOCKS.states.HANDSHAKE:
|
|
this.handleHandshake(data);
|
|
break;
|
|
case SOCKS.states.AUTH:
|
|
this.handleAuth(data);
|
|
break;
|
|
case SOCKS.states.PROXY:
|
|
this.handleProxy(data);
|
|
break;
|
|
case SOCKS.states.RESOLVE:
|
|
this.handleResolve(data);
|
|
break;
|
|
case SOCKS.states.PROXY_DONE:
|
|
case SOCKS.states.RESOLVE_DONE:
|
|
break;
|
|
default:
|
|
assert(false, 'Bad state.');
|
|
break;
|
|
}
|
|
};
|
|
|
|
SOCKS.prototype.sendHandshake = function sendHandshake() {
|
|
var packet;
|
|
|
|
if (this.username) {
|
|
packet = new Buffer(4);
|
|
packet[0] = 0x05;
|
|
packet[1] = 0x02;
|
|
packet[2] = 0x00;
|
|
packet[3] = 0x02;
|
|
} else {
|
|
packet = new Buffer(3);
|
|
packet[0] = 0x05;
|
|
packet[1] = 0x01;
|
|
packet[2] = 0x00;
|
|
}
|
|
|
|
this.state = SOCKS.states.HANDSHAKE;
|
|
this.socket.write(packet);
|
|
};
|
|
|
|
SOCKS.prototype.handleHandshake = function handleHandshake(data) {
|
|
if (data.length !== 2) {
|
|
this.error('Bad SOCKS handshake response (size).');
|
|
return;
|
|
}
|
|
|
|
if (data[0] !== 0x05) {
|
|
this.error('Bad SOCKS version for handshake.');
|
|
return;
|
|
}
|
|
|
|
this.emit('handshake');
|
|
|
|
switch (data[1]) {
|
|
case 0xff:
|
|
this.error('No acceptable SOCKS auth methods.');
|
|
break;
|
|
case 0x02:
|
|
this.sendAuth();
|
|
break;
|
|
case 0x00:
|
|
this.state = SOCKS.states.AUTH;
|
|
this.auth();
|
|
break;
|
|
default:
|
|
this.error('SOCKS handshake error: %d.', data[1]);
|
|
break;
|
|
}
|
|
};
|
|
|
|
SOCKS.prototype.sendAuth = function sendAuth() {
|
|
var user = this.username;
|
|
var pass = this.password;
|
|
var ulen, plen, size, packet;
|
|
|
|
if (!user) {
|
|
this.error('No username passed for SOCKS auth.');
|
|
return;
|
|
}
|
|
|
|
if (!pass) {
|
|
this.error('No password passed for SOCKS auth.');
|
|
return;
|
|
}
|
|
|
|
ulen = Buffer.byteLength(user, 'ascii');
|
|
plen = Buffer.byteLength(pass, 'ascii');
|
|
size = 3 + ulen + plen;
|
|
|
|
packet = new StaticWriter(size);
|
|
packet.writeU8(0x01);
|
|
packet.writeU8(ulen);
|
|
packet.writeString(user, 'ascii');
|
|
packet.writeU8(plen);
|
|
packet.writeString(pass, 'ascii');
|
|
packet = packet.render();
|
|
|
|
this.state = SOCKS.states.AUTH;
|
|
this.socket.write(packet);
|
|
};
|
|
|
|
SOCKS.prototype.handleAuth = function handleAuth(data) {
|
|
if (data.length !== 2) {
|
|
this.error('Bad packet size for SOCKS auth.');
|
|
return;
|
|
}
|
|
|
|
if (data[0] !== 0x01) {
|
|
this.error('Bad SOCKS auth version number.');
|
|
return;
|
|
}
|
|
|
|
if (data[1] !== 0x00) {
|
|
this.error('SOCKS auth failure: %d.', data[0]);
|
|
return;
|
|
}
|
|
|
|
this.auth();
|
|
};
|
|
|
|
SOCKS.prototype.auth = function auth() {
|
|
this.emit('auth');
|
|
|
|
switch (this.target) {
|
|
case SOCKS.states.PROXY_DONE:
|
|
this.sendProxy();
|
|
break;
|
|
case SOCKS.states.RESOLVE_DONE:
|
|
this.sendResolve();
|
|
break;
|
|
default:
|
|
this.error('Bad target state.');
|
|
break;
|
|
}
|
|
};
|
|
|
|
SOCKS.prototype.sendProxy = function sendProxy() {
|
|
var host = this.destHost;
|
|
var port = this.destPort;
|
|
var ip, len, type, name, packet;
|
|
|
|
switch (IP.getStringType(host)) {
|
|
case IP.types.IPV4:
|
|
ip = IP.toBuffer(host);
|
|
type = 0x01;
|
|
name = ip.slice(12, 16);
|
|
len = 4;
|
|
break;
|
|
case IP.types.IPV6:
|
|
ip = IP.toBuffer(host);
|
|
type = 0x04;
|
|
name = ip;
|
|
len = 16;
|
|
break;
|
|
default:
|
|
type = 0x03;
|
|
name = new Buffer(host, 'ascii');
|
|
len = 1 + name.length;
|
|
break;
|
|
}
|
|
|
|
packet = new StaticWriter(6 + len);
|
|
|
|
packet.writeU8(0x05);
|
|
packet.writeU8(0x01);
|
|
packet.writeU8(0x00);
|
|
packet.writeU8(type);
|
|
|
|
if (type === 0x03)
|
|
packet.writeU8(name.length);
|
|
|
|
packet.writeBytes(name);
|
|
packet.writeU16BE(port);
|
|
|
|
packet = packet.render();
|
|
|
|
this.state = SOCKS.states.PROXY;
|
|
this.socket.write(packet);
|
|
};
|
|
|
|
SOCKS.prototype.handleProxy = function handleProxy(data) {
|
|
var addr, msg;
|
|
|
|
if (data.length < 6) {
|
|
this.error('Bad packet size for SOCKS connect.');
|
|
return;
|
|
}
|
|
|
|
if (data[0] !== 0x05) {
|
|
this.error('Bad SOCKS version for connect.');
|
|
return;
|
|
}
|
|
|
|
if (data[1] !== 0x00) {
|
|
msg = this.getError(data[1]);
|
|
this.error('SOCKS connect error: %s.', msg);
|
|
return;
|
|
}
|
|
|
|
if (data[2] !== 0x00) {
|
|
this.error('SOCKS connect failed (padding).');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
addr = parseAddr(data, 3);
|
|
} catch (e) {
|
|
this.error(e);
|
|
return;
|
|
}
|
|
|
|
this.state = SOCKS.states.PROXY_DONE;
|
|
this.stopTimeout();
|
|
this.proxied = true;
|
|
|
|
this.emit('proxy address', addr);
|
|
this.emit('proxy', this.socket);
|
|
};
|
|
|
|
SOCKS.prototype.sendResolve = function sendResolve() {
|
|
var name = this.name;
|
|
var len = Buffer.byteLength(name, 'utf8');
|
|
var packet = new StaticWriter(7 + len);
|
|
|
|
packet.writeU8(0x05);
|
|
packet.writeU8(0xf0);
|
|
packet.writeU8(0x00);
|
|
packet.writeU8(0x03);
|
|
packet.writeU8(len);
|
|
packet.writeString(name, 'utf8');
|
|
packet.writeU16BE(0);
|
|
packet = packet.render();
|
|
|
|
this.state = SOCKS.states.RESOLVE;
|
|
this.socket.write(packet);
|
|
};
|
|
|
|
SOCKS.prototype.handleResolve = function handleResolve(data) {
|
|
var addr, msg;
|
|
|
|
if (data.length < 6) {
|
|
this.error('Bad packet size for tor resolve.');
|
|
return;
|
|
}
|
|
|
|
if (data[0] !== 0x05) {
|
|
this.error('Bad SOCKS version for tor resolve.');
|
|
return;
|
|
}
|
|
|
|
if (data[1] !== 0x00) {
|
|
msg = this.getError(data[1]);
|
|
this.error('Tor resolve error: %s (%s).', msg, this.name);
|
|
return;
|
|
}
|
|
|
|
if (data[2] !== 0x00) {
|
|
this.error('Tor resolve failed (padding).');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
addr = parseAddr(data, 3);
|
|
} catch (e) {
|
|
this.error(e);
|
|
return;
|
|
}
|
|
|
|
if (addr.type === 0x03) {
|
|
this.error('Bad address type for tor resolve.');
|
|
return;
|
|
}
|
|
|
|
this.state = SOCKS.states.RESOLVE_DONE;
|
|
this.destroy();
|
|
|
|
this.emit('resolve', [addr.host]);
|
|
};
|
|
|
|
SOCKS.resolve = function resolve(options) {
|
|
var socks = new SOCKS();
|
|
return new Promise(function(resolve, reject) {
|
|
socks.resolve(options);
|
|
socks.on('resolve', resolve);
|
|
socks.on('error', reject);
|
|
});
|
|
};
|
|
|
|
SOCKS.proxy = function proxy(options) {
|
|
var socks = new SOCKS();
|
|
return new Promise(function(resolve, reject) {
|
|
socks.proxy(options);
|
|
socks.on('proxy', resolve);
|
|
socks.on('error', reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Proxy Socket
|
|
* @constructor
|
|
* @param {String} host
|
|
* @param {Number} port
|
|
* @param {String?} user
|
|
* @param {String?} pass
|
|
*/
|
|
|
|
function Proxy(host, port, user, pass) {
|
|
if (!(this instanceof Proxy))
|
|
return new Proxy(host, port, user, pass);
|
|
|
|
EventEmitter.call(this);
|
|
|
|
assert(typeof host === 'string');
|
|
assert(typeof port === 'number');
|
|
|
|
this.socket = null;
|
|
this.host = host;
|
|
this.port = port;
|
|
this.username = user || null;
|
|
this.password = pass || null;
|
|
this.bytesWritten = 0;
|
|
this.bytesRead = 0;
|
|
this.remoteAddress = null;
|
|
this.remotePort = 0;
|
|
this.ops = [];
|
|
}
|
|
|
|
util.inherits(Proxy, EventEmitter);
|
|
|
|
Proxy.prototype.connect = co(function* connect(port, host) {
|
|
var self = this;
|
|
var i, options, socket;
|
|
|
|
assert(!this.socket, 'Already connected.');
|
|
|
|
options = {
|
|
host: this.host,
|
|
port: this.port,
|
|
username: this.username,
|
|
password: this.password,
|
|
destHost: host,
|
|
destPort: port
|
|
};
|
|
|
|
try {
|
|
socket = yield SOCKS.proxy(options);
|
|
} catch (e) {
|
|
this.emit('error', e);
|
|
return;
|
|
}
|
|
|
|
this.remoteAddress = host;
|
|
this.remotePort = port;
|
|
this.socket = socket;
|
|
|
|
this.socket.on('error', function(err) {
|
|
self.emit('error', err);
|
|
});
|
|
|
|
this.socket.on('close', function() {
|
|
self.emit('close');
|
|
});
|
|
|
|
this.socket.on('data', function(data) {
|
|
self.bytesRead += data.length;
|
|
self.emit('data', data);
|
|
});
|
|
|
|
this.socket.on('drain', function() {
|
|
self.emit('drain');
|
|
});
|
|
|
|
this.socket.on('timeout', function() {
|
|
self.emit('timeout');
|
|
});
|
|
|
|
for (i = 0; i < this.ops.length; i++)
|
|
this.ops[i].call(this);
|
|
|
|
this.ops.length = 0;
|
|
|
|
this.emit('connect');
|
|
});
|
|
|
|
Proxy.prototype.setKeepAlive = function setKeepAlive(enable, delay) {
|
|
if (!this.socket) {
|
|
this.ops.push(function() {
|
|
this.socket.setKeepAlive(enable, delay);
|
|
});
|
|
return;
|
|
}
|
|
this.socket.setKeepAlive(enable, delay);
|
|
};
|
|
|
|
Proxy.prototype.setNoDelay = function setNoDelay(enable) {
|
|
if (!this.socket) {
|
|
this.ops.push(function() {
|
|
this.socket.setNoDelay(enable);
|
|
});
|
|
return;
|
|
}
|
|
this.socket.setNoDelay(enable);
|
|
};
|
|
|
|
Proxy.prototype.setTimeout = function setTimeout(timeout, callback) {
|
|
if (!this.socket) {
|
|
this.ops.push(function() {
|
|
this.socket.setTimeout(timeout, callback);
|
|
});
|
|
return;
|
|
}
|
|
this.socket.setTimeout(timeout, callback);
|
|
};
|
|
|
|
Proxy.prototype.write = function write(data, callback) {
|
|
assert(this.socket, 'Not connected.');
|
|
this.bytesWritten += data.length;
|
|
return this.socket.write(data, callback);
|
|
};
|
|
|
|
Proxy.prototype.end = function end() {
|
|
assert(this.socket, 'Not connected.');
|
|
return this.socket.end();
|
|
};
|
|
|
|
Proxy.prototype.pause = function pause() {
|
|
assert(this.socket, 'Not connected.');
|
|
return this.socket.pause();
|
|
};
|
|
|
|
Proxy.prototype.resume = function resume() {
|
|
assert(this.socket, 'Not connected.');
|
|
return this.socket.resume();
|
|
};
|
|
|
|
Proxy.prototype.destroy = function destroy() {
|
|
if (!this.socket)
|
|
return;
|
|
return this.socket.destroy();
|
|
};
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
function parseProxy(host) {
|
|
var index = host.indexOf('@');
|
|
var addr, left, right, parts;
|
|
|
|
if (index === -1) {
|
|
addr = IP.fromHostname(host, 1080);
|
|
return {
|
|
host: addr.host,
|
|
port: addr.port
|
|
};
|
|
}
|
|
|
|
left = host.substring(0, index);
|
|
right = host.substring(index + 1);
|
|
|
|
parts = left.split(':');
|
|
assert(parts.length > 1, 'Bad username and password.');
|
|
|
|
addr = IP.fromHostname(right, 1080);
|
|
|
|
return {
|
|
host: addr.host,
|
|
port: addr.port,
|
|
username: parts[0],
|
|
password: parts[1]
|
|
};
|
|
}
|
|
|
|
function parseAddr(data, offset) {
|
|
var br = new BufferReader(data);
|
|
var type, len, host, port;
|
|
|
|
if (br.left() < offset + 2)
|
|
throw new Error('Bad SOCKS address length.');
|
|
|
|
br.seek(offset);
|
|
|
|
type = br.readU8();
|
|
|
|
switch (type) {
|
|
case 0x01:
|
|
if (br.left() < 6)
|
|
throw new Error('Bad SOCKS ipv4 length.');
|
|
|
|
host = IP.toString(br.readBytes(4));
|
|
port = br.readU16BE();
|
|
break;
|
|
case 0x03:
|
|
len = br.readU8();
|
|
|
|
if (br.left() < len + 2)
|
|
throw new Error('Bad SOCKS domain length.');
|
|
|
|
host = br.readString(len, 'utf8');
|
|
port = br.readU16BE();
|
|
break;
|
|
case 0x04:
|
|
if (br.left() < 18)
|
|
throw new Error('Bad SOCKS ipv6 length.');
|
|
|
|
host = IP.toString(br.readBytes(16));
|
|
port = br.readU16BE();
|
|
break;
|
|
default:
|
|
throw new Error('Unknown SOCKS address type: ' + type + '.');
|
|
}
|
|
|
|
return {
|
|
type: type,
|
|
host: host,
|
|
port: port
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
exports.connect = function connect(proxy, destPort, destHost) {
|
|
var addr = parseProxy(proxy);
|
|
var host = addr.host;
|
|
var port = addr.port;
|
|
var user = addr.username;
|
|
var pass = addr.password;
|
|
var socket;
|
|
|
|
socket = new Proxy(host, port, user, pass);
|
|
socket.connect(destPort, destHost);
|
|
|
|
return socket;
|
|
};
|
|
|
|
exports.resolve = function resolve(proxy, name) {
|
|
var addr = parseProxy(proxy);
|
|
return SOCKS.resolve({
|
|
host: addr.host,
|
|
port: addr.port,
|
|
username: addr.username,
|
|
password: addr.password,
|
|
name: name
|
|
});
|
|
};
|