undici
Version:
An HTTP/1.1 client, written from scratch for Node.js
213 lines (187 loc) • 6.06 kB
JavaScript
'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
}