469 lines
10 KiB
JavaScript
469 lines
10 KiB
JavaScript
|
|
|
|
var net = require('net');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var inherits = require('util').inherits;
|
|
|
|
function proxysocket(socksHost, socksPort, socket) {
|
|
var self = this;
|
|
|
|
// Set when we start connect()
|
|
var connecting = false;
|
|
|
|
// Set when connection stages are finished
|
|
var connected = false;
|
|
|
|
// Stages in the SOCKS5 connection
|
|
var connectionStages = [
|
|
receiveSocksAuth,
|
|
receiveSocksConnect
|
|
];
|
|
|
|
// What stage we are in currently
|
|
var stage = 0;
|
|
|
|
// Set from connect() to what host and port we are connecting to
|
|
var host = '', port = 0;
|
|
|
|
// While the SOCKS connection is going we buffer the
|
|
// requests to write()
|
|
var unsent = [];
|
|
// and to pipe()
|
|
var unpiped = [];
|
|
|
|
// While the socket is still being setup the encoding
|
|
// is saved as we expect binray encoding on the socket
|
|
// until then
|
|
var socketEncoding = null;
|
|
|
|
// Default host/ports to use if not given
|
|
self.socksHost = socksHost = socksHost || 'localhost';
|
|
self.socksPort = socksPort = socksPort || '9050';
|
|
|
|
// Users can pass their own socket if they already have one
|
|
// connected to the SOCKS proxy
|
|
self.realSocket = socket = socket || (new net.Socket({
|
|
readable: true,
|
|
writable: true
|
|
}));
|
|
|
|
// A socket emits events like 'data' and 'connect'
|
|
EventEmitter.call(self);
|
|
|
|
self.readable = true;
|
|
self.writable = true;
|
|
|
|
// Read event for the real socket
|
|
socket.on('data', function (buffer) {
|
|
if (connected) {
|
|
self.emit('data', buffer);
|
|
} else {
|
|
// Emit an event useful for debugging the raw SOCKS data
|
|
self.emit('socksdata', buffer);
|
|
|
|
// Handle SOCKS protocol data
|
|
receiveSocksData(buffer);
|
|
}
|
|
});
|
|
|
|
socket.on('error', function (e) {
|
|
self.emit('error', e);
|
|
});
|
|
|
|
socket.on('end', function () {
|
|
self.writable = false;
|
|
|
|
self.emit('end');
|
|
});
|
|
|
|
socket.on('timeout', function () {
|
|
self.emit('timeout');
|
|
});
|
|
|
|
socket.on('close', function () {
|
|
self.writable = false;
|
|
|
|
if (connected) {
|
|
self.emit('close');
|
|
}
|
|
});
|
|
|
|
socket.on('drain', function () {
|
|
if (connected) {
|
|
self.emit('drain');
|
|
}
|
|
});
|
|
|
|
socket.on('readable', function () {
|
|
if (connected) {
|
|
self.emit('readable');
|
|
}
|
|
});
|
|
|
|
self.read = function (size) {
|
|
if (!connected) {
|
|
return null;
|
|
}
|
|
|
|
return socket.read(size);
|
|
};
|
|
|
|
self.destroy = function () {
|
|
self.writable = false;
|
|
|
|
return socket.destroy();
|
|
};
|
|
|
|
self.destroySoon = function () {
|
|
self.writable = false;
|
|
|
|
return socket.destroySoon();
|
|
};
|
|
|
|
self.ref = function () {
|
|
return socket.ref();
|
|
};
|
|
|
|
self.unref = function () {
|
|
return socket.unref();
|
|
};
|
|
|
|
self.setKeepAlive = function (enable, initialDelay) {
|
|
return socket.setKeepAlive(enable, initialDelay);
|
|
};
|
|
|
|
self.pipe = function (dest, opts) {
|
|
if (connected) {
|
|
return socket.pipe(dest, opts);
|
|
}
|
|
|
|
unpiped.push([dest, opts]);
|
|
return dest;
|
|
};
|
|
|
|
// Handle SOCKS protocol specific data
|
|
function receiveSocksData(data) {
|
|
while (data && stage < connectionStages.length) {
|
|
data = connectionStages[stage](data);
|
|
stage++;
|
|
}
|
|
|
|
// Emit the sockets first packet
|
|
if (connected && data) {
|
|
self.emit('data', data);
|
|
}
|
|
}
|
|
|
|
// Handle the response after sending authentication
|
|
function receiveSocksAuth(d) {
|
|
var error;
|
|
|
|
if (d.length !== 2) {
|
|
error = new Error('SOCKS authentication failed. Unexpected number of bytes received.');
|
|
} else if (d[0] !== 0x05) {
|
|
error = new Error('SOCKS authentication failed. Unexpected SOCKS version number: ' + d[0] + '.');
|
|
} else if (d[1] !== 0x00) {
|
|
error = new Error('SOCKS authentication failed. Unexpected SOCKS authentication method: ' + d[1] + '.');
|
|
}
|
|
|
|
if (error) {
|
|
self.emit('error', error);
|
|
return;
|
|
}
|
|
|
|
sendConnect();
|
|
}
|
|
|
|
// Handle the response after sending connection request
|
|
function receiveSocksConnect(d) {
|
|
var error;
|
|
|
|
if (d[0] !== 0x05) {
|
|
error = new Error('SOCKS connection failed. Unexpected SOCKS version number: ' + d[0] + '.');
|
|
} else if (d[1] !== 0x00) {
|
|
error = new Error('SOCKS connection failed. ' + connectErrors[d[1]] + '.');
|
|
} else if (d[2] !== 0x00) {
|
|
error = new Error('SOCKS connection failed. The reserved byte must be 0x00.');
|
|
}
|
|
|
|
if (error) {
|
|
self.emit('error', error);
|
|
return;
|
|
}
|
|
|
|
connected = true;
|
|
|
|
// TODO map some of the addresses?
|
|
self.localPort = socket.localPort;
|
|
self.localAddress = socket.localAddress;
|
|
self.remotePort = socket.remotePort;
|
|
self.remoteAddress = socket.remoteAddress;
|
|
self.bufferSize = socket.bufferSize;
|
|
|
|
// Set the real encoding which could have been
|
|
// changed while the socket was connecting
|
|
setEncoding(socketEncoding);
|
|
|
|
if (unsent.length) {
|
|
for (var i=0; i < unsent.length; i++) {
|
|
socket.write(unsent[i][0], unsent[i][1], unsent[i][2]);
|
|
}
|
|
|
|
unsent = [];
|
|
}
|
|
|
|
if (unpiped.length) {
|
|
for (var i=0; i < unpiped.length; i++) {
|
|
socket.pipe(unpiped[i][0], unpiped[i][1]);
|
|
}
|
|
|
|
unpiped = [];
|
|
}
|
|
|
|
// Emit the real 'connect' event
|
|
self.emit('connect');
|
|
}
|
|
|
|
function sendAuth() {
|
|
var request = new Buffer(3);
|
|
request[0] = 0x05; // SOCKS version
|
|
request[1] = 0x01; // number of authentication methods
|
|
request[2] = 0x00; // no authentication
|
|
|
|
if (!socket.write(request)) {
|
|
throw new Error("Unable to write to SOCKS socket");
|
|
}
|
|
}
|
|
|
|
// Parse a domain name into a buffer
|
|
function parseDomainName(host, buffer) {
|
|
var i, c;
|
|
|
|
buffer.push(host.length);
|
|
|
|
for (i = 0; i < host.length; i++) {
|
|
c = host.charCodeAt(i);
|
|
buffer.push(c);
|
|
}
|
|
}
|
|
|
|
// Parse an host like 1.2.3.4 into a 32-bit number
|
|
function parseIPv4(host, buffer) {
|
|
var i, n;
|
|
var parts = host.split('.');
|
|
|
|
for (i = 0; i < parts.length; ++i) {
|
|
n = parseInt(parts[i], 10);
|
|
buffer.push(n);
|
|
}
|
|
}
|
|
|
|
// Parse a IPv6 host into a buffer
|
|
function parseIPv6(host, buffer) {
|
|
var parts = host.split(':');
|
|
var i, ind;
|
|
var zeros = [];
|
|
|
|
parts[0] = parts[0] || '0000';
|
|
parts[parts.length - 1] = parts[parts.length - 1] || '0000';
|
|
ind = parts.indexOf('');
|
|
|
|
if (ind >= 0) {
|
|
for (i = 0; i < 8 - parts.length + 1; ++i) {
|
|
zeros.push('0000');
|
|
}
|
|
|
|
parts = parts.slice(0, ind).concat(zeros).concat(parts.slice(ind + 1));
|
|
}
|
|
|
|
for (i = 0; i < 8; ++i) {
|
|
var num = parseInt(parts[i], 16);
|
|
buffer.push(num / 256 | 0);
|
|
buffer.push(num % 256);
|
|
}
|
|
}
|
|
|
|
function sendConnect() {
|
|
var request;
|
|
var buffer = [
|
|
0x05, // SOCKS version
|
|
0x01, // Command code: establish a TCP/IP stream connection
|
|
0x00 // Reserved - myst be 0x00
|
|
];
|
|
|
|
switch (net.isIP(host)) {
|
|
default:
|
|
case 0:
|
|
buffer.push(0x03);
|
|
parseDomainName(host, buffer);
|
|
break;
|
|
case 4:
|
|
buffer.push(0x01);
|
|
parseIPv4(host, buffer);
|
|
break;
|
|
case 6:
|
|
buffer.push(0x04);
|
|
parseIPv6(host, buffer);
|
|
break;
|
|
}
|
|
|
|
htons(buffer, buffer.length, port);
|
|
request = new Buffer(buffer);
|
|
|
|
if (!socket.write(request)) {
|
|
throw new Error("Unable to write to SOCKS socket");
|
|
}
|
|
}
|
|
|
|
self.setTimeout = function (timeout, f) {
|
|
return socket.setTimeout(timeout, f);
|
|
};
|
|
|
|
self.setNoDelay = function (noDelay) {
|
|
return socket.setNoDelay(noDelay);
|
|
};
|
|
|
|
self.connect = function (connectPort, connectHost, f) {
|
|
var tmp;
|
|
|
|
// Backward compatibility
|
|
if (typeof(connectHost) === 'number' || isNaN(connectPort)) {
|
|
tmp = connectHost;
|
|
connectHost = connectPort;
|
|
connectPort = tmp;
|
|
}
|
|
|
|
if (connected) {
|
|
throw new Error("Socket is already connected");
|
|
}
|
|
|
|
if (connecting) {
|
|
throw new Error("Socket is already connecting");
|
|
}
|
|
|
|
host = connectHost;
|
|
port = connectPort;
|
|
connected = false;
|
|
connecting = true;
|
|
|
|
if (f) {
|
|
self.on('connect', f);
|
|
}
|
|
|
|
setEncoding(null);
|
|
|
|
return socket.connect(socksPort, socksHost, function () {
|
|
connecting = false;
|
|
sendAuth();
|
|
});
|
|
};
|
|
|
|
self.write = function (data, encoding, f) {
|
|
if (!connected) {
|
|
unsent.push([data, encoding, f]);
|
|
return;
|
|
}
|
|
|
|
return socket.write(data, encoding, f);
|
|
};
|
|
|
|
self.pause = function () {
|
|
socket.pause();
|
|
};
|
|
|
|
self.resume = function () {
|
|
socket.resume();
|
|
};
|
|
|
|
self.address = function () {
|
|
return socket.address();
|
|
};
|
|
|
|
self.end = function (data, encoding) {
|
|
socket.writable = false;
|
|
|
|
if (!connected) {
|
|
return socket.end();
|
|
}
|
|
|
|
return socket.end(data, encoding);
|
|
};
|
|
|
|
self.setEncoding = function (encoding) {
|
|
if (connected) {
|
|
setEncoding(encoding);
|
|
} else {
|
|
// Save encoding to be set once connected
|
|
socketEncoding = encoding;
|
|
}
|
|
};
|
|
|
|
function setEncoding(enc) {
|
|
if (enc === null) {
|
|
// Accroding to nodejs documentation, readable.setEncoding(null)
|
|
// is a way to disable encoding.
|
|
// However, it's not. So, need to hack into readable structure.
|
|
socket.decoder = null;
|
|
socket.encoding = null;
|
|
} else {
|
|
socket.setEncoding(enc);
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
inherits(proxysocket, EventEmitter);
|
|
|
|
proxysocket.create = function (socksHost, socksPort, socket) {
|
|
return new proxysocket(socksHost, socksPort, socket);
|
|
};
|
|
|
|
// A simple agent so that requests can be made using http.request()
|
|
// and anything else using the same Agent API
|
|
proxysocket.createAgent = function (socksHost, socksPort) {
|
|
var http = require('http');
|
|
|
|
var agent = new http.Agent({
|
|
// keepAlive: true
|
|
});
|
|
|
|
function connect(host, port, f) {
|
|
var socket = proxysocket.create(socksHost, socksPort);
|
|
socket.connect(port, host, f);
|
|
return socket;
|
|
}
|
|
|
|
agent.createConnection = function (options, f) {
|
|
return connect(
|
|
options.host,
|
|
options.port,
|
|
f
|
|
);
|
|
};
|
|
|
|
return agent;
|
|
};
|
|
|
|
module.exports = proxysocket;
|
|
|
|
// Converts a 16-bit short from Host To Network Storage
|
|
function htons(b, i, v) {
|
|
b[i] = (0xff & (v >> 8));
|
|
b[i + 1] = (0xff & (v));
|
|
}
|
|
|
|
// Error messages for when the proxy responds to sendConnect() used in handleConnect()
|
|
var connectErrors = {
|
|
// Messages are taken from Wikipedia
|
|
0x00: 'request granted',
|
|
0x01: 'general failure',
|
|
0x02: 'connection not allowed by ruleset',
|
|
0x03: 'network unreachable',
|
|
0x04: 'host unreachable',
|
|
0x05: 'connection refused by destination host',
|
|
0x06: 'TTL expired',
|
|
0x07: 'command not supported / protocol error',
|
|
0x08: 'address type not supported'
|
|
};
|