You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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