net: add socks proxy.

This commit is contained in:
Christopher Jeffrey 2017-01-24 03:51:27 -08:00
parent b8324e0b09
commit 6c9dd723cd
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD

626
lib/net/socks.js Normal file
View File

@ -0,0 +1,626 @@
/*!
* socks.js - socks proxy for bcoin
* Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
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.domain = 'localhost';
this.destroyed = false;
this.timeout = null;
}
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.prototype.error = function error(msg) {
if (this.destroyed)
return;
if (msg instanceof Error) {
this.emit('error', msg);
this.destroy();
return;
}
this.emit('error', new Error(msg));
this.destroy();
};
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() {
self.timeout = null;
self.error('Request timed out.');
}, 5000);
};
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() {
self.handleConnect();
});
this.socket.on('data', function(data) {
self.handleData(data);
});
this.socket.on('error', function(err) {
self.handleError(err);
});
this.socket.on('close', function() {
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;
}
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.state.PROXY_DONE;
this.open(options);
};
SOCKS.prototype.resolve = function resolve(options) {
assert(options);
assert(typeof options.domain === 'string');
this.domain = options.domain;
this.target = SOCKS.state.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() {
if (this.state !== this.target) {
this.error('State did not reach target state of: ' + this.target);
return;
}
this.destroy();
};
SOCKS.prototype.handleData = function handleData(data) {
switch (this.state) {
case SOCKS.states.INIT:
this.error('Data before connection.');
break;
case SOCKS.states.CONNECT:
this.error('Data before 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;
default:
assert(false, 'Bad state.');
break;
}
};
SOCKS.prototype.sendHandshake = function sendHandshake() {
var packet = new Buffer(4);
packet[0] = 0x05;
packet[1] = 0x02;
packet[2] = 0x00;
packet[3] = 0x02;
this.state = SOCKS.states.HANDSHAKE;
this.socket.write(packet);
};
SOCKS.prototype.handleHandshake = function handleHandshake(data) {
if (data.length !== 2) {
this.error('Bad handshake response.');
return;
}
if (data[0] !== 0x05) {
this.error('Bad SOCKS version.');
return;
}
this.emit('handshake');
switch (data[1]) {
case 0xff:
this.error('No acceptable auth methods.');
break;
case 0x02:
this.sendAuth();
break;
case 0x00:
this.state = SOCKS.states.AUTH;
this.emit('auth');
break;
default:
this.error('Handshake error: ' + 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 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 auth response.');
return;
}
if (data[0] !== 0x01) {
this.error('Bad version number.');
return;
}
if (data[1] !== 0x00) {
this.error('Auth failure: ' + data[0]);
return;
}
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(IP.toString(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 br, len, host, port;
if (data.length < 6) {
this.error('Setup failed.');
return;
}
if (data[0] !== 0x05) {
this.error('Bad SOCKS version.');
return;
}
switch (data[1]) {
case 0x00:
break;
case 0x01:
this.error('General failure.');
return;
case 0x02:
this.error('Connection not allowed.');
return;
case 0x03:
this.error('Network is unreachable.');
return;
case 0x04:
this.error('Host is unreachable.');
return;
case 0x05:
this.error('Connection refused.');
return;
case 0x06:
this.error('TTL expired.');
return;
case 0x07:
this.error('Command not supported.');
return;
case 0x08:
this.error('Address type not supported.');
return;
default:
this.error('Unknown proxy error: ' + data[1]);
return;
}
if (data[2] !== 0x00) {
this.error('Data corruption.');
return;
}
br = new BufferReader(data);
br.seek(2);
switch (data.readU8()) {
case 0x01:
if (br.left() < 6) {
this.error('Bad packet length.');
return;
}
host = IP.toString(br.readBytes(4));
port = br.readU16BE();
break;
case 0x03:
len = br.readU8();
if (br.left() < len + 2) {
this.error('Bad packet length.');
return;
}
host = br.readString(len, 'utf8');
port = br.readU16BE();
break;
case 0x04:
if (br.left() < 18) {
this.error('Bad packet length.');
return;
}
host = IP.toString(br.readBytes(16));
port = br.readU16BE();
break;
default:
this.error('Unknown response.');
return;
}
this.state = SOCKS.states.PROXY_DONE;
this.destroy();
this.emit('proxy', { host: host, port: port });
};
SOCKS.prototype.sendResolve = function sendResolve() {
var domain = this.domain;
var len = Buffer.byteLength(domain, 'utf8');
var packet = new StaticWriter(7 + len);
packet.writeU8(0x05);
packet.writeU8(0xf0);
packet.wruteU8(0x00);
packet.writeU8(0x03);
packet.writeU8(len);
packet.writeString(domain, 'utf8');
packet.writeU16BE(0);
packet = packet.render();
this.state = SOCKS.states.RESOLVE;
this.socket.write(packet);
};
SOCKS.prototype.handleResolve = function handleResolve(data) {
var ip;
if (data.length !== 8) {
this.error('Resolve failed.');
return;
}
if (data[0] !== 0x05) {
this.error('Bad SOCKS version.');
return;
}
if (data[1] !== 0x00) {
this.error('Tor error: ' + data[1]);
return;
}
if (data[3] !== 0x01) {
this.error('Tor error.');
return;
}
try {
ip = IP.toString(data.slice(4, 8));
} catch (e) {
this.error(e);
return;
}
this.state = SOCKS.states.RESOLVE_DONE;
this.destroy();
this.emit('resolve', ip);
};
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} proxy
* @param {String?} user
* @param {String?} pass
*/
function Proxy(proxy, user, pass) {
if (!(this instanceof Proxy))
return new Proxy(proxy, user, pass);
EventEmitter.call(this);
this.socket = new net.Socket();
this.proxy = IP.fromHostname(proxy);
this.username = user;
this.password = pass;
this.bytesWritten = 0;
this.bytesRead = 0;
this.remoteAddress = null;
this.remotePort = 0;
}
util.inherits(Proxy, EventEmitter);
Proxy.prototype.connect = co(function* connect(port, host) {
var self = this;
var options, addr;
options = {
host: this.proxy.host,
port: this.proxy.port,
username: this.username,
password: this.password,
destHost: host,
destPort: port
};
try {
addr = yield SOCKS.proxy(options);
} catch (e) {
this.emit('error', e);
return;
}
this.socket.connect(addr.port, addr.host);
this.socket.on('connect', function() {
self.remoteAddress = self.socket.remoteAddress;
self.remotePort = self.socket.remotePort;
self.emit('connect');
});
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');
});
this.socket.on('drain', function() {
self.emit('drain');
});
});
Proxy.prototype.write = function write(data, callback) {
this.bytesWritten += data.length;
return this.socket.write(data, callback);
};
Proxy.prototype.end = function end() {
return this.socket.end();
};
Proxy.prototype.pause = function pause() {
return this.socket.pause();
};
Proxy.prototype.resume = function resume() {
return this.socket.resume();
};
Proxy.prototype.destroy = function destroy() {
return this.socket.destroy();
};
/*
* Expose
*/
exports.connect = function connect(proxy, port, host, user, pass) {
var socket = new Proxy(proxy, user, pass);
socket.connect(port, host);
return socket;
};
exports.resolve = function resolve(proxy, domain, user, pass) {
var addr = IP.fromHostname(proxy);
return SOCKS.resolve({
host: addr.host,
port: addr.port,
username: user,
password: pass,
domain: domain
});
};