1'use strict' 2 3const { 4 InvalidArgumentError, 5 NotSupportedError 6} = require('./errors') 7const assert = require('assert') 8const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols') 9const util = require('./util') 10 11// tokenRegExp and headerCharRegex have been lifted from 12// https://github.com/nodejs/node/blob/main/lib/_http_common.js 13 14/** 15 * Verifies that the given val is a valid HTTP token 16 * per the rules defined in RFC 7230 17 * See https://tools.ietf.org/html/rfc7230#section-3.2.6 18 */ 19const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ 20 21/** 22 * Matches if val contains an invalid field-vchar 23 * field-value = *( field-content / obs-fold ) 24 * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] 25 * field-vchar = VCHAR / obs-text 26 */ 27const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ 28 29// Verifies that a given path is valid does not contain control chars \x00 to \x20 30const invalidPathRegex = /[^\u0021-\u00ff]/ 31 32const kHandler = Symbol('handler') 33 34const channels = {} 35 36let extractBody 37 38try { 39 const diagnosticsChannel = require('diagnostics_channel') 40 channels.create = diagnosticsChannel.channel('undici:request:create') 41 channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent') 42 channels.headers = diagnosticsChannel.channel('undici:request:headers') 43 channels.trailers = diagnosticsChannel.channel('undici:request:trailers') 44 channels.error = diagnosticsChannel.channel('undici:request:error') 45} catch { 46 channels.create = { hasSubscribers: false } 47 channels.bodySent = { hasSubscribers: false } 48 channels.headers = { hasSubscribers: false } 49 channels.trailers = { hasSubscribers: false } 50 channels.error = { hasSubscribers: false } 51} 52 53class Request { 54 constructor (origin, { 55 path, 56 method, 57 body, 58 headers, 59 query, 60 idempotent, 61 blocking, 62 upgrade, 63 headersTimeout, 64 bodyTimeout, 65 reset, 66 throwOnError, 67 expectContinue 68 }, handler) { 69 if (typeof path !== 'string') { 70 throw new InvalidArgumentError('path must be a string') 71 } else if ( 72 path[0] !== '/' && 73 !(path.startsWith('http://') || path.startsWith('https://')) && 74 method !== 'CONNECT' 75 ) { 76 throw new InvalidArgumentError('path must be an absolute URL or start with a slash') 77 } else if (invalidPathRegex.exec(path) !== null) { 78 throw new InvalidArgumentError('invalid request path') 79 } 80 81 if (typeof method !== 'string') { 82 throw new InvalidArgumentError('method must be a string') 83 } else if (tokenRegExp.exec(method) === null) { 84 throw new InvalidArgumentError('invalid request method') 85 } 86 87 if (upgrade && typeof upgrade !== 'string') { 88 throw new InvalidArgumentError('upgrade must be a string') 89 } 90 91 if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { 92 throw new InvalidArgumentError('invalid headersTimeout') 93 } 94 95 if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { 96 throw new InvalidArgumentError('invalid bodyTimeout') 97 } 98 99 if (reset != null && typeof reset !== 'boolean') { 100 throw new InvalidArgumentError('invalid reset') 101 } 102 103 if (expectContinue != null && typeof expectContinue !== 'boolean') { 104 throw new InvalidArgumentError('invalid expectContinue') 105 } 106 107 this.headersTimeout = headersTimeout 108 109 this.bodyTimeout = bodyTimeout 110 111 this.throwOnError = throwOnError === true 112 113 this.method = method 114 115 this.abort = null 116 117 if (body == null) { 118 this.body = null 119 } else if (util.isStream(body)) { 120 this.body = body 121 122 const rState = this.body._readableState 123 if (!rState || !rState.autoDestroy) { 124 this.endHandler = function autoDestroy () { 125 util.destroy(this) 126 } 127 this.body.on('end', this.endHandler) 128 } 129 130 this.errorHandler = err => { 131 if (this.abort) { 132 this.abort(err) 133 } else { 134 this.error = err 135 } 136 } 137 this.body.on('error', this.errorHandler) 138 } else if (util.isBuffer(body)) { 139 this.body = body.byteLength ? body : null 140 } else if (ArrayBuffer.isView(body)) { 141 this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null 142 } else if (body instanceof ArrayBuffer) { 143 this.body = body.byteLength ? Buffer.from(body) : null 144 } else if (typeof body === 'string') { 145 this.body = body.length ? Buffer.from(body) : null 146 } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { 147 this.body = body 148 } else { 149 throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') 150 } 151 152 this.completed = false 153 154 this.aborted = false 155 156 this.upgrade = upgrade || null 157 158 this.path = query ? util.buildURL(path, query) : path 159 160 this.origin = origin 161 162 this.idempotent = idempotent == null 163 ? method === 'HEAD' || method === 'GET' 164 : idempotent 165 166 this.blocking = blocking == null ? false : blocking 167 168 this.reset = reset == null ? null : reset 169 170 this.host = null 171 172 this.contentLength = null 173 174 this.contentType = null 175 176 this.headers = '' 177 178 // Only for H2 179 this.expectContinue = expectContinue != null ? expectContinue : false 180 181 if (Array.isArray(headers)) { 182 if (headers.length % 2 !== 0) { 183 throw new InvalidArgumentError('headers array must be even') 184 } 185 for (let i = 0; i < headers.length; i += 2) { 186 processHeader(this, headers[i], headers[i + 1]) 187 } 188 } else if (headers && typeof headers === 'object') { 189 const keys = Object.keys(headers) 190 for (let i = 0; i < keys.length; i++) { 191 const key = keys[i] 192 processHeader(this, key, headers[key]) 193 } 194 } else if (headers != null) { 195 throw new InvalidArgumentError('headers must be an object or an array') 196 } 197 198 if (util.isFormDataLike(this.body)) { 199 if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) { 200 throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') 201 } 202 203 if (!extractBody) { 204 extractBody = require('../fetch/body.js').extractBody 205 } 206 207 const [bodyStream, contentType] = extractBody(body) 208 if (this.contentType == null) { 209 this.contentType = contentType 210 this.headers += `content-type: ${contentType}\r\n` 211 } 212 this.body = bodyStream.stream 213 this.contentLength = bodyStream.length 214 } else if (util.isBlobLike(body) && this.contentType == null && body.type) { 215 this.contentType = body.type 216 this.headers += `content-type: ${body.type}\r\n` 217 } 218 219 util.validateHandler(handler, method, upgrade) 220 221 this.servername = util.getServerName(this.host) 222 223 this[kHandler] = handler 224 225 if (channels.create.hasSubscribers) { 226 channels.create.publish({ request: this }) 227 } 228 } 229 230 onBodySent (chunk) { 231 if (this[kHandler].onBodySent) { 232 try { 233 return this[kHandler].onBodySent(chunk) 234 } catch (err) { 235 this.abort(err) 236 } 237 } 238 } 239 240 onRequestSent () { 241 if (channels.bodySent.hasSubscribers) { 242 channels.bodySent.publish({ request: this }) 243 } 244 245 if (this[kHandler].onRequestSent) { 246 try { 247 return this[kHandler].onRequestSent() 248 } catch (err) { 249 this.abort(err) 250 } 251 } 252 } 253 254 onConnect (abort) { 255 assert(!this.aborted) 256 assert(!this.completed) 257 258 if (this.error) { 259 abort(this.error) 260 } else { 261 this.abort = abort 262 return this[kHandler].onConnect(abort) 263 } 264 } 265 266 onHeaders (statusCode, headers, resume, statusText) { 267 assert(!this.aborted) 268 assert(!this.completed) 269 270 if (channels.headers.hasSubscribers) { 271 channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) 272 } 273 274 try { 275 return this[kHandler].onHeaders(statusCode, headers, resume, statusText) 276 } catch (err) { 277 this.abort(err) 278 } 279 } 280 281 onData (chunk) { 282 assert(!this.aborted) 283 assert(!this.completed) 284 285 try { 286 return this[kHandler].onData(chunk) 287 } catch (err) { 288 this.abort(err) 289 return false 290 } 291 } 292 293 onUpgrade (statusCode, headers, socket) { 294 assert(!this.aborted) 295 assert(!this.completed) 296 297 return this[kHandler].onUpgrade(statusCode, headers, socket) 298 } 299 300 onComplete (trailers) { 301 this.onFinally() 302 303 assert(!this.aborted) 304 305 this.completed = true 306 if (channels.trailers.hasSubscribers) { 307 channels.trailers.publish({ request: this, trailers }) 308 } 309 310 try { 311 return this[kHandler].onComplete(trailers) 312 } catch (err) { 313 // TODO (fix): This might be a bad idea? 314 this.onError(err) 315 } 316 } 317 318 onError (error) { 319 this.onFinally() 320 321 if (channels.error.hasSubscribers) { 322 channels.error.publish({ request: this, error }) 323 } 324 325 if (this.aborted) { 326 return 327 } 328 this.aborted = true 329 330 return this[kHandler].onError(error) 331 } 332 333 onFinally () { 334 if (this.errorHandler) { 335 this.body.off('error', this.errorHandler) 336 this.errorHandler = null 337 } 338 339 if (this.endHandler) { 340 this.body.off('end', this.endHandler) 341 this.endHandler = null 342 } 343 } 344 345 // TODO: adjust to support H2 346 addHeader (key, value) { 347 processHeader(this, key, value) 348 return this 349 } 350 351 static [kHTTP1BuildRequest] (origin, opts, handler) { 352 // TODO: Migrate header parsing here, to make Requests 353 // HTTP agnostic 354 return new Request(origin, opts, handler) 355 } 356 357 static [kHTTP2BuildRequest] (origin, opts, handler) { 358 const headers = opts.headers 359 opts = { ...opts, headers: null } 360 361 const request = new Request(origin, opts, handler) 362 363 request.headers = {} 364 365 if (Array.isArray(headers)) { 366 if (headers.length % 2 !== 0) { 367 throw new InvalidArgumentError('headers array must be even') 368 } 369 for (let i = 0; i < headers.length; i += 2) { 370 processHeader(request, headers[i], headers[i + 1], true) 371 } 372 } else if (headers && typeof headers === 'object') { 373 const keys = Object.keys(headers) 374 for (let i = 0; i < keys.length; i++) { 375 const key = keys[i] 376 processHeader(request, key, headers[key], true) 377 } 378 } else if (headers != null) { 379 throw new InvalidArgumentError('headers must be an object or an array') 380 } 381 382 return request 383 } 384 385 static [kHTTP2CopyHeaders] (raw) { 386 const rawHeaders = raw.split('\r\n') 387 const headers = {} 388 389 for (const header of rawHeaders) { 390 const [key, value] = header.split(': ') 391 392 if (value == null || value.length === 0) continue 393 394 if (headers[key]) headers[key] += `,${value}` 395 else headers[key] = value 396 } 397 398 return headers 399 } 400} 401 402function processHeaderValue (key, val, skipAppend) { 403 if (val && typeof val === 'object') { 404 throw new InvalidArgumentError(`invalid ${key} header`) 405 } 406 407 val = val != null ? `${val}` : '' 408 409 if (headerCharRegex.exec(val) !== null) { 410 throw new InvalidArgumentError(`invalid ${key} header`) 411 } 412 413 return skipAppend ? val : `${key}: ${val}\r\n` 414} 415 416function processHeader (request, key, val, skipAppend = false) { 417 if (val && (typeof val === 'object' && !Array.isArray(val))) { 418 throw new InvalidArgumentError(`invalid ${key} header`) 419 } else if (val === undefined) { 420 return 421 } 422 423 if ( 424 request.host === null && 425 key.length === 4 && 426 key.toLowerCase() === 'host' 427 ) { 428 if (headerCharRegex.exec(val) !== null) { 429 throw new InvalidArgumentError(`invalid ${key} header`) 430 } 431 // Consumed by Client 432 request.host = val 433 } else if ( 434 request.contentLength === null && 435 key.length === 14 && 436 key.toLowerCase() === 'content-length' 437 ) { 438 request.contentLength = parseInt(val, 10) 439 if (!Number.isFinite(request.contentLength)) { 440 throw new InvalidArgumentError('invalid content-length header') 441 } 442 } else if ( 443 request.contentType === null && 444 key.length === 12 && 445 key.toLowerCase() === 'content-type' 446 ) { 447 request.contentType = val 448 if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) 449 else request.headers += processHeaderValue(key, val) 450 } else if ( 451 key.length === 17 && 452 key.toLowerCase() === 'transfer-encoding' 453 ) { 454 throw new InvalidArgumentError('invalid transfer-encoding header') 455 } else if ( 456 key.length === 10 && 457 key.toLowerCase() === 'connection' 458 ) { 459 const value = typeof val === 'string' ? val.toLowerCase() : null 460 if (value !== 'close' && value !== 'keep-alive') { 461 throw new InvalidArgumentError('invalid connection header') 462 } else if (value === 'close') { 463 request.reset = true 464 } 465 } else if ( 466 key.length === 10 && 467 key.toLowerCase() === 'keep-alive' 468 ) { 469 throw new InvalidArgumentError('invalid keep-alive header') 470 } else if ( 471 key.length === 7 && 472 key.toLowerCase() === 'upgrade' 473 ) { 474 throw new InvalidArgumentError('invalid upgrade header') 475 } else if ( 476 key.length === 6 && 477 key.toLowerCase() === 'expect' 478 ) { 479 throw new NotSupportedError('expect header not supported') 480 } else if (tokenRegExp.exec(key) === null) { 481 throw new InvalidArgumentError('invalid header key') 482 } else { 483 if (Array.isArray(val)) { 484 for (let i = 0; i < val.length; i++) { 485 if (skipAppend) { 486 if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` 487 else request.headers[key] = processHeaderValue(key, val[i], skipAppend) 488 } else { 489 request.headers += processHeaderValue(key, val[i]) 490 } 491 } 492 } else { 493 if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) 494 else request.headers += processHeaderValue(key, val) 495 } 496 } 497} 498 499module.exports = Request 500