UNPKG

undici

Version:

An HTTP/1.1 client, written from scratch for Node.js

213 lines (187 loc) 6.06 kB
'use strict' const { Buffer } = require('node:buffer') const net = require('node:net') const { InvalidArgumentError } = require('./errors') /** * Parse an address and determine its type * @param {string} address - The address to parse * @returns {{type: number, buffer: Buffer}} Address type and buffer */ function parseAddress (address) { // Check if it's an IPv4 address if (net.isIPv4(address)) { const parts = address.split('.').map(Number) return { type: 0x01, // IPv4 buffer: Buffer.from(parts) } } // Check if it's an IPv6 address if (net.isIPv6(address)) { return { type: 0x04, // IPv6 buffer: parseIPv6(address) } } // Otherwise, treat as domain name const domainBuffer = Buffer.from(address, 'utf8') if (domainBuffer.length > 255) { throw new InvalidArgumentError('Domain name too long (max 255 bytes)') } return { type: 0x03, // Domain buffer: Buffer.concat([Buffer.from([domainBuffer.length]), domainBuffer]) } } /** * Parse IPv6 address to buffer * @param {string} address - IPv6 address string * @returns {Buffer} 16-byte buffer */ function parseIPv6 (address) { const buffer = Buffer.alloc(16) let normalizedAddress = address // Expand an embedded IPv4 tail into the last two IPv6 groups. if (address.includes('.')) { const lastColonIndex = address.lastIndexOf(':') const ipv4Part = address.slice(lastColonIndex + 1) if (net.isIPv4(ipv4Part)) { const octets = ipv4Part.split('.').map(Number) const high = ((octets[0] << 8) | octets[1]).toString(16) const low = ((octets[2] << 8) | octets[3]).toString(16) normalizedAddress = `${address.slice(0, lastColonIndex)}:${high}:${low}` } } // Handle compressed notation (::) const doubleColonIndex = normalizedAddress.indexOf('::') if (doubleColonIndex !== -1) { const before = normalizedAddress.slice(0, doubleColonIndex) const after = normalizedAddress.slice(doubleColonIndex + 2) const beforeParts = before === '' ? [] : before.split(':') const afterParts = after === '' ? [] : after.split(':') let bufferIndex = 0 for (const part of beforeParts) { buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) bufferIndex += 2 } bufferIndex = 16 - afterParts.length * 2 for (const part of afterParts) { buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) bufferIndex += 2 } } else { const parts = normalizedAddress.split(':') for (let i = 0; i < parts.length; i++) { buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2) } } return buffer } /** * Build a SOCKS5 address buffer * @param {number} type - Address type (1=IPv4, 3=Domain, 4=IPv6) * @param {Buffer} addressBuffer - The address data * @param {number} port - Port number * @returns {Buffer} Complete address buffer including type, address, and port */ function buildAddressBuffer (type, addressBuffer, port) { const portBuffer = Buffer.allocUnsafe(2) portBuffer.writeUInt16BE(port, 0) return Buffer.concat([ Buffer.from([type]), addressBuffer, portBuffer ]) } /** * Parse address from SOCKS5 response * @param {Buffer} buffer - Buffer containing the address * @param {number} offset - Starting offset in buffer * @returns {{address: string, port: number, bytesRead: number}} */ function parseResponseAddress (buffer, offset = 0) { if (buffer.length < offset + 1) { throw new InvalidArgumentError('Buffer too small to contain address type') } const addressType = buffer[offset] let address let currentOffset = offset + 1 switch (addressType) { case 0x01: { // IPv4 if (buffer.length < currentOffset + 6) { throw new InvalidArgumentError('Buffer too small for IPv4 address') } address = Array.from(buffer.subarray(currentOffset, currentOffset + 4)).join('.') currentOffset += 4 break } case 0x03: { // Domain if (buffer.length < currentOffset + 1) { throw new InvalidArgumentError('Buffer too small for domain length') } const domainLength = buffer[currentOffset] currentOffset += 1 if (buffer.length < currentOffset + domainLength + 2) { throw new InvalidArgumentError('Buffer too small for domain address') } address = buffer.subarray(currentOffset, currentOffset + domainLength).toString('utf8') currentOffset += domainLength break } case 0x04: { // IPv6 if (buffer.length < currentOffset + 18) { throw new InvalidArgumentError('Buffer too small for IPv6 address') } // Convert buffer to IPv6 string const parts = [] for (let i = 0; i < 8; i++) { const value = buffer.readUInt16BE(currentOffset + i * 2) parts.push(value.toString(16)) } address = parts.join(':') currentOffset += 16 break } default: throw new InvalidArgumentError(`Invalid address type: ${addressType}`) } // Parse port if (buffer.length < currentOffset + 2) { throw new InvalidArgumentError('Buffer too small for port') } const port = buffer.readUInt16BE(currentOffset) currentOffset += 2 return { address, port, bytesRead: currentOffset - offset } } /** * Create error for SOCKS5 reply code * @param {number} replyCode - SOCKS5 reply code * @returns {Error} Appropriate error object */ function createReplyError (replyCode) { const messages = { 0x01: 'General SOCKS server failure', 0x02: 'Connection not allowed by ruleset', 0x03: 'Network unreachable', 0x04: 'Host unreachable', 0x05: 'Connection refused', 0x06: 'TTL expired', 0x07: 'Command not supported', 0x08: 'Address type not supported' } const message = messages[replyCode] || `Unknown SOCKS5 error code: ${replyCode}` const error = new Error(message) error.code = `SOCKS5_${replyCode}` return error } module.exports = { parseAddress, parseIPv6, buildAddressBuffer, parseResponseAddress, createReplyError }