swc/crates/swc_bundler/tests/.cache/deno/056fcea72bc25d7fa9eafac174ce56b80cf99571.ts
2021-11-09 20:42:49 +09:00

354 lines
9.2 KiB
TypeScript

// Loaded from https://deno.land/x/oak@v6.3.1/cookies.ts
// Copyright 2018-2020 the oak authors. All rights reserved. MIT license.
// This was heavily influenced by
// [cookies](https://github.com/pillarjs/cookies/blob/master/index.js)
import type { KeyStack } from "./keyStack.ts";
import type { Request } from "./request.ts";
import type { Response } from "./response.ts";
export interface CookiesOptions {
keys?: KeyStack;
secure?: boolean;
}
export interface CookiesGetOptions {
signed?: boolean;
}
export interface CookiesSetDeleteOptions {
domain?: string;
expires?: Date;
httpOnly?: boolean;
maxAge?: number;
overwrite?: boolean;
path?: string;
secure?: boolean;
sameSite?: "strict" | "lax" | "none" | boolean;
signed?: boolean;
}
type CookieAttributes = CookiesSetDeleteOptions;
const matchCache: Record<string, RegExp> = {};
// deno-lint-ignore no-control-regex
const FIELD_CONTENT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
const KEY_REGEXP = /(?:^|;) *([^=]*)=[^;]*/g;
const SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i;
function getPattern(name: string): RegExp {
if (name in matchCache) {
return matchCache[name];
}
return matchCache[name] = new RegExp(
`(?:^|;) *${name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")}=([^;]*)`,
);
}
function pushCookie(headers: string[], cookie: Cookie): void {
if (cookie.overwrite) {
for (let i = headers.length - 1; i >= 0; i--) {
if (headers[i].indexOf(`${cookie.name}=`) === 0) {
headers.splice(i, 1);
}
}
}
headers.push(cookie.toHeader());
}
function validateCookieProperty(
key: string,
value: string | undefined | null,
): void {
if (value && !FIELD_CONTENT_REGEXP.test(value)) {
throw new TypeError(`The ${key} of the cookie (${value}) is invalid.`);
}
}
class Cookie implements CookieAttributes {
domain?: string;
expires?: Date;
httpOnly = true;
maxAge?: number;
name: string;
overwrite = false;
path = "/";
sameSite: "strict" | "lax" | "none" | boolean = false;
secure = false;
signed?: boolean;
value: string;
/** A logical representation of a cookie, used to internally manage the
* cookie instances. */
constructor(
name: string,
value: string | null,
attributes: CookieAttributes,
) {
validateCookieProperty("name", name);
validateCookieProperty("value", value);
this.name = name;
this.value = value ?? "";
Object.assign(this, attributes);
if (!this.value) {
this.expires = new Date(0);
this.maxAge = undefined;
}
validateCookieProperty("path", this.path);
validateCookieProperty("domain", this.domain);
if (
this.sameSite && typeof this.sameSite === "string" &&
!SAME_SITE_REGEXP.test(this.sameSite)
) {
throw new TypeError(
`The sameSite of the cookie ("${this.sameSite}") is invalid.`,
);
}
}
toHeader(): string {
let header = this.toString();
if (this.maxAge) {
this.expires = new Date(Date.now() + (this.maxAge * 1000));
}
if (this.path) {
header += `; path=${this.path}`;
}
if (this.expires) {
header += `; expires=${this.expires.toUTCString()}`;
}
if (this.domain) {
header += `; domain=${this.domain}`;
}
if (this.sameSite) {
header += `; samesite=${
this.sameSite === true ? "strict" : this.sameSite.toLowerCase()
}`;
}
if (this.secure) {
header += "; secure";
}
if (this.httpOnly) {
header += "; httponly";
}
return header;
}
toString(): string {
return `${this.name}=${this.value}`;
}
}
/** An interface which allows setting and accessing cookies related to both the
* current request and response. */
export class Cookies {
#cookieKeys?: string[];
#keys?: KeyStack;
#request: Request;
#response: Response;
#secure?: boolean;
#requestKeys = (): string[] => {
if (this.#cookieKeys) {
return this.#cookieKeys;
}
const result = this.#cookieKeys = [] as string[];
const header = this.#request.headers.get("cookie");
if (!header) {
return result;
}
let matches: RegExpExecArray | null;
while ((matches = KEY_REGEXP.exec(header))) {
const [, key] = matches;
result.push(key);
}
return result;
};
constructor(
request: Request,
response: Response,
options: CookiesOptions = {},
) {
const { keys, secure } = options;
this.#keys = keys;
this.#request = request;
this.#response = response;
this.#secure = secure;
}
/** Set a cookie to be deleted in the response. This is a "shortcut" to
* `.set(name, null, options?)`. */
delete(name: string, options: CookiesSetDeleteOptions = {}): boolean {
this.set(name, null, options);
return true;
}
/** Iterate over the request's cookies, yielding up a tuple containing the
* key and the value.
*
* If there are keys set on the application, only keys and values that are
* properly signed will be returned. */
*entries(): IterableIterator<[string, string]> {
const keys = this.#requestKeys();
for (const key of keys) {
const value = this.get(key);
if (value) {
yield [key, value];
}
}
}
forEach(
callback: (key: string, value: string, cookies: this) => void,
// deno-lint-ignore no-explicit-any
thisArg: any = null,
): void {
const keys = this.#requestKeys();
for (const key of keys) {
const value = this.get(key);
if (value) {
callback.call(thisArg, key, value, this);
}
}
}
/** Get the value of a cookie from the request.
*
* If the cookie is signed, and the signature is invalid, the cookie will
* be set to be deleted in the the response. If the signature uses an "old"
* key, the cookie will be re-signed with the current key and be added to the
* response to be updated. */
get(name: string, options: CookiesGetOptions = {}): string | undefined {
const signed = options.signed ?? !!this.#keys;
const nameSig = `${name}.sig`;
const header = this.#request.headers.get("cookie");
if (!header) {
return;
}
const match = header.match(getPattern(name));
if (!match) {
return;
}
const [, value] = match;
if (!signed) {
return value;
}
const digest = this.get(nameSig, { signed: false });
if (!digest) {
return;
}
const data = `${name}=${value}`;
if (!this.#keys) {
throw new TypeError("keys required for signed cookies");
}
const index = this.#keys.indexOf(data, digest);
if (index < 0) {
this.delete(nameSig, { path: "/", signed: false });
} else {
if (index) {
// the key has "aged" and needs to be re-signed
this.set(nameSig, this.#keys.sign(data), { signed: false });
}
return value;
}
}
/** Iterate over the request's cookies, yielding up the keys.
*
* If there are keys set on the application, only the keys that are properly
* signed will be returned. */
*keys(): IterableIterator<string> {
const keys = this.#requestKeys();
for (const key of keys) {
const value = this.get(key);
if (value) {
yield key;
}
}
}
/** Set a cookie in the response.
*
* If there are keys set in the application, cookies will be automatically
* signed, unless overridden by the set options. Cookies can be deleted by
* setting the value to `null`. */
set(
name: string,
value: string | null,
options: CookiesSetDeleteOptions = {},
): this {
const request = this.#request;
const response = this.#response;
let headers = response.headers.get("Set-Cookie") ?? [] as string[];
if (typeof headers === "string") {
headers = [headers];
}
const secure = this.#secure !== undefined ? this.#secure : request.secure;
const signed = options.signed ?? !!this.#keys;
if (!secure && options.secure) {
throw new TypeError(
"Cannot send secure cookie over unencrypted connection.",
);
}
const cookie = new Cookie(name, value, options);
cookie.secure = options.secure ?? secure;
pushCookie(headers, cookie);
if (signed) {
if (!this.#keys) {
throw new TypeError(".keys required for signed cookies.");
}
cookie.value = this.#keys.sign(cookie.toString());
cookie.name += ".sig";
pushCookie(headers, cookie);
}
for (const header of headers) {
response.headers.append("Set-Cookie", header);
}
return this;
}
/** Iterate over the request's cookies, yielding up each value.
*
* If there are keys set on the application, only the values that are
* properly signed will be returned. */
*values(): IterableIterator<string> {
const keys = this.#requestKeys();
for (const key of keys) {
const value = this.get(key);
if (value) {
yield value;
}
}
}
/** Iterate over the request's cookies, yielding up a tuple containing the
* key and the value.
*
* If there are keys set on the application, only keys and values that are
* properly signed will be returned. */
*[Symbol.iterator](): IterableIterator<[string, string]> {
const keys = this.#requestKeys();
for (const key of keys) {
const value = this.get(key);
if (value) {
yield [key, value];
}
}
}
}