| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f95bedd commit cf44df3
37 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,6 +24,9 @@ Returns: `Client` | |||
| 24 | 24 | * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds. | |
| 25 | 25 | * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. | |
| 26 | 26 | * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. | |
| 27 | + * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. | ||
| 28 | + * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. | ||
| 29 | + * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. | ||
| 27 | 30 | * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. | |
| 28 | 31 | * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. | |
| 29 | 32 | * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -80,6 +80,33 @@ Arguments: | |||
| 80 | 80 | ||
| 81 | 81 | Returns: `Cookie[]` | |
| 82 | 82 | ||
| 83 | + ## `parseCookie(cookie)` | ||
| 84 | + | ||
| 85 | + Parses a single `Set-Cookie` header value into a `Cookie` object. | ||
| 86 | + | ||
| 87 | + ```js | ||
| 88 | + import { parseCookie } from 'undici' | ||
| 89 | + | ||
| 90 | + console.log(parseCookie('undici=getSetCookies; Secure; SameSite=Lax')) | ||
| 91 | + // { | ||
| 92 | + // name: 'undici', | ||
| 93 | + // value: 'getSetCookies', | ||
| 94 | + // secure: true, | ||
| 95 | + // sameSite: 'Lax' | ||
| 96 | + // } | ||
| 97 | + ``` | ||
| 98 | + | ||
| 99 | + Notes: | ||
| 100 | + | ||
| 101 | + * The cookie value is returned as it appears in the header. Percent-encoded sequences such as `%20` or `%0D%0A` are **not** decoded. | ||
| 102 | + * `sameSite` is only set for exact case-insensitive matches of `Strict`, `Lax`, or `None`. | ||
| 103 | + | ||
| 104 | + Arguments: | ||
| 105 | + | ||
| 106 | + * **cookie** `string` | ||
| 107 | + | ||
| 108 | + Returns: `Cookie | null` | ||
| 109 | + | ||
| 83 | 110 | ## `setCookie(headers, cookie)` | |
| 84 | 111 | ||
| 85 | 112 | Appends a cookie to the `Set-Cookie` header. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -22,6 +22,7 @@ Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions) | |||
| 22 | 22 | * **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL. | |
| 23 | 23 | * **connect** `Function` (optional) - Custom connector function for the proxy connection. | |
| 24 | 24 | * **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS). | |
| 25 | + * **requestTls** `BuildOptions` (optional) - TLS options applied to the HTTPS connection to the target server through the SOCKS5 tunnel. Use this to configure `ca`, `cert`, `key`, `rejectUnauthorized`, `servername`, etc. for the target HTTPS endpoint. | ||
| 25 | 26 | ||
| 26 | 27 | Examples: | |
| 27 | 28 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,7 +21,7 @@ class RequestHandler extends AsyncResource { | |||
| 21 | 21 | throw new InvalidArgumentError('invalid callback') | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | - if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { | ||
| 24 | + if (highWaterMark != null && (!Number.isFinite(highWaterMark) || highWaterMark < 0)) { | ||
| 25 | 25 | throw new InvalidArgumentError('invalid highWaterMark') | |
| 26 | 26 | } | |
| 27 | 27 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -216,7 +216,7 @@ module.exports = class SqliteCacheStore { | |||
| 216 | 216 | SELECT | |
| 217 | 217 | id | |
| 218 | 218 | FROM cacheInterceptorV${VERSION} | |
| 219 | - ORDER BY cachedAt DESC | ||
| 219 | + ORDER BY cachedAt ASC | ||
| 220 | 220 | LIMIT ? | |
| 221 | 221 | ) | |
| 222 | 222 | `) | |
@@ -283,7 +283,6 @@ module.exports = class SqliteCacheStore { | |||
| 283 | 283 | existingValue.id | |
| 284 | 284 | ) | |
| 285 | 285 | } else { | |
| 286 | - this.#prune() | ||
| 287 | 286 | // New response, let's insert it | |
| 288 | 287 | this.#insertValueQuery.run( | |
| 289 | 288 | url, | |
@@ -299,6 +298,7 @@ module.exports = class SqliteCacheStore { | |||
| 299 | 298 | value.cachedAt, | |
| 300 | 299 | value.staleAt | |
| 301 | 300 | ) | |
| 301 | + this.#prune() | ||
| 302 | 302 | } | |
| 303 | 303 | } | |
| 304 | 304 | ||
@@ -409,7 +409,7 @@ module.exports = class SqliteCacheStore { | |||
| 409 | 409 | const now = Date.now() | |
| 410 | 410 | for (const value of values) { | |
| 411 | 411 | if (now >= value.deleteAt && !canBeExpired) { | |
| 412 | - return undefined | ||
| 412 | + continue | ||
| 413 | 413 | } | |
| 414 | 414 | ||
| 415 | 415 | let matches = true | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,6 +38,22 @@ const SessionCache = class WeakSessionCache { | |||
| 38 | 38 | return | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | + if (this._sessionCache.has(sessionKey)) { | ||
| 42 | + this._sessionCache.delete(sessionKey) | ||
| 43 | + } else if (this._sessionCache.size >= this._maxCachedSessions) { | ||
| 44 | + for (const [key, ref] of this._sessionCache) { | ||
| 45 | + if (ref.deref() === undefined) { | ||
| 46 | + this._sessionCache.delete(key) | ||
| 47 | + return | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + const oldest = this._sessionCache.keys().next() | ||
| 52 | + if (!oldest.done) { | ||
| 53 | + this._sessionCache.delete(oldest.value) | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + | ||
| 41 | 57 | this._sessionCache.set(sessionKey, new WeakRef(session)) | |
| 42 | 58 | this._sessionRegistry.register(session, sessionKey) | |
| 43 | 59 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,6 +27,21 @@ const { headerNameLowerCasedRecord } = require('./constants') | |||
| 27 | 27 | // Verifies that a given path is valid does not contain control chars \x00 to \x20 | |
| 28 | 28 | const invalidPathRegex = /[^\u0021-\u00ff]/ | |
| 29 | 29 | ||
| 30 | + function isValidContentLengthHeaderValue (val) { | ||
| 31 | + if (typeof val !== 'string' || val.length === 0) { | ||
| 32 | + return false | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + for (let i = 0; i < val.length; i++) { | ||
| 36 | + const charCode = val.charCodeAt(i) | ||
| 37 | + if (charCode < 48 || charCode > 57) { | ||
| 38 | + return false | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + return true | ||
| 43 | + } | ||
| 44 | + | ||
| 30 | 45 | const kHandler = Symbol('handler') | |
| 31 | 46 | ||
| 32 | 47 | class Request { | |
@@ -402,10 +417,10 @@ function processHeader (request, key, val) { | |||
| 402 | 417 | if (request.contentLength !== null) { | |
| 403 | 418 | throw new InvalidArgumentError('duplicate content-length header') | |
| 404 | 419 | } | |
| 405 | - request.contentLength = parseInt(val, 10) | ||
| 406 | - if (!Number.isFinite(request.contentLength)) { | ||
| 420 | + if (!isValidContentLengthHeaderValue(val)) { | ||
| 407 | 421 | throw new InvalidArgumentError('invalid content-length header') | |
| 408 | 422 | } | |
| 423 | + request.contentLength = parseInt(val, 10) | ||
| 409 | 424 | } else if (request.contentType === null && headerName === 'content-type') { | |
| 410 | 425 | request.contentType = val | |
| 411 | 426 | request.headers.push(key, val) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,6 +7,7 @@ const { debuglog } = require('node:util') | |||
| 7 | 7 | const { parseAddress } = require('./socks5-utils') | |
| 8 | 8 | ||
| 9 | 9 | const debug = debuglog('undici:socks5') | |
| 10 | + const EMPTY_BUFFER = Buffer.alloc(0) | ||
| 10 | 11 | ||
| 11 | 12 | // SOCKS5 constants | |
| 12 | 13 | const SOCKS_VERSION = 0x05 | |
@@ -51,6 +52,7 @@ const STATES = { | |||
| 51 | 52 | INITIAL: 'initial', | |
| 52 | 53 | HANDSHAKING: 'handshaking', | |
| 53 | 54 | AUTHENTICATING: 'authenticating', | |
| 55 | + AUTHENTICATED: 'authenticated', | ||
| 54 | 56 | CONNECTING: 'connecting', | |
| 55 | 57 | CONNECTED: 'connected', | |
| 56 | 58 | ERROR: 'error', | |
@@ -72,7 +74,10 @@ class Socks5Client extends EventEmitter { | |||
| 72 | 74 | this.socket = socket | |
| 73 | 75 | this.options = options | |
| 74 | 76 | this.state = STATES.INITIAL | |
| 75 | - this.buffer = Buffer.alloc(0) | ||
| 77 | + this.buffer = EMPTY_BUFFER | ||
| 78 | + this.onSocketData = this.onData.bind(this) | ||
| 79 | + this.onSocketError = this.onError.bind(this) | ||
| 80 | + this.onSocketClose = this.onClose.bind(this) | ||
| 76 | 81 | ||
| 77 | 82 | // Authentication settings | |
| 78 | 83 | this.authMethods = [] | |
@@ -82,9 +87,9 @@ class Socks5Client extends EventEmitter { | |||
| 82 | 87 | this.authMethods.push(AUTH_METHODS.NO_AUTH) | |
| 83 | 88 | ||
| 84 | 89 | // Socket event handlers | |
| 85 | - this.socket.on('data', this.onData.bind(this)) | ||
| 86 | - this.socket.on('error', this.onError.bind(this)) | ||
| 87 | - this.socket.on('close', this.onClose.bind(this)) | ||
| 90 | + this.socket.on('data', this.onSocketData) | ||
| 91 | + this.socket.on('error', this.onSocketError) | ||
| 92 | + this.socket.on('close', this.onSocketClose) | ||
| 88 | 93 | } | |
| 89 | 94 | ||
| 90 | 95 | /** | |
@@ -139,6 +144,11 @@ class Socks5Client extends EventEmitter { | |||
| 139 | 144 | } | |
| 140 | 145 | } | |
| 141 | 146 | ||
| 147 | + markAuthenticated () { | ||
| 148 | + this.state = STATES.AUTHENTICATED | ||
| 149 | + this.emit('authenticated') | ||
| 150 | + } | ||
| 151 | + | ||
| 142 | 152 | /** | |
| 143 | 153 | * Start the SOCKS5 handshake | |
| 144 | 154 | */ | |
@@ -189,7 +199,7 @@ class Socks5Client extends EventEmitter { | |||
| 189 | 199 | debug('server selected auth method', method) | |
| 190 | 200 | ||
| 191 | 201 | if (method === AUTH_METHODS.NO_AUTH) { | |
| 192 | - this.emit('authenticated') | ||
| 202 | + this.markAuthenticated() | ||
| 193 | 203 | } else if (method === AUTH_METHODS.USERNAME_PASSWORD) { | |
| 194 | 204 | this.state = STATES.AUTHENTICATING | |
| 195 | 205 | this.sendAuthRequest() | |
@@ -254,7 +264,7 @@ class Socks5Client extends EventEmitter { | |||
| 254 | 264 | ||
| 255 | 265 | this.buffer = this.buffer.subarray(2) | |
| 256 | 266 | debug('authentication successful') | |
| 257 | - this.emit('authenticated') | ||
| 267 | + this.markAuthenticated() | ||
| 258 | 268 | } | |
| 259 | 269 | ||
| 260 | 270 | /** | |
@@ -263,8 +273,12 @@ class Socks5Client extends EventEmitter { | |||
| 263 | 273 | * @param {number} port - Target port | |
| 264 | 274 | */ | |
| 265 | 275 | connect (address, port) { | |
| 266 | - if (this.state === STATES.CONNECTED) { | ||
| 267 | - throw new InvalidArgumentError('Already connected') | ||
| 276 | + if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) { | ||
| 277 | + throw new InvalidArgumentError('Connection already in progress') | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + if (this.state !== STATES.AUTHENTICATED) { | ||
| 281 | + throw new InvalidArgumentError('Client must be authenticated before CONNECT') | ||
| 268 | 282 | } | |
| 269 | 283 | ||
| 270 | 284 | debug('connecting to', address, port) | |
@@ -363,8 +377,9 @@ class Socks5Client extends EventEmitter { | |||
| 363 | 377 | ||
| 364 | 378 | const boundPort = this.buffer.readUInt16BE(offset) | |
| 365 | 379 | ||
| 366 | - this.buffer = this.buffer.subarray(responseLength) | ||
| 380 | + this.buffer = EMPTY_BUFFER | ||
| 367 | 381 | this.state = STATES.CONNECTED | |
| 382 | + this.socket.removeListener('data', this.onSocketData) | ||
| 368 | 383 | ||
| 369 | 384 | debug('connected, bound address:', boundAddress, 'port:', boundPort) | |
| 370 | 385 | this.emit('connected', { address: boundAddress, port: boundPort }) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -46,34 +46,43 @@ function parseAddress (address) { | |||
| 46 | 46 | */ | |
| 47 | 47 | function parseIPv6 (address) { | |
| 48 | 48 | const buffer = Buffer.alloc(16) | |
| 49 | - const parts = address.split(':') | ||
| 50 | - let partIndex = 0 | ||
| 51 | - let bufferIndex = 0 | ||
| 49 | + let normalizedAddress = address | ||
| 50 | + | ||
| 51 | + // Expand an embedded IPv4 tail into the last two IPv6 groups. | ||
| 52 | + if (address.includes('.')) { | ||
| 53 | + const lastColonIndex = address.lastIndexOf(':') | ||
| 54 | + const ipv4Part = address.slice(lastColonIndex + 1) | ||
| 55 | + | ||
| 56 | + if (net.isIPv4(ipv4Part)) { | ||
| 57 | + const octets = ipv4Part.split('.').map(Number) | ||
| 58 | + const high = ((octets[0] << 8) | octets[1]).toString(16) | ||
| 59 | + const low = ((octets[2] << 8) | octets[3]).toString(16) | ||
| 60 | + normalizedAddress = `${address.slice(0, lastColonIndex)}:${high}:${low}` | ||
| 61 | + } | ||
| 62 | + } | ||
| 52 | 63 | ||
| 53 | 64 | // Handle compressed notation (::) | |
| 54 | - const doubleColonIndex = address.indexOf('::') | ||
| 65 | + const doubleColonIndex = normalizedAddress.indexOf('::') | ||
| 55 | 66 | if (doubleColonIndex !== -1) { | |
| 56 | - // Count non-empty parts | ||
| 57 | - const nonEmptyParts = parts.filter(p => p.length > 0).length | ||
| 58 | - const skipParts = 8 - nonEmptyParts | ||
| 59 | - | ||
| 60 | - for (let i = 0; i < parts.length; i++) { | ||
| 61 | - if (parts[i] === '' && i === doubleColonIndex / 3) { | ||
| 62 | - // Skip empty parts for :: | ||
| 63 | - bufferIndex += skipParts * 2 | ||
| 64 | - } else if (parts[i] !== '') { | ||
| 65 | - const value = parseInt(parts[i], 16) | ||
| 66 | - buffer.writeUInt16BE(value, bufferIndex) | ||
| 67 | - bufferIndex += 2 | ||
| 68 | - } | ||
| 67 | + const before = normalizedAddress.slice(0, doubleColonIndex) | ||
| 68 | + const after = normalizedAddress.slice(doubleColonIndex + 2) | ||
| 69 | + const beforeParts = before === '' ? [] : before.split(':') | ||
| 70 | + const afterParts = after === '' ? [] : after.split(':') | ||
| 71 | + | ||
| 72 | + let bufferIndex = 0 | ||
| 73 | + for (const part of beforeParts) { | ||
| 74 | + buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) | ||
| 75 | + bufferIndex += 2 | ||
| 76 | + } | ||
| 77 | + bufferIndex = 16 - afterParts.length * 2 | ||
| 78 | + for (const part of afterParts) { | ||
| 79 | + buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) | ||
| 80 | + bufferIndex += 2 | ||
| 69 | 81 | } | |
| 70 | 82 | } else { | |
| 71 | - // No compression, parse normally | ||
| 72 | - for (const part of parts) { | ||
| 73 | - if (part === '') continue | ||
| 74 | - const value = parseInt(part, 16) | ||
| 75 | - buffer.writeUInt16BE(value, partIndex * 2) | ||
| 76 | - partIndex++ | ||
| 83 | + const parts = normalizedAddress.split(':') | ||
| 84 | + for (let i = 0; i < parts.length; i++) { | ||
| 85 | + buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2) | ||
| 77 | 86 | } | |
| 78 | 87 | } | |
| 79 | 88 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,7 +35,7 @@ class Agent extends DispatcherBase { | |||
| 35 | 35 | throw new InvalidArgumentError('maxOrigins must be a number greater than 0') | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | - super() | ||
| 38 | + super(options) | ||
| 39 | 39 | ||
| 40 | 40 | if (connect && typeof connect !== 'function') { | |
| 41 | 41 | connect = { ...connect } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments