1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-25 16:49:18 -05:00
denoland-deno/cli/js/web/headers.ts
2020-05-31 16:07:24 -04:00

263 lines
8.1 KiB
TypeScript

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { DomIterableMixin } from "./dom_iterable.ts";
import { requiredArguments } from "./util.ts";
import { customInspect } from "./console.ts";
// From node-fetch
// Copyright (c) 2016 David Frank. MIT License.
const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isHeaders(value: any): value is Headers {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return value instanceof Headers;
}
const headersData = Symbol("headers data");
// TODO: headerGuard? Investigate if it is needed
// node-fetch did not implement this but it is in the spec
function normalizeParams(name: string, value?: string): string[] {
name = String(name).toLowerCase();
value = String(value).trim();
return [name, value];
}
// The following name/value validations are copied from
// https://github.com/bitinn/node-fetch/blob/master/src/headers.js
// Copyright (c) 2016 David Frank. MIT License.
function validateName(name: string): void {
if (invalidTokenRegex.test(name) || name === "") {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
}
function validateValue(value: string): void {
if (invalidHeaderCharRegex.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
}
/** Appends a key and value to the header list.
*
* The spec indicates that when a key already exists, the append adds the new
* value onto the end of the existing value. The behaviour of this though
* varies when the key is `set-cookie`. In this case, if the key of the cookie
* already exists, the value is replaced, but if the key of the cookie does not
* exist, and additional `set-cookie` header is added.
*
* The browser specification of `Headers` is written for clients, and not
* servers, and Deno is a server, meaning that it needs to follow the patterns
* expected for servers, of which a `set-cookie` header is expected for each
* unique cookie key, but duplicate cookie keys should not exist. */
function dataAppend(
data: Array<[string, string]>,
key: string,
value: string
): void {
for (let i = 0; i < data.length; i++) {
const [dataKey] = data[i];
if (key === "set-cookie" && dataKey === "set-cookie") {
const [, dataValue] = data[i];
const [dataCookieKey] = dataValue.split("=");
const [cookieKey] = value.split("=");
if (dataCookieKey === cookieKey) {
data[i][1] = value;
return;
}
} else {
if (dataKey === key) {
data[i][1] += `, ${value}`;
return;
}
}
}
data.push([key, value]);
}
/** Gets a value of a key in the headers list.
*
* This varies slightly from spec behaviour in that when the key is `set-cookie`
* the value returned will look like a concatenated value, when in fact, if the
* headers were iterated over, each individual `set-cookie` value is a unique
* entry in the headers list. */
function dataGet(
data: Array<[string, string]>,
key: string
): string | undefined {
const setCookieValues = [];
for (const [dataKey, value] of data) {
if (dataKey === key) {
if (key === "set-cookie") {
setCookieValues.push(value);
} else {
return value;
}
}
}
if (setCookieValues.length) {
return setCookieValues.join(", ");
}
return undefined;
}
/** Sets a value of a key in the headers list.
*
* The spec indicates that the value should be replaced if the key already
* exists. The behaviour here varies, where if the key is `set-cookie` the key
* of the cookie is inspected, and if the key of the cookie already exists,
* then the value is replaced. If the key of the cookie is not found, then
* the value of the `set-cookie` is added to the list of headers.
*
* The browser specification of `Headers` is written for clients, and not
* servers, and Deno is a server, meaning that it needs to follow the patterns
* expected for servers, of which a `set-cookie` header is expected for each
* unique cookie key, but duplicate cookie keys should not exist. */
function dataSet(
data: Array<[string, string]>,
key: string,
value: string
): void {
for (let i = 0; i < data.length; i++) {
const [dataKey] = data[i];
if (dataKey === key) {
// there could be multiple set-cookie headers, but all others are unique
if (key === "set-cookie") {
const [, dataValue] = data[i];
const [dataCookieKey] = dataValue.split("=");
const [cookieKey] = value.split("=");
if (cookieKey === dataCookieKey) {
data[i][1] = value;
return;
}
} else {
data[i][1] = value;
return;
}
}
}
data.push([key, value]);
}
function dataDelete(data: Array<[string, string]>, key: string): void {
let i = 0;
while (i < data.length) {
const [dataKey] = data[i];
if (dataKey === key) {
data.splice(i, 1);
} else {
i++;
}
}
}
function dataHas(data: Array<[string, string]>, key: string): boolean {
for (const [dataKey] of data) {
if (dataKey === key) {
return true;
}
}
return false;
}
// ref: https://fetch.spec.whatwg.org/#dom-headers
class HeadersBase {
[headersData]: Array<[string, string]>;
constructor(init?: HeadersInit) {
if (init === null) {
throw new TypeError(
"Failed to construct 'Headers'; The provided value was not valid"
);
} else if (isHeaders(init)) {
this[headersData] = [...init];
} else {
this[headersData] = [];
if (Array.isArray(init)) {
for (const tuple of init) {
// If header does not contain exactly two items,
// then throw a TypeError.
// ref: https://fetch.spec.whatwg.org/#concept-headers-fill
requiredArguments(
"Headers.constructor tuple array argument",
tuple.length,
2
);
this.append(tuple[0], tuple[1]);
}
} else if (init) {
for (const [rawName, rawValue] of Object.entries(init)) {
this.append(rawName, rawValue);
}
}
}
}
[customInspect](): string {
let length = this[headersData].length;
let output = "";
for (const [key, value] of this[headersData]) {
const prefix = length === this[headersData].length ? " " : "";
const postfix = length === 1 ? " " : ", ";
output = output + `${prefix}${key}: ${value}${postfix}`;
length--;
}
return `Headers {${output}}`;
}
// ref: https://fetch.spec.whatwg.org/#concept-headers-append
append(name: string, value: string): void {
requiredArguments("Headers.append", arguments.length, 2);
const [newname, newvalue] = normalizeParams(name, value);
validateName(newname);
validateValue(newvalue);
dataAppend(this[headersData], newname, newvalue);
}
delete(name: string): void {
requiredArguments("Headers.delete", arguments.length, 1);
const [newname] = normalizeParams(name);
validateName(newname);
dataDelete(this[headersData], newname);
}
get(name: string): string | null {
requiredArguments("Headers.get", arguments.length, 1);
const [newname] = normalizeParams(name);
validateName(newname);
return dataGet(this[headersData], newname) ?? null;
}
has(name: string): boolean {
requiredArguments("Headers.has", arguments.length, 1);
const [newname] = normalizeParams(name);
validateName(newname);
return dataHas(this[headersData], newname);
}
set(name: string, value: string): void {
requiredArguments("Headers.set", arguments.length, 2);
const [newname, newvalue] = normalizeParams(name, value);
validateName(newname);
validateValue(newvalue);
dataSet(this[headersData], newname, newvalue);
}
get [Symbol.toStringTag](): string {
return "Headers";
}
}
// @internal
export class HeadersImpl extends DomIterableMixin<
string,
string,
typeof HeadersBase
>(HeadersBase, headersData) {}
Object.defineProperty(HeadersImpl, "name", {
value: "Headers",
configurable: true,
});