mockttp
Version:
Mock HTTP server for testing HTTP clients and stubbing webservices
294 lines • 11.6 kB
JavaScript
;
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