You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
793 lines
34 KiB
793 lines
34 KiB
"use strict"; |
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { |
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } |
|
return new (P || (P = Promise))(function (resolve, reject) { |
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } |
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } |
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } |
|
step((generator = generator.apply(thisArg, _arguments || [])).next()); |
|
}); |
|
}; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.SocksClientError = exports.SocksClient = void 0; |
|
const events_1 = require("events"); |
|
const net = require("net"); |
|
const ip = require("ip"); |
|
const smart_buffer_1 = require("smart-buffer"); |
|
const constants_1 = require("../common/constants"); |
|
const helpers_1 = require("../common/helpers"); |
|
const receivebuffer_1 = require("../common/receivebuffer"); |
|
const util_1 = require("../common/util"); |
|
Object.defineProperty(exports, "SocksClientError", { enumerable: true, get: function () { return util_1.SocksClientError; } }); |
|
class SocksClient extends events_1.EventEmitter { |
|
constructor(options) { |
|
super(); |
|
this.options = Object.assign({}, options); |
|
// Validate SocksClientOptions |
|
(0, helpers_1.validateSocksClientOptions)(options); |
|
// Default state |
|
this.setState(constants_1.SocksClientState.Created); |
|
} |
|
/** |
|
* Creates a new SOCKS connection. |
|
* |
|
* Note: Supports callbacks and promises. Only supports the connect command. |
|
* @param options { SocksClientOptions } Options. |
|
* @param callback { Function } An optional callback function. |
|
* @returns { Promise } |
|
*/ |
|
static createConnection(options, callback) { |
|
return new Promise((resolve, reject) => { |
|
// Validate SocksClientOptions |
|
try { |
|
(0, helpers_1.validateSocksClientOptions)(options, ['connect']); |
|
} |
|
catch (err) { |
|
if (typeof callback === 'function') { |
|
callback(err); |
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
return resolve(err); // Resolves pending promise (prevents memory leaks). |
|
} |
|
else { |
|
return reject(err); |
|
} |
|
} |
|
const client = new SocksClient(options); |
|
client.connect(options.existing_socket); |
|
client.once('established', (info) => { |
|
client.removeAllListeners(); |
|
if (typeof callback === 'function') { |
|
callback(null, info); |
|
resolve(info); // Resolves pending promise (prevents memory leaks). |
|
} |
|
else { |
|
resolve(info); |
|
} |
|
}); |
|
// Error occurred, failed to establish connection. |
|
client.once('error', (err) => { |
|
client.removeAllListeners(); |
|
if (typeof callback === 'function') { |
|
callback(err); |
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
resolve(err); // Resolves pending promise (prevents memory leaks). |
|
} |
|
else { |
|
reject(err); |
|
} |
|
}); |
|
}); |
|
} |
|
/** |
|
* Creates a new SOCKS connection chain to a destination host through 2 or more SOCKS proxies. |
|
* |
|
* Note: Supports callbacks and promises. Only supports the connect method. |
|
* Note: Implemented via createConnection() factory function. |
|
* @param options { SocksClientChainOptions } Options |
|
* @param callback { Function } An optional callback function. |
|
* @returns { Promise } |
|
*/ |
|
static createConnectionChain(options, callback) { |
|
// eslint-disable-next-line no-async-promise-executor |
|
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { |
|
// Validate SocksClientChainOptions |
|
try { |
|
(0, helpers_1.validateSocksClientChainOptions)(options); |
|
} |
|
catch (err) { |
|
if (typeof callback === 'function') { |
|
callback(err); |
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
return resolve(err); // Resolves pending promise (prevents memory leaks). |
|
} |
|
else { |
|
return reject(err); |
|
} |
|
} |
|
// Shuffle proxies |
|
if (options.randomizeChain) { |
|
(0, util_1.shuffleArray)(options.proxies); |
|
} |
|
try { |
|
let sock; |
|
for (let i = 0; i < options.proxies.length; i++) { |
|
const nextProxy = options.proxies[i]; |
|
// If we've reached the last proxy in the chain, the destination is the actual destination, otherwise it's the next proxy. |
|
const nextDestination = i === options.proxies.length - 1 |
|
? options.destination |
|
: { |
|
host: options.proxies[i + 1].host || |
|
options.proxies[i + 1].ipaddress, |
|
port: options.proxies[i + 1].port, |
|
}; |
|
// Creates the next connection in the chain. |
|
const result = yield SocksClient.createConnection({ |
|
command: 'connect', |
|
proxy: nextProxy, |
|
destination: nextDestination, |
|
existing_socket: sock, |
|
}); |
|
// If sock is undefined, assign it here. |
|
sock = sock || result.socket; |
|
} |
|
if (typeof callback === 'function') { |
|
callback(null, { socket: sock }); |
|
resolve({ socket: sock }); // Resolves pending promise (prevents memory leaks). |
|
} |
|
else { |
|
resolve({ socket: sock }); |
|
} |
|
} |
|
catch (err) { |
|
if (typeof callback === 'function') { |
|
callback(err); |
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
resolve(err); // Resolves pending promise (prevents memory leaks). |
|
} |
|
else { |
|
reject(err); |
|
} |
|
} |
|
})); |
|
} |
|
/** |
|
* Creates a SOCKS UDP Frame. |
|
* @param options |
|
*/ |
|
static createUDPFrame(options) { |
|
const buff = new smart_buffer_1.SmartBuffer(); |
|
buff.writeUInt16BE(0); |
|
buff.writeUInt8(options.frameNumber || 0); |
|
// IPv4/IPv6/Hostname |
|
if (net.isIPv4(options.remoteHost.host)) { |
|
buff.writeUInt8(constants_1.Socks5HostType.IPv4); |
|
buff.writeUInt32BE(ip.toLong(options.remoteHost.host)); |
|
} |
|
else if (net.isIPv6(options.remoteHost.host)) { |
|
buff.writeUInt8(constants_1.Socks5HostType.IPv6); |
|
buff.writeBuffer(ip.toBuffer(options.remoteHost.host)); |
|
} |
|
else { |
|
buff.writeUInt8(constants_1.Socks5HostType.Hostname); |
|
buff.writeUInt8(Buffer.byteLength(options.remoteHost.host)); |
|
buff.writeString(options.remoteHost.host); |
|
} |
|
// Port |
|
buff.writeUInt16BE(options.remoteHost.port); |
|
// Data |
|
buff.writeBuffer(options.data); |
|
return buff.toBuffer(); |
|
} |
|
/** |
|
* Parses a SOCKS UDP frame. |
|
* @param data |
|
*/ |
|
static parseUDPFrame(data) { |
|
const buff = smart_buffer_1.SmartBuffer.fromBuffer(data); |
|
buff.readOffset = 2; |
|
const frameNumber = buff.readUInt8(); |
|
const hostType = buff.readUInt8(); |
|
let remoteHost; |
|
if (hostType === constants_1.Socks5HostType.IPv4) { |
|
remoteHost = ip.fromLong(buff.readUInt32BE()); |
|
} |
|
else if (hostType === constants_1.Socks5HostType.IPv6) { |
|
remoteHost = ip.toString(buff.readBuffer(16)); |
|
} |
|
else { |
|
remoteHost = buff.readString(buff.readUInt8()); |
|
} |
|
const remotePort = buff.readUInt16BE(); |
|
return { |
|
frameNumber, |
|
remoteHost: { |
|
host: remoteHost, |
|
port: remotePort, |
|
}, |
|
data: buff.readBuffer(), |
|
}; |
|
} |
|
/** |
|
* Internal state setter. If the SocksClient is in an error state, it cannot be changed to a non error state. |
|
*/ |
|
setState(newState) { |
|
if (this.state !== constants_1.SocksClientState.Error) { |
|
this.state = newState; |
|
} |
|
} |
|
/** |
|
* Starts the connection establishment to the proxy and destination. |
|
* @param existingSocket Connected socket to use instead of creating a new one (internal use). |
|
*/ |
|
connect(existingSocket) { |
|
this.onDataReceived = (data) => this.onDataReceivedHandler(data); |
|
this.onClose = () => this.onCloseHandler(); |
|
this.onError = (err) => this.onErrorHandler(err); |
|
this.onConnect = () => this.onConnectHandler(); |
|
// Start timeout timer (defaults to 30 seconds) |
|
const timer = setTimeout(() => this.onEstablishedTimeout(), this.options.timeout || constants_1.DEFAULT_TIMEOUT); |
|
// check whether unref is available as it differs from browser to NodeJS (#33) |
|
if (timer.unref && typeof timer.unref === 'function') { |
|
timer.unref(); |
|
} |
|
// If an existing socket is provided, use it to negotiate SOCKS handshake. Otherwise create a new Socket. |
|
if (existingSocket) { |
|
this.socket = existingSocket; |
|
} |
|
else { |
|
this.socket = new net.Socket(); |
|
} |
|
// Attach Socket error handlers. |
|
this.socket.once('close', this.onClose); |
|
this.socket.once('error', this.onError); |
|
this.socket.once('connect', this.onConnect); |
|
this.socket.on('data', this.onDataReceived); |
|
this.setState(constants_1.SocksClientState.Connecting); |
|
this.receiveBuffer = new receivebuffer_1.ReceiveBuffer(); |
|
if (existingSocket) { |
|
this.socket.emit('connect'); |
|
} |
|
else { |
|
this.socket.connect(this.getSocketOptions()); |
|
if (this.options.set_tcp_nodelay !== undefined && |
|
this.options.set_tcp_nodelay !== null) { |
|
this.socket.setNoDelay(!!this.options.set_tcp_nodelay); |
|
} |
|
} |
|
// Listen for established event so we can re-emit any excess data received during handshakes. |
|
this.prependOnceListener('established', (info) => { |
|
setImmediate(() => { |
|
if (this.receiveBuffer.length > 0) { |
|
const excessData = this.receiveBuffer.get(this.receiveBuffer.length); |
|
info.socket.emit('data', excessData); |
|
} |
|
info.socket.resume(); |
|
}); |
|
}); |
|
} |
|
// Socket options (defaults host/port to options.proxy.host/options.proxy.port) |
|
getSocketOptions() { |
|
return Object.assign(Object.assign({}, this.options.socket_options), { host: this.options.proxy.host || this.options.proxy.ipaddress, port: this.options.proxy.port }); |
|
} |
|
/** |
|
* Handles internal Socks timeout callback. |
|
* Note: If the Socks client is not BoundWaitingForConnection or Established, the connection will be closed. |
|
*/ |
|
onEstablishedTimeout() { |
|
if (this.state !== constants_1.SocksClientState.Established && |
|
this.state !== constants_1.SocksClientState.BoundWaitingForConnection) { |
|
this.closeSocket(constants_1.ERRORS.ProxyConnectionTimedOut); |
|
} |
|
} |
|
/** |
|
* Handles Socket connect event. |
|
*/ |
|
onConnectHandler() { |
|
this.setState(constants_1.SocksClientState.Connected); |
|
// Send initial handshake. |
|
if (this.options.proxy.type === 4) { |
|
this.sendSocks4InitialHandshake(); |
|
} |
|
else { |
|
this.sendSocks5InitialHandshake(); |
|
} |
|
this.setState(constants_1.SocksClientState.SentInitialHandshake); |
|
} |
|
/** |
|
* Handles Socket data event. |
|
* @param data |
|
*/ |
|
onDataReceivedHandler(data) { |
|
/* |
|
All received data is appended to a ReceiveBuffer. |
|
This makes sure that all the data we need is received before we attempt to process it. |
|
*/ |
|
this.receiveBuffer.append(data); |
|
// Process data that we have. |
|
this.processData(); |
|
} |
|
/** |
|
* Handles processing of the data we have received. |
|
*/ |
|
processData() { |
|
// If we have enough data to process the next step in the SOCKS handshake, proceed. |
|
while (this.state !== constants_1.SocksClientState.Established && |
|
this.state !== constants_1.SocksClientState.Error && |
|
this.receiveBuffer.length >= this.nextRequiredPacketBufferSize) { |
|
// Sent initial handshake, waiting for response. |
|
if (this.state === constants_1.SocksClientState.SentInitialHandshake) { |
|
if (this.options.proxy.type === 4) { |
|
// Socks v4 only has one handshake response. |
|
this.handleSocks4FinalHandshakeResponse(); |
|
} |
|
else { |
|
// Socks v5 has two handshakes, handle initial one here. |
|
this.handleInitialSocks5HandshakeResponse(); |
|
} |
|
// Sent auth request for Socks v5, waiting for response. |
|
} |
|
else if (this.state === constants_1.SocksClientState.SentAuthentication) { |
|
this.handleInitialSocks5AuthenticationHandshakeResponse(); |
|
// Sent final Socks v5 handshake, waiting for final response. |
|
} |
|
else if (this.state === constants_1.SocksClientState.SentFinalHandshake) { |
|
this.handleSocks5FinalHandshakeResponse(); |
|
// Socks BIND established. Waiting for remote connection via proxy. |
|
} |
|
else if (this.state === constants_1.SocksClientState.BoundWaitingForConnection) { |
|
if (this.options.proxy.type === 4) { |
|
this.handleSocks4IncomingConnectionResponse(); |
|
} |
|
else { |
|
this.handleSocks5IncomingConnectionResponse(); |
|
} |
|
} |
|
else { |
|
this.closeSocket(constants_1.ERRORS.InternalError); |
|
break; |
|
} |
|
} |
|
} |
|
/** |
|
* Handles Socket close event. |
|
* @param had_error |
|
*/ |
|
onCloseHandler() { |
|
this.closeSocket(constants_1.ERRORS.SocketClosed); |
|
} |
|
/** |
|
* Handles Socket error event. |
|
* @param err |
|
*/ |
|
onErrorHandler(err) { |
|
this.closeSocket(err.message); |
|
} |
|
/** |
|
* Removes internal event listeners on the underlying Socket. |
|
*/ |
|
removeInternalSocketHandlers() { |
|
// Pauses data flow of the socket (this is internally resumed after 'established' is emitted) |
|
this.socket.pause(); |
|
this.socket.removeListener('data', this.onDataReceived); |
|
this.socket.removeListener('close', this.onClose); |
|
this.socket.removeListener('error', this.onError); |
|
this.socket.removeListener('connect', this.onConnect); |
|
} |
|
/** |
|
* Closes and destroys the underlying Socket. Emits an error event. |
|
* @param err { String } An error string to include in error event. |
|
*/ |
|
closeSocket(err) { |
|
// Make sure only one 'error' event is fired for the lifetime of this SocksClient instance. |
|
if (this.state !== constants_1.SocksClientState.Error) { |
|
// Set internal state to Error. |
|
this.setState(constants_1.SocksClientState.Error); |
|
// Destroy Socket |
|
this.socket.destroy(); |
|
// Remove internal listeners |
|
this.removeInternalSocketHandlers(); |
|
// Fire 'error' event. |
|
this.emit('error', new util_1.SocksClientError(err, this.options)); |
|
} |
|
} |
|
/** |
|
* Sends initial Socks v4 handshake request. |
|
*/ |
|
sendSocks4InitialHandshake() { |
|
const userId = this.options.proxy.userId || ''; |
|
const buff = new smart_buffer_1.SmartBuffer(); |
|
buff.writeUInt8(0x04); |
|
buff.writeUInt8(constants_1.SocksCommand[this.options.command]); |
|
buff.writeUInt16BE(this.options.destination.port); |
|
// Socks 4 (IPv4) |
|
if (net.isIPv4(this.options.destination.host)) { |
|
buff.writeBuffer(ip.toBuffer(this.options.destination.host)); |
|
buff.writeStringNT(userId); |
|
// Socks 4a (hostname) |
|
} |
|
else { |
|
buff.writeUInt8(0x00); |
|
buff.writeUInt8(0x00); |
|
buff.writeUInt8(0x00); |
|
buff.writeUInt8(0x01); |
|
buff.writeStringNT(userId); |
|
buff.writeStringNT(this.options.destination.host); |
|
} |
|
this.nextRequiredPacketBufferSize = |
|
constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks4Response; |
|
this.socket.write(buff.toBuffer()); |
|
} |
|
/** |
|
* Handles Socks v4 handshake response. |
|
* @param data |
|
*/ |
|
handleSocks4FinalHandshakeResponse() { |
|
const data = this.receiveBuffer.get(8); |
|
if (data[1] !== constants_1.Socks4Response.Granted) { |
|
this.closeSocket(`${constants_1.ERRORS.Socks4ProxyRejectedConnection} - (${constants_1.Socks4Response[data[1]]})`); |
|
} |
|
else { |
|
// Bind response |
|
if (constants_1.SocksCommand[this.options.command] === constants_1.SocksCommand.bind) { |
|
const buff = smart_buffer_1.SmartBuffer.fromBuffer(data); |
|
buff.readOffset = 2; |
|
const remoteHost = { |
|
port: buff.readUInt16BE(), |
|
host: ip.fromLong(buff.readUInt32BE()), |
|
}; |
|
// If host is 0.0.0.0, set to proxy host. |
|
if (remoteHost.host === '0.0.0.0') { |
|
remoteHost.host = this.options.proxy.ipaddress; |
|
} |
|
this.setState(constants_1.SocksClientState.BoundWaitingForConnection); |
|
this.emit('bound', { remoteHost, socket: this.socket }); |
|
// Connect response |
|
} |
|
else { |
|
this.setState(constants_1.SocksClientState.Established); |
|
this.removeInternalSocketHandlers(); |
|
this.emit('established', { socket: this.socket }); |
|
} |
|
} |
|
} |
|
/** |
|
* Handles Socks v4 incoming connection request (BIND) |
|
* @param data |
|
*/ |
|
handleSocks4IncomingConnectionResponse() { |
|
const data = this.receiveBuffer.get(8); |
|
if (data[1] !== constants_1.Socks4Response.Granted) { |
|
this.closeSocket(`${constants_1.ERRORS.Socks4ProxyRejectedIncomingBoundConnection} - (${constants_1.Socks4Response[data[1]]})`); |
|
} |
|
else { |
|
const buff = smart_buffer_1.SmartBuffer.fromBuffer(data); |
|
buff.readOffset = 2; |
|
const remoteHost = { |
|
port: buff.readUInt16BE(), |
|
host: ip.fromLong(buff.readUInt32BE()), |
|
}; |
|
this.setState(constants_1.SocksClientState.Established); |
|
this.removeInternalSocketHandlers(); |
|
this.emit('established', { remoteHost, socket: this.socket }); |
|
} |
|
} |
|
/** |
|
* Sends initial Socks v5 handshake request. |
|
*/ |
|
sendSocks5InitialHandshake() { |
|
const buff = new smart_buffer_1.SmartBuffer(); |
|
// By default we always support no auth. |
|
const supportedAuthMethods = [constants_1.Socks5Auth.NoAuth]; |
|
// We should only tell the proxy we support user/pass auth if auth info is actually provided. |
|
// Note: As of Tor v0.3.5.7+, if user/pass auth is an option from the client, by default it will always take priority. |
|
if (this.options.proxy.userId || this.options.proxy.password) { |
|
supportedAuthMethods.push(constants_1.Socks5Auth.UserPass); |
|
} |
|
// Custom auth method? |
|
if (this.options.proxy.custom_auth_method !== undefined) { |
|
supportedAuthMethods.push(this.options.proxy.custom_auth_method); |
|
} |
|
// Build handshake packet |
|
buff.writeUInt8(0x05); |
|
buff.writeUInt8(supportedAuthMethods.length); |
|
for (const authMethod of supportedAuthMethods) { |
|
buff.writeUInt8(authMethod); |
|
} |
|
this.nextRequiredPacketBufferSize = |
|
constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5InitialHandshakeResponse; |
|
this.socket.write(buff.toBuffer()); |
|
this.setState(constants_1.SocksClientState.SentInitialHandshake); |
|
} |
|
/** |
|
* Handles initial Socks v5 handshake response. |
|
* @param data |
|
*/ |
|
handleInitialSocks5HandshakeResponse() { |
|
const data = this.receiveBuffer.get(2); |
|
if (data[0] !== 0x05) { |
|
this.closeSocket(constants_1.ERRORS.InvalidSocks5IntiailHandshakeSocksVersion); |
|
} |
|
else if (data[1] === constants_1.SOCKS5_NO_ACCEPTABLE_AUTH) { |
|
this.closeSocket(constants_1.ERRORS.InvalidSocks5InitialHandshakeNoAcceptedAuthType); |
|
} |
|
else { |
|
// If selected Socks v5 auth method is no auth, send final handshake request. |
|
if (data[1] === constants_1.Socks5Auth.NoAuth) { |
|
this.socks5ChosenAuthType = constants_1.Socks5Auth.NoAuth; |
|
this.sendSocks5CommandRequest(); |
|
// If selected Socks v5 auth method is user/password, send auth handshake. |
|
} |
|
else if (data[1] === constants_1.Socks5Auth.UserPass) { |
|
this.socks5ChosenAuthType = constants_1.Socks5Auth.UserPass; |
|
this.sendSocks5UserPassAuthentication(); |
|
// If selected Socks v5 auth method is the custom_auth_method, send custom handshake. |
|
} |
|
else if (data[1] === this.options.proxy.custom_auth_method) { |
|
this.socks5ChosenAuthType = this.options.proxy.custom_auth_method; |
|
this.sendSocks5CustomAuthentication(); |
|
} |
|
else { |
|
this.closeSocket(constants_1.ERRORS.InvalidSocks5InitialHandshakeUnknownAuthType); |
|
} |
|
} |
|
} |
|
/** |
|
* Sends Socks v5 user & password auth handshake. |
|
* |
|
* Note: No auth and user/pass are currently supported. |
|
*/ |
|
sendSocks5UserPassAuthentication() { |
|
const userId = this.options.proxy.userId || ''; |
|
const password = this.options.proxy.password || ''; |
|
const buff = new smart_buffer_1.SmartBuffer(); |
|
buff.writeUInt8(0x01); |
|
buff.writeUInt8(Buffer.byteLength(userId)); |
|
buff.writeString(userId); |
|
buff.writeUInt8(Buffer.byteLength(password)); |
|
buff.writeString(password); |
|
this.nextRequiredPacketBufferSize = |
|
constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5UserPassAuthenticationResponse; |
|
this.socket.write(buff.toBuffer()); |
|
this.setState(constants_1.SocksClientState.SentAuthentication); |
|
} |
|
sendSocks5CustomAuthentication() { |
|
return __awaiter(this, void 0, void 0, function* () { |
|
this.nextRequiredPacketBufferSize = |
|
this.options.proxy.custom_auth_response_size; |
|
this.socket.write(yield this.options.proxy.custom_auth_request_handler()); |
|
this.setState(constants_1.SocksClientState.SentAuthentication); |
|
}); |
|
} |
|
handleSocks5CustomAuthHandshakeResponse(data) { |
|
return __awaiter(this, void 0, void 0, function* () { |
|
return yield this.options.proxy.custom_auth_response_handler(data); |
|
}); |
|
} |
|
handleSocks5AuthenticationNoAuthHandshakeResponse(data) { |
|
return __awaiter(this, void 0, void 0, function* () { |
|
return data[1] === 0x00; |
|
}); |
|
} |
|
handleSocks5AuthenticationUserPassHandshakeResponse(data) { |
|
return __awaiter(this, void 0, void 0, function* () { |
|
return data[1] === 0x00; |
|
}); |
|
} |
|
/** |
|
* Handles Socks v5 auth handshake response. |
|
* @param data |
|
*/ |
|
handleInitialSocks5AuthenticationHandshakeResponse() { |
|
return __awaiter(this, void 0, void 0, function* () { |
|
this.setState(constants_1.SocksClientState.ReceivedAuthenticationResponse); |
|
let authResult = false; |
|
if (this.socks5ChosenAuthType === constants_1.Socks5Auth.NoAuth) { |
|
authResult = yield this.handleSocks5AuthenticationNoAuthHandshakeResponse(this.receiveBuffer.get(2)); |
|
} |
|
else if (this.socks5ChosenAuthType === constants_1.Socks5Auth.UserPass) { |
|
authResult = |
|
yield this.handleSocks5AuthenticationUserPassHandshakeResponse(this.receiveBuffer.get(2)); |
|
} |
|
else if (this.socks5ChosenAuthType === this.options.proxy.custom_auth_method) { |
|
authResult = yield this.handleSocks5CustomAuthHandshakeResponse(this.receiveBuffer.get(this.options.proxy.custom_auth_response_size)); |
|
} |
|
if (!authResult) { |
|
this.closeSocket(constants_1.ERRORS.Socks5AuthenticationFailed); |
|
} |
|
else { |
|
this.sendSocks5CommandRequest(); |
|
} |
|
}); |
|
} |
|
/** |
|
* Sends Socks v5 final handshake request. |
|
*/ |
|
sendSocks5CommandRequest() { |
|
const buff = new smart_buffer_1.SmartBuffer(); |
|
buff.writeUInt8(0x05); |
|
buff.writeUInt8(constants_1.SocksCommand[this.options.command]); |
|
buff.writeUInt8(0x00); |
|
// ipv4, ipv6, domain? |
|
if (net.isIPv4(this.options.destination.host)) { |
|
buff.writeUInt8(constants_1.Socks5HostType.IPv4); |
|
buff.writeBuffer(ip.toBuffer(this.options.destination.host)); |
|
} |
|
else if (net.isIPv6(this.options.destination.host)) { |
|
buff.writeUInt8(constants_1.Socks5HostType.IPv6); |
|
buff.writeBuffer(ip.toBuffer(this.options.destination.host)); |
|
} |
|
else { |
|
buff.writeUInt8(constants_1.Socks5HostType.Hostname); |
|
buff.writeUInt8(this.options.destination.host.length); |
|
buff.writeString(this.options.destination.host); |
|
} |
|
buff.writeUInt16BE(this.options.destination.port); |
|
this.nextRequiredPacketBufferSize = |
|
constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseHeader; |
|
this.socket.write(buff.toBuffer()); |
|
this.setState(constants_1.SocksClientState.SentFinalHandshake); |
|
} |
|
/** |
|
* Handles Socks v5 final handshake response. |
|
* @param data |
|
*/ |
|
handleSocks5FinalHandshakeResponse() { |
|
// Peek at available data (we need at least 5 bytes to get the hostname length) |
|
const header = this.receiveBuffer.peek(5); |
|
if (header[0] !== 0x05 || header[1] !== constants_1.Socks5Response.Granted) { |
|
this.closeSocket(`${constants_1.ERRORS.InvalidSocks5FinalHandshakeRejected} - ${constants_1.Socks5Response[header[1]]}`); |
|
} |
|
else { |
|
// Read address type |
|
const addressType = header[3]; |
|
let remoteHost; |
|
let buff; |
|
// IPv4 |
|
if (addressType === constants_1.Socks5HostType.IPv4) { |
|
// Check if data is available. |
|
const dataNeeded = constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseIPv4; |
|
if (this.receiveBuffer.length < dataNeeded) { |
|
this.nextRequiredPacketBufferSize = dataNeeded; |
|
return; |
|
} |
|
buff = smart_buffer_1.SmartBuffer.fromBuffer(this.receiveBuffer.get(dataNeeded).slice(4)); |
|
remoteHost = { |
|
host: ip.fromLong(buff.readUInt32BE()), |
|
port: buff.readUInt16BE(), |
|
}; |
|
// If given host is 0.0.0.0, assume remote proxy ip instead. |
|
if (remoteHost.host === '0.0.0.0') { |
|
remoteHost.host = this.options.proxy.ipaddress; |
|
} |
|
// Hostname |
|
} |
|
else if (addressType === constants_1.Socks5HostType.Hostname) { |
|
const hostLength = header[4]; |
|
const dataNeeded = constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseHostname(hostLength); // header + host length + host + port |
|
// Check if data is available. |
|
if (this.receiveBuffer.length < dataNeeded) { |
|
this.nextRequiredPacketBufferSize = dataNeeded; |
|
return; |
|
} |
|
buff = smart_buffer_1.SmartBuffer.fromBuffer(this.receiveBuffer.get(dataNeeded).slice(5)); |
|
remoteHost = { |
|
host: buff.readString(hostLength), |
|
port: buff.readUInt16BE(), |
|
}; |
|
// IPv6 |
|
} |
|
else if (addressType === constants_1.Socks5HostType.IPv6) { |
|
// Check if data is available. |
|
const dataNeeded = constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseIPv6; |
|
if (this.receiveBuffer.length < dataNeeded) { |
|
this.nextRequiredPacketBufferSize = dataNeeded; |
|
return; |
|
} |
|
buff = smart_buffer_1.SmartBuffer.fromBuffer(this.receiveBuffer.get(dataNeeded).slice(4)); |
|
remoteHost = { |
|
host: ip.toString(buff.readBuffer(16)), |
|
port: buff.readUInt16BE(), |
|
}; |
|
} |
|
// We have everything we need |
|
this.setState(constants_1.SocksClientState.ReceivedFinalResponse); |
|
// If using CONNECT, the client is now in the established state. |
|
if (constants_1.SocksCommand[this.options.command] === constants_1.SocksCommand.connect) { |
|
this.setState(constants_1.SocksClientState.Established); |
|
this.removeInternalSocketHandlers(); |
|
this.emit('established', { remoteHost, socket: this.socket }); |
|
} |
|
else if (constants_1.SocksCommand[this.options.command] === constants_1.SocksCommand.bind) { |
|
/* If using BIND, the Socks client is now in BoundWaitingForConnection state. |
|
This means that the remote proxy server is waiting for a remote connection to the bound port. */ |
|
this.setState(constants_1.SocksClientState.BoundWaitingForConnection); |
|
this.nextRequiredPacketBufferSize = |
|
constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseHeader; |
|
this.emit('bound', { remoteHost, socket: this.socket }); |
|
/* |
|
If using Associate, the Socks client is now Established. And the proxy server is now accepting UDP packets at the |
|
given bound port. This initial Socks TCP connection must remain open for the UDP relay to continue to work. |
|
*/ |
|
} |
|
else if (constants_1.SocksCommand[this.options.command] === constants_1.SocksCommand.associate) { |
|
this.setState(constants_1.SocksClientState.Established); |
|
this.removeInternalSocketHandlers(); |
|
this.emit('established', { |
|
remoteHost, |
|
socket: this.socket, |
|
}); |
|
} |
|
} |
|
} |
|
/** |
|
* Handles Socks v5 incoming connection request (BIND). |
|
*/ |
|
handleSocks5IncomingConnectionResponse() { |
|
// Peek at available data (we need at least 5 bytes to get the hostname length) |
|
const header = this.receiveBuffer.peek(5); |
|
if (header[0] !== 0x05 || header[1] !== constants_1.Socks5Response.Granted) { |
|
this.closeSocket(`${constants_1.ERRORS.Socks5ProxyRejectedIncomingBoundConnection} - ${constants_1.Socks5Response[header[1]]}`); |
|
} |
|
else { |
|
// Read address type |
|
const addressType = header[3]; |
|
let remoteHost; |
|
let buff; |
|
// IPv4 |
|
if (addressType === constants_1.Socks5HostType.IPv4) { |
|
// Check if data is available. |
|
const dataNeeded = constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseIPv4; |
|
if (this.receiveBuffer.length < dataNeeded) { |
|
this.nextRequiredPacketBufferSize = dataNeeded; |
|
return; |
|
} |
|
buff = smart_buffer_1.SmartBuffer.fromBuffer(this.receiveBuffer.get(dataNeeded).slice(4)); |
|
remoteHost = { |
|
host: ip.fromLong(buff.readUInt32BE()), |
|
port: buff.readUInt16BE(), |
|
}; |
|
// If given host is 0.0.0.0, assume remote proxy ip instead. |
|
if (remoteHost.host === '0.0.0.0') { |
|
remoteHost.host = this.options.proxy.ipaddress; |
|
} |
|
// Hostname |
|
} |
|
else if (addressType === constants_1.Socks5HostType.Hostname) { |
|
const hostLength = header[4]; |
|
const dataNeeded = constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseHostname(hostLength); // header + host length + port |
|
// Check if data is available. |
|
if (this.receiveBuffer.length < dataNeeded) { |
|
this.nextRequiredPacketBufferSize = dataNeeded; |
|
return; |
|
} |
|
buff = smart_buffer_1.SmartBuffer.fromBuffer(this.receiveBuffer.get(dataNeeded).slice(5)); |
|
remoteHost = { |
|
host: buff.readString(hostLength), |
|
port: buff.readUInt16BE(), |
|
}; |
|
// IPv6 |
|
} |
|
else if (addressType === constants_1.Socks5HostType.IPv6) { |
|
// Check if data is available. |
|
const dataNeeded = constants_1.SOCKS_INCOMING_PACKET_SIZES.Socks5ResponseIPv6; |
|
if (this.receiveBuffer.length < dataNeeded) { |
|
this.nextRequiredPacketBufferSize = dataNeeded; |
|
return; |
|
} |
|
buff = smart_buffer_1.SmartBuffer.fromBuffer(this.receiveBuffer.get(dataNeeded).slice(4)); |
|
remoteHost = { |
|
host: ip.toString(buff.readBuffer(16)), |
|
port: buff.readUInt16BE(), |
|
}; |
|
} |
|
this.setState(constants_1.SocksClientState.Established); |
|
this.removeInternalSocketHandlers(); |
|
this.emit('established', { remoteHost, socket: this.socket }); |
|
} |
|
} |
|
get socksClientOptions() { |
|
return Object.assign({}, this.options); |
|
} |
|
} |
|
exports.SocksClient = SocksClient; |
|
//# sourceMappingURL=socksclient.js.map
|