268 lines
6.4 KiB
JavaScript
268 lines
6.4 KiB
JavaScript
|
'use strict'
|
||
|
const invalidTokenRegex = /[^^_`a-zA-Z\-0-9!#$%&'*+.|~]/
|
||
|
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/
|
||
|
|
||
|
const validateName = name => {
|
||
|
name = `${name}`
|
||
|
if (invalidTokenRegex.test(name) || name === '') {
|
||
|
throw new TypeError(`${name} is not a legal HTTP header name`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const validateValue = value => {
|
||
|
value = `${value}`
|
||
|
if (invalidHeaderCharRegex.test(value)) {
|
||
|
throw new TypeError(`${value} is not a legal HTTP header value`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const find = (map, name) => {
|
||
|
name = name.toLowerCase()
|
||
|
for (const key in map) {
|
||
|
if (key.toLowerCase() === name) {
|
||
|
return key
|
||
|
}
|
||
|
}
|
||
|
return undefined
|
||
|
}
|
||
|
|
||
|
const MAP = Symbol('map')
|
||
|
class Headers {
|
||
|
constructor (init = undefined) {
|
||
|
this[MAP] = Object.create(null)
|
||
|
if (init instanceof Headers) {
|
||
|
const rawHeaders = init.raw()
|
||
|
const headerNames = Object.keys(rawHeaders)
|
||
|
for (const headerName of headerNames) {
|
||
|
for (const value of rawHeaders[headerName]) {
|
||
|
this.append(headerName, value)
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// no-op
|
||
|
if (init === undefined || init === null) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (typeof init === 'object') {
|
||
|
const method = init[Symbol.iterator]
|
||
|
if (method !== null && method !== undefined) {
|
||
|
if (typeof method !== 'function') {
|
||
|
throw new TypeError('Header pairs must be iterable')
|
||
|
}
|
||
|
|
||
|
// sequence<sequence<ByteString>>
|
||
|
// Note: per spec we have to first exhaust the lists then process them
|
||
|
const pairs = []
|
||
|
for (const pair of init) {
|
||
|
if (typeof pair !== 'object' ||
|
||
|
typeof pair[Symbol.iterator] !== 'function') {
|
||
|
throw new TypeError('Each header pair must be iterable')
|
||
|
}
|
||
|
const arrPair = Array.from(pair)
|
||
|
if (arrPair.length !== 2) {
|
||
|
throw new TypeError('Each header pair must be a name/value tuple')
|
||
|
}
|
||
|
pairs.push(arrPair)
|
||
|
}
|
||
|
|
||
|
for (const pair of pairs) {
|
||
|
this.append(pair[0], pair[1])
|
||
|
}
|
||
|
} else {
|
||
|
// record<ByteString, ByteString>
|
||
|
for (const key of Object.keys(init)) {
|
||
|
this.append(key, init[key])
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
throw new TypeError('Provided initializer must be an object')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get (name) {
|
||
|
name = `${name}`
|
||
|
validateName(name)
|
||
|
const key = find(this[MAP], name)
|
||
|
if (key === undefined) {
|
||
|
return null
|
||
|
}
|
||
|
|
||
|
return this[MAP][key].join(', ')
|
||
|
}
|
||
|
|
||
|
forEach (callback, thisArg = undefined) {
|
||
|
let pairs = getHeaders(this)
|
||
|
for (let i = 0; i < pairs.length; i++) {
|
||
|
const [name, value] = pairs[i]
|
||
|
callback.call(thisArg, value, name, this)
|
||
|
// refresh in case the callback added more headers
|
||
|
pairs = getHeaders(this)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
set (name, value) {
|
||
|
name = `${name}`
|
||
|
value = `${value}`
|
||
|
validateName(name)
|
||
|
validateValue(value)
|
||
|
const key = find(this[MAP], name)
|
||
|
this[MAP][key !== undefined ? key : name] = [value]
|
||
|
}
|
||
|
|
||
|
append (name, value) {
|
||
|
name = `${name}`
|
||
|
value = `${value}`
|
||
|
validateName(name)
|
||
|
validateValue(value)
|
||
|
const key = find(this[MAP], name)
|
||
|
if (key !== undefined) {
|
||
|
this[MAP][key].push(value)
|
||
|
} else {
|
||
|
this[MAP][name] = [value]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
has (name) {
|
||
|
name = `${name}`
|
||
|
validateName(name)
|
||
|
return find(this[MAP], name) !== undefined
|
||
|
}
|
||
|
|
||
|
delete (name) {
|
||
|
name = `${name}`
|
||
|
validateName(name)
|
||
|
const key = find(this[MAP], name)
|
||
|
if (key !== undefined) {
|
||
|
delete this[MAP][key]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
raw () {
|
||
|
return this[MAP]
|
||
|
}
|
||
|
|
||
|
keys () {
|
||
|
return new HeadersIterator(this, 'key')
|
||
|
}
|
||
|
|
||
|
values () {
|
||
|
return new HeadersIterator(this, 'value')
|
||
|
}
|
||
|
|
||
|
[Symbol.iterator] () {
|
||
|
return new HeadersIterator(this, 'key+value')
|
||
|
}
|
||
|
|
||
|
entries () {
|
||
|
return new HeadersIterator(this, 'key+value')
|
||
|
}
|
||
|
|
||
|
get [Symbol.toStringTag] () {
|
||
|
return 'Headers'
|
||
|
}
|
||
|
|
||
|
static exportNodeCompatibleHeaders (headers) {
|
||
|
const obj = Object.assign(Object.create(null), headers[MAP])
|
||
|
|
||
|
// http.request() only supports string as Host header. This hack makes
|
||
|
// specifying custom Host header possible.
|
||
|
const hostHeaderKey = find(headers[MAP], 'Host')
|
||
|
if (hostHeaderKey !== undefined) {
|
||
|
obj[hostHeaderKey] = obj[hostHeaderKey][0]
|
||
|
}
|
||
|
|
||
|
return obj
|
||
|
}
|
||
|
|
||
|
static createHeadersLenient (obj) {
|
||
|
const headers = new Headers()
|
||
|
for (const name of Object.keys(obj)) {
|
||
|
if (invalidTokenRegex.test(name)) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if (Array.isArray(obj[name])) {
|
||
|
for (const val of obj[name]) {
|
||
|
if (invalidHeaderCharRegex.test(val)) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if (headers[MAP][name] === undefined) {
|
||
|
headers[MAP][name] = [val]
|
||
|
} else {
|
||
|
headers[MAP][name].push(val)
|
||
|
}
|
||
|
}
|
||
|
} else if (!invalidHeaderCharRegex.test(obj[name])) {
|
||
|
headers[MAP][name] = [obj[name]]
|
||
|
}
|
||
|
}
|
||
|
return headers
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Object.defineProperties(Headers.prototype, {
|
||
|
get: { enumerable: true },
|
||
|
forEach: { enumerable: true },
|
||
|
set: { enumerable: true },
|
||
|
append: { enumerable: true },
|
||
|
has: { enumerable: true },
|
||
|
delete: { enumerable: true },
|
||
|
keys: { enumerable: true },
|
||
|
values: { enumerable: true },
|
||
|
entries: { enumerable: true },
|
||
|
})
|
||
|
|
||
|
const getHeaders = (headers, kind = 'key+value') =>
|
||
|
Object.keys(headers[MAP]).sort().map(
|
||
|
kind === 'key' ? k => k.toLowerCase()
|
||
|
: kind === 'value' ? k => headers[MAP][k].join(', ')
|
||
|
: k => [k.toLowerCase(), headers[MAP][k].join(', ')]
|
||
|
)
|
||
|
|
||
|
const INTERNAL = Symbol('internal')
|
||
|
|
||
|
class HeadersIterator {
|
||
|
constructor (target, kind) {
|
||
|
this[INTERNAL] = {
|
||
|
target,
|
||
|
kind,
|
||
|
index: 0,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get [Symbol.toStringTag] () {
|
||
|
return 'HeadersIterator'
|
||
|
}
|
||
|
|
||
|
next () {
|
||
|
/* istanbul ignore if: should be impossible */
|
||
|
if (!this || Object.getPrototypeOf(this) !== HeadersIterator.prototype) {
|
||
|
throw new TypeError('Value of `this` is not a HeadersIterator')
|
||
|
}
|
||
|
|
||
|
const { target, kind, index } = this[INTERNAL]
|
||
|
const values = getHeaders(target, kind)
|
||
|
const len = values.length
|
||
|
if (index >= len) {
|
||
|
return {
|
||
|
value: undefined,
|
||
|
done: true,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this[INTERNAL].index++
|
||
|
|
||
|
return { value: values[index], done: false }
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// manually extend because 'extends' requires a ctor
|
||
|
Object.setPrototypeOf(HeadersIterator.prototype,
|
||
|
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())))
|
||
|
|
||
|
module.exports = Headers
|