123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- 'use strict'
- const {
- InvalidArgumentError,
- NotSupportedError
- } = require('./errors')
- const assert = require('assert')
- const util = require('./util')
- // tokenRegExp and headerCharRegex have been lifted from
- // https://github.com/nodejs/node/blob/main/lib/_http_common.js
- /**
- * Verifies that the given val is a valid HTTP token
- * per the rules defined in RFC 7230
- * See https://tools.ietf.org/html/rfc7230#section-3.2.6
- */
- const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
- /**
- * Matches if val contains an invalid field-vchar
- * field-value = *( field-content / obs-fold )
- * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
- * field-vchar = VCHAR / obs-text
- */
- const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
- // Verifies that a given path is valid does not contain control chars \x00 to \x20
- const invalidPathRegex = /[^\u0021-\u00ff]/
- const kHandler = Symbol('handler')
- const channels = {}
- let extractBody
- try {
- const diagnosticsChannel = require('diagnostics_channel')
- channels.create = diagnosticsChannel.channel('undici:request:create')
- channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
- channels.headers = diagnosticsChannel.channel('undici:request:headers')
- channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
- channels.error = diagnosticsChannel.channel('undici:request:error')
- } catch {
- channels.create = { hasSubscribers: false }
- channels.bodySent = { hasSubscribers: false }
- channels.headers = { hasSubscribers: false }
- channels.trailers = { hasSubscribers: false }
- channels.error = { hasSubscribers: false }
- }
- class Request {
- constructor (origin, {
- path,
- method,
- body,
- headers,
- query,
- idempotent,
- blocking,
- upgrade,
- headersTimeout,
- bodyTimeout,
- reset,
- throwOnError
- }, 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.exec(path) !== null) {
- throw new InvalidArgumentError('invalid request path')
- }
- if (typeof method !== 'string') {
- throw new InvalidArgumentError('method must be a string')
- } else if (tokenRegExp.exec(method) === null) {
- throw new InvalidArgumentError('invalid request method')
- }
- if (upgrade && typeof upgrade !== 'string') {
- throw new InvalidArgumentError('upgrade must be a string')
- }
- 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')
- }
- this.headersTimeout = headersTimeout
- this.bodyTimeout = bodyTimeout
- this.throwOnError = throwOnError === true
- this.method = method
- if (body == null) {
- this.body = null
- } else if (util.isStream(body)) {
- this.body = body
- } else if (util.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 (util.isFormDataLike(body) || util.isIterable(body) || util.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 ? util.buildURL(path, query) : path
- this.origin = origin
- this.idempotent = idempotent == null
- ? method === 'HEAD' || method === 'GET'
- : idempotent
- this.blocking = blocking == null ? false : blocking
- this.reset = reset == null ? null : reset
- this.host = null
- this.contentLength = null
- this.contentType = null
- this.headers = ''
- 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') {
- const keys = Object.keys(headers)
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i]
- processHeader(this, key, headers[key])
- }
- } else if (headers != null) {
- throw new InvalidArgumentError('headers must be an object or an array')
- }
- if (util.isFormDataLike(this.body)) {
- if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
- throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
- }
- if (!extractBody) {
- extractBody = require('../fetch/body.js').extractBody
- }
- const [bodyStream, contentType] = extractBody(body)
- if (this.contentType == null) {
- this.contentType = contentType
- this.headers += `content-type: ${contentType}\r\n`
- }
- this.body = bodyStream.stream
- this.contentLength = bodyStream.length
- } else if (util.isBlobLike(body) && this.contentType == null && body.type) {
- this.contentType = body.type
- this.headers += `content-type: ${body.type}\r\n`
- }
- util.validateHandler(handler, method, upgrade)
- this.servername = util.getServerName(this.host)
- this[kHandler] = handler
- if (channels.create.hasSubscribers) {
- channels.create.publish({ request: this })
- }
- }
- onBodySent (chunk) {
- if (this[kHandler].onBodySent) {
- try {
- this[kHandler].onBodySent(chunk)
- } catch (err) {
- this.onError(err)
- }
- }
- }
- onRequestSent () {
- if (channels.bodySent.hasSubscribers) {
- channels.bodySent.publish({ request: this })
- }
- }
- onConnect (abort) {
- assert(!this.aborted)
- assert(!this.completed)
- return this[kHandler].onConnect(abort)
- }
- onHeaders (statusCode, headers, resume, statusText) {
- assert(!this.aborted)
- assert(!this.completed)
- if (channels.headers.hasSubscribers) {
- channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
- }
- return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
- }
- onData (chunk) {
- assert(!this.aborted)
- assert(!this.completed)
- return this[kHandler].onData(chunk)
- }
- onUpgrade (statusCode, headers, socket) {
- assert(!this.aborted)
- assert(!this.completed)
- return this[kHandler].onUpgrade(statusCode, headers, socket)
- }
- onComplete (trailers) {
- assert(!this.aborted)
- this.completed = true
- if (channels.trailers.hasSubscribers) {
- channels.trailers.publish({ request: this, trailers })
- }
- return this[kHandler].onComplete(trailers)
- }
- onError (error) {
- if (channels.error.hasSubscribers) {
- channels.error.publish({ request: this, error })
- }
- if (this.aborted) {
- return
- }
- this.aborted = true
- return this[kHandler].onError(error)
- }
- addHeader (key, value) {
- processHeader(this, key, value)
- return this
- }
- }
- function processHeaderValue (key, val) {
- if (val && typeof val === 'object') {
- throw new InvalidArgumentError(`invalid ${key} header`)
- }
- val = val != null ? `${val}` : ''
- if (headerCharRegex.exec(val) !== null) {
- throw new InvalidArgumentError(`invalid ${key} header`)
- }
- return `${key}: ${val}\r\n`
- }
- function processHeader (request, key, val) {
- if (val && (typeof val === 'object' && !Array.isArray(val))) {
- throw new InvalidArgumentError(`invalid ${key} header`)
- } else if (val === undefined) {
- return
- }
- if (
- request.host === null &&
- key.length === 4 &&
- key.toLowerCase() === 'host'
- ) {
- if (headerCharRegex.exec(val) !== null) {
- throw new InvalidArgumentError(`invalid ${key} header`)
- }
- // Consumed by Client
- request.host = val
- } else if (
- request.contentLength === null &&
- key.length === 14 &&
- key.toLowerCase() === 'content-length'
- ) {
- request.contentLength = parseInt(val, 10)
- if (!Number.isFinite(request.contentLength)) {
- throw new InvalidArgumentError('invalid content-length header')
- }
- } else if (
- request.contentType === null &&
- key.length === 12 &&
- key.toLowerCase() === 'content-type'
- ) {
- request.contentType = val
- request.headers += processHeaderValue(key, val)
- } else if (
- key.length === 17 &&
- key.toLowerCase() === 'transfer-encoding'
- ) {
- throw new InvalidArgumentError('invalid transfer-encoding header')
- } else if (
- key.length === 10 &&
- key.toLowerCase() === 'connection'
- ) {
- const value = typeof val === 'string' ? val.toLowerCase() : null
- if (value !== 'close' && value !== 'keep-alive') {
- throw new InvalidArgumentError('invalid connection header')
- } else if (value === 'close') {
- request.reset = true
- }
- } else if (
- key.length === 10 &&
- key.toLowerCase() === 'keep-alive'
- ) {
- throw new InvalidArgumentError('invalid keep-alive header')
- } else if (
- key.length === 7 &&
- key.toLowerCase() === 'upgrade'
- ) {
- throw new InvalidArgumentError('invalid upgrade header')
- } else if (
- key.length === 6 &&
- key.toLowerCase() === 'expect'
- ) {
- throw new NotSupportedError('expect header not supported')
- } else if (tokenRegExp.exec(key) === null) {
- throw new InvalidArgumentError('invalid header key')
- } else {
- if (Array.isArray(val)) {
- for (let i = 0; i < val.length; i++) {
- request.headers += processHeaderValue(key, val[i])
- }
- } else {
- request.headers += processHeaderValue(key, val)
- }
- }
- }
- module.exports = Request
|