// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // @ts-check /// /// /// /// /// /// /// /// "use strict"; ((window) => { const webidl = window.__bootstrap.webidl; const { HTTP_TAB_OR_SPACE_PREFIX_RE, HTTP_TAB_OR_SPACE_SUFFIX_RE, HTTP_WHITESPACE_PREFIX_RE, HTTP_WHITESPACE_SUFFIX_RE, HTTP_TOKEN_CODE_POINT_RE, byteLowerCase, collectSequenceOfCodepoints, collectHttpQuotedString, } = window.__bootstrap.infra; const _headerList = Symbol("header list"); const _iterableHeaders = Symbol("iterable headers"); const _guard = Symbol("guard"); /** * @typedef Header * @type {[string, string]} */ /** * @typedef HeaderList * @type {Header[]} */ /** * @param {string} potentialValue * @returns {string} */ function normalizeHeaderValue(potentialValue) { potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_PREFIX_RE, ""); potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, ""); return potentialValue; } /** * @param {Headers} headers * @param {HeadersInit} object */ function fillHeaders(headers, object) { if (Array.isArray(object)) { for (const header of object) { if (header.length !== 2) { throw new TypeError( `Invalid header. Length must be 2, but is ${header.length}`, ); } appendHeader(headers, header[0], header[1]); } } else { for (const key of Object.keys(object)) { appendHeader(headers, key, object[key]); } } } /** * https://fetch.spec.whatwg.org/#concept-headers-append * @param {Headers} headers * @param {string} name * @param {string} value */ function appendHeader(headers, name, value) { // 1. value = normalizeHeaderValue(value); // 2. if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } if ( value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D") ) { throw new TypeError("Header value is not valid."); } // 3. if (headers[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } // 7. const list = headers[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { name = list[i][0]; break; } } list.push([name, value]); } /** * https://fetch.spec.whatwg.org/#concept-header-list-get * @param {HeaderList} list * @param {string} name */ function getHeader(list, name) { const lowercaseName = byteLowerCase(name); const entries = list.filter((entry) => byteLowerCase(entry[0]) === lowercaseName ).map((entry) => entry[1]); if (entries.length === 0) { return null; } else { return entries.join("\x2C\x20"); } } /** * https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split * @param {HeaderList} list * @param {string} name * @returns {string[] | null} */ function getDecodeSplitHeader(list, name) { const initialValue = getHeader(list, name); if (initialValue === null) return null; const input = initialValue; let position = 0; const values = []; let value = ""; while (position < initialValue.length) { // 7.1. collect up to " or , const res = collectSequenceOfCodepoints( initialValue, position, (c) => c !== "\u0022" && c !== "\u002C", ); value += res.result; position = res.position; if (position < initialValue.length) { if (input[position] === "\u0022") { const res = collectHttpQuotedString(input, position, false); value += res.result; position = res.position; if (position < initialValue.length) { continue; } } else { if (input[position] !== "\u002C") throw new TypeError("Unreachable"); position += 1; } } value = value.replaceAll(HTTP_TAB_OR_SPACE_PREFIX_RE, ""); value = value.replaceAll(HTTP_TAB_OR_SPACE_SUFFIX_RE, ""); values.push(value); value = ""; } return values; } class Headers { /** @type {HeaderList} */ [_headerList] = []; /** @type {"immutable" | "request" | "request-no-cors" | "response" | "none"} */ [_guard]; get [_iterableHeaders]() { const list = this[_headerList]; // The order of steps are not similar to the ones suggested by the // spec but produce the same result. const headers = {}; const cookies = []; for (const entry of list) { const name = byteLowerCase(entry[0]); const value = entry[1]; if (value === null) throw new TypeError("Unreachable"); // The following if statement is not spec compliant. // `set-cookie` is the only header that can not be concatentated, // so must be given to the user as multiple headers. // The else block of the if statement is spec compliant again. if (name === "set-cookie") { cookies.push([name, value]); } else { // The following code has the same behaviour as getHeader() // at the end of loop. But it avoids looping through the entire // list to combine multiple values with same header name. It // instead gradually combines them as they are found. let header = headers[name]; if (header && header.length > 0) { header += "\x2C\x20" + value; } else { header = value; } headers[name] = header; } } return [...Object.entries(headers), ...cookies].sort((a, b) => { const akey = a[0]; const bkey = b[0]; if (akey > bkey) return 1; if (akey < bkey) return -1; return 0; }); } /** @param {HeadersInit} [init] */ constructor(init = undefined) { const prefix = "Failed to construct 'Event'"; if (init !== undefined) { init = webidl.converters["HeadersInit"](init, { prefix, context: "Argument 1", }); } this[webidl.brand] = webidl.brand; this[_guard] = "none"; if (init !== undefined) { fillHeaders(this, init); } } /** * @param {string} name * @param {string} value */ append(name, value) { webidl.assertBranded(this, Headers); const prefix = "Failed to execute 'append' on 'Headers'"; webidl.requiredArguments(arguments.length, 2, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); value = webidl.converters["ByteString"](value, { prefix, context: "Argument 2", }); appendHeader(this, name, value); } /** * @param {string} name */ delete(name) { const prefix = "Failed to execute 'delete' on 'Headers'"; webidl.requiredArguments(arguments.length, 1, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } if (this[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } const list = this[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { list.splice(i, 1); i--; } } } /** * @param {string} name */ get(name) { const prefix = "Failed to execute 'get' on 'Headers'"; webidl.requiredArguments(arguments.length, 1, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } const list = this[_headerList]; return getHeader(list, name); } /** * @param {string} name */ has(name) { const prefix = "Failed to execute 'has' on 'Headers'"; webidl.requiredArguments(arguments.length, 1, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } const list = this[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { return true; } } return false; } /** * @param {string} name * @param {string} value */ set(name, value) { webidl.assertBranded(this, Headers); const prefix = "Failed to execute 'set' on 'Headers'"; webidl.requiredArguments(arguments.length, 2, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); value = webidl.converters["ByteString"](value, { prefix, context: "Argument 2", }); value = normalizeHeaderValue(value); // 2. if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } if ( value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D") ) { throw new TypeError("Header value is not valid."); } if (this[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } const list = this[_headerList]; const lowercaseName = byteLowerCase(name); let added = false; for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { if (!added) { list[i][1] = value; added = true; } else { list.splice(i, 1); i--; } } } if (!added) { list.push([name, value]); } } [Symbol.for("Deno.customInspect")](inspect) { const headers = {}; for (const header of this) { headers[header[0]] = header[1]; } return `Headers ${inspect(headers)}`; } get [Symbol.toStringTag]() { return "Headers"; } } webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1); webidl.converters["sequence"] = webidl .createSequenceConverter(webidl.converters["ByteString"]); webidl.converters["sequence>"] = webidl .createSequenceConverter(webidl.converters["sequence"]); webidl.converters["record"] = webidl .createRecordConverter( webidl.converters["ByteString"], webidl.converters["ByteString"], ); webidl.converters["HeadersInit"] = (V, opts) => { // Union for (sequence> or record) if (typeof V === "object" && V !== null) { if (V[Symbol.iterator] !== undefined) { return webidl.converters["sequence>"](V, opts); } return webidl.converters["record"](V, opts); } throw webidl.makeException( TypeError, "The provided value is not of type '(sequence> or record)'", opts, ); }; webidl.converters["Headers"] = webidl.createInterfaceConverter( "Headers", Headers, ); /** * @param {HeaderList} list * @param {"immutable" | "request" | "request-no-cors" | "response" | "none"} guard * @returns {Headers} */ function headersFromHeaderList(list, guard) { const headers = webidl.createBranded(Headers); headers[_headerList] = list; headers[_guard] = guard; return headers; } /** * @param {Headers} * @returns {HeaderList} */ function headerListFromHeaders(headers) { return headers[_headerList]; } /** * @param {Headers} * @returns {"immutable" | "request" | "request-no-cors" | "response" | "none"} */ function guardFromHeaders(headers) { return headers[_guard]; } window.__bootstrap.headers = { Headers, headersFromHeaderList, headerListFromHeaders, fillHeaders, getDecodeSplitHeader, guardFromHeaders, }; })(this);