UNPKG

mockttp

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

294 lines 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildSocksServer = buildSocksServer; const buffer_1 = require("buffer"); const net = require("net"); const socket_util_1 = require("../util/socket-util"); const socket_extensions_1 = require("../util/socket-extensions"); const socket_metadata_1 = require("../util/socket-metadata"); const AUTH_METHODS = { 'no-auth': { id: 0x0, handler: handleNoAuth }, 'user-password-metadata': { id: 0x2, handler: handleUsernamePasswordMetadata }, 'custom-metadata': { id: 0xDA, handler: handleCustomMetadata } }; const AUTH_METHOD_KEYS = Object.keys(AUTH_METHODS); function buildSocksServer(options) { const authMethods = options.authMethods ?? ['no-auth']; if (authMethods.length === 0) throw new Error('At least one SOCKS auth method must be specified'); if (authMethods.some(method => !AUTH_METHOD_KEYS.includes(method))) { throw new Error(`Invalid SOCKS auth method specified. Supported methods are: ${AUTH_METHOD_KEYS.join(', ')}`); } return net.createServer(handleSocksConnect); async function handleSocksConnect(socket) { const server = this; // Until we pass this socket onwards, we handle (and drop) any errors on it: socket.on('error', ignoreError); try { const firstByte = await readBytes(socket, 1); ; const version = firstByte[0]; if (version === 0x04) { await handleSocksV4(socket, (address) => { socket.removeListener('error', ignoreError); server.emit('socks-tcp-connect', socket, address); }); } else if (version === 0x05) { await handleSocksV5(socket, (address) => { socket.removeListener('error', ignoreError); server.emit('socks-tcp-connect', socket, address); }); } else { // Should never happen, since this is sniffed by Httpolyglot, but just in case: (0, socket_util_1.resetOrDestroy)(socket); } } catch (err) { // We log but otherwise ignore failures, e.g. if the client closes the // connection after sending just half a message. console.warn(`Failed to process SOCKS connection`, err); socket.destroy(); } } async function handleSocksV4(socket, cb) { const buffer = await readBytes(socket, 7); // N.b version already read if (!authMethods.includes('no-auth')) { // We only support no-auth for now, so reject anything else return writeS4Rejection(socket); } const command = buffer[0]; if (command !== 0x01) { // Only CONNECT is supported, reject anything else return writeS4Rejection(socket); } const port = buffer.readUInt16BE(1); const ip = buffer.subarray(3, 7).join('.'); await readUntilNullByte(socket); // Read (and ignore) the user id if (ip.startsWith('0.0.0')) { // SOCKSv4a - the hostname will be sent (null-terminated) after the user id: const domain = await readUntilNullByte(socket); socket.write(buffer_1.Buffer.from([ 0x00, 0x5A, // Success // Omit the bound address & port here. It doesn't make sense for // our use case, and clients generally shouldn't need this info. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])); cb({ type: 'hostname', hostname: domain.toString('utf8'), port }); } else { // SOCKSv4 - we have an IPv4 address and we're good to go: socket.write(buffer_1.Buffer.from([ 0x00, 0x5A, // Success // Omit the bound address & port here. It doesn't make sense for // our use case, and clients generally shouldn't need this info. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])); cb({ type: 'ipv4', ip: ip, port }); } } async function handleSocksV5(socket, cb) { const buffer = await readBytes(socket, 1); // N.b version already read const authMethodsCount = buffer[0]; const clientMethods = await readBytes(socket, authMethodsCount); const selectedAuthMethodId = authMethods.find(methodKey => clientMethods.includes(AUTH_METHODS[methodKey].id)); if (selectedAuthMethodId === undefined) { // Reject any connections that don't match our supported auth methods: return socket.end(buffer_1.Buffer.from([ 0x05, // Version 0xFF, // No acceptable auth methods ])); } const authMethod = AUTH_METHODS[selectedAuthMethodId]; // Confirm the selected auth method: socket.write(buffer_1.Buffer.from([ 0x05, // Version authMethod.id ])); try { const success = await authMethod.handler(socket); if (!success) return; } catch (err) { console.warn(`SOCKS auth failed`, err); // Not actually totally clear what to return for an unknown error like this // but this should always make it clear that we're done in any case: return socket.end(buffer_1.Buffer.from([ 0x05, 0x01 // General failure ])); } // Ok - we're authenticated, now negotiate the connection itself: const [version, command, _reserved, addressType] = await readBytes(socket, 4); if (version !== 0x05) { // Should never happen, but just in case return writeS5ConnFailure(socket, 0x01); // General error } if (command !== 0x01) { // Only CONNECT is supported for now, reject anything else return writeS5ConnFailure(socket, 0x07); // General error } let address; if (addressType === 0x1) { // IPv4 const addressData = await readBytes(socket, 6); const ip = addressData.subarray(0, 4).join('.'); const port = addressData.readUInt16BE(4); address = { type: 'ipv4', ip, port }; } else if (addressType === 0x3) { // DNS const nameLength = await readBytes(socket, 1); const nameAndPortData = await readBytes(socket, nameLength[0] + 2); const name = nameAndPortData.subarray(0, nameLength[0]).toString('utf8'); const port = nameAndPortData.readUInt16BE(nameLength[0]); address = { type: 'hostname', hostname: name, port }; } else if (addressType === 0x4) { // IPv6 const addressData = await readBytes(socket, 18); const ipv6Bytes = addressData.subarray(0, 16); const hextets = []; for (let i = 0; i < ipv6Bytes.length; i += 2) { const hextet = ((ipv6Bytes[i] << 8) | ipv6Bytes[i + 1]).toString(16); hextets.push(hextet); } const ip = hextets.join(':'); const port = addressData.readUInt16BE(16); address = { type: 'ipv6', ip, port }; } else { return writeS5ConnFailure(socket, 0x08); // Unsupported address type } socket.write(buffer_1.Buffer.from([ 0x05, // Version 0x00, // Success 0x00, // Reserved 0x01, // IPv4 bind address 0x00, 0x00, 0x00, 0x00, // Blank bind address 0x00, 0x00 // Blank bind port ])); cb(address); } } async function handleNoAuth() { return true; } async function handleCustomMetadata(socket) { const length = (await readBytes(socket, 2)).readUint16BE(); const metadata = await readBytes(socket, length); const metadataString = metadata.toString('utf8'); try { socket[socket_extensions_1.SocketMetadata] = (0, socket_metadata_1.getSocketMetadata)(socket[socket_extensions_1.SocketMetadata], metadataString); } catch (e) { const errorData = buffer_1.Buffer.from(JSON.stringify({ message: 'Invalid JSON' })); const errorResponse = buffer_1.Buffer.alloc(4 + errorData.byteLength); errorResponse.writeUInt8(0x05, 0); errorResponse.writeUInt8(0xDA, 1); errorResponse.writeUInt16BE(errorData.byteLength, 2); errorData.copy(errorResponse, 4); socket.end(errorResponse); return false; } socket.write(buffer_1.Buffer.from([ 0x05, // Version 0x00 // Success ])); return true; } async function handleUsernamePasswordMetadata(socket) { const versionAndLength = await readBytes(socket, 2); const usernameLength = versionAndLength.readUint8(1); const username = await readBytes(socket, usernameLength); const passwordLength = await readBytes(socket, 1); const password = await readBytes(socket, passwordLength[0]); if (username.toString('utf8') !== 'metadata') { socket.end(buffer_1.Buffer.from([ 0x05, 0x01 // Generic rejection ])); return false; } try { socket[socket_extensions_1.SocketMetadata] = (0, socket_metadata_1.getSocketMetadata)(socket[socket_extensions_1.SocketMetadata], password); } catch (e) { socket.end(buffer_1.Buffer.from([ 0x05, 0x02 // Rejected (with a different error code to distinguish this case) ])); return false; } socket.write(buffer_1.Buffer.from([ 0x05, // Version 0x00 // Success ])); return true; } async function readBytes(socket, length) { const buffer = socket.read(length); if (buffer === null) { return new Promise((resolve, reject) => { socket.once('readable', () => resolve(readBytes(socket, length))); socket.once('close', () => reject(new Error('Socket closed'))); socket.once('error', reject); }); } else if (length !== undefined && buffer.byteLength != length) { throw new Error(`Socket closed before we received ${length} bytes`); } return buffer; } async function readUntilNullByte(socket) { let buffers = []; while (true) { const data = await readBytes(socket); const endOfIdIndex = data.indexOf(0x00); if (endOfIdIndex !== -1) { const remainingData = data.subarray(endOfIdIndex + 1); if (remainingData.length > 0) socket.unshift(remainingData); buffers.push(data.subarray(0, endOfIdIndex)); break; } else { buffers.push(data); } } return buffer_1.Buffer.concat(buffers); } const writeS4Rejection = (socket) => { socket.end(buffer_1.Buffer.from([ 0x00, 0x5B, // Generic rejection 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])); }; const writeS5ConnFailure = (socket, errorCode) => { socket.end(buffer_1.Buffer.from([ 0x05, // Version errorCode, // Failure code 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Blank bind address ])); }; function ignoreError() { } //# sourceMappingURL=socks-server.js.map