Protokoll-Import: KI-Erkennung (Gruppe, Teilnehmer, Status, Mehrfachpunkte), Ladeanzeige, Erstellungsdatum je Punkt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f18848f86
commit
11eabe42ae
275 files changed
+71952
-3029
No files matched your search
+192
@@ -0,0 +1,192 @@
|
||||
'use strict'
|
||||
|
||||
const net = require('node:net')
|
||||
const assert = require('node:assert')
|
||||
const util = require('./util')
|
||||
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
||||
|
||||
let tls // include tls conditionally since it is not always available
|
||||
|
||||
// TODO: session re-use does not wait for the first
|
||||
// connection to resolve the session and might therefore
|
||||
// resolve the same servername multiple times even when
|
||||
// re-use is enabled.
|
||||
|
||||
const SessionCache = class WeakSessionCache {
|
||||
constructor (maxCachedSessions) {
|
||||
this._maxCachedSessions = maxCachedSessions
|
||||
this._sessionCache = new Map()
|
||||
this._sessionRegistry = new FinalizationRegistry((key) => {
|
||||
if (this._sessionCache.size < this._maxCachedSessions) {
|
||||
return
|
||||
}
|
||||
|
||||
const ref = this._sessionCache.get(key)
|
||||
if (ref !== undefined && ref.deref() === undefined) {
|
||||
this._sessionCache.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get (sessionKey) {
|
||||
const ref = this._sessionCache.get(sessionKey)
|
||||
return ref ? ref.deref() : null
|
||||
}
|
||||
|
||||
set (sessionKey, session) {
|
||||
if (this._maxCachedSessions === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._sessionCache.has(sessionKey)) {
|
||||
this._sessionCache.delete(sessionKey)
|
||||
} else if (this._sessionCache.size >= this._maxCachedSessions) {
|
||||
for (const [key, ref] of this._sessionCache) {
|
||||
if (ref.deref() === undefined) {
|
||||
this._sessionCache.delete(key)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const oldest = this._sessionCache.keys().next()
|
||||
if (!oldest.done) {
|
||||
this._sessionCache.delete(oldest.value)
|
||||
}
|
||||
}
|
||||
|
||||
this._sessionCache.set(sessionKey, new WeakRef(session))
|
||||
this._sessionRegistry.register(session, sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
|
||||
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
|
||||
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
|
||||
}
|
||||
|
||||
const options = { path: socketPath, ...opts }
|
||||
const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
|
||||
timeout = timeout == null ? 10e3 : timeout
|
||||
allowH2 = allowH2 != null ? allowH2 : true
|
||||
return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
|
||||
let socket
|
||||
if (protocol === 'https:') {
|
||||
if (!tls) {
|
||||
tls = require('node:tls')
|
||||
}
|
||||
servername = servername || options.servername || util.getServerName(host) || null
|
||||
|
||||
const sessionKey = servername || hostname
|
||||
assert(sessionKey)
|
||||
|
||||
const session = customSession || sessionCache.get(sessionKey) || null
|
||||
|
||||
port = port || 443
|
||||
|
||||
socket = tls.connect({
|
||||
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
|
||||
...options,
|
||||
servername,
|
||||
session,
|
||||
localAddress,
|
||||
ALPNProtocols: allowH2 ? (preferH2 ? ['h2', 'http/1.1'] : ['http/1.1', 'h2']) : ['http/1.1'],
|
||||
socket: httpSocket, // upgrade socket connection
|
||||
port,
|
||||
host: hostname
|
||||
})
|
||||
|
||||
socket
|
||||
.on('session', function (session) {
|
||||
// TODO (fix): Can a session become invalid once established? Don't think so?
|
||||
sessionCache.set(sessionKey, session)
|
||||
})
|
||||
} else {
|
||||
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
|
||||
|
||||
port = port || 80
|
||||
|
||||
const connectOptions = {
|
||||
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
|
||||
...options,
|
||||
localAddress,
|
||||
port,
|
||||
host: hostname
|
||||
}
|
||||
|
||||
const family = net.isIP(hostname)
|
||||
if (family !== 0 && servername && servername !== hostname) {
|
||||
connectOptions.host = servername
|
||||
connectOptions.lookup = (_hostname, lookupOptions, cb) => {
|
||||
if (lookupOptions.all) {
|
||||
cb(null, [{ address: hostname, family }])
|
||||
} else {
|
||||
cb(null, hostname, family)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
socket = net.connect(connectOptions)
|
||||
if (useH2c === true) {
|
||||
socket.alpnProtocol = 'h2'
|
||||
}
|
||||
}
|
||||
|
||||
// Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
|
||||
if (options.keepAlive == null || options.keepAlive) {
|
||||
const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
|
||||
socket.setKeepAlive(true, keepAliveInitialDelay)
|
||||
}
|
||||
|
||||
const clearConnectTimeout = util.setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
|
||||
|
||||
socket
|
||||
.setNoDelay(true)
|
||||
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
|
||||
queueMicrotask(clearConnectTimeout)
|
||||
|
||||
if (callback) {
|
||||
const cb = callback
|
||||
callback = null
|
||||
cb(null, this)
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
queueMicrotask(clearConnectTimeout)
|
||||
|
||||
if (callback) {
|
||||
const cb = callback
|
||||
callback = null
|
||||
cb(maybeNormalizeConnectError(err, this, { timeout, hostname, port }))
|
||||
}
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
}
|
||||
|
||||
// `net.connect` with `autoSelectFamily` raises an `AggregateError` when every
|
||||
// attempted address fails. If any of those failures is a timeout, surface the
|
||||
// error as a `ConnectTimeoutError` so callers see the same error regardless of
|
||||
// which timer (Node's internal one or undici's `connectTimeout`) wins the race.
|
||||
// The original `AggregateError` is preserved on `.cause`.
|
||||
function maybeNormalizeConnectError (err, socket, opts) {
|
||||
if (
|
||||
err instanceof AggregateError &&
|
||||
(err.code === 'ETIMEDOUT' || err.errors.some((e) => e != null && e.code === 'ETIMEDOUT'))
|
||||
) {
|
||||
let message = 'Connect Timeout Error'
|
||||
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
|
||||
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
|
||||
} else {
|
||||
message += ` (attempted address: ${opts.hostname}:${opts.port},`
|
||||
}
|
||||
message += ` timeout: ${opts.timeout}ms)`
|
||||
|
||||
const wrapped = new ConnectTimeoutError(message)
|
||||
wrapped.cause = err
|
||||
return wrapped
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
module.exports = buildConnector
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/docs/Web/HTTP/Headers
|
||||
*/
|
||||
const wellknownHeaderNames = /** @type {const} */ ([
|
||||
'Accept',
|
||||
'Accept-Encoding',
|
||||
'Accept-Language',
|
||||
'Accept-Ranges',
|
||||
'Access-Control-Allow-Credentials',
|
||||
'Access-Control-Allow-Headers',
|
||||
'Access-Control-Allow-Methods',
|
||||
'Access-Control-Allow-Origin',
|
||||
'Access-Control-Expose-Headers',
|
||||
'Access-Control-Max-Age',
|
||||
'Access-Control-Request-Headers',
|
||||
'Access-Control-Request-Method',
|
||||
'Age',
|
||||
'Allow',
|
||||
'Alt-Svc',
|
||||
'Alt-Used',
|
||||
'Authorization',
|
||||
'Cache-Control',
|
||||
'Clear-Site-Data',
|
||||
'Connection',
|
||||
'Content-Disposition',
|
||||
'Content-Encoding',
|
||||
'Content-Language',
|
||||
'Content-Length',
|
||||
'Content-Location',
|
||||
'Content-Range',
|
||||
'Content-Security-Policy',
|
||||
'Content-Security-Policy-Report-Only',
|
||||
'Content-Type',
|
||||
'Cookie',
|
||||
'Cross-Origin-Embedder-Policy',
|
||||
'Cross-Origin-Opener-Policy',
|
||||
'Cross-Origin-Resource-Policy',
|
||||
'Date',
|
||||
'Device-Memory',
|
||||
'Downlink',
|
||||
'ECT',
|
||||
'ETag',
|
||||
'Expect',
|
||||
'Expect-CT',
|
||||
'Expires',
|
||||
'Forwarded',
|
||||
'From',
|
||||
'Host',
|
||||
'If-Match',
|
||||
'If-Modified-Since',
|
||||
'If-None-Match',
|
||||
'If-Range',
|
||||
'If-Unmodified-Since',
|
||||
'Keep-Alive',
|
||||
'Last-Modified',
|
||||
'Link',
|
||||
'Location',
|
||||
'Max-Forwards',
|
||||
'Origin',
|
||||
'Permissions-Policy',
|
||||
'Pragma',
|
||||
'Proxy-Authenticate',
|
||||
'Proxy-Authorization',
|
||||
'RTT',
|
||||
'Range',
|
||||
'Referer',
|
||||
'Referrer-Policy',
|
||||
'Refresh',
|
||||
'Retry-After',
|
||||
'Sec-WebSocket-Accept',
|
||||
'Sec-WebSocket-Extensions',
|
||||
'Sec-WebSocket-Key',
|
||||
'Sec-WebSocket-Protocol',
|
||||
'Sec-WebSocket-Version',
|
||||
'Server',
|
||||
'Server-Timing',
|
||||
'Service-Worker-Allowed',
|
||||
'Service-Worker-Navigation-Preload',
|
||||
'Set-Cookie',
|
||||
'SourceMap',
|
||||
'Strict-Transport-Security',
|
||||
'Supports-Loading-Mode',
|
||||
'TE',
|
||||
'Timing-Allow-Origin',
|
||||
'Trailer',
|
||||
'Transfer-Encoding',
|
||||
'Upgrade',
|
||||
'Upgrade-Insecure-Requests',
|
||||
'User-Agent',
|
||||
'Vary',
|
||||
'Via',
|
||||
'WWW-Authenticate',
|
||||
'X-Content-Type-Options',
|
||||
'X-DNS-Prefetch-Control',
|
||||
'X-Frame-Options',
|
||||
'X-Permitted-Cross-Domain-Policies',
|
||||
'X-Powered-By',
|
||||
'X-Requested-With',
|
||||
'X-XSS-Protection'
|
||||
])
|
||||
|
||||
/** @type {Record<typeof wellknownHeaderNames[number]|Lowercase<typeof wellknownHeaderNames[number]>, string>} */
|
||||
const headerNameLowerCasedRecord = {}
|
||||
|
||||
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
||||
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
||||
|
||||
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
||||
const key = wellknownHeaderNames[i]
|
||||
const lowerCasedKey = key.toLowerCase()
|
||||
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
|
||||
lowerCasedKey
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wellknownHeaderNames,
|
||||
headerNameLowerCasedRecord
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
'use strict'
|
||||
|
||||
const diagnosticsChannel = require('node:diagnostics_channel')
|
||||
const util = require('node:util')
|
||||
|
||||
const undiciDebugLog = util.debuglog('undici')
|
||||
const fetchDebuglog = util.debuglog('fetch')
|
||||
const websocketDebuglog = util.debuglog('websocket')
|
||||
|
||||
const channels = {
|
||||
// Client
|
||||
beforeConnect: diagnosticsChannel.channel('undici:client:beforeConnect'),
|
||||
connected: diagnosticsChannel.channel('undici:client:connected'),
|
||||
connectError: diagnosticsChannel.channel('undici:client:connectError'),
|
||||
sendHeaders: diagnosticsChannel.channel('undici:client:sendHeaders'),
|
||||
// Request
|
||||
create: diagnosticsChannel.channel('undici:request:create'),
|
||||
bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
|
||||
bodyChunkSent: diagnosticsChannel.channel('undici:request:bodyChunkSent'),
|
||||
bodyChunkReceived: diagnosticsChannel.channel('undici:request:bodyChunkReceived'),
|
||||
headers: diagnosticsChannel.channel('undici:request:headers'),
|
||||
trailers: diagnosticsChannel.channel('undici:request:trailers'),
|
||||
error: diagnosticsChannel.channel('undici:request:error'),
|
||||
// WebSocket
|
||||
open: diagnosticsChannel.channel('undici:websocket:open'),
|
||||
close: diagnosticsChannel.channel('undici:websocket:close'),
|
||||
socketError: diagnosticsChannel.channel('undici:websocket:socket_error'),
|
||||
ping: diagnosticsChannel.channel('undici:websocket:ping'),
|
||||
pong: diagnosticsChannel.channel('undici:websocket:pong'),
|
||||
// ProxyAgent
|
||||
proxyConnected: diagnosticsChannel.channel('undici:proxy:connected')
|
||||
}
|
||||
|
||||
let isTrackingClientEvents = false
|
||||
|
||||
function trackClientEvents (debugLog = undiciDebugLog) {
|
||||
if (isTrackingClientEvents) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any of the channels already have subscribers to prevent duplicate subscriptions
|
||||
// This can happen when both Node.js built-in undici and undici as a dependency are present
|
||||
if (channels.beforeConnect.hasSubscribers || channels.connected.hasSubscribers ||
|
||||
channels.connectError.hasSubscribers || channels.sendHeaders.hasSubscribers) {
|
||||
isTrackingClientEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
isTrackingClientEvents = true
|
||||
|
||||
diagnosticsChannel.subscribe('undici:client:beforeConnect',
|
||||
evt => {
|
||||
const {
|
||||
connectParams: { version, protocol, port, host }
|
||||
} = evt
|
||||
debugLog(
|
||||
'connecting to %s%s using %s%s',
|
||||
host,
|
||||
port ? `:${port}` : '',
|
||||
protocol,
|
||||
version
|
||||
)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:client:connected',
|
||||
evt => {
|
||||
const {
|
||||
connectParams: { version, protocol, port, host }
|
||||
} = evt
|
||||
debugLog(
|
||||
'connected to %s%s using %s%s',
|
||||
host,
|
||||
port ? `:${port}` : '',
|
||||
protocol,
|
||||
version
|
||||
)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:client:connectError',
|
||||
evt => {
|
||||
const {
|
||||
connectParams: { version, protocol, port, host },
|
||||
error
|
||||
} = evt
|
||||
debugLog(
|
||||
'connection to %s%s using %s%s errored - %s',
|
||||
host,
|
||||
port ? `:${port}` : '',
|
||||
protocol,
|
||||
version,
|
||||
error.message
|
||||
)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:client:sendHeaders',
|
||||
evt => {
|
||||
const {
|
||||
request: { method, path, origin }
|
||||
} = evt
|
||||
debugLog('sending request to %s %s%s', method, origin, path)
|
||||
})
|
||||
}
|
||||
|
||||
let isTrackingRequestEvents = false
|
||||
|
||||
function trackRequestEvents (debugLog = undiciDebugLog) {
|
||||
if (isTrackingRequestEvents) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any of the channels already have subscribers to prevent duplicate subscriptions
|
||||
// This can happen when both Node.js built-in undici and undici as a dependency are present
|
||||
if (channels.headers.hasSubscribers || channels.trailers.hasSubscribers ||
|
||||
channels.error.hasSubscribers) {
|
||||
isTrackingRequestEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
isTrackingRequestEvents = true
|
||||
|
||||
diagnosticsChannel.subscribe('undici:request:headers',
|
||||
evt => {
|
||||
const {
|
||||
request: { method, path, origin },
|
||||
response: { statusCode }
|
||||
} = evt
|
||||
debugLog(
|
||||
'received response to %s %s%s - HTTP %d',
|
||||
method,
|
||||
origin,
|
||||
path,
|
||||
statusCode
|
||||
)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:request:trailers',
|
||||
evt => {
|
||||
const {
|
||||
request: { method, path, origin }
|
||||
} = evt
|
||||
debugLog('trailers received from %s %s%s', method, origin, path)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:request:error',
|
||||
evt => {
|
||||
const {
|
||||
request: { method, path, origin },
|
||||
error
|
||||
} = evt
|
||||
debugLog(
|
||||
'request to %s %s%s errored - %s',
|
||||
method,
|
||||
origin,
|
||||
path,
|
||||
error.message
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let isTrackingWebSocketEvents = false
|
||||
|
||||
function trackWebSocketEvents (debugLog = websocketDebuglog) {
|
||||
if (isTrackingWebSocketEvents) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any of the channels already have subscribers to prevent duplicate subscriptions
|
||||
// This can happen when both Node.js built-in undici and undici as a dependency are present
|
||||
if (channels.open.hasSubscribers || channels.close.hasSubscribers ||
|
||||
channels.socketError.hasSubscribers || channels.ping.hasSubscribers ||
|
||||
channels.pong.hasSubscribers) {
|
||||
isTrackingWebSocketEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
isTrackingWebSocketEvents = true
|
||||
|
||||
diagnosticsChannel.subscribe('undici:websocket:open',
|
||||
evt => {
|
||||
if (evt.address != null) {
|
||||
const { address, port } = evt.address
|
||||
debugLog('connection opened %s%s', address, port ? `:${port}` : '')
|
||||
} else {
|
||||
debugLog('connection opened')
|
||||
}
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:websocket:close',
|
||||
evt => {
|
||||
const { websocket, code, reason } = evt
|
||||
debugLog(
|
||||
'closed connection to %s - %s %s',
|
||||
websocket.url,
|
||||
code,
|
||||
reason
|
||||
)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:websocket:socket_error',
|
||||
err => {
|
||||
debugLog('connection errored - %s', err.message)
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:websocket:ping',
|
||||
evt => {
|
||||
debugLog('ping received')
|
||||
})
|
||||
|
||||
diagnosticsChannel.subscribe('undici:websocket:pong',
|
||||
evt => {
|
||||
debugLog('pong received')
|
||||
})
|
||||
}
|
||||
|
||||
if (undiciDebugLog.enabled || fetchDebuglog.enabled) {
|
||||
trackClientEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog)
|
||||
trackRequestEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog)
|
||||
}
|
||||
|
||||
if (websocketDebuglog.enabled) {
|
||||
trackClientEvents(undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog)
|
||||
trackWebSocketEvents(websocketDebuglog)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
channels
|
||||
}
|
||||
+497
@@ -0,0 +1,497 @@
|
||||
'use strict'
|
||||
|
||||
const kUndiciError = Symbol.for('undici.error.UND_ERR')
|
||||
class UndiciError extends Error {
|
||||
constructor (message, options) {
|
||||
super(message, options)
|
||||
this.name = 'UndiciError'
|
||||
this.code = 'UND_ERR'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kUndiciError] === true
|
||||
}
|
||||
|
||||
get [kUndiciError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kConnectTimeoutError = Symbol.for('undici.error.UND_ERR_CONNECT_TIMEOUT')
|
||||
class ConnectTimeoutError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'ConnectTimeoutError'
|
||||
this.message = message || 'Connect Timeout Error'
|
||||
this.code = 'UND_ERR_CONNECT_TIMEOUT'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kConnectTimeoutError] === true
|
||||
}
|
||||
|
||||
get [kConnectTimeoutError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kHeadersTimeoutError = Symbol.for('undici.error.UND_ERR_HEADERS_TIMEOUT')
|
||||
class HeadersTimeoutError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'HeadersTimeoutError'
|
||||
this.message = message || 'Headers Timeout Error'
|
||||
this.code = 'UND_ERR_HEADERS_TIMEOUT'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kHeadersTimeoutError] === true
|
||||
}
|
||||
|
||||
get [kHeadersTimeoutError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kHeadersOverflowError = Symbol.for('undici.error.UND_ERR_HEADERS_OVERFLOW')
|
||||
class HeadersOverflowError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'HeadersOverflowError'
|
||||
this.message = message || 'Headers Overflow Error'
|
||||
this.code = 'UND_ERR_HEADERS_OVERFLOW'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kHeadersOverflowError] === true
|
||||
}
|
||||
|
||||
get [kHeadersOverflowError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kBodyTimeoutError = Symbol.for('undici.error.UND_ERR_BODY_TIMEOUT')
|
||||
class BodyTimeoutError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'BodyTimeoutError'
|
||||
this.message = message || 'Body Timeout Error'
|
||||
this.code = 'UND_ERR_BODY_TIMEOUT'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kBodyTimeoutError] === true
|
||||
}
|
||||
|
||||
get [kBodyTimeoutError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kInvalidArgumentError = Symbol.for('undici.error.UND_ERR_INVALID_ARG')
|
||||
class InvalidArgumentError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'InvalidArgumentError'
|
||||
this.message = message || 'Invalid Argument Error'
|
||||
this.code = 'UND_ERR_INVALID_ARG'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kInvalidArgumentError] === true
|
||||
}
|
||||
|
||||
get [kInvalidArgumentError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kInvalidReturnValueError = Symbol.for('undici.error.UND_ERR_INVALID_RETURN_VALUE')
|
||||
class InvalidReturnValueError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'InvalidReturnValueError'
|
||||
this.message = message || 'Invalid Return Value Error'
|
||||
this.code = 'UND_ERR_INVALID_RETURN_VALUE'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kInvalidReturnValueError] === true
|
||||
}
|
||||
|
||||
get [kInvalidReturnValueError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kAbortError = Symbol.for('undici.error.UND_ERR_ABORT')
|
||||
class AbortError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AbortError'
|
||||
this.message = message || 'The operation was aborted'
|
||||
this.code = 'UND_ERR_ABORT'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kAbortError] === true
|
||||
}
|
||||
|
||||
get [kAbortError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kRequestAbortedError = Symbol.for('undici.error.UND_ERR_ABORTED')
|
||||
class RequestAbortedError extends AbortError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AbortError'
|
||||
this.message = message || 'Request aborted'
|
||||
this.code = 'UND_ERR_ABORTED'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kRequestAbortedError] === true
|
||||
}
|
||||
|
||||
get [kRequestAbortedError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO')
|
||||
class InformationalError extends UndiciError {
|
||||
constructor (message, options) {
|
||||
super(message, options)
|
||||
this.name = 'InformationalError'
|
||||
this.message = message || 'Request information'
|
||||
this.code = 'UND_ERR_INFO'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kInformationalError] === true
|
||||
}
|
||||
|
||||
get [kInformationalError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kRequestContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH')
|
||||
class RequestContentLengthMismatchError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'RequestContentLengthMismatchError'
|
||||
this.message = message || 'Request body length does not match content-length header'
|
||||
this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kRequestContentLengthMismatchError] === true
|
||||
}
|
||||
|
||||
get [kRequestContentLengthMismatchError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kResponseContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
|
||||
class ResponseContentLengthMismatchError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'ResponseContentLengthMismatchError'
|
||||
this.message = message || 'Response body length does not match content-length header'
|
||||
this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kResponseContentLengthMismatchError] === true
|
||||
}
|
||||
|
||||
get [kResponseContentLengthMismatchError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kClientDestroyedError = Symbol.for('undici.error.UND_ERR_DESTROYED')
|
||||
class ClientDestroyedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'ClientDestroyedError'
|
||||
this.message = message || 'The client is destroyed'
|
||||
this.code = 'UND_ERR_DESTROYED'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kClientDestroyedError] === true
|
||||
}
|
||||
|
||||
get [kClientDestroyedError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kClientClosedError = Symbol.for('undici.error.UND_ERR_CLOSED')
|
||||
class ClientClosedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'ClientClosedError'
|
||||
this.message = message || 'The client is closed'
|
||||
this.code = 'UND_ERR_CLOSED'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kClientClosedError] === true
|
||||
}
|
||||
|
||||
get [kClientClosedError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kSocketError = Symbol.for('undici.error.UND_ERR_SOCKET')
|
||||
class SocketError extends UndiciError {
|
||||
constructor (message, socket) {
|
||||
super(message)
|
||||
this.name = 'SocketError'
|
||||
this.message = message || 'Socket error'
|
||||
this.code = 'UND_ERR_SOCKET'
|
||||
this.socket = socket
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kSocketError] === true
|
||||
}
|
||||
|
||||
get [kSocketError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kNotSupportedError = Symbol.for('undici.error.UND_ERR_NOT_SUPPORTED')
|
||||
class NotSupportedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'NotSupportedError'
|
||||
this.message = message || 'Not supported error'
|
||||
this.code = 'UND_ERR_NOT_SUPPORTED'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kNotSupportedError] === true
|
||||
}
|
||||
|
||||
get [kNotSupportedError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kBalancedPoolMissingUpstreamError = Symbol.for('undici.error.UND_ERR_BPL_MISSING_UPSTREAM')
|
||||
class BalancedPoolMissingUpstreamError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'MissingUpstreamError'
|
||||
this.message = message || 'No upstream has been added to the BalancedPool'
|
||||
this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kBalancedPoolMissingUpstreamError] === true
|
||||
}
|
||||
|
||||
get [kBalancedPoolMissingUpstreamError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kHTTPParserError = Symbol.for('undici.error.UND_ERR_HTTP_PARSER')
|
||||
class HTTPParserError extends Error {
|
||||
constructor (message, code, data) {
|
||||
super(message)
|
||||
this.name = 'HTTPParserError'
|
||||
this.code = code ? `HPE_${code}` : undefined
|
||||
this.data = data ? data.toString() : undefined
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kHTTPParserError] === true
|
||||
}
|
||||
|
||||
get [kHTTPParserError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kResponseExceededMaxSizeError = Symbol.for('undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE')
|
||||
class ResponseExceededMaxSizeError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'ResponseExceededMaxSizeError'
|
||||
this.message = message || 'Response content exceeded max size'
|
||||
this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kResponseExceededMaxSizeError] === true
|
||||
}
|
||||
|
||||
get [kResponseExceededMaxSizeError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kRequestRetryError = Symbol.for('undici.error.UND_ERR_REQ_RETRY')
|
||||
class RequestRetryError extends UndiciError {
|
||||
constructor (message, code, { headers, data }) {
|
||||
super(message)
|
||||
this.name = 'RequestRetryError'
|
||||
this.message = message || 'Request retry error'
|
||||
this.code = 'UND_ERR_REQ_RETRY'
|
||||
this.statusCode = code
|
||||
this.data = data
|
||||
this.headers = headers
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kRequestRetryError] === true
|
||||
}
|
||||
|
||||
get [kRequestRetryError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE')
|
||||
class ResponseError extends UndiciError {
|
||||
constructor (message, code, { headers, body }) {
|
||||
super(message)
|
||||
this.name = 'ResponseError'
|
||||
this.message = message || 'Response error'
|
||||
this.code = 'UND_ERR_RESPONSE'
|
||||
this.statusCode = code
|
||||
this.body = body
|
||||
this.headers = headers
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kResponseError] === true
|
||||
}
|
||||
|
||||
get [kResponseError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kSecureProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_TLS')
|
||||
class SecureProxyConnectionError extends UndiciError {
|
||||
constructor (cause, message, options = {}) {
|
||||
super(message, { cause, ...options })
|
||||
this.name = 'SecureProxyConnectionError'
|
||||
this.message = message || 'Secure Proxy Connection failed'
|
||||
this.code = 'UND_ERR_PRX_TLS'
|
||||
this.cause = cause
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kSecureProxyConnectionError] === true
|
||||
}
|
||||
|
||||
get [kSecureProxyConnectionError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_CONN')
|
||||
class ProxyConnectionError extends UndiciError {
|
||||
constructor (cause, message, options = {}) {
|
||||
super(message, { cause, ...options })
|
||||
this.name = 'ProxyConnectionError'
|
||||
this.message = message || 'Proxy Connection failed'
|
||||
this.code = 'UND_ERR_PRX_CONN'
|
||||
this.cause = cause
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kProxyConnectionError] === true
|
||||
}
|
||||
|
||||
get [kProxyConnectionError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED')
|
||||
class MaxOriginsReachedError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'MaxOriginsReachedError'
|
||||
this.message = message || 'Maximum allowed origins reached'
|
||||
this.code = 'UND_ERR_MAX_ORIGINS_REACHED'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kMaxOriginsReachedError] === true
|
||||
}
|
||||
|
||||
get [kMaxOriginsReachedError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class Socks5ProxyError extends UndiciError {
|
||||
constructor (message, code) {
|
||||
super(message)
|
||||
this.name = 'Socks5ProxyError'
|
||||
this.message = message || 'SOCKS5 proxy error'
|
||||
this.code = code || 'UND_ERR_SOCKS5'
|
||||
}
|
||||
}
|
||||
|
||||
const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
|
||||
class MessageSizeExceededError extends UndiciError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'MessageSizeExceededError'
|
||||
this.message = message || 'Max decompressed message size exceeded'
|
||||
this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kMessageSizeExceededError] === true
|
||||
}
|
||||
|
||||
get [kMessageSizeExceededError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AbortError,
|
||||
HTTPParserError,
|
||||
UndiciError,
|
||||
HeadersTimeoutError,
|
||||
HeadersOverflowError,
|
||||
BodyTimeoutError,
|
||||
RequestContentLengthMismatchError,
|
||||
ConnectTimeoutError,
|
||||
InvalidArgumentError,
|
||||
InvalidReturnValueError,
|
||||
RequestAbortedError,
|
||||
ClientDestroyedError,
|
||||
ClientClosedError,
|
||||
InformationalError,
|
||||
SocketError,
|
||||
NotSupportedError,
|
||||
ResponseContentLengthMismatchError,
|
||||
BalancedPoolMissingUpstreamError,
|
||||
ResponseExceededMaxSizeError,
|
||||
RequestRetryError,
|
||||
ResponseError,
|
||||
SecureProxyConnectionError,
|
||||
ProxyConnectionError,
|
||||
MaxOriginsReachedError,
|
||||
Socks5ProxyError,
|
||||
MessageSizeExceededError
|
||||
}
|
||||
+567
@@ -0,0 +1,567 @@
|
||||
'use strict'
|
||||
|
||||
const {
|
||||
InvalidArgumentError,
|
||||
NotSupportedError
|
||||
} = require('./errors')
|
||||
const assert = require('node:assert')
|
||||
const {
|
||||
isValidHTTPToken,
|
||||
isValidHeaderValue,
|
||||
isStream,
|
||||
destroy,
|
||||
isBuffer,
|
||||
isFormDataLike,
|
||||
isIterable,
|
||||
hasSafeIterator,
|
||||
isBlobLike,
|
||||
serializePathWithQuery,
|
||||
parseHeaders,
|
||||
assertRequestHandler,
|
||||
getServerName,
|
||||
normalizedMethodRecords,
|
||||
getProtocolFromUrlString
|
||||
} = require('./util')
|
||||
const { channels } = require('./diagnostics.js')
|
||||
const { headerNameLowerCasedRecord } = require('./constants')
|
||||
|
||||
// Verifies that a given path is valid does not contain control chars \x00 to \x20
|
||||
const invalidPathRegex = /[^\u0021-\u00ff]/
|
||||
|
||||
function isValidContentLengthHeaderValue (val) {
|
||||
if (typeof val !== 'string' || val.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
const charCode = val.charCodeAt(i)
|
||||
if (charCode < 48 || charCode > 57) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const kHandler = Symbol('handler')
|
||||
const kController = Symbol('controller')
|
||||
const kResume = Symbol('resume')
|
||||
|
||||
class RequestController {
|
||||
#paused = false
|
||||
#reason = null
|
||||
#aborted = false
|
||||
#abort
|
||||
|
||||
[kResume] = null
|
||||
|
||||
rawHeaders = null
|
||||
rawTrailers = null
|
||||
|
||||
constructor (abort) {
|
||||
this.#abort = abort
|
||||
}
|
||||
|
||||
pause () {
|
||||
this.#paused = true
|
||||
}
|
||||
|
||||
resume () {
|
||||
if (this.#paused) {
|
||||
this.#paused = false
|
||||
this[kResume]?.()
|
||||
}
|
||||
}
|
||||
|
||||
abort (reason) {
|
||||
if (!this.#aborted) {
|
||||
this.#aborted = true
|
||||
this.#reason = reason
|
||||
this.#abort(reason)
|
||||
}
|
||||
}
|
||||
|
||||
get aborted () {
|
||||
return this.#aborted
|
||||
}
|
||||
|
||||
get reason () {
|
||||
return this.#reason
|
||||
}
|
||||
|
||||
get paused () {
|
||||
return this.#paused
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
constructor (origin, {
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
query,
|
||||
idempotent,
|
||||
blocking,
|
||||
upgrade,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
reset,
|
||||
expectContinue,
|
||||
servername,
|
||||
throwOnError,
|
||||
maxRedirections,
|
||||
typeOfService
|
||||
}, handler) {
|
||||
if (typeof path !== 'string') {
|
||||
throw new InvalidArgumentError('path must be a string')
|
||||
} else if (
|
||||
path[0] !== '/' &&
|
||||
!(path.startsWith('http://') || path.startsWith('https://')) &&
|
||||
method !== 'CONNECT'
|
||||
) {
|
||||
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
|
||||
} else if (invalidPathRegex.test(path)) {
|
||||
throw new InvalidArgumentError('invalid request path')
|
||||
}
|
||||
|
||||
if (typeof method !== 'string') {
|
||||
throw new InvalidArgumentError('method must be a string')
|
||||
} else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) {
|
||||
throw new InvalidArgumentError('invalid request method')
|
||||
}
|
||||
|
||||
if (upgrade && typeof upgrade !== 'string') {
|
||||
throw new InvalidArgumentError('upgrade must be a string')
|
||||
}
|
||||
|
||||
if (upgrade && !isValidHeaderValue(upgrade)) {
|
||||
throw new InvalidArgumentError('invalid upgrade header')
|
||||
}
|
||||
|
||||
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
|
||||
throw new InvalidArgumentError('invalid headersTimeout')
|
||||
}
|
||||
|
||||
if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
|
||||
throw new InvalidArgumentError('invalid bodyTimeout')
|
||||
}
|
||||
|
||||
if (reset != null && typeof reset !== 'boolean') {
|
||||
throw new InvalidArgumentError('invalid reset')
|
||||
}
|
||||
|
||||
if (expectContinue != null && typeof expectContinue !== 'boolean') {
|
||||
throw new InvalidArgumentError('invalid expectContinue')
|
||||
}
|
||||
|
||||
if (throwOnError != null) {
|
||||
throw new InvalidArgumentError('invalid throwOnError')
|
||||
}
|
||||
|
||||
if (maxRedirections != null && maxRedirections !== 0) {
|
||||
throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor')
|
||||
}
|
||||
|
||||
if (typeOfService != null && (!Number.isInteger(typeOfService) || typeOfService < 0 || typeOfService > 255)) {
|
||||
throw new InvalidArgumentError('typeOfService must be an integer between 0 and 255')
|
||||
}
|
||||
|
||||
this.headersTimeout = headersTimeout
|
||||
|
||||
this.bodyTimeout = bodyTimeout
|
||||
|
||||
this.method = method
|
||||
|
||||
this.typeOfService = typeOfService
|
||||
|
||||
this.abort = null
|
||||
|
||||
if (body == null) {
|
||||
this.body = null
|
||||
} else if (isStream(body)) {
|
||||
this.body = body
|
||||
|
||||
const rState = this.body._readableState
|
||||
if (!rState || !rState.autoDestroy) {
|
||||
this.endHandler = function autoDestroy () {
|
||||
destroy(this)
|
||||
}
|
||||
this.body.on('end', this.endHandler)
|
||||
}
|
||||
|
||||
this.errorHandler = err => {
|
||||
if (this.abort) {
|
||||
this.abort(err)
|
||||
} else {
|
||||
this.error = err
|
||||
}
|
||||
}
|
||||
this.body.on('error', this.errorHandler)
|
||||
} else if (isBuffer(body)) {
|
||||
this.body = body.byteLength ? body : null
|
||||
} else if (ArrayBuffer.isView(body)) {
|
||||
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
|
||||
} else if (body instanceof ArrayBuffer) {
|
||||
this.body = body.byteLength ? Buffer.from(body) : null
|
||||
} else if (typeof body === 'string') {
|
||||
this.body = body.length ? Buffer.from(body) : null
|
||||
} else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) {
|
||||
this.body = body
|
||||
} else {
|
||||
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
|
||||
}
|
||||
|
||||
this.completed = false
|
||||
this.aborted = false
|
||||
|
||||
this.upgrade = upgrade || null
|
||||
|
||||
this.path = query ? serializePathWithQuery(path, query) : path
|
||||
|
||||
// TODO: shall we maybe standardize it to an URL object?
|
||||
this.origin = origin
|
||||
|
||||
this.protocol = getProtocolFromUrlString(origin)
|
||||
|
||||
this.idempotent = idempotent == null
|
||||
? method === 'HEAD' || method === 'GET' || method === 'QUERY'
|
||||
: idempotent
|
||||
|
||||
this.blocking = blocking ?? this.method !== 'HEAD'
|
||||
|
||||
this.reset = reset == null ? null : reset
|
||||
|
||||
this.host = null
|
||||
|
||||
this.contentLength = null
|
||||
|
||||
this.contentType = null
|
||||
|
||||
this.headers = []
|
||||
|
||||
// Only for H2
|
||||
this.expectContinue = expectContinue != null ? expectContinue : false
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
if (headers.length % 2 !== 0) {
|
||||
throw new InvalidArgumentError('headers array must be even')
|
||||
}
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
processHeader(this, headers[i], headers[i + 1])
|
||||
}
|
||||
} else if (headers && typeof headers === 'object') {
|
||||
if (hasSafeIterator(headers)) {
|
||||
for (const header of headers) {
|
||||
if (!Array.isArray(header) || header.length !== 2) {
|
||||
throw new InvalidArgumentError('headers must be in key-value pair format')
|
||||
}
|
||||
processHeader(this, header[0], header[1])
|
||||
}
|
||||
} else {
|
||||
const keys = Object.keys(headers)
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
processHeader(this, keys[i], headers[keys[i]])
|
||||
}
|
||||
}
|
||||
} else if (headers != null) {
|
||||
throw new InvalidArgumentError('headers must be an object or an array')
|
||||
}
|
||||
|
||||
assertRequestHandler(handler, method, upgrade)
|
||||
|
||||
this.servername = servername || getServerName(this.host) || null
|
||||
|
||||
this[kHandler] = handler
|
||||
|
||||
if (channels.create.hasSubscribers) {
|
||||
channels.create.publish({ request: this })
|
||||
}
|
||||
}
|
||||
|
||||
onBodySent (chunk) {
|
||||
if (channels.bodyChunkSent.hasSubscribers) {
|
||||
channels.bodyChunkSent.publish({ request: this, chunk })
|
||||
}
|
||||
if (this[kHandler].onBodySent) {
|
||||
try {
|
||||
return this[kHandler].onBodySent(chunk)
|
||||
} catch (err) {
|
||||
this.abort(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRequestSent () {
|
||||
if (channels.bodySent.hasSubscribers) {
|
||||
channels.bodySent.publish({ request: this })
|
||||
}
|
||||
|
||||
if (this[kHandler].onRequestSent) {
|
||||
try {
|
||||
return this[kHandler].onRequestSent()
|
||||
} catch (err) {
|
||||
this.abort(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRequestStart (abort, context) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
this[kController] = new RequestController(abort)
|
||||
|
||||
if (this.error) {
|
||||
this[kController].abort(this.error)
|
||||
return
|
||||
}
|
||||
|
||||
this.abort = abort
|
||||
return this[kHandler].onRequestStart(this[kController], context)
|
||||
}
|
||||
|
||||
onResponseStarted () {
|
||||
return this[kHandler].onResponseStarted?.()
|
||||
}
|
||||
|
||||
onResponseStart (statusCode, headers, resume, statusText) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
if (channels.headers.hasSubscribers) {
|
||||
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
||||
}
|
||||
|
||||
const controller = this[kController]
|
||||
if (controller) {
|
||||
controller[kResume] = resume
|
||||
controller.rawHeaders = headers
|
||||
}
|
||||
|
||||
const parsedHeaders = Array.isArray(headers) ? parseHeaders(headers) : headers
|
||||
|
||||
try {
|
||||
this[kHandler].onResponseStart?.(controller, statusCode, parsedHeaders, statusText)
|
||||
return !controller?.paused
|
||||
} catch (err) {
|
||||
this.abort(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
onResponseData (chunk) {
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
if (channels.bodyChunkReceived.hasSubscribers) {
|
||||
channels.bodyChunkReceived.publish({ request: this, chunk })
|
||||
}
|
||||
|
||||
const controller = this[kController]
|
||||
try {
|
||||
this[kHandler].onResponseData?.(controller, chunk)
|
||||
return !controller?.paused
|
||||
} catch (err) {
|
||||
this.abort(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} statusCode
|
||||
* @param {Buffer[]|string[]|import('../../types/header.d.ts').IncomingHttpHeaders} headers
|
||||
* @param {import('node:stream').Duplex} socket
|
||||
* @param {string} [statusText]
|
||||
*/
|
||||
onRequestUpgrade (statusCode, headers, socket, statusText = '') {
|
||||
this.onFinally()
|
||||
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
if (channels.headers.hasSubscribers) {
|
||||
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
||||
}
|
||||
|
||||
const controller = this[kController]
|
||||
if (controller) {
|
||||
controller.rawHeaders = headers
|
||||
}
|
||||
|
||||
const parsedHeaders = Array.isArray(headers) ? parseHeaders(headers) : headers
|
||||
|
||||
const result = this[kHandler].onRequestUpgrade?.(controller, statusCode, parsedHeaders, socket)
|
||||
|
||||
if (!this.aborted) {
|
||||
this.completed = true
|
||||
if (channels.trailers.hasSubscribers) {
|
||||
channels.trailers.publish({ request: this, trailers: [] })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
onResponseEnd (trailers) {
|
||||
this.onFinally()
|
||||
|
||||
assert(!this.aborted)
|
||||
assert(!this.completed)
|
||||
|
||||
this.completed = true
|
||||
if (channels.trailers.hasSubscribers) {
|
||||
channels.trailers.publish({ request: this, trailers })
|
||||
}
|
||||
|
||||
const controller = this[kController]
|
||||
if (controller) {
|
||||
controller.rawTrailers = trailers
|
||||
}
|
||||
|
||||
const parsedTrailers = Array.isArray(trailers) ? parseHeaders(trailers) : trailers
|
||||
|
||||
try {
|
||||
return this[kHandler].onResponseEnd?.(controller, parsedTrailers)
|
||||
} catch (err) {
|
||||
// TODO (fix): This might be a bad idea?
|
||||
this.onResponseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
onResponseError (error) {
|
||||
this.onFinally()
|
||||
|
||||
if (channels.error.hasSubscribers) {
|
||||
channels.error.publish({ request: this, error })
|
||||
}
|
||||
|
||||
if (this.aborted) {
|
||||
return
|
||||
}
|
||||
this.aborted = true
|
||||
|
||||
const controller = this[kController]
|
||||
|
||||
return this[kHandler].onResponseError?.(controller, error)
|
||||
}
|
||||
|
||||
onFinally () {
|
||||
if (this.errorHandler) {
|
||||
this.body.off('error', this.errorHandler)
|
||||
this.errorHandler = null
|
||||
}
|
||||
|
||||
if (this.endHandler) {
|
||||
this.body.off('end', this.endHandler)
|
||||
this.endHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
addHeader (key, value) {
|
||||
processHeader(this, key, value)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
function processHeader (request, key, val) {
|
||||
if (val && (typeof val === 'object' && !Array.isArray(val))) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
} else if (val === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let headerName = headerNameLowerCasedRecord[key]
|
||||
|
||||
if (headerName === undefined) {
|
||||
headerName = key.toLowerCase()
|
||||
if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) {
|
||||
throw new InvalidArgumentError('invalid header key')
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
const arr = []
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
if (typeof val[i] === 'string') {
|
||||
if (!isValidHeaderValue(val[i])) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
arr.push(val[i])
|
||||
} else if (val[i] === null) {
|
||||
arr.push('')
|
||||
} else if (typeof val[i] === 'object') {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
} else {
|
||||
// Coerce primitives (and reject unsafe coercions such as functions
|
||||
// with a crafted toString/Symbol.toPrimitive).
|
||||
const str = `${val[i]}`
|
||||
if (!isValidHeaderValue(str)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
arr.push(str)
|
||||
}
|
||||
}
|
||||
val = arr
|
||||
} else if (typeof val === 'string') {
|
||||
if (!isValidHeaderValue(val)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
} else if (val === null) {
|
||||
val = ''
|
||||
} else {
|
||||
// Coerce primitives (and reject unsafe coercions such as functions
|
||||
// with a crafted toString/Symbol.toPrimitive).
|
||||
val = `${val}`
|
||||
if (!isValidHeaderValue(val)) {
|
||||
throw new InvalidArgumentError(`invalid ${key} header`)
|
||||
}
|
||||
}
|
||||
|
||||
if (headerName === 'host') {
|
||||
if (request.host !== null) {
|
||||
throw new InvalidArgumentError('duplicate host header')
|
||||
}
|
||||
if (typeof val !== 'string') {
|
||||
throw new InvalidArgumentError('invalid host header')
|
||||
}
|
||||
// Consumed by Client
|
||||
request.host = val
|
||||
} else if (headerName === 'content-length') {
|
||||
if (request.contentLength !== null) {
|
||||
throw new InvalidArgumentError('duplicate content-length header')
|
||||
}
|
||||
if (!isValidContentLengthHeaderValue(val)) {
|
||||
throw new InvalidArgumentError('invalid content-length header')
|
||||
}
|
||||
request.contentLength = parseInt(val, 10)
|
||||
} else if (request.contentType === null && headerName === 'content-type') {
|
||||
request.contentType = val
|
||||
request.headers.push(key, val)
|
||||
} else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
|
||||
throw new InvalidArgumentError(`invalid ${headerName} header`)
|
||||
} else if (headerName === 'connection') {
|
||||
// Per RFC 7230 Section 6.1, Connection header can contain
|
||||
// a comma-separated list of connection option tokens (header names)
|
||||
const value = typeof val === 'string' ? val : null
|
||||
if (value === null) {
|
||||
throw new InvalidArgumentError('invalid connection header')
|
||||
}
|
||||
|
||||
for (const token of value.toLowerCase().split(',')) {
|
||||
const trimmed = token.trim()
|
||||
if (!isValidHTTPToken(trimmed)) {
|
||||
throw new InvalidArgumentError('invalid connection header')
|
||||
}
|
||||
if (trimmed === 'close') {
|
||||
request.reset = true
|
||||
}
|
||||
}
|
||||
} else if (headerName === 'expect') {
|
||||
throw new NotSupportedError('expect header not supported')
|
||||
} else {
|
||||
request.headers.push(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Request
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
'use strict'
|
||||
|
||||
const { EventEmitter } = require('node:events')
|
||||
const { Buffer } = require('node:buffer')
|
||||
const { InvalidArgumentError, Socks5ProxyError } = require('./errors')
|
||||
const { debuglog } = require('node:util')
|
||||
const { parseAddress } = require('./socks5-utils')
|
||||
|
||||
const debug = debuglog('undici:socks5')
|
||||
const EMPTY_BUFFER = Buffer.alloc(0)
|
||||
|
||||
// SOCKS5 constants
|
||||
const SOCKS_VERSION = 0x05
|
||||
|
||||
// Authentication methods
|
||||
const AUTH_METHODS = {
|
||||
NO_AUTH: 0x00,
|
||||
GSSAPI: 0x01,
|
||||
USERNAME_PASSWORD: 0x02,
|
||||
NO_ACCEPTABLE: 0xFF
|
||||
}
|
||||
|
||||
// SOCKS5 commands
|
||||
const COMMANDS = {
|
||||
CONNECT: 0x01,
|
||||
BIND: 0x02,
|
||||
UDP_ASSOCIATE: 0x03
|
||||
}
|
||||
|
||||
// Address types
|
||||
const ADDRESS_TYPES = {
|
||||
IPV4: 0x01,
|
||||
DOMAIN: 0x03,
|
||||
IPV6: 0x04
|
||||
}
|
||||
|
||||
// Reply codes
|
||||
const REPLY_CODES = {
|
||||
SUCCEEDED: 0x00,
|
||||
GENERAL_FAILURE: 0x01,
|
||||
CONNECTION_NOT_ALLOWED: 0x02,
|
||||
NETWORK_UNREACHABLE: 0x03,
|
||||
HOST_UNREACHABLE: 0x04,
|
||||
CONNECTION_REFUSED: 0x05,
|
||||
TTL_EXPIRED: 0x06,
|
||||
COMMAND_NOT_SUPPORTED: 0x07,
|
||||
ADDRESS_TYPE_NOT_SUPPORTED: 0x08
|
||||
}
|
||||
|
||||
// State machine states
|
||||
const STATES = {
|
||||
INITIAL: 'initial',
|
||||
HANDSHAKING: 'handshaking',
|
||||
AUTHENTICATING: 'authenticating',
|
||||
AUTHENTICATED: 'authenticated',
|
||||
CONNECTING: 'connecting',
|
||||
CONNECTED: 'connected',
|
||||
ERROR: 'error',
|
||||
CLOSED: 'closed'
|
||||
}
|
||||
|
||||
/**
|
||||
* SOCKS5 client implementation
|
||||
* Handles SOCKS5 protocol negotiation and connection establishment
|
||||
*/
|
||||
class Socks5Client extends EventEmitter {
|
||||
constructor (socket, options = {}) {
|
||||
super()
|
||||
|
||||
if (!socket) {
|
||||
throw new InvalidArgumentError('socket is required')
|
||||
}
|
||||
|
||||
this.socket = socket
|
||||
this.options = options
|
||||
this.state = STATES.INITIAL
|
||||
this.buffer = EMPTY_BUFFER
|
||||
this.onSocketData = this.onData.bind(this)
|
||||
this.onSocketError = this.onError.bind(this)
|
||||
this.onSocketClose = this.onClose.bind(this)
|
||||
|
||||
// Authentication settings
|
||||
this.authMethods = []
|
||||
if (options.username && options.password) {
|
||||
this.authMethods.push(AUTH_METHODS.USERNAME_PASSWORD)
|
||||
}
|
||||
this.authMethods.push(AUTH_METHODS.NO_AUTH)
|
||||
|
||||
// Socket event handlers
|
||||
this.socket.on('data', this.onSocketData)
|
||||
this.socket.on('error', this.onSocketError)
|
||||
this.socket.on('close', this.onSocketClose)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming data from the socket
|
||||
*/
|
||||
onData (data) {
|
||||
debug('received data', data.length, 'bytes in state', this.state)
|
||||
this.buffer = Buffer.concat([this.buffer, data])
|
||||
|
||||
try {
|
||||
switch (this.state) {
|
||||
case STATES.HANDSHAKING:
|
||||
this.handleHandshakeResponse()
|
||||
break
|
||||
case STATES.AUTHENTICATING:
|
||||
this.handleAuthResponse()
|
||||
break
|
||||
case STATES.CONNECTING:
|
||||
this.handleConnectResponse()
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
this.onError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle socket errors
|
||||
*/
|
||||
onError (err) {
|
||||
debug('socket error', err)
|
||||
this.state = STATES.ERROR
|
||||
this.emit('error', err)
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle socket close
|
||||
*/
|
||||
onClose () {
|
||||
debug('socket closed')
|
||||
this.state = STATES.CLOSED
|
||||
this.emit('close')
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the client and underlying socket
|
||||
*/
|
||||
destroy () {
|
||||
if (this.socket && !this.socket.destroyed) {
|
||||
this.socket.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
markAuthenticated () {
|
||||
this.state = STATES.AUTHENTICATED
|
||||
this.emit('authenticated')
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the SOCKS5 handshake
|
||||
*/
|
||||
handshake () {
|
||||
if (this.state !== STATES.INITIAL) {
|
||||
throw new InvalidArgumentError('Handshake already started')
|
||||
}
|
||||
|
||||
debug('starting handshake with', this.authMethods.length, 'auth methods')
|
||||
this.state = STATES.HANDSHAKING
|
||||
|
||||
// Build handshake request
|
||||
// +----+----------+----------+
|
||||
// |VER | NMETHODS | METHODS |
|
||||
// +----+----------+----------+
|
||||
// | 1 | 1 | 1 to 255 |
|
||||
// +----+----------+----------+
|
||||
const request = Buffer.alloc(2 + this.authMethods.length)
|
||||
request[0] = SOCKS_VERSION
|
||||
request[1] = this.authMethods.length
|
||||
this.authMethods.forEach((method, i) => {
|
||||
request[2 + i] = method
|
||||
})
|
||||
|
||||
this.socket.write(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle handshake response from server
|
||||
*/
|
||||
handleHandshakeResponse () {
|
||||
if (this.buffer.length < 2) {
|
||||
return // Not enough data yet
|
||||
}
|
||||
|
||||
const version = this.buffer[0]
|
||||
const method = this.buffer[1]
|
||||
|
||||
if (version !== SOCKS_VERSION) {
|
||||
throw new Socks5ProxyError(`Invalid SOCKS version: ${version}`, 'UND_ERR_SOCKS5_VERSION')
|
||||
}
|
||||
|
||||
if (method === AUTH_METHODS.NO_ACCEPTABLE) {
|
||||
throw new Socks5ProxyError('No acceptable authentication method', 'UND_ERR_SOCKS5_AUTH_REJECTED')
|
||||
}
|
||||
|
||||
this.buffer = this.buffer.subarray(2)
|
||||
debug('server selected auth method', method)
|
||||
|
||||
if (method === AUTH_METHODS.NO_AUTH) {
|
||||
this.markAuthenticated()
|
||||
} else if (method === AUTH_METHODS.USERNAME_PASSWORD) {
|
||||
this.state = STATES.AUTHENTICATING
|
||||
this.sendAuthRequest()
|
||||
} else {
|
||||
throw new Socks5ProxyError(`Unsupported authentication method: ${method}`, 'UND_ERR_SOCKS5_AUTH_METHOD')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send username/password authentication request
|
||||
*/
|
||||
sendAuthRequest () {
|
||||
const { username, password } = this.options
|
||||
|
||||
if (!username || !password) {
|
||||
throw new InvalidArgumentError('Username and password required for authentication')
|
||||
}
|
||||
|
||||
debug('sending username/password auth')
|
||||
|
||||
// Username/Password authentication request (RFC 1929)
|
||||
// +----+------+----------+------+----------+
|
||||
// |VER | ULEN | UNAME | PLEN | PASSWD |
|
||||
// +----+------+----------+------+----------+
|
||||
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
|
||||
// +----+------+----------+------+----------+
|
||||
const usernameBuffer = Buffer.from(username)
|
||||
const passwordBuffer = Buffer.from(password)
|
||||
|
||||
if (usernameBuffer.length > 255 || passwordBuffer.length > 255) {
|
||||
throw new InvalidArgumentError('Username or password too long')
|
||||
}
|
||||
|
||||
const request = Buffer.alloc(3 + usernameBuffer.length + passwordBuffer.length)
|
||||
request[0] = 0x01 // Sub-negotiation version
|
||||
request[1] = usernameBuffer.length
|
||||
usernameBuffer.copy(request, 2)
|
||||
request[2 + usernameBuffer.length] = passwordBuffer.length
|
||||
passwordBuffer.copy(request, 3 + usernameBuffer.length)
|
||||
|
||||
this.socket.write(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle authentication response
|
||||
*/
|
||||
handleAuthResponse () {
|
||||
if (this.buffer.length < 2) {
|
||||
return // Not enough data yet
|
||||
}
|
||||
|
||||
const version = this.buffer[0]
|
||||
const status = this.buffer[1]
|
||||
|
||||
if (version !== 0x01) {
|
||||
throw new Socks5ProxyError(`Invalid auth sub-negotiation version: ${version}`, 'UND_ERR_SOCKS5_AUTH_VERSION')
|
||||
}
|
||||
|
||||
if (status !== 0x00) {
|
||||
throw new Socks5ProxyError('Authentication failed', 'UND_ERR_SOCKS5_AUTH_FAILED')
|
||||
}
|
||||
|
||||
this.buffer = this.buffer.subarray(2)
|
||||
debug('authentication successful')
|
||||
this.markAuthenticated()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send CONNECT command
|
||||
* @param {string} address - Target address (IP or domain)
|
||||
* @param {number} port - Target port
|
||||
*/
|
||||
connect (address, port) {
|
||||
if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) {
|
||||
throw new InvalidArgumentError('Connection already in progress')
|
||||
}
|
||||
|
||||
if (this.state !== STATES.AUTHENTICATED) {
|
||||
throw new InvalidArgumentError('Client must be authenticated before CONNECT')
|
||||
}
|
||||
|
||||
debug('connecting to', address, port)
|
||||
this.state = STATES.CONNECTING
|
||||
|
||||
const request = this.buildConnectRequest(COMMANDS.CONNECT, address, port)
|
||||
this.socket.write(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SOCKS5 request
|
||||
*/
|
||||
buildConnectRequest (command, address, port) {
|
||||
// Parse address to determine type and buffer
|
||||
const { type: addressType, buffer: addressBuffer } = parseAddress(address)
|
||||
|
||||
// Build request
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
const request = Buffer.alloc(4 + addressBuffer.length + 2)
|
||||
request[0] = SOCKS_VERSION
|
||||
request[1] = command
|
||||
request[2] = 0x00 // Reserved
|
||||
request[3] = addressType
|
||||
addressBuffer.copy(request, 4)
|
||||
request.writeUInt16BE(port, 4 + addressBuffer.length)
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CONNECT response
|
||||
*/
|
||||
handleConnectResponse () {
|
||||
if (this.buffer.length < 4) {
|
||||
return // Not enough data for header
|
||||
}
|
||||
|
||||
const version = this.buffer[0]
|
||||
const reply = this.buffer[1]
|
||||
const addressType = this.buffer[3]
|
||||
|
||||
if (version !== SOCKS_VERSION) {
|
||||
throw new Socks5ProxyError(`Invalid SOCKS version in reply: ${version}`, 'UND_ERR_SOCKS5_REPLY_VERSION')
|
||||
}
|
||||
|
||||
// Calculate the expected response length
|
||||
let responseLength = 4 // VER + REP + RSV + ATYP
|
||||
if (addressType === ADDRESS_TYPES.IPV4) {
|
||||
responseLength += 4 + 2 // IPv4 + port
|
||||
} else if (addressType === ADDRESS_TYPES.DOMAIN) {
|
||||
if (this.buffer.length < 5) {
|
||||
return // Need domain length byte
|
||||
}
|
||||
responseLength += 1 + this.buffer[4] + 2 // length byte + domain + port
|
||||
} else if (addressType === ADDRESS_TYPES.IPV6) {
|
||||
responseLength += 16 + 2 // IPv6 + port
|
||||
} else {
|
||||
throw new Socks5ProxyError(`Invalid address type in reply: ${addressType}`, 'UND_ERR_SOCKS5_ADDR_TYPE')
|
||||
}
|
||||
|
||||
if (this.buffer.length < responseLength) {
|
||||
return // Not enough data for full response
|
||||
}
|
||||
|
||||
if (reply !== REPLY_CODES.SUCCEEDED) {
|
||||
const errorMessage = this.getReplyErrorMessage(reply)
|
||||
throw new Socks5ProxyError(`SOCKS5 connection failed: ${errorMessage}`, `UND_ERR_SOCKS5_REPLY_${reply}`)
|
||||
}
|
||||
|
||||
// Parse bound address and port
|
||||
let boundAddress
|
||||
let offset = 4
|
||||
|
||||
if (addressType === ADDRESS_TYPES.IPV4) {
|
||||
boundAddress = Array.from(this.buffer.subarray(offset, offset + 4)).join('.')
|
||||
offset += 4
|
||||
} else if (addressType === ADDRESS_TYPES.DOMAIN) {
|
||||
const domainLength = this.buffer[offset]
|
||||
offset += 1
|
||||
boundAddress = this.buffer.subarray(offset, offset + domainLength).toString()
|
||||
offset += domainLength
|
||||
} else if (addressType === ADDRESS_TYPES.IPV6) {
|
||||
// Parse IPv6 address from 16-byte buffer
|
||||
const parts = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const value = this.buffer.readUInt16BE(offset + i * 2)
|
||||
parts.push(value.toString(16))
|
||||
}
|
||||
boundAddress = parts.join(':')
|
||||
offset += 16
|
||||
}
|
||||
|
||||
const boundPort = this.buffer.readUInt16BE(offset)
|
||||
|
||||
this.buffer = EMPTY_BUFFER
|
||||
this.state = STATES.CONNECTED
|
||||
this.socket.removeListener('data', this.onSocketData)
|
||||
|
||||
debug('connected, bound address:', boundAddress, 'port:', boundPort)
|
||||
this.emit('connected', { address: boundAddress, port: boundPort })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable error message for reply code
|
||||
*/
|
||||
getReplyErrorMessage (reply) {
|
||||
switch (reply) {
|
||||
case REPLY_CODES.GENERAL_FAILURE:
|
||||
return 'General SOCKS server failure'
|
||||
case REPLY_CODES.CONNECTION_NOT_ALLOWED:
|
||||
return 'Connection not allowed by ruleset'
|
||||
case REPLY_CODES.NETWORK_UNREACHABLE:
|
||||
return 'Network unreachable'
|
||||
case REPLY_CODES.HOST_UNREACHABLE:
|
||||
return 'Host unreachable'
|
||||
case REPLY_CODES.CONNECTION_REFUSED:
|
||||
return 'Connection refused'
|
||||
case REPLY_CODES.TTL_EXPIRED:
|
||||
return 'TTL expired'
|
||||
case REPLY_CODES.COMMAND_NOT_SUPPORTED:
|
||||
return 'Command not supported'
|
||||
case REPLY_CODES.ADDRESS_TYPE_NOT_SUPPORTED:
|
||||
return 'Address type not supported'
|
||||
default:
|
||||
return `Unknown error code: ${reply}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Socks5Client,
|
||||
AUTH_METHODS,
|
||||
COMMANDS,
|
||||
ADDRESS_TYPES,
|
||||
REPLY_CODES,
|
||||
STATES
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
'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
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
kClose: Symbol('close'),
|
||||
kDestroy: Symbol('destroy'),
|
||||
kDispatch: Symbol('dispatch'),
|
||||
kUrl: Symbol('url'),
|
||||
kRequestOrigin: Symbol('request origin'),
|
||||
kOriginless: Symbol('originless'),
|
||||
kWriting: Symbol('writing'),
|
||||
kResuming: Symbol('resuming'),
|
||||
kQueue: Symbol('queue'),
|
||||
kConnect: Symbol('connect'),
|
||||
kConnecting: Symbol('connecting'),
|
||||
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
|
||||
kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
|
||||
kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
|
||||
kKeepAliveTimeoutValue: Symbol('keep alive timeout'),
|
||||
kKeepAlive: Symbol('keep alive'),
|
||||
kHeadersTimeout: Symbol('headers timeout'),
|
||||
kBodyTimeout: Symbol('body timeout'),
|
||||
kServerName: Symbol('server name'),
|
||||
kLocalAddress: Symbol('local address'),
|
||||
kHost: Symbol('host'),
|
||||
kNoRef: Symbol('no ref'),
|
||||
kBodyUsed: Symbol('used'),
|
||||
kBody: Symbol('abstracted request body'),
|
||||
kRunning: Symbol('running'),
|
||||
kBlocking: Symbol('blocking'),
|
||||
kPending: Symbol('pending'),
|
||||
kSize: Symbol('size'),
|
||||
kBusy: Symbol('busy'),
|
||||
kQueued: Symbol('queued'),
|
||||
kFree: Symbol('free'),
|
||||
kConnected: Symbol('connected'),
|
||||
kClosed: Symbol('closed'),
|
||||
kNeedDrain: Symbol('need drain'),
|
||||
kReset: Symbol('reset'),
|
||||
kDestroyed: Symbol.for('nodejs.stream.destroyed'),
|
||||
kResume: Symbol('resume'),
|
||||
kOnError: Symbol('on error'),
|
||||
kMaxHeadersSize: Symbol('max headers size'),
|
||||
kRunningIdx: Symbol('running index'),
|
||||
kPendingIdx: Symbol('pending index'),
|
||||
kError: Symbol('error'),
|
||||
kClients: Symbol('clients'),
|
||||
kClient: Symbol('client'),
|
||||
kParser: Symbol('parser'),
|
||||
kOnDestroyed: Symbol('destroy callbacks'),
|
||||
kPipelining: Symbol('pipelining'),
|
||||
kSocket: Symbol('socket'),
|
||||
kHostHeader: Symbol('host header'),
|
||||
kConnector: Symbol('connector'),
|
||||
kStrictContentLength: Symbol('strict content length'),
|
||||
kMaxRedirections: Symbol('maxRedirections'),
|
||||
kMaxRequests: Symbol('maxRequestsPerClient'),
|
||||
kProxy: Symbol('proxy agent options'),
|
||||
kCounter: Symbol('socket request counter'),
|
||||
kMaxResponseSize: Symbol('max response size'),
|
||||
kHTTP2Session: Symbol('http2Session'),
|
||||
kHTTP2Options: Symbol('http2 options'),
|
||||
kHTTP2SessionState: Symbol('http2Session state'),
|
||||
kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
|
||||
kConstruct: Symbol('constructable'),
|
||||
kListeners: Symbol('listeners'),
|
||||
kHTTPContext: Symbol('http context'),
|
||||
kMaxConcurrentStreams: Symbol('max concurrent streams'),
|
||||
kHostAuthority: Symbol('host authority'),
|
||||
kHTTP2InitialWindowSize: Symbol('http2 initial window size'),
|
||||
kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'),
|
||||
kEnableConnectProtocol: Symbol('http2session connect protocol'),
|
||||
kRemoteSettings: Symbol('http2session remote settings'),
|
||||
kHTTP2Stream: Symbol('http2session client stream'),
|
||||
kPingInterval: Symbol('ping interval'),
|
||||
kNoProxyAgent: Symbol('no proxy agent'),
|
||||
kHttpProxyAgent: Symbol('http proxy agent'),
|
||||
kHttpsProxyAgent: Symbol('https proxy agent'),
|
||||
kSocks5ProxyAgent: Symbol('socks5 proxy agent')
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
'use strict'
|
||||
|
||||
const {
|
||||
wellknownHeaderNames,
|
||||
headerNameLowerCasedRecord
|
||||
} = require('./constants')
|
||||
|
||||
class TstNode {
|
||||
/** @type {any} */
|
||||
value = null
|
||||
/** @type {null | TstNode} */
|
||||
left = null
|
||||
/** @type {null | TstNode} */
|
||||
middle = null
|
||||
/** @type {null | TstNode} */
|
||||
right = null
|
||||
/** @type {number} */
|
||||
code
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @param {number} index
|
||||
*/
|
||||
constructor (key, value, index) {
|
||||
if (index === undefined || index >= key.length) {
|
||||
throw new TypeError('Unreachable')
|
||||
}
|
||||
const code = this.code = key.charCodeAt(index)
|
||||
// check code is ascii string
|
||||
if (code > 0x7F) {
|
||||
throw new TypeError('key must be ascii string')
|
||||
}
|
||||
if (key.length !== ++index) {
|
||||
this.middle = new TstNode(key, value, index)
|
||||
} else {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @returns {void}
|
||||
*/
|
||||
add (key, value) {
|
||||
const length = key.length
|
||||
if (length === 0) {
|
||||
throw new TypeError('Unreachable')
|
||||
}
|
||||
let index = 0
|
||||
/**
|
||||
* @type {TstNode}
|
||||
*/
|
||||
let node = this
|
||||
while (true) {
|
||||
const code = key.charCodeAt(index)
|
||||
// check code is ascii string
|
||||
if (code > 0x7F) {
|
||||
throw new TypeError('key must be ascii string')
|
||||
}
|
||||
if (node.code === code) {
|
||||
if (length === ++index) {
|
||||
node.value = value
|
||||
break
|
||||
} else if (node.middle !== null) {
|
||||
node = node.middle
|
||||
} else {
|
||||
node.middle = new TstNode(key, value, index)
|
||||
break
|
||||
}
|
||||
} else if (node.code < code) {
|
||||
if (node.left !== null) {
|
||||
node = node.left
|
||||
} else {
|
||||
node.left = new TstNode(key, value, index)
|
||||
break
|
||||
}
|
||||
} else if (node.right !== null) {
|
||||
node = node.right
|
||||
} else {
|
||||
node.right = new TstNode(key, value, index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} key
|
||||
* @returns {TstNode | null}
|
||||
*/
|
||||
search (key) {
|
||||
const keylength = key.length
|
||||
let index = 0
|
||||
/**
|
||||
* @type {TstNode|null}
|
||||
*/
|
||||
let node = this
|
||||
while (node !== null && index < keylength) {
|
||||
let code = key[index]
|
||||
// A-Z
|
||||
// First check if it is bigger than 0x5a.
|
||||
// Lowercase letters have higher char codes than uppercase ones.
|
||||
// Also we assume that headers will mostly contain lowercase characters.
|
||||
if (code <= 0x5a && code >= 0x41) {
|
||||
// Lowercase for uppercase.
|
||||
code |= 32
|
||||
}
|
||||
while (node !== null) {
|
||||
if (code === node.code) {
|
||||
if (keylength === ++index) {
|
||||
// Returns Node since it is the last key.
|
||||
return node
|
||||
}
|
||||
node = node.middle
|
||||
break
|
||||
}
|
||||
node = node.code < code ? node.left : node.right
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class TernarySearchTree {
|
||||
/** @type {TstNode | null} */
|
||||
node = null
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @returns {void}
|
||||
* */
|
||||
insert (key, value) {
|
||||
if (this.node === null) {
|
||||
this.node = new TstNode(key, value, 0)
|
||||
} else {
|
||||
this.node.add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} key
|
||||
* @returns {any}
|
||||
*/
|
||||
lookup (key) {
|
||||
return this.node?.search(key)?.value ?? null
|
||||
}
|
||||
}
|
||||
|
||||
const tree = new TernarySearchTree()
|
||||
|
||||
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
||||
const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]]
|
||||
tree.insert(key, key)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TernarySearchTree,
|
||||
tree
|
||||
}
|
||||
+1010
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user