net: add socks proxy.
This commit is contained in:
parent
b8324e0b09
commit
6c9dd723cd
626
lib/net/socks.js
Normal file
626
lib/net/socks.js
Normal 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
|
||||
});
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user