mirror of
https://github.com/denoland/std.git
synced 2024-11-22 04:59:05 +00:00
BREAKING(http): move cookie_map, errors, server_sent_event, and method to unstable category, deprecate server.ts (#3661)
See https://github.com/denoland/deno_std/issues/3655 and https://github.com/denoland/deno_std/issues/3646 for details. Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
This commit is contained in:
parent
413dd7928c
commit
65125db61f
@ -1,7 +1,10 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Provides a iterable map interfaces for managing cookies server side.
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*
|
||||
* Provides a iterable map interfaces for managing cookies server side.
|
||||
*
|
||||
* @example
|
||||
* To access the keys in a request and have any set keys available for creating
|
||||
@ -11,7 +14,7 @@
|
||||
* import {
|
||||
* CookieMap,
|
||||
* mergeHeaders
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/cookie_map.ts";
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/unstable_cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
@ -39,7 +42,7 @@
|
||||
* SecureCookieMap,
|
||||
* mergeHeaders,
|
||||
* type KeyRing,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/cookie_map.ts";
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/unstable_cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
@ -65,7 +68,7 @@
|
||||
* set cookies will be added directly to those headers:
|
||||
*
|
||||
* ```ts
|
||||
* import { CookieMap } from "https://deno.land/std@$STD_VERSION/http/cookie_map.ts";
|
||||
* import { CookieMap } from "https://deno.land/std@$STD_VERSION/http/unstable_cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
@ -83,338 +86,76 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
export interface CookieMapOptions {
|
||||
/** The {@linkcode Response} or the headers that will be used with the
|
||||
* response. When provided, `Set-Cookie` headers will be set in the headers
|
||||
* when cookies are set or deleted in the map.
|
||||
*
|
||||
* An alternative way to extract the headers is to pass the cookie map to the
|
||||
* {@linkcode mergeHeaders} function to merge various sources of the
|
||||
* headers to be provided when creating or updating a response.
|
||||
*/
|
||||
response?: Headered | Headers;
|
||||
/** A flag that indicates if the request and response are being handled over
|
||||
* a secure (e.g. HTTPS/TLS) connection.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
secure?: boolean;
|
||||
}
|
||||
import {
|
||||
CookieMap as CookieMap_,
|
||||
cookieMapHeadersInitSymbol as cookieMapHeadersInitSymbol_,
|
||||
type CookieMapOptions as CookieMapOptions_,
|
||||
type CookieMapSetDeleteOptions as CookieMapSetDeleteOptions_,
|
||||
type Data as Data_,
|
||||
type Headered as Headered_,
|
||||
type KeyRing as KeyRing_,
|
||||
type Mergeable as Mergeable_,
|
||||
mergeHeaders as mergeHeaders_,
|
||||
SecureCookieMap as SecureCookieMap_,
|
||||
type SecureCookieMapGetOptions as SecureCookieMapGetOptions_,
|
||||
type SecureCookieMapOptions as SecureCookieMapOptions_,
|
||||
type SecureCookieMapSetDeleteOptions as SecureCookieMapSetDeleteOptions_,
|
||||
} from "./unstable_cookie_map.ts";
|
||||
|
||||
export interface CookieMapSetDeleteOptions {
|
||||
/** The domain to scope the cookie for. */
|
||||
domain?: string;
|
||||
/** When the cookie expires. */
|
||||
expires?: Date;
|
||||
/** Number of seconds until the cookie expires */
|
||||
maxAge?: number;
|
||||
/** A flag that indicates if the cookie is valid over HTTP only. */
|
||||
httpOnly?: boolean;
|
||||
/** Do not error when signing and validating cookies over an insecure
|
||||
* connection. */
|
||||
ignoreInsecure?: boolean;
|
||||
/** Overwrite an existing value. */
|
||||
overwrite?: boolean;
|
||||
/** The path the cookie is valid for. */
|
||||
path?: string;
|
||||
/** Override the flag that was set when the instance was created. */
|
||||
secure?: boolean;
|
||||
/** Set the same-site indicator for a cookie. */
|
||||
sameSite?: "strict" | "lax" | "none" | boolean;
|
||||
}
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type CookieMapOptions = CookieMapOptions_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type CookieMapSetDeleteOptions = CookieMapSetDeleteOptions_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type Headered = Headered_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type Mergeable = Mergeable_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type SecureCookieMapGetOptions = SecureCookieMapGetOptions_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type SecureCookieMapOptions = SecureCookieMapOptions_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*/
|
||||
export type SecureCookieMapSetDeleteOptions = SecureCookieMapSetDeleteOptions_;
|
||||
|
||||
/** An object which contains a `headers` property which has a value of an
|
||||
* instance of {@linkcode Headers}, like {@linkcode Request} and
|
||||
* {@linkcode Response}. */
|
||||
export interface Headered {
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
export interface Mergeable {
|
||||
[cookieMapHeadersInitSymbol](): [string, string][];
|
||||
}
|
||||
|
||||
export interface SecureCookieMapOptions {
|
||||
/** Keys which will be used to validate and sign cookies. The key ring should
|
||||
* implement the {@linkcode KeyRing} interface. */
|
||||
keys?: KeyRing;
|
||||
|
||||
/** The {@linkcode Response} or the headers that will be used with the
|
||||
* response. When provided, `Set-Cookie` headers will be set in the headers
|
||||
* when cookies are set or deleted in the map.
|
||||
*
|
||||
* An alternative way to extract the headers is to pass the cookie map to the
|
||||
* {@linkcode mergeHeaders} function to merge various sources of the
|
||||
* headers to be provided when creating or updating a response.
|
||||
*/
|
||||
response?: Headered | Headers;
|
||||
|
||||
/** A flag that indicates if the request and response are being handled over
|
||||
* a secure (e.g. HTTPS/TLS) connection.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureCookieMapGetOptions {
|
||||
/** Overrides the flag that was set when the instance was created. */
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureCookieMapSetDeleteOptions {
|
||||
/** The domain to scope the cookie for. */
|
||||
domain?: string;
|
||||
/** When the cookie expires. */
|
||||
expires?: Date;
|
||||
/** Number of seconds until the cookie expires */
|
||||
maxAge?: number;
|
||||
/** A flag that indicates if the cookie is valid over HTTP only. */
|
||||
httpOnly?: boolean;
|
||||
/** Do not error when signing and validating cookies over an insecure
|
||||
* connection. */
|
||||
ignoreInsecure?: boolean;
|
||||
/** Overwrite an existing value. */
|
||||
overwrite?: boolean;
|
||||
/** The path the cookie is valid for. */
|
||||
path?: string;
|
||||
/** Override the flag that was set when the instance was created. */
|
||||
secure?: boolean;
|
||||
/** Set the same-site indicator for a cookie. */
|
||||
sameSite?: "strict" | "lax" | "none" | boolean;
|
||||
/** Override the default behavior of signing the cookie. */
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
type CookieAttributes = SecureCookieMapSetDeleteOptions;
|
||||
|
||||
// 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;
|
||||
|
||||
const matchCache: Record<string, RegExp> = {};
|
||||
function getPattern(name: string): RegExp {
|
||||
if (name in matchCache) {
|
||||
return matchCache[name];
|
||||
}
|
||||
|
||||
return matchCache[name] = new RegExp(
|
||||
`(?:^|;) *${name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")}=([^;]*)`,
|
||||
);
|
||||
}
|
||||
|
||||
function pushCookie(values: string[], cookie: Cookie) {
|
||||
if (cookie.overwrite) {
|
||||
for (let i = values.length - 1; i >= 0; i--) {
|
||||
if (values[i].indexOf(`${cookie.name}=`) === 0) {
|
||||
values.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
values.push(cookie.toHeaderValue());
|
||||
}
|
||||
|
||||
function validateCookieProperty(
|
||||
key: string,
|
||||
value: string | undefined | null,
|
||||
) {
|
||||
if (value && !FIELD_CONTENT_REGEXP.test(value)) {
|
||||
throw new TypeError(`The "${key}" of the cookie (${value}) is invalid.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** An internal abstraction to manage cookies. */
|
||||
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;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
value: string | null,
|
||||
attributes: CookieAttributes,
|
||||
) {
|
||||
validateCookieProperty("name", name);
|
||||
this.name = name;
|
||||
validateCookieProperty("value", value);
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toHeaderValue(): string {
|
||||
let value = this.toString();
|
||||
if (this.maxAge) {
|
||||
this.expires = new Date(Date.now() + (this.maxAge * 1000));
|
||||
}
|
||||
if (this.path) {
|
||||
value += `; path=${this.path}`;
|
||||
}
|
||||
if (this.expires) {
|
||||
value += `; expires=${this.expires.toUTCString()}`;
|
||||
}
|
||||
if (this.domain) {
|
||||
value += `; domain=${this.domain}`;
|
||||
}
|
||||
if (this.sameSite) {
|
||||
value += `; samesite=${
|
||||
this.sameSite === true ? "strict" : this.sameSite.toLowerCase()
|
||||
}`;
|
||||
}
|
||||
if (this.secure) {
|
||||
value += "; secure";
|
||||
}
|
||||
if (this.httpOnly) {
|
||||
value += "; httponly";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.name}=${this.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Symbol which is used in {@link mergeHeaders} to extract a
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*
|
||||
* Symbol which is used in {@link mergeHeaders} to extract a
|
||||
* `[string | string][]` from an instance to generate the final set of
|
||||
* headers. */
|
||||
export const cookieMapHeadersInitSymbol = Symbol.for(
|
||||
"Deno.std.cookieMap.headersInit",
|
||||
);
|
||||
* headers.
|
||||
*/
|
||||
export const cookieMapHeadersInitSymbol = cookieMapHeadersInitSymbol_;
|
||||
|
||||
function isMergeable(value: unknown): value is Mergeable {
|
||||
return value !== null && value !== undefined && typeof value === "object" &&
|
||||
cookieMapHeadersInitSymbol in value;
|
||||
}
|
||||
|
||||
/** Allows merging of various sources of headers into a final set of headers
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*
|
||||
* Allows merging of various sources of headers into a final set of headers
|
||||
* which can be used in a {@linkcode Response}.
|
||||
*
|
||||
* Note, that unlike when passing a `Response` or {@linkcode Headers} used in a
|
||||
* response to {@linkcode CookieMap} or {@linkcode SecureCookieMap}, merging
|
||||
* will not ensure that there are no other `Set-Cookie` headers from other
|
||||
* sources, it will simply append the various headers together. */
|
||||
export function mergeHeaders(
|
||||
...sources: (Headered | HeadersInit | Mergeable)[]
|
||||
): Headers {
|
||||
const headers = new Headers();
|
||||
for (const source of sources) {
|
||||
let entries: Iterable<[string, string]>;
|
||||
if (source instanceof Headers) {
|
||||
entries = source;
|
||||
} else if ("headers" in source && source.headers instanceof Headers) {
|
||||
entries = source.headers;
|
||||
} else if (isMergeable(source)) {
|
||||
entries = source[cookieMapHeadersInitSymbol]();
|
||||
} else if (Array.isArray(source)) {
|
||||
entries = source as [string, string][];
|
||||
} else {
|
||||
entries = Object.entries(source);
|
||||
}
|
||||
for (const [key, value] of entries) {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
const keys = Symbol("#keys");
|
||||
const requestHeaders = Symbol("#requestHeaders");
|
||||
const responseHeaders = Symbol("#responseHeaders");
|
||||
const isSecure = Symbol("#secure");
|
||||
const requestKeys = Symbol("#requestKeys");
|
||||
|
||||
/** An internal abstract class which provides common functionality for
|
||||
* {@link CookieMap} and {@link SecureCookieMap}. */
|
||||
abstract class CookieMapBase implements Mergeable {
|
||||
[keys]?: string[];
|
||||
[requestHeaders]: Headers;
|
||||
[responseHeaders]: Headers;
|
||||
[isSecure]: boolean;
|
||||
|
||||
[requestKeys](): string[] {
|
||||
if (this[keys]) {
|
||||
return this[keys];
|
||||
}
|
||||
const result = this[keys] = [] as string[];
|
||||
const header = this[requestHeaders].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: Headers | Headered, options: CookieMapOptions) {
|
||||
this[requestHeaders] = "headers" in request ? request.headers : request;
|
||||
const { secure = false, response = new Headers() } = options;
|
||||
this[responseHeaders] = "headers" in response ? response.headers : response;
|
||||
this[isSecure] = secure;
|
||||
}
|
||||
|
||||
/** A method used by {@linkcode mergeHeaders} to be able to merge
|
||||
* headers from various sources when forming a {@linkcode Response}. */
|
||||
[cookieMapHeadersInitSymbol](): [string, string][] {
|
||||
const init: [string, string][] = [];
|
||||
for (const [key, value] of this[responseHeaders]) {
|
||||
if (key === "set-cookie") {
|
||||
init.push([key, value]);
|
||||
}
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
return `${this.constructor.name} []`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect([], newOptions)
|
||||
}`;
|
||||
}
|
||||
}
|
||||
* sources, it will simply append the various headers together.
|
||||
*/
|
||||
export const mergeHeaders = mergeHeaders_;
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*
|
||||
* Provides a way to manage cookies in a request and response on the server
|
||||
* as a single iterable collection.
|
||||
*
|
||||
@ -423,146 +164,27 @@ abstract class CookieMapBase implements Mergeable {
|
||||
* provided, as well as optionally the {@linkcode Response} or `Headers` for the
|
||||
* response can be provided. Alternatively the {@linkcode mergeHeaders}
|
||||
* function can be used to generate a final set of headers for sending in the
|
||||
* response. */
|
||||
export class CookieMap extends CookieMapBase {
|
||||
/** Contains the number of valid cookies in the request headers. */
|
||||
get size(): number {
|
||||
return [...this].length;
|
||||
}
|
||||
* response.
|
||||
*/
|
||||
export const CookieMap = CookieMap_;
|
||||
|
||||
constructor(request: Headers | Headered, options: CookieMapOptions = {}) {
|
||||
super(request, options);
|
||||
}
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
* Types of data that can be signed cryptographically.
|
||||
*/
|
||||
export type Data = Data_;
|
||||
|
||||
/** Deletes all the cookies from the {@linkcode Request} in the response. */
|
||||
clear(options: CookieMapSetDeleteOptions = {}) {
|
||||
for (const key of this.keys()) {
|
||||
this.set(key, null, options);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*
|
||||
* An interface which describes the methods that {@linkcode SecureCookieMap} uses to sign and verify cookies.
|
||||
*/
|
||||
export type KeyRing = KeyRing_;
|
||||
|
||||
/** Set a cookie to be deleted in the response.
|
||||
*
|
||||
* This is a convenience function for `set(key, null, options?)`.
|
||||
*/
|
||||
delete(key: string, options: CookieMapSetDeleteOptions = {}): boolean {
|
||||
this.set(key, null, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return the value of a matching key present in the {@linkcode Request}. If
|
||||
* the key is not present `undefined` is returned. */
|
||||
get(key: string): string | undefined {
|
||||
const headerValue = this[requestHeaders].get("cookie");
|
||||
if (!headerValue) {
|
||||
return undefined;
|
||||
}
|
||||
const match = headerValue.match(getPattern(key));
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, value] = match;
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Returns `true` if the matching key is present in the {@linkcode Request},
|
||||
* otherwise `false`. */
|
||||
has(key: string): boolean {
|
||||
const headerValue = this[requestHeaders].get("cookie");
|
||||
if (!headerValue) {
|
||||
return false;
|
||||
}
|
||||
return getPattern(key).test(headerValue);
|
||||
}
|
||||
|
||||
/** Set a named cookie in the response. The optional
|
||||
* {@linkcode CookieMapSetDeleteOptions} are applied to the cookie being set.
|
||||
*/
|
||||
set(
|
||||
key: string,
|
||||
value: string | null,
|
||||
options: CookieMapSetDeleteOptions = {},
|
||||
): this {
|
||||
const resHeaders = this[responseHeaders];
|
||||
const values: string[] = [];
|
||||
for (const [key, value] of resHeaders) {
|
||||
if (key === "set-cookie") {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
const secure = this[isSecure];
|
||||
|
||||
if (!secure && options.secure && !options.ignoreInsecure) {
|
||||
throw new TypeError(
|
||||
"Cannot send secure cookie over unencrypted connection.",
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = new Cookie(key, value, options);
|
||||
cookie.secure = options.secure ?? secure;
|
||||
pushCookie(values, cookie);
|
||||
|
||||
resHeaders.delete("set-cookie");
|
||||
for (const value of values) {
|
||||
resHeaders.append("set-cookie", value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys and values that are present in the
|
||||
* {@linkcode Request}. This is an alias of the `[Symbol.iterator]` method
|
||||
* present on the class. */
|
||||
entries(): IterableIterator<[string, string]> {
|
||||
return this[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*keys(): IterableIterator<string> {
|
||||
for (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the cookie values that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*values(): IterableIterator<string> {
|
||||
for (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys and values that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*[Symbol.iterator](): IterableIterator<[string, string]> {
|
||||
const keys = this[requestKeys]();
|
||||
for (const key of keys) {
|
||||
const value = this.get(key);
|
||||
if (value) {
|
||||
yield [key, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Types of data that can be signed cryptographically. */
|
||||
export type Data = string | number[] | ArrayBuffer | Uint8Array;
|
||||
|
||||
/** An interface which describes the methods that {@linkcode SecureCookieMap}
|
||||
* uses to sign and verify cookies. */
|
||||
export interface KeyRing {
|
||||
/** Given a set of data and a digest, return the key index of the key used
|
||||
* to sign the data. The index is 0 based. A non-negative number indices the
|
||||
* digest is valid and a key was found. */
|
||||
indexOf(data: Data, digest: string): Promise<number> | number;
|
||||
/** Sign the data, returning a string based digest of the data. */
|
||||
sign(data: Data): Promise<string> | string;
|
||||
/** Verifies the digest matches the provided data, indicating the data was
|
||||
* signed by the keyring and has not been tampered with. */
|
||||
verify(data: Data, digest: string): Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
/** Provides an way to manage cookies in a request and response on the server
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_cookie_map.ts` instead.
|
||||
*
|
||||
* Provides an way to manage cookies in a request and response on the server
|
||||
* as a single iterable collection, as well as the ability to sign and verify
|
||||
* cookies to prevent tampering.
|
||||
*
|
||||
@ -580,233 +202,4 @@ export interface KeyRing {
|
||||
*
|
||||
* @example
|
||||
*/
|
||||
export class SecureCookieMap extends CookieMapBase {
|
||||
#keyRing?: KeyRing;
|
||||
|
||||
/** Is set to a promise which resolves with the number of cookies in the
|
||||
* {@linkcode Request}. */
|
||||
get size(): Promise<number> {
|
||||
return (async () => {
|
||||
let size = 0;
|
||||
for await (const _ of this) {
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
})();
|
||||
}
|
||||
|
||||
constructor(
|
||||
request: Headers | Headered,
|
||||
options: SecureCookieMapOptions = {},
|
||||
) {
|
||||
super(request, options);
|
||||
const { keys } = options;
|
||||
this.#keyRing = keys;
|
||||
}
|
||||
|
||||
/** Sets all cookies in the {@linkcode Request} to be deleted in the
|
||||
* response. */
|
||||
async clear(options: SecureCookieMapSetDeleteOptions) {
|
||||
const promises = [];
|
||||
for await (const key of this.keys()) {
|
||||
promises.push(this.set(key, null, options));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/** Set a cookie to be deleted in the response.
|
||||
*
|
||||
* This is a convenience function for `set(key, null, options?)`. */
|
||||
async delete(
|
||||
key: string,
|
||||
options: SecureCookieMapSetDeleteOptions = {},
|
||||
): Promise<boolean> {
|
||||
await this.set(key, null, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Get the value of a cookie from the {@linkcode Request}.
|
||||
*
|
||||
* If the cookie is signed, and the signature is invalid, `undefined` will be
|
||||
* returned and the cookie will be set to be deleted in the response. If the
|
||||
* cookie is using an "old" key from the keyring, the cookie will be re-signed
|
||||
* with the current key and be added to the response to be updated. */
|
||||
async get(
|
||||
key: string,
|
||||
options: SecureCookieMapGetOptions = {},
|
||||
): Promise<string | undefined> {
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
const nameSig = `${key}.sig`;
|
||||
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
const match = header.match(getPattern(key));
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const [, value] = match;
|
||||
if (!signed) {
|
||||
return value;
|
||||
}
|
||||
const digest = await this.get(nameSig, { signed: false });
|
||||
if (!digest) {
|
||||
return;
|
||||
}
|
||||
const data = `${key}=${value}`;
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("key ring required for signed cookies");
|
||||
}
|
||||
const index = await this.#keyRing.indexOf(data, digest);
|
||||
|
||||
if (index < 0) {
|
||||
await this.delete(nameSig, { path: "/", signed: false });
|
||||
} else {
|
||||
if (index) {
|
||||
await this.set(nameSig, await this.#keyRing.sign(data), {
|
||||
signed: false,
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if the key is in the {@linkcode Request}.
|
||||
*
|
||||
* If the cookie is signed, and the signature is invalid, `false` will be
|
||||
* returned and the cookie will be set to be deleted in the response. If the
|
||||
* cookie is using an "old" key from the keyring, the cookie will be re-signed
|
||||
* with the current key and be added to the response to be updated. */
|
||||
async has(
|
||||
key: string,
|
||||
options: SecureCookieMapGetOptions = {},
|
||||
): Promise<boolean> {
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
const nameSig = `${key}.sig`;
|
||||
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return false;
|
||||
}
|
||||
const match = header.match(getPattern(key));
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
if (!signed) {
|
||||
return true;
|
||||
}
|
||||
const digest = await this.get(nameSig, { signed: false });
|
||||
if (!digest) {
|
||||
return false;
|
||||
}
|
||||
const [, value] = match;
|
||||
const data = `${key}=${value}`;
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("key ring required for signed cookies");
|
||||
}
|
||||
const index = await this.#keyRing.indexOf(data, digest);
|
||||
|
||||
if (index < 0) {
|
||||
await this.delete(nameSig, { path: "/", signed: false });
|
||||
return false;
|
||||
} else {
|
||||
if (index) {
|
||||
await this.set(nameSig, await this.#keyRing.sign(data), {
|
||||
signed: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a cookie in the response headers.
|
||||
*
|
||||
* If there was a keyring set, cookies will be automatically signed, unless
|
||||
* overridden by the passed options. Cookies can be deleted by setting the
|
||||
* value to `null`. */
|
||||
async set(
|
||||
key: string,
|
||||
value: string | null,
|
||||
options: SecureCookieMapSetDeleteOptions = {},
|
||||
): Promise<this> {
|
||||
const resHeaders = this[responseHeaders];
|
||||
const headers: string[] = [];
|
||||
for (const [key, value] of resHeaders.entries()) {
|
||||
if (key === "set-cookie") {
|
||||
headers.push(value);
|
||||
}
|
||||
}
|
||||
const secure = this[isSecure];
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
|
||||
if (!secure && options.secure && !options.ignoreInsecure) {
|
||||
throw new TypeError(
|
||||
"Cannot send secure cookie over unencrypted connection.",
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = new Cookie(key, value, options);
|
||||
cookie.secure = options.secure ?? secure;
|
||||
pushCookie(headers, cookie);
|
||||
|
||||
if (signed) {
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("keys required for signed cookies.");
|
||||
}
|
||||
cookie.value = await this.#keyRing.sign(cookie.toString());
|
||||
cookie.name += ".sig";
|
||||
pushCookie(headers, cookie);
|
||||
}
|
||||
|
||||
resHeaders.delete("set-cookie");
|
||||
for (const header of headers) {
|
||||
resHeaders.append("set-cookie", header);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Iterate over the {@linkcode Request} cookies, yielding up a tuple
|
||||
* containing the key and value of each cookie.
|
||||
*
|
||||
* If a key ring was provided, only properly signed cookie keys and values are
|
||||
* returned. */
|
||||
entries(): AsyncIterableIterator<[string, string]> {
|
||||
return this[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
/** Iterate over the request's cookies, yielding up the key of each cookie.
|
||||
*
|
||||
* If a keyring was provided, only properly signed cookie keys are
|
||||
* returned. */
|
||||
async *keys(): AsyncIterableIterator<string> {
|
||||
for await (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the request's cookies, yielding up the value of each cookie.
|
||||
*
|
||||
* If a keyring was provided, only properly signed cookie values are
|
||||
* returned. */
|
||||
async *values(): AsyncIterableIterator<string> {
|
||||
for await (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the {@linkcode Request} cookies, yielding up a tuple
|
||||
* containing the key and value of each cookie.
|
||||
*
|
||||
* If a key ring was provided, only properly signed cookie keys and values are
|
||||
* returned. */
|
||||
async *[Symbol.asyncIterator](): AsyncIterableIterator<[string, string]> {
|
||||
const keys = this[requestKeys]();
|
||||
for (const key of keys) {
|
||||
const value = await this.get(key);
|
||||
if (value) {
|
||||
yield [key, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export const SecureCookieMap = SecureCookieMap_;
|
||||
|
@ -12,7 +12,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { encode as base64Encode } from "../encoding/base64.ts";
|
||||
import { encodeBase64 as base64Encode } from "../encoding/base64.ts";
|
||||
|
||||
/** Just the part of `Deno.FileInfo` that is required to calculate an `ETag`,
|
||||
* so partial or user generated file information can be passed. */
|
||||
|
@ -40,11 +40,11 @@ import { resolve } from "../path/resolve.ts";
|
||||
import { SEP_PATTERN } from "../path/separator.ts";
|
||||
import { contentType } from "../media_types/content_type.ts";
|
||||
import { calculate, ifNoneMatch } from "./etag.ts";
|
||||
import { isRedirectStatus, Status } from "./http_status.ts";
|
||||
import { isRedirectStatus, Status, STATUS_TEXT } from "./status.ts";
|
||||
import { ByteSliceStream } from "../streams/byte_slice_stream.ts";
|
||||
import { parse } from "../flags/mod.ts";
|
||||
import { red } from "../fmt/colors.ts";
|
||||
import { createCommonResponse } from "./util.ts";
|
||||
import { deepMerge } from "../collections/deep_merge.ts";
|
||||
import { VERSION } from "../version.ts";
|
||||
import { format as formatBytes } from "../fmt/bytes.ts";
|
||||
|
||||
@ -87,6 +87,24 @@ function modeToString(isDir: boolean, maybeMode: number | null): string {
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal utility for returning a standardized response, automatically defining the body, status code and status text, according to the response type.
|
||||
*/
|
||||
function createCommonResponse(
|
||||
status: Status,
|
||||
body?: BodyInit | null,
|
||||
init?: ResponseInit,
|
||||
): Response {
|
||||
if (body === undefined) {
|
||||
body = STATUS_TEXT[status];
|
||||
}
|
||||
init = deepMerge({
|
||||
status,
|
||||
statusText: STATUS_TEXT[status],
|
||||
}, init ?? {});
|
||||
return new Response(body, init);
|
||||
}
|
||||
|
||||
/**
|
||||
* parse range header.
|
||||
*
|
||||
|
@ -1,7 +1,10 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** A collection of HTTP errors and utilities.
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*
|
||||
* A collection of HTTP errors and utilities.
|
||||
*
|
||||
* The export {@linkcode errors} contains an individual class that extends
|
||||
* {@linkcode HttpError} which makes handling HTTP errors in a structured way.
|
||||
@ -14,7 +17,7 @@
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { errors, isHttpError } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
|
||||
* import { errors, isHttpError } from "https://deno.land/std@$STD_VERSION/http/unstable_errors.ts";
|
||||
*
|
||||
* try {
|
||||
* throw new errors.NotFound();
|
||||
@ -29,8 +32,8 @@
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createHttpError } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
|
||||
* import { Status } from "https://deno.land/std@$STD_VERSION/http/http_status.ts";
|
||||
* import { createHttpError } from "https://deno.land/std@$STD_VERSION/http/unstable_errors.ts";
|
||||
* import { Status } from "https://deno.land/std@$STD_VERSION/http/status.ts";
|
||||
*
|
||||
* try {
|
||||
* throw createHttpError(
|
||||
@ -46,121 +49,36 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*/
|
||||
import {
|
||||
type ErrorStatus,
|
||||
isClientErrorStatus,
|
||||
Status,
|
||||
STATUS_TEXT,
|
||||
} from "./http_status.ts";
|
||||
|
||||
const ERROR_STATUS_MAP = {
|
||||
"BadRequest": 400,
|
||||
"Unauthorized": 401,
|
||||
"PaymentRequired": 402,
|
||||
"Forbidden": 403,
|
||||
"NotFound": 404,
|
||||
"MethodNotAllowed": 405,
|
||||
"NotAcceptable": 406,
|
||||
"ProxyAuthRequired": 407,
|
||||
"RequestTimeout": 408,
|
||||
"Conflict": 409,
|
||||
"Gone": 410,
|
||||
"LengthRequired": 411,
|
||||
"PreconditionFailed": 412,
|
||||
"RequestEntityTooLarge": 413,
|
||||
"RequestURITooLong": 414,
|
||||
"UnsupportedMediaType": 415,
|
||||
"RequestedRangeNotSatisfiable": 416,
|
||||
"ExpectationFailed": 417,
|
||||
"Teapot": 418,
|
||||
"MisdirectedRequest": 421,
|
||||
"UnprocessableEntity": 422,
|
||||
"Locked": 423,
|
||||
"FailedDependency": 424,
|
||||
"UpgradeRequired": 426,
|
||||
"PreconditionRequired": 428,
|
||||
"TooManyRequests": 429,
|
||||
"RequestHeaderFieldsTooLarge": 431,
|
||||
"UnavailableForLegalReasons": 451,
|
||||
"InternalServerError": 500,
|
||||
"NotImplemented": 501,
|
||||
"BadGateway": 502,
|
||||
"ServiceUnavailable": 503,
|
||||
"GatewayTimeout": 504,
|
||||
"HTTPVersionNotSupported": 505,
|
||||
"VariantAlsoNegotiates": 506,
|
||||
"InsufficientStorage": 507,
|
||||
"LoopDetected": 508,
|
||||
"NotExtended": 510,
|
||||
"NetworkAuthenticationRequired": 511,
|
||||
} as const;
|
||||
|
||||
export type ErrorStatusKeys = keyof typeof ERROR_STATUS_MAP;
|
||||
|
||||
export interface HttpErrorOptions extends ErrorOptions {
|
||||
expose?: boolean;
|
||||
headers?: HeadersInit;
|
||||
}
|
||||
|
||||
/** The base class that all derivative HTTP extend, providing a `status` and an
|
||||
* `expose` property. */
|
||||
export class HttpError extends Error {
|
||||
#status: ErrorStatus = Status.InternalServerError;
|
||||
#expose: boolean;
|
||||
#headers?: Headers;
|
||||
constructor(
|
||||
message = "Http Error",
|
||||
options?: HttpErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
this.#expose = options?.expose === undefined
|
||||
? isClientErrorStatus(this.status)
|
||||
: options.expose;
|
||||
if (options?.headers) {
|
||||
this.#headers = new Headers(options.headers);
|
||||
}
|
||||
}
|
||||
/** A flag to indicate if the internals of the error, like the stack, should
|
||||
* be exposed to a client, or if they are "private" and should not be leaked.
|
||||
* By default, all client errors are `true` and all server errors are
|
||||
* `false`. */
|
||||
get expose(): boolean {
|
||||
return this.#expose;
|
||||
}
|
||||
/** The optional headers object that is set on the error. */
|
||||
get headers(): Headers | undefined {
|
||||
return this.#headers;
|
||||
}
|
||||
/** The error status that is set on the error. */
|
||||
get status(): ErrorStatus {
|
||||
return this.#status;
|
||||
}
|
||||
}
|
||||
|
||||
function createHttpErrorConstructor(status: ErrorStatus): typeof HttpError {
|
||||
const name = `${Status[status]}Error`;
|
||||
const ErrorCtor = class extends HttpError {
|
||||
constructor(
|
||||
message = STATUS_TEXT[status],
|
||||
options?: HttpErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
Object.defineProperty(this, "name", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: name,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
override get status() {
|
||||
return status;
|
||||
}
|
||||
};
|
||||
return ErrorCtor;
|
||||
}
|
||||
createHttpError as createHttpError_,
|
||||
errors as errors_,
|
||||
type ErrorStatusKeys as ErrorStatusKeys_,
|
||||
HttpError as HttpError_,
|
||||
type HttpErrorOptions as HttpErrorOptions_,
|
||||
isHttpError as isHttpError_,
|
||||
} from "./unstable_errors.ts";
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*/
|
||||
export type ErrorStatusKeys = ErrorStatusKeys_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*/
|
||||
export type HttpErrorOptions = HttpErrorOptions_;
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*
|
||||
* The base class that all derivative HTTP extend, providing a `status` and an expose` property.
|
||||
*/
|
||||
export const HttpError = HttpError_;
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*
|
||||
* A namespace that contains each error constructor. Each error extends
|
||||
* `HTTPError` and provides `.status` and `.expose` properties, where the
|
||||
* `.status` will be an error `Status` value and `.expose` indicates if
|
||||
@ -171,35 +89,26 @@ function createHttpErrorConstructor(status: ErrorStatus): typeof HttpError {
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { errors } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
|
||||
* import { errors } from "https://deno.land/std@$STD_VERSION/http/unstable_errors.ts";
|
||||
*
|
||||
* throw new errors.InternalServerError("Ooops!");
|
||||
* ```
|
||||
*/
|
||||
export const errors: Record<ErrorStatusKeys, typeof HttpError> = {} as Record<
|
||||
ErrorStatusKeys,
|
||||
typeof HttpError
|
||||
>;
|
||||
|
||||
for (const [key, value] of Object.entries(ERROR_STATUS_MAP)) {
|
||||
errors[key as ErrorStatusKeys] = createHttpErrorConstructor(value);
|
||||
}
|
||||
export const errors = errors_;
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*
|
||||
* A factory function which provides a way to create errors. It takes up to 3
|
||||
* arguments, the error `Status`, an message, which defaults to the status text
|
||||
* and error options, which includes the `expose` property to set the `.expose`
|
||||
* value on the error.
|
||||
*/
|
||||
export function createHttpError(
|
||||
status: ErrorStatus = Status.InternalServerError,
|
||||
message?: string,
|
||||
options?: HttpErrorOptions,
|
||||
): HttpError {
|
||||
return new errors[Status[status] as ErrorStatusKeys](message, options);
|
||||
}
|
||||
export const createHttpError = createHttpError_;
|
||||
|
||||
/** A type guard that determines if the value is an HttpError or not. */
|
||||
export function isHttpError(value: unknown): value is HttpError {
|
||||
return value instanceof HttpError;
|
||||
}
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_errors.ts` instead.
|
||||
*
|
||||
* A type guard that determines if the value is an HttpError or not.
|
||||
*/
|
||||
export const isHttpError = isHttpError_;
|
||||
|
@ -2,6 +2,8 @@
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*
|
||||
* Contains the enum {@linkcode Status} which enumerates standard HTTP status
|
||||
* codes and provides several type guards for handling status codes with type
|
||||
* safety.
|
||||
@ -30,314 +32,65 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Standard HTTP status codes. */
|
||||
export enum Status {
|
||||
/** RFC 7231, 6.2.1 */
|
||||
Continue = 100,
|
||||
/** RFC 7231, 6.2.2 */
|
||||
SwitchingProtocols = 101,
|
||||
/** RFC 2518, 10.1 */
|
||||
Processing = 102,
|
||||
/** RFC 8297 **/
|
||||
EarlyHints = 103,
|
||||
import * as status from "./status.ts";
|
||||
|
||||
/** RFC 7231, 6.3.1 */
|
||||
OK = 200,
|
||||
/** RFC 7231, 6.3.2 */
|
||||
Created = 201,
|
||||
/** RFC 7231, 6.3.3 */
|
||||
Accepted = 202,
|
||||
/** RFC 7231, 6.3.4 */
|
||||
NonAuthoritativeInfo = 203,
|
||||
/** RFC 7231, 6.3.5 */
|
||||
NoContent = 204,
|
||||
/** RFC 7231, 6.3.6 */
|
||||
ResetContent = 205,
|
||||
/** RFC 7233, 4.1 */
|
||||
PartialContent = 206,
|
||||
/** RFC 4918, 11.1 */
|
||||
MultiStatus = 207,
|
||||
/** RFC 5842, 7.1 */
|
||||
AlreadyReported = 208,
|
||||
/** RFC 3229, 10.4.1 */
|
||||
IMUsed = 226,
|
||||
|
||||
/** RFC 7231, 6.4.1 */
|
||||
MultipleChoices = 300,
|
||||
/** RFC 7231, 6.4.2 */
|
||||
MovedPermanently = 301,
|
||||
/** RFC 7231, 6.4.3 */
|
||||
Found = 302,
|
||||
/** RFC 7231, 6.4.4 */
|
||||
SeeOther = 303,
|
||||
/** RFC 7232, 4.1 */
|
||||
NotModified = 304,
|
||||
/** RFC 7231, 6.4.5 */
|
||||
UseProxy = 305,
|
||||
/** RFC 7231, 6.4.7 */
|
||||
TemporaryRedirect = 307,
|
||||
/** RFC 7538, 3 */
|
||||
PermanentRedirect = 308,
|
||||
|
||||
/** RFC 7231, 6.5.1 */
|
||||
BadRequest = 400,
|
||||
/** RFC 7235, 3.1 */
|
||||
Unauthorized = 401,
|
||||
/** RFC 7231, 6.5.2 */
|
||||
PaymentRequired = 402,
|
||||
/** RFC 7231, 6.5.3 */
|
||||
Forbidden = 403,
|
||||
/** RFC 7231, 6.5.4 */
|
||||
NotFound = 404,
|
||||
/** RFC 7231, 6.5.5 */
|
||||
MethodNotAllowed = 405,
|
||||
/** RFC 7231, 6.5.6 */
|
||||
NotAcceptable = 406,
|
||||
/** RFC 7235, 3.2 */
|
||||
ProxyAuthRequired = 407,
|
||||
/** RFC 7231, 6.5.7 */
|
||||
RequestTimeout = 408,
|
||||
/** RFC 7231, 6.5.8 */
|
||||
Conflict = 409,
|
||||
/** RFC 7231, 6.5.9 */
|
||||
Gone = 410,
|
||||
/** RFC 7231, 6.5.10 */
|
||||
LengthRequired = 411,
|
||||
/** RFC 7232, 4.2 */
|
||||
PreconditionFailed = 412,
|
||||
/** RFC 7231, 6.5.11 */
|
||||
RequestEntityTooLarge = 413,
|
||||
/** RFC 7231, 6.5.12 */
|
||||
RequestURITooLong = 414,
|
||||
/** RFC 7231, 6.5.13 */
|
||||
UnsupportedMediaType = 415,
|
||||
/** RFC 7233, 4.4 */
|
||||
RequestedRangeNotSatisfiable = 416,
|
||||
/** RFC 7231, 6.5.14 */
|
||||
ExpectationFailed = 417,
|
||||
/** RFC 7168, 2.3.3 */
|
||||
Teapot = 418,
|
||||
/** RFC 7540, 9.1.2 */
|
||||
MisdirectedRequest = 421,
|
||||
/** RFC 4918, 11.2 */
|
||||
UnprocessableEntity = 422,
|
||||
/** RFC 4918, 11.3 */
|
||||
Locked = 423,
|
||||
/** RFC 4918, 11.4 */
|
||||
FailedDependency = 424,
|
||||
/** RFC 8470, 5.2 */
|
||||
TooEarly = 425,
|
||||
/** RFC 7231, 6.5.15 */
|
||||
UpgradeRequired = 426,
|
||||
/** RFC 6585, 3 */
|
||||
PreconditionRequired = 428,
|
||||
/** RFC 6585, 4 */
|
||||
TooManyRequests = 429,
|
||||
/** RFC 6585, 5 */
|
||||
RequestHeaderFieldsTooLarge = 431,
|
||||
/** RFC 7725, 3 */
|
||||
UnavailableForLegalReasons = 451,
|
||||
|
||||
/** RFC 7231, 6.6.1 */
|
||||
InternalServerError = 500,
|
||||
/** RFC 7231, 6.6.2 */
|
||||
NotImplemented = 501,
|
||||
/** RFC 7231, 6.6.3 */
|
||||
BadGateway = 502,
|
||||
/** RFC 7231, 6.6.4 */
|
||||
ServiceUnavailable = 503,
|
||||
/** RFC 7231, 6.6.5 */
|
||||
GatewayTimeout = 504,
|
||||
/** RFC 7231, 6.6.6 */
|
||||
HTTPVersionNotSupported = 505,
|
||||
/** RFC 2295, 8.1 */
|
||||
VariantAlsoNegotiates = 506,
|
||||
/** RFC 4918, 11.5 */
|
||||
InsufficientStorage = 507,
|
||||
/** RFC 5842, 7.2 */
|
||||
LoopDetected = 508,
|
||||
/** RFC 2774, 7 */
|
||||
NotExtended = 510,
|
||||
/** RFC 6585, 6 */
|
||||
NetworkAuthenticationRequired = 511,
|
||||
}
|
||||
|
||||
/** A record of all the status codes text. */
|
||||
export const STATUS_TEXT: Readonly<Record<Status, string>> = {
|
||||
[Status.Accepted]: "Accepted",
|
||||
[Status.AlreadyReported]: "Already Reported",
|
||||
[Status.BadGateway]: "Bad Gateway",
|
||||
[Status.BadRequest]: "Bad Request",
|
||||
[Status.Conflict]: "Conflict",
|
||||
[Status.Continue]: "Continue",
|
||||
[Status.Created]: "Created",
|
||||
[Status.EarlyHints]: "Early Hints",
|
||||
[Status.ExpectationFailed]: "Expectation Failed",
|
||||
[Status.FailedDependency]: "Failed Dependency",
|
||||
[Status.Forbidden]: "Forbidden",
|
||||
[Status.Found]: "Found",
|
||||
[Status.GatewayTimeout]: "Gateway Timeout",
|
||||
[Status.Gone]: "Gone",
|
||||
[Status.HTTPVersionNotSupported]: "HTTP Version Not Supported",
|
||||
[Status.IMUsed]: "IM Used",
|
||||
[Status.InsufficientStorage]: "Insufficient Storage",
|
||||
[Status.InternalServerError]: "Internal Server Error",
|
||||
[Status.LengthRequired]: "Length Required",
|
||||
[Status.Locked]: "Locked",
|
||||
[Status.LoopDetected]: "Loop Detected",
|
||||
[Status.MethodNotAllowed]: "Method Not Allowed",
|
||||
[Status.MisdirectedRequest]: "Misdirected Request",
|
||||
[Status.MovedPermanently]: "Moved Permanently",
|
||||
[Status.MultiStatus]: "Multi Status",
|
||||
[Status.MultipleChoices]: "Multiple Choices",
|
||||
[Status.NetworkAuthenticationRequired]: "Network Authentication Required",
|
||||
[Status.NoContent]: "No Content",
|
||||
[Status.NonAuthoritativeInfo]: "Non Authoritative Info",
|
||||
[Status.NotAcceptable]: "Not Acceptable",
|
||||
[Status.NotExtended]: "Not Extended",
|
||||
[Status.NotFound]: "Not Found",
|
||||
[Status.NotImplemented]: "Not Implemented",
|
||||
[Status.NotModified]: "Not Modified",
|
||||
[Status.OK]: "OK",
|
||||
[Status.PartialContent]: "Partial Content",
|
||||
[Status.PaymentRequired]: "Payment Required",
|
||||
[Status.PermanentRedirect]: "Permanent Redirect",
|
||||
[Status.PreconditionFailed]: "Precondition Failed",
|
||||
[Status.PreconditionRequired]: "Precondition Required",
|
||||
[Status.Processing]: "Processing",
|
||||
[Status.ProxyAuthRequired]: "Proxy Auth Required",
|
||||
[Status.RequestEntityTooLarge]: "Request Entity Too Large",
|
||||
[Status.RequestHeaderFieldsTooLarge]: "Request Header Fields Too Large",
|
||||
[Status.RequestTimeout]: "Request Timeout",
|
||||
[Status.RequestURITooLong]: "Request URI Too Long",
|
||||
[Status.RequestedRangeNotSatisfiable]: "Requested Range Not Satisfiable",
|
||||
[Status.ResetContent]: "Reset Content",
|
||||
[Status.SeeOther]: "See Other",
|
||||
[Status.ServiceUnavailable]: "Service Unavailable",
|
||||
[Status.SwitchingProtocols]: "Switching Protocols",
|
||||
[Status.Teapot]: "I'm a teapot",
|
||||
[Status.TemporaryRedirect]: "Temporary Redirect",
|
||||
[Status.TooEarly]: "Too Early",
|
||||
[Status.TooManyRequests]: "Too Many Requests",
|
||||
[Status.Unauthorized]: "Unauthorized",
|
||||
[Status.UnavailableForLegalReasons]: "Unavailable For Legal Reasons",
|
||||
[Status.UnprocessableEntity]: "Unprocessable Entity",
|
||||
[Status.UnsupportedMediaType]: "Unsupported Media Type",
|
||||
[Status.UpgradeRequired]: "Upgrade Required",
|
||||
[Status.UseProxy]: "Use Proxy",
|
||||
[Status.VariantAlsoNegotiates]: "Variant Also Negotiates",
|
||||
};
|
||||
|
||||
/** An HTTP status that is a informational (1XX). */
|
||||
export type InformationalStatus =
|
||||
| Status.Continue
|
||||
| Status.SwitchingProtocols
|
||||
| Status.Processing
|
||||
| Status.EarlyHints;
|
||||
|
||||
/** An HTTP status that is a success (2XX). */
|
||||
export type SuccessfulStatus =
|
||||
| Status.OK
|
||||
| Status.Created
|
||||
| Status.Accepted
|
||||
| Status.NonAuthoritativeInfo
|
||||
| Status.NoContent
|
||||
| Status.ResetContent
|
||||
| Status.PartialContent
|
||||
| Status.MultiStatus
|
||||
| Status.AlreadyReported
|
||||
| Status.IMUsed;
|
||||
|
||||
/** An HTTP status that is a redirect (3XX). */
|
||||
export type RedirectStatus =
|
||||
| Status.MultipleChoices // 300
|
||||
| Status.MovedPermanently // 301
|
||||
| Status.Found // 302
|
||||
| Status.SeeOther // 303
|
||||
| Status.UseProxy // 305 - DEPRECATED
|
||||
| Status.TemporaryRedirect // 307
|
||||
| Status.PermanentRedirect; // 308
|
||||
|
||||
/** An HTTP status that is a client error (4XX). */
|
||||
export type ClientErrorStatus =
|
||||
| Status.BadRequest
|
||||
| Status.Unauthorized
|
||||
| Status.PaymentRequired
|
||||
| Status.Forbidden
|
||||
| Status.NotFound
|
||||
| Status.MethodNotAllowed
|
||||
| Status.NotAcceptable
|
||||
| Status.ProxyAuthRequired
|
||||
| Status.RequestTimeout
|
||||
| Status.Conflict
|
||||
| Status.Gone
|
||||
| Status.LengthRequired
|
||||
| Status.PreconditionFailed
|
||||
| Status.RequestEntityTooLarge
|
||||
| Status.RequestURITooLong
|
||||
| Status.UnsupportedMediaType
|
||||
| Status.RequestedRangeNotSatisfiable
|
||||
| Status.ExpectationFailed
|
||||
| Status.Teapot
|
||||
| Status.MisdirectedRequest
|
||||
| Status.UnprocessableEntity
|
||||
| Status.Locked
|
||||
| Status.FailedDependency
|
||||
| Status.UpgradeRequired
|
||||
| Status.PreconditionRequired
|
||||
| Status.TooManyRequests
|
||||
| Status.RequestHeaderFieldsTooLarge
|
||||
| Status.UnavailableForLegalReasons;
|
||||
|
||||
/** An HTTP status that is a server error (5XX). */
|
||||
export type ServerErrorStatus =
|
||||
| Status.InternalServerError
|
||||
| Status.NotImplemented
|
||||
| Status.BadGateway
|
||||
| Status.ServiceUnavailable
|
||||
| Status.GatewayTimeout
|
||||
| Status.HTTPVersionNotSupported
|
||||
| Status.VariantAlsoNegotiates
|
||||
| Status.InsufficientStorage
|
||||
| Status.LoopDetected
|
||||
| Status.NotExtended
|
||||
| Status.NetworkAuthenticationRequired;
|
||||
|
||||
/** An HTTP status that is an error (4XX and 5XX). */
|
||||
export type ErrorStatus = ClientErrorStatus | ServerErrorStatus;
|
||||
|
||||
/** A type guard that determines if the status code is informational. */
|
||||
export function isInformationalStatus(
|
||||
status: Status,
|
||||
): status is InformationalStatus {
|
||||
return status >= 100 && status < 200;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is successful. */
|
||||
export function isSuccessfulStatus(status: Status): status is SuccessfulStatus {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a redirection. */
|
||||
export function isRedirectStatus(status: Status): status is RedirectStatus {
|
||||
return status >= 300 && status < 400;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a client error. */
|
||||
export function isClientErrorStatus(
|
||||
status: Status,
|
||||
): status is ClientErrorStatus {
|
||||
return status >= 400 && status < 500;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a server error. */
|
||||
export function isServerErrorStatus(
|
||||
status: Status,
|
||||
): status is ServerErrorStatus {
|
||||
return status >= 500 && status < 600;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is an error. */
|
||||
export function isErrorStatus(status: Status): status is ErrorStatus {
|
||||
return status >= 400 && status < 600;
|
||||
}
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const Status = status.Status;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type Status = status.Status;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const STATUS_TEXT = status.STATUS_TEXT;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type InformationalStatus = status.InformationalStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type SuccessfulStatus = status.SuccessfulStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type RedirectStatus = status.RedirectStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type ClientErrorStatus = status.ClientErrorStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type ServerErrorStatus = status.ServerErrorStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export type ErrorStatus = status.ErrorStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const isInformationalStatus = status.isInformationalStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const isSuccessfulStatus = status.isSuccessfulStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const isRedirectStatus = status.isRedirectStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const isClientErrorStatus = status.isClientErrorStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const isServerErrorStatus = status.isServerErrorStatus;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/status.ts` instead.
|
||||
*/
|
||||
export const isErrorStatus = status.isErrorStatus;
|
||||
|
@ -2,58 +2,39 @@
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_method.ts` instead.
|
||||
*
|
||||
* Contains the constant {@linkcode HTTP_METHODS} and the type
|
||||
* {@linkcode HttpMethod} and the type guard {@linkcode isHttpMethod} for
|
||||
* working with HTTP methods with type safety.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
import {
|
||||
HTTP_METHODS as HTTP_METHODS_,
|
||||
type HttpMethod as HttpMethod_,
|
||||
isHttpMethod as isHttpMethod_,
|
||||
} from "./unstable_method.ts";
|
||||
|
||||
/** A constant array of common HTTP methods.
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_method.ts` instead.
|
||||
*
|
||||
* A constant array of common HTTP methods.
|
||||
*
|
||||
* This list is compatible with Node.js `http` module.
|
||||
*/
|
||||
export const HTTP_METHODS = [
|
||||
"ACL",
|
||||
"BIND",
|
||||
"CHECKOUT",
|
||||
"CONNECT",
|
||||
"COPY",
|
||||
"DELETE",
|
||||
"GET",
|
||||
"HEAD",
|
||||
"LINK",
|
||||
"LOCK",
|
||||
"M-SEARCH",
|
||||
"MERGE",
|
||||
"MKACTIVITY",
|
||||
"MKCALENDAR",
|
||||
"MKCOL",
|
||||
"MOVE",
|
||||
"NOTIFY",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PROPFIND",
|
||||
"PROPPATCH",
|
||||
"PURGE",
|
||||
"PUT",
|
||||
"REBIND",
|
||||
"REPORT",
|
||||
"SEARCH",
|
||||
"SOURCE",
|
||||
"SUBSCRIBE",
|
||||
"TRACE",
|
||||
"UNBIND",
|
||||
"UNLINK",
|
||||
"UNLOCK",
|
||||
"UNSUBSCRIBE",
|
||||
] as const;
|
||||
export const HTTP_METHODS = HTTP_METHODS_;
|
||||
|
||||
/** A type representing string literals of each of the common HTTP method. */
|
||||
export type HttpMethod = typeof HTTP_METHODS[number];
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_method.ts` instead.
|
||||
*
|
||||
* A type representing string literals of each of the common HTTP method.
|
||||
*/
|
||||
export type HttpMethod = HttpMethod_;
|
||||
|
||||
/** A type guard that determines if a value is a valid HTTP method. */
|
||||
export function isHttpMethod(value: unknown): value is HttpMethod {
|
||||
return HTTP_METHODS.includes(value as HttpMethod);
|
||||
}
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_method.ts` instead.
|
||||
*
|
||||
* A type guard that determines if a value is a valid HTTP method.
|
||||
*/
|
||||
export const isHttpMethod = isHttpMethod_;
|
||||
|
@ -17,6 +17,8 @@ const INITIAL_ACCEPT_BACKOFF_DELAY = 5;
|
||||
const MAX_ACCEPT_BACKOFF_DELAY = 1000;
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 1.0.0) Use `Deno.ServeHandlerInfo` instead.
|
||||
*
|
||||
* Information about the connection a request arrived on.
|
||||
*/
|
||||
export interface ConnInfo {
|
||||
@ -27,6 +29,8 @@ export interface ConnInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 1.0.0) Use `Deno.ServeHandler` instead.
|
||||
*
|
||||
* A handler for HTTP requests. Consumes a request and connection information
|
||||
* and returns a response.
|
||||
*
|
||||
@ -40,6 +44,8 @@ export type Handler = (
|
||||
) => Response | Promise<Response>;
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 1.0.0) Use `Deno.ServeInit` instead.
|
||||
*
|
||||
* Options for running an HTTP server.
|
||||
*/
|
||||
export interface ServerInit extends Partial<Deno.ListenOptions> {
|
||||
@ -55,6 +61,8 @@ export interface ServerInit extends Partial<Deno.ListenOptions> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 1.0.0) Use `Deno.serve` instead.
|
||||
*
|
||||
* Used to construct an HTTP server.
|
||||
*/
|
||||
export class Server {
|
||||
@ -506,6 +514,8 @@ export interface ServeInit extends Partial<Deno.ListenOptions> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 1.0.0) Use `Deno.ServeOptions` instead.
|
||||
*
|
||||
* Additional serve listener options.
|
||||
*/
|
||||
export interface ServeListenerOptions {
|
||||
@ -520,6 +530,8 @@ export interface ServeListenerOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 1.0.0) Use `Deno.serve` instead.
|
||||
*
|
||||
* Constructs a server, accepts incoming connections on the given listener, and
|
||||
* handles requests on these connections with the given handler.
|
||||
*
|
||||
|
@ -2,6 +2,8 @@
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*
|
||||
* Provides {@linkcode ServerSentEvent} and
|
||||
* {@linkcode ServerSentEventStreamTarget} which provides an interface to send
|
||||
* server sent events to a browser using the DOM event model.
|
||||
@ -20,7 +22,7 @@
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/unstable_server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
@ -43,50 +45,34 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { assert } from "../assert/assert.ts";
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*/
|
||||
import {
|
||||
ServerSentEvent as ServerSentEvent_,
|
||||
type ServerSentEventInit as ServerSentEventInit_,
|
||||
ServerSentEventStreamTarget as ServerSentEventStreamTarget_,
|
||||
type ServerSentEventTarget as ServerSentEventTarget_,
|
||||
type ServerSentEventTargetOptions as ServerSentEventTargetOptions_,
|
||||
} from "./unstable_server_sent_event.ts";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*/
|
||||
export type ServerSentEventInit = ServerSentEventInit_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*/
|
||||
export type ServerSentEventTarget = ServerSentEventTarget_;
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*/
|
||||
export type ServerSentEventTargetOptions = ServerSentEventTargetOptions_;
|
||||
|
||||
const DEFAULT_KEEP_ALIVE_INTERVAL = 30_000;
|
||||
|
||||
export interface ServerSentEventInit extends EventInit {
|
||||
/** Optional arbitrary data to send to the client, data this is a string will
|
||||
* be sent unmodified, otherwise `JSON.parse()` will be used to serialize the
|
||||
* value. */
|
||||
data?: unknown;
|
||||
|
||||
/** An optional `id` which will be sent with the event and exposed in the
|
||||
* client `EventSource`. */
|
||||
id?: number;
|
||||
|
||||
/** The replacer is passed to `JSON.stringify` when converting the `data`
|
||||
* property to a JSON string. */
|
||||
replacer?:
|
||||
| (string | number)[]
|
||||
// deno-lint-ignore no-explicit-any
|
||||
| ((this: any, key: string, value: any) => any);
|
||||
|
||||
/** Space is passed to `JSON.stringify` when converting the `data` property
|
||||
* to a JSON string. */
|
||||
space?: string | number;
|
||||
}
|
||||
|
||||
export interface ServerSentEventTargetOptions {
|
||||
/** Keep client connections alive by sending a comment event to the client
|
||||
* at a specified interval. If `true`, then it polls every 30000 milliseconds
|
||||
* (30 seconds). If set to a number, then it polls that number of
|
||||
* milliseconds. The feature is disabled if set to `false`. It defaults to
|
||||
* `false`. */
|
||||
keepAlive?: boolean | number;
|
||||
}
|
||||
|
||||
class CloseEvent extends Event {
|
||||
constructor(eventInit: EventInit) {
|
||||
super("close", eventInit);
|
||||
}
|
||||
}
|
||||
|
||||
/** An event which contains information which will be sent to the remote
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*
|
||||
* An event which contains information which will be sent to the remote
|
||||
* connection and be made available in an `EventSource` as an event. A server
|
||||
* creates new events and dispatches them on the target which will then be
|
||||
* sent to a client.
|
||||
@ -112,294 +98,13 @@ class CloseEvent extends Event {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class ServerSentEvent extends Event {
|
||||
#data: string;
|
||||
#id?: number;
|
||||
#type: string;
|
||||
export const ServerSentEvent = ServerSentEvent_;
|
||||
|
||||
/**
|
||||
* @param type the event type that will be available on the client. The type
|
||||
* of `"message"` will be handled specifically as a message
|
||||
* server-side event.
|
||||
* @param eventInit initialization options for the event
|
||||
*/
|
||||
constructor(type: string, eventInit: ServerSentEventInit = {}) {
|
||||
super(type, eventInit);
|
||||
const { data, replacer, space } = eventInit;
|
||||
this.#type = type;
|
||||
try {
|
||||
this.#data = typeof data === "string"
|
||||
? data
|
||||
: data !== undefined
|
||||
? JSON.stringify(data, replacer as (string | number)[], space)
|
||||
: "";
|
||||
} catch (e) {
|
||||
assert(e instanceof Error);
|
||||
throw new TypeError(
|
||||
`data could not be coerced into a serialized string.\n ${e.message}`,
|
||||
);
|
||||
}
|
||||
const { id } = eventInit;
|
||||
this.#id = id;
|
||||
}
|
||||
|
||||
/** The data associated with the event, which will be sent to the client and
|
||||
* be made available in the `EventSource`. */
|
||||
get data(): string {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
/** The optional ID associated with the event that will be sent to the client
|
||||
* and be made available in the `EventSource`. */
|
||||
get id(): number | undefined {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
const data = `data: ${this.#data.split("\n").join("\ndata: ")}\n`;
|
||||
return `${this.#type === "__message" ? "" : `event: ${this.#type}\n`}${
|
||||
this.#id ? `id: ${String(this.#id)}\n` : ""
|
||||
}${data}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const RESPONSE_HEADERS = [
|
||||
["Connection", "Keep-Alive"],
|
||||
["Content-Type", "text/event-stream"],
|
||||
["Cache-Control", "no-cache"],
|
||||
["Keep-Alive", `timeout=${Number.MAX_SAFE_INTEGER}`],
|
||||
] as const;
|
||||
|
||||
export interface ServerSentEventTarget extends EventTarget {
|
||||
/** Is set to `true` if events cannot be sent to the remote connection.
|
||||
* Otherwise it is set to `false`.
|
||||
*
|
||||
* *Note*: This flag is lazily set, and might not reflect a closed state until
|
||||
* another event, comment or message is attempted to be processed. */
|
||||
readonly closed: boolean;
|
||||
|
||||
/** Close the target, refusing to accept any more events. */
|
||||
close(): Promise<void>;
|
||||
|
||||
/** Send a comment to the remote connection. Comments are not exposed to the
|
||||
* client `EventSource` but are used for diagnostics and helping ensure a
|
||||
* connection is kept alive.
|
||||
*
|
||||
* ```ts
|
||||
* import { ServerSentEventStreamTarget } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* target.dispatchComment("this is a comment");
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
dispatchComment(comment: string): boolean;
|
||||
|
||||
/** Dispatch a message to the client. This message will contain `data: ` only
|
||||
* and be available on the client `EventSource` on the `onmessage` or an event
|
||||
* listener of type `"message"`. */
|
||||
dispatchMessage(data: unknown): boolean;
|
||||
|
||||
/** Dispatch a server sent event to the client. The event `type` will be
|
||||
* sent as `event: ` to the client which will be raised as a `MessageEvent`
|
||||
* on the `EventSource` in the client.
|
||||
*
|
||||
* Any local event handlers will be dispatched to first, and if the event
|
||||
* is cancelled, it will not be sent to the client.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("ping", { data: "hello" });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
dispatchEvent(event: ServerSentEvent): boolean;
|
||||
|
||||
/** Dispatch a server sent event to the client. The event `type` will be
|
||||
* sent as `event: ` to the client which will be raised as a `MessageEvent`
|
||||
* on the `EventSource` in the client.
|
||||
*
|
||||
* Any local event handlers will be dispatched to first, and if the event
|
||||
* is cancelled, it will not be sent to the client.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("ping", { data: "hello" });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
dispatchEvent(event: CloseEvent | ErrorEvent): boolean;
|
||||
}
|
||||
|
||||
/** An implementation of {@linkcode ServerSentEventTarget} that provides a
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0) Import from `std/http/unstable_server_sent_event.ts` instead.
|
||||
*
|
||||
* An implementation of {@linkcode ServerSentEventTarget} that provides a
|
||||
* readable stream as a body of a response to establish a connection to a
|
||||
* client. */
|
||||
export class ServerSentEventStreamTarget extends EventTarget
|
||||
implements ServerSentEventTarget {
|
||||
#bodyInit: ReadableStream<Uint8Array>;
|
||||
#closed = false;
|
||||
#controller?: ReadableStreamDefaultController<Uint8Array>;
|
||||
// we are ignoring any here, because when exporting to npm/Node.js, the timer
|
||||
// handle isn't a number.
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#keepAliveId?: any;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#error(error: any) {
|
||||
this.dispatchEvent(new CloseEvent({ cancelable: false }));
|
||||
const errorEvent = new ErrorEvent("error", { error });
|
||||
this.dispatchEvent(errorEvent);
|
||||
}
|
||||
|
||||
#push(payload: string) {
|
||||
if (!this.#controller) {
|
||||
this.#error(new Error("The controller has not been set."));
|
||||
return;
|
||||
}
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
this.#controller.enqueue(encoder.encode(payload));
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
return this.#closed;
|
||||
}
|
||||
|
||||
constructor({ keepAlive = false }: ServerSentEventTargetOptions = {}) {
|
||||
super();
|
||||
|
||||
this.#bodyInit = new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
this.#controller = controller;
|
||||
},
|
||||
cancel: (error) => {
|
||||
// connections closing are considered "normal" for SSE events and just
|
||||
// mean the far side has closed.
|
||||
if (
|
||||
error instanceof Error && error.message.includes("connection closed")
|
||||
) {
|
||||
this.close();
|
||||
} else {
|
||||
this.#error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addEventListener("close", () => {
|
||||
this.#closed = true;
|
||||
if (this.#keepAliveId !== null && this.#keepAliveId !== undefined) {
|
||||
clearInterval(this.#keepAliveId);
|
||||
this.#keepAliveId = undefined;
|
||||
}
|
||||
if (this.#controller) {
|
||||
try {
|
||||
this.#controller.close();
|
||||
} catch {
|
||||
// we ignore any errors here, as it is likely that the controller
|
||||
// is already closed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (keepAlive) {
|
||||
const interval = typeof keepAlive === "number"
|
||||
? keepAlive
|
||||
: DEFAULT_KEEP_ALIVE_INTERVAL;
|
||||
this.#keepAliveId = setInterval(() => {
|
||||
this.dispatchComment("keep-alive comment");
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a {@linkcode Response} which contains the body and headers needed
|
||||
* to initiate a SSE connection with the client. */
|
||||
asResponse(responseInit?: ResponseInit): Response {
|
||||
return new Response(...this.asResponseInit(responseInit));
|
||||
}
|
||||
|
||||
/** Returns a tuple which contains the {@linkcode BodyInit} and
|
||||
* {@linkcode ResponseInit} needed to create a response that will establish
|
||||
* a SSE connection with the client. */
|
||||
asResponseInit(responseInit: ResponseInit = {}): [BodyInit, ResponseInit] {
|
||||
const headers = new Headers(responseInit.headers);
|
||||
for (const [key, value] of RESPONSE_HEADERS) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
responseInit.headers = headers;
|
||||
return [this.#bodyInit, responseInit];
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.dispatchEvent(new CloseEvent({ cancelable: false }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dispatchComment(comment: string): boolean {
|
||||
this.#push(`: ${comment.split("\n").join("\n: ")}\n\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
dispatchMessage(data: any): boolean {
|
||||
const event = new ServerSentEvent("__message", { data });
|
||||
return this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
override dispatchEvent(event: ServerSentEvent): boolean;
|
||||
override dispatchEvent(event: CloseEvent | ErrorEvent): boolean;
|
||||
override dispatchEvent(
|
||||
event: ServerSentEvent | CloseEvent | ErrorEvent,
|
||||
): boolean {
|
||||
const dispatched = super.dispatchEvent(event);
|
||||
if (dispatched && event instanceof ServerSentEvent) {
|
||||
this.#push(String(event));
|
||||
}
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) {
|
||||
return `${this.constructor.name} ${
|
||||
inspect({ "#bodyInit": this.#bodyInit, "#closed": this.#closed })
|
||||
}`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect(
|
||||
{ "#bodyInit": this.#bodyInit, "#closed": this.#closed },
|
||||
newOptions,
|
||||
)
|
||||
}`;
|
||||
}
|
||||
}
|
||||
* client.
|
||||
*/
|
||||
export const ServerSentEventStreamTarget = ServerSentEventStreamTarget_;
|
||||
|
343
http/status.ts
Normal file
343
http/status.ts
Normal file
@ -0,0 +1,343 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Contains the enum {@linkcode Status} which enumerates standard HTTP status
|
||||
* codes and provides several type guards for handling status codes with type
|
||||
* safety.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* Status,
|
||||
* STATUS_TEXT,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/status.ts";
|
||||
*
|
||||
* console.log(Status.NotFound); //=> 404
|
||||
* console.log(STATUS_TEXT[Status.NotFound]); //=> "Not Found"
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* import { isErrorStatus } from "https://deno.land/std@$STD_VERSION/http/status.ts";
|
||||
*
|
||||
* const res = await fetch("https://example.com/");
|
||||
*
|
||||
* if (isErrorStatus(res.status)) {
|
||||
* // error handling here...
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Standard HTTP status codes. */
|
||||
export enum Status {
|
||||
/** RFC 7231, 6.2.1 */
|
||||
Continue = 100,
|
||||
/** RFC 7231, 6.2.2 */
|
||||
SwitchingProtocols = 101,
|
||||
/** RFC 2518, 10.1 */
|
||||
Processing = 102,
|
||||
/** RFC 8297 **/
|
||||
EarlyHints = 103,
|
||||
|
||||
/** RFC 7231, 6.3.1 */
|
||||
OK = 200,
|
||||
/** RFC 7231, 6.3.2 */
|
||||
Created = 201,
|
||||
/** RFC 7231, 6.3.3 */
|
||||
Accepted = 202,
|
||||
/** RFC 7231, 6.3.4 */
|
||||
NonAuthoritativeInfo = 203,
|
||||
/** RFC 7231, 6.3.5 */
|
||||
NoContent = 204,
|
||||
/** RFC 7231, 6.3.6 */
|
||||
ResetContent = 205,
|
||||
/** RFC 7233, 4.1 */
|
||||
PartialContent = 206,
|
||||
/** RFC 4918, 11.1 */
|
||||
MultiStatus = 207,
|
||||
/** RFC 5842, 7.1 */
|
||||
AlreadyReported = 208,
|
||||
/** RFC 3229, 10.4.1 */
|
||||
IMUsed = 226,
|
||||
|
||||
/** RFC 7231, 6.4.1 */
|
||||
MultipleChoices = 300,
|
||||
/** RFC 7231, 6.4.2 */
|
||||
MovedPermanently = 301,
|
||||
/** RFC 7231, 6.4.3 */
|
||||
Found = 302,
|
||||
/** RFC 7231, 6.4.4 */
|
||||
SeeOther = 303,
|
||||
/** RFC 7232, 4.1 */
|
||||
NotModified = 304,
|
||||
/** RFC 7231, 6.4.5 */
|
||||
UseProxy = 305,
|
||||
/** RFC 7231, 6.4.7 */
|
||||
TemporaryRedirect = 307,
|
||||
/** RFC 7538, 3 */
|
||||
PermanentRedirect = 308,
|
||||
|
||||
/** RFC 7231, 6.5.1 */
|
||||
BadRequest = 400,
|
||||
/** RFC 7235, 3.1 */
|
||||
Unauthorized = 401,
|
||||
/** RFC 7231, 6.5.2 */
|
||||
PaymentRequired = 402,
|
||||
/** RFC 7231, 6.5.3 */
|
||||
Forbidden = 403,
|
||||
/** RFC 7231, 6.5.4 */
|
||||
NotFound = 404,
|
||||
/** RFC 7231, 6.5.5 */
|
||||
MethodNotAllowed = 405,
|
||||
/** RFC 7231, 6.5.6 */
|
||||
NotAcceptable = 406,
|
||||
/** RFC 7235, 3.2 */
|
||||
ProxyAuthRequired = 407,
|
||||
/** RFC 7231, 6.5.7 */
|
||||
RequestTimeout = 408,
|
||||
/** RFC 7231, 6.5.8 */
|
||||
Conflict = 409,
|
||||
/** RFC 7231, 6.5.9 */
|
||||
Gone = 410,
|
||||
/** RFC 7231, 6.5.10 */
|
||||
LengthRequired = 411,
|
||||
/** RFC 7232, 4.2 */
|
||||
PreconditionFailed = 412,
|
||||
/** RFC 7231, 6.5.11 */
|
||||
RequestEntityTooLarge = 413,
|
||||
/** RFC 7231, 6.5.12 */
|
||||
RequestURITooLong = 414,
|
||||
/** RFC 7231, 6.5.13 */
|
||||
UnsupportedMediaType = 415,
|
||||
/** RFC 7233, 4.4 */
|
||||
RequestedRangeNotSatisfiable = 416,
|
||||
/** RFC 7231, 6.5.14 */
|
||||
ExpectationFailed = 417,
|
||||
/** RFC 7168, 2.3.3 */
|
||||
Teapot = 418,
|
||||
/** RFC 7540, 9.1.2 */
|
||||
MisdirectedRequest = 421,
|
||||
/** RFC 4918, 11.2 */
|
||||
UnprocessableEntity = 422,
|
||||
/** RFC 4918, 11.3 */
|
||||
Locked = 423,
|
||||
/** RFC 4918, 11.4 */
|
||||
FailedDependency = 424,
|
||||
/** RFC 8470, 5.2 */
|
||||
TooEarly = 425,
|
||||
/** RFC 7231, 6.5.15 */
|
||||
UpgradeRequired = 426,
|
||||
/** RFC 6585, 3 */
|
||||
PreconditionRequired = 428,
|
||||
/** RFC 6585, 4 */
|
||||
TooManyRequests = 429,
|
||||
/** RFC 6585, 5 */
|
||||
RequestHeaderFieldsTooLarge = 431,
|
||||
/** RFC 7725, 3 */
|
||||
UnavailableForLegalReasons = 451,
|
||||
|
||||
/** RFC 7231, 6.6.1 */
|
||||
InternalServerError = 500,
|
||||
/** RFC 7231, 6.6.2 */
|
||||
NotImplemented = 501,
|
||||
/** RFC 7231, 6.6.3 */
|
||||
BadGateway = 502,
|
||||
/** RFC 7231, 6.6.4 */
|
||||
ServiceUnavailable = 503,
|
||||
/** RFC 7231, 6.6.5 */
|
||||
GatewayTimeout = 504,
|
||||
/** RFC 7231, 6.6.6 */
|
||||
HTTPVersionNotSupported = 505,
|
||||
/** RFC 2295, 8.1 */
|
||||
VariantAlsoNegotiates = 506,
|
||||
/** RFC 4918, 11.5 */
|
||||
InsufficientStorage = 507,
|
||||
/** RFC 5842, 7.2 */
|
||||
LoopDetected = 508,
|
||||
/** RFC 2774, 7 */
|
||||
NotExtended = 510,
|
||||
/** RFC 6585, 6 */
|
||||
NetworkAuthenticationRequired = 511,
|
||||
}
|
||||
|
||||
/** A record of all the status codes text. */
|
||||
export const STATUS_TEXT: Readonly<Record<Status, string>> = {
|
||||
[Status.Accepted]: "Accepted",
|
||||
[Status.AlreadyReported]: "Already Reported",
|
||||
[Status.BadGateway]: "Bad Gateway",
|
||||
[Status.BadRequest]: "Bad Request",
|
||||
[Status.Conflict]: "Conflict",
|
||||
[Status.Continue]: "Continue",
|
||||
[Status.Created]: "Created",
|
||||
[Status.EarlyHints]: "Early Hints",
|
||||
[Status.ExpectationFailed]: "Expectation Failed",
|
||||
[Status.FailedDependency]: "Failed Dependency",
|
||||
[Status.Forbidden]: "Forbidden",
|
||||
[Status.Found]: "Found",
|
||||
[Status.GatewayTimeout]: "Gateway Timeout",
|
||||
[Status.Gone]: "Gone",
|
||||
[Status.HTTPVersionNotSupported]: "HTTP Version Not Supported",
|
||||
[Status.IMUsed]: "IM Used",
|
||||
[Status.InsufficientStorage]: "Insufficient Storage",
|
||||
[Status.InternalServerError]: "Internal Server Error",
|
||||
[Status.LengthRequired]: "Length Required",
|
||||
[Status.Locked]: "Locked",
|
||||
[Status.LoopDetected]: "Loop Detected",
|
||||
[Status.MethodNotAllowed]: "Method Not Allowed",
|
||||
[Status.MisdirectedRequest]: "Misdirected Request",
|
||||
[Status.MovedPermanently]: "Moved Permanently",
|
||||
[Status.MultiStatus]: "Multi Status",
|
||||
[Status.MultipleChoices]: "Multiple Choices",
|
||||
[Status.NetworkAuthenticationRequired]: "Network Authentication Required",
|
||||
[Status.NoContent]: "No Content",
|
||||
[Status.NonAuthoritativeInfo]: "Non Authoritative Info",
|
||||
[Status.NotAcceptable]: "Not Acceptable",
|
||||
[Status.NotExtended]: "Not Extended",
|
||||
[Status.NotFound]: "Not Found",
|
||||
[Status.NotImplemented]: "Not Implemented",
|
||||
[Status.NotModified]: "Not Modified",
|
||||
[Status.OK]: "OK",
|
||||
[Status.PartialContent]: "Partial Content",
|
||||
[Status.PaymentRequired]: "Payment Required",
|
||||
[Status.PermanentRedirect]: "Permanent Redirect",
|
||||
[Status.PreconditionFailed]: "Precondition Failed",
|
||||
[Status.PreconditionRequired]: "Precondition Required",
|
||||
[Status.Processing]: "Processing",
|
||||
[Status.ProxyAuthRequired]: "Proxy Auth Required",
|
||||
[Status.RequestEntityTooLarge]: "Request Entity Too Large",
|
||||
[Status.RequestHeaderFieldsTooLarge]: "Request Header Fields Too Large",
|
||||
[Status.RequestTimeout]: "Request Timeout",
|
||||
[Status.RequestURITooLong]: "Request URI Too Long",
|
||||
[Status.RequestedRangeNotSatisfiable]: "Requested Range Not Satisfiable",
|
||||
[Status.ResetContent]: "Reset Content",
|
||||
[Status.SeeOther]: "See Other",
|
||||
[Status.ServiceUnavailable]: "Service Unavailable",
|
||||
[Status.SwitchingProtocols]: "Switching Protocols",
|
||||
[Status.Teapot]: "I'm a teapot",
|
||||
[Status.TemporaryRedirect]: "Temporary Redirect",
|
||||
[Status.TooEarly]: "Too Early",
|
||||
[Status.TooManyRequests]: "Too Many Requests",
|
||||
[Status.Unauthorized]: "Unauthorized",
|
||||
[Status.UnavailableForLegalReasons]: "Unavailable For Legal Reasons",
|
||||
[Status.UnprocessableEntity]: "Unprocessable Entity",
|
||||
[Status.UnsupportedMediaType]: "Unsupported Media Type",
|
||||
[Status.UpgradeRequired]: "Upgrade Required",
|
||||
[Status.UseProxy]: "Use Proxy",
|
||||
[Status.VariantAlsoNegotiates]: "Variant Also Negotiates",
|
||||
};
|
||||
|
||||
/** An HTTP status that is a informational (1XX). */
|
||||
export type InformationalStatus =
|
||||
| Status.Continue
|
||||
| Status.SwitchingProtocols
|
||||
| Status.Processing
|
||||
| Status.EarlyHints;
|
||||
|
||||
/** An HTTP status that is a success (2XX). */
|
||||
export type SuccessfulStatus =
|
||||
| Status.OK
|
||||
| Status.Created
|
||||
| Status.Accepted
|
||||
| Status.NonAuthoritativeInfo
|
||||
| Status.NoContent
|
||||
| Status.ResetContent
|
||||
| Status.PartialContent
|
||||
| Status.MultiStatus
|
||||
| Status.AlreadyReported
|
||||
| Status.IMUsed;
|
||||
|
||||
/** An HTTP status that is a redirect (3XX). */
|
||||
export type RedirectStatus =
|
||||
| Status.MultipleChoices // 300
|
||||
| Status.MovedPermanently // 301
|
||||
| Status.Found // 302
|
||||
| Status.SeeOther // 303
|
||||
| Status.UseProxy // 305 - DEPRECATED
|
||||
| Status.TemporaryRedirect // 307
|
||||
| Status.PermanentRedirect; // 308
|
||||
|
||||
/** An HTTP status that is a client error (4XX). */
|
||||
export type ClientErrorStatus =
|
||||
| Status.BadRequest
|
||||
| Status.Unauthorized
|
||||
| Status.PaymentRequired
|
||||
| Status.Forbidden
|
||||
| Status.NotFound
|
||||
| Status.MethodNotAllowed
|
||||
| Status.NotAcceptable
|
||||
| Status.ProxyAuthRequired
|
||||
| Status.RequestTimeout
|
||||
| Status.Conflict
|
||||
| Status.Gone
|
||||
| Status.LengthRequired
|
||||
| Status.PreconditionFailed
|
||||
| Status.RequestEntityTooLarge
|
||||
| Status.RequestURITooLong
|
||||
| Status.UnsupportedMediaType
|
||||
| Status.RequestedRangeNotSatisfiable
|
||||
| Status.ExpectationFailed
|
||||
| Status.Teapot
|
||||
| Status.MisdirectedRequest
|
||||
| Status.UnprocessableEntity
|
||||
| Status.Locked
|
||||
| Status.FailedDependency
|
||||
| Status.UpgradeRequired
|
||||
| Status.PreconditionRequired
|
||||
| Status.TooManyRequests
|
||||
| Status.RequestHeaderFieldsTooLarge
|
||||
| Status.UnavailableForLegalReasons;
|
||||
|
||||
/** An HTTP status that is a server error (5XX). */
|
||||
export type ServerErrorStatus =
|
||||
| Status.InternalServerError
|
||||
| Status.NotImplemented
|
||||
| Status.BadGateway
|
||||
| Status.ServiceUnavailable
|
||||
| Status.GatewayTimeout
|
||||
| Status.HTTPVersionNotSupported
|
||||
| Status.VariantAlsoNegotiates
|
||||
| Status.InsufficientStorage
|
||||
| Status.LoopDetected
|
||||
| Status.NotExtended
|
||||
| Status.NetworkAuthenticationRequired;
|
||||
|
||||
/** An HTTP status that is an error (4XX and 5XX). */
|
||||
export type ErrorStatus = ClientErrorStatus | ServerErrorStatus;
|
||||
|
||||
/** A type guard that determines if the status code is informational. */
|
||||
export function isInformationalStatus(
|
||||
status: Status,
|
||||
): status is InformationalStatus {
|
||||
return status >= 100 && status < 200;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is successful. */
|
||||
export function isSuccessfulStatus(status: Status): status is SuccessfulStatus {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a redirection. */
|
||||
export function isRedirectStatus(status: Status): status is RedirectStatus {
|
||||
return status >= 300 && status < 400;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a client error. */
|
||||
export function isClientErrorStatus(
|
||||
status: Status,
|
||||
): status is ClientErrorStatus {
|
||||
return status >= 400 && status < 500;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a server error. */
|
||||
export function isServerErrorStatus(
|
||||
status: Status,
|
||||
): status is ServerErrorStatus {
|
||||
return status >= 500 && status < 600;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is an error. */
|
||||
export function isErrorStatus(status: Status): status is ErrorStatus {
|
||||
return status >= 400 && status < 600;
|
||||
}
|
@ -9,7 +9,7 @@ import {
|
||||
isSuccessfulStatus,
|
||||
Status,
|
||||
STATUS_TEXT,
|
||||
} from "./http_status.ts";
|
||||
} from "./status.ts";
|
||||
import { assert, assertEquals } from "../assert/mod.ts";
|
||||
|
||||
Deno.test({
|
812
http/unstable_cookie_map.ts
Normal file
812
http/unstable_cookie_map.ts
Normal file
@ -0,0 +1,812 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Provides a iterable map interfaces for managing cookies server side.
|
||||
*
|
||||
* @example
|
||||
* To access the keys in a request and have any set keys available for creating
|
||||
* a response:
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* CookieMap,
|
||||
* mergeHeaders
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/unstable_cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
* });
|
||||
*
|
||||
* const cookies = new CookieMap(request, { secure: true });
|
||||
* console.log(cookies.get("foo")); // logs "bar"
|
||||
* cookies.set("session", "1234567", { secure: true });
|
||||
*
|
||||
* const response = new Response("hello", {
|
||||
* headers: mergeHeaders({
|
||||
* "content-type": "text/plain",
|
||||
* }, cookies),
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* To have automatic management of cryptographically signed cookies, you can use
|
||||
* the {@linkcode SecureCookieMap} instead of {@linkcode CookieMap}. The biggest
|
||||
* difference is that the methods operate async in order to be able to support
|
||||
* async signing and validation of cookies:
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* SecureCookieMap,
|
||||
* mergeHeaders,
|
||||
* type KeyRing,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/unstable_cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
* });
|
||||
*
|
||||
* // The keys must implement the `KeyRing` interface.
|
||||
* declare const keys: KeyRing;
|
||||
*
|
||||
* const cookies = new SecureCookieMap(request, { keys, secure: true });
|
||||
* console.log(await cookies.get("foo")); // logs "bar"
|
||||
* // the cookie will be automatically signed using the supplied key ring.
|
||||
* await cookies.set("session", "1234567");
|
||||
*
|
||||
* const response = new Response("hello", {
|
||||
* headers: mergeHeaders({
|
||||
* "content-type": "text/plain",
|
||||
* }, cookies),
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* In addition, if you have a {@linkcode Response} or {@linkcode Headers} for a
|
||||
* response at construction of the cookies object, they can be passed and any
|
||||
* set cookies will be added directly to those headers:
|
||||
*
|
||||
* ```ts
|
||||
* import { CookieMap } from "https://deno.land/std@$STD_VERSION/http/unstable_cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
* });
|
||||
*
|
||||
* const response = new Response("hello", {
|
||||
* headers: { "content-type": "text/plain" },
|
||||
* });
|
||||
*
|
||||
* const cookies = new CookieMap(request, { response });
|
||||
* console.log(cookies.get("foo")); // logs "bar"
|
||||
* cookies.set("session", "1234567");
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export interface CookieMapOptions {
|
||||
/** The {@linkcode Response} or the headers that will be used with the
|
||||
* response. When provided, `Set-Cookie` headers will be set in the headers
|
||||
* when cookies are set or deleted in the map.
|
||||
*
|
||||
* An alternative way to extract the headers is to pass the cookie map to the
|
||||
* {@linkcode mergeHeaders} function to merge various sources of the
|
||||
* headers to be provided when creating or updating a response.
|
||||
*/
|
||||
response?: Headered | Headers;
|
||||
/** A flag that indicates if the request and response are being handled over
|
||||
* a secure (e.g. HTTPS/TLS) connection.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface CookieMapSetDeleteOptions {
|
||||
/** The domain to scope the cookie for. */
|
||||
domain?: string;
|
||||
/** When the cookie expires. */
|
||||
expires?: Date;
|
||||
/** Number of seconds until the cookie expires */
|
||||
maxAge?: number;
|
||||
/** A flag that indicates if the cookie is valid over HTTP only. */
|
||||
httpOnly?: boolean;
|
||||
/** Do not error when signing and validating cookies over an insecure
|
||||
* connection. */
|
||||
ignoreInsecure?: boolean;
|
||||
/** Overwrite an existing value. */
|
||||
overwrite?: boolean;
|
||||
/** The path the cookie is valid for. */
|
||||
path?: string;
|
||||
/** Override the flag that was set when the instance was created. */
|
||||
secure?: boolean;
|
||||
/** Set the same-site indicator for a cookie. */
|
||||
sameSite?: "strict" | "lax" | "none" | boolean;
|
||||
}
|
||||
|
||||
/** An object which contains a `headers` property which has a value of an
|
||||
* instance of {@linkcode Headers}, like {@linkcode Request} and
|
||||
* {@linkcode Response}. */
|
||||
export interface Headered {
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
export interface Mergeable {
|
||||
[cookieMapHeadersInitSymbol](): [string, string][];
|
||||
}
|
||||
|
||||
export interface SecureCookieMapOptions {
|
||||
/** Keys which will be used to validate and sign cookies. The key ring should
|
||||
* implement the {@linkcode KeyRing} interface. */
|
||||
keys?: KeyRing;
|
||||
|
||||
/** The {@linkcode Response} or the headers that will be used with the
|
||||
* response. When provided, `Set-Cookie` headers will be set in the headers
|
||||
* when cookies are set or deleted in the map.
|
||||
*
|
||||
* An alternative way to extract the headers is to pass the cookie map to the
|
||||
* {@linkcode mergeHeaders} function to merge various sources of the
|
||||
* headers to be provided when creating or updating a response.
|
||||
*/
|
||||
response?: Headered | Headers;
|
||||
|
||||
/** A flag that indicates if the request and response are being handled over
|
||||
* a secure (e.g. HTTPS/TLS) connection.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureCookieMapGetOptions {
|
||||
/** Overrides the flag that was set when the instance was created. */
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureCookieMapSetDeleteOptions {
|
||||
/** The domain to scope the cookie for. */
|
||||
domain?: string;
|
||||
/** When the cookie expires. */
|
||||
expires?: Date;
|
||||
/** Number of seconds until the cookie expires */
|
||||
maxAge?: number;
|
||||
/** A flag that indicates if the cookie is valid over HTTP only. */
|
||||
httpOnly?: boolean;
|
||||
/** Do not error when signing and validating cookies over an insecure
|
||||
* connection. */
|
||||
ignoreInsecure?: boolean;
|
||||
/** Overwrite an existing value. */
|
||||
overwrite?: boolean;
|
||||
/** The path the cookie is valid for. */
|
||||
path?: string;
|
||||
/** Override the flag that was set when the instance was created. */
|
||||
secure?: boolean;
|
||||
/** Set the same-site indicator for a cookie. */
|
||||
sameSite?: "strict" | "lax" | "none" | boolean;
|
||||
/** Override the default behavior of signing the cookie. */
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
type CookieAttributes = SecureCookieMapSetDeleteOptions;
|
||||
|
||||
// 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;
|
||||
|
||||
const matchCache: Record<string, RegExp> = {};
|
||||
function getPattern(name: string): RegExp {
|
||||
if (name in matchCache) {
|
||||
return matchCache[name];
|
||||
}
|
||||
|
||||
return matchCache[name] = new RegExp(
|
||||
`(?:^|;) *${name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")}=([^;]*)`,
|
||||
);
|
||||
}
|
||||
|
||||
function pushCookie(values: string[], cookie: Cookie) {
|
||||
if (cookie.overwrite) {
|
||||
for (let i = values.length - 1; i >= 0; i--) {
|
||||
if (values[i].indexOf(`${cookie.name}=`) === 0) {
|
||||
values.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
values.push(cookie.toHeaderValue());
|
||||
}
|
||||
|
||||
function validateCookieProperty(
|
||||
key: string,
|
||||
value: string | undefined | null,
|
||||
) {
|
||||
if (value && !FIELD_CONTENT_REGEXP.test(value)) {
|
||||
throw new TypeError(`The "${key}" of the cookie (${value}) is invalid.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** An internal abstraction to manage cookies. */
|
||||
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;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
value: string | null,
|
||||
attributes: CookieAttributes,
|
||||
) {
|
||||
validateCookieProperty("name", name);
|
||||
this.name = name;
|
||||
validateCookieProperty("value", value);
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toHeaderValue(): string {
|
||||
let value = this.toString();
|
||||
if (this.maxAge) {
|
||||
this.expires = new Date(Date.now() + (this.maxAge * 1000));
|
||||
}
|
||||
if (this.path) {
|
||||
value += `; path=${this.path}`;
|
||||
}
|
||||
if (this.expires) {
|
||||
value += `; expires=${this.expires.toUTCString()}`;
|
||||
}
|
||||
if (this.domain) {
|
||||
value += `; domain=${this.domain}`;
|
||||
}
|
||||
if (this.sameSite) {
|
||||
value += `; samesite=${
|
||||
this.sameSite === true ? "strict" : this.sameSite.toLowerCase()
|
||||
}`;
|
||||
}
|
||||
if (this.secure) {
|
||||
value += "; secure";
|
||||
}
|
||||
if (this.httpOnly) {
|
||||
value += "; httponly";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.name}=${this.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Symbol which is used in {@link mergeHeaders} to extract a
|
||||
* `[string | string][]` from an instance to generate the final set of
|
||||
* headers. */
|
||||
export const cookieMapHeadersInitSymbol = Symbol.for(
|
||||
"Deno.std.cookieMap.headersInit",
|
||||
);
|
||||
|
||||
function isMergeable(value: unknown): value is Mergeable {
|
||||
return value !== null && value !== undefined && typeof value === "object" &&
|
||||
cookieMapHeadersInitSymbol in value;
|
||||
}
|
||||
|
||||
/** Allows merging of various sources of headers into a final set of headers
|
||||
* which can be used in a {@linkcode Response}.
|
||||
*
|
||||
* Note, that unlike when passing a `Response` or {@linkcode Headers} used in a
|
||||
* response to {@linkcode CookieMap} or {@linkcode SecureCookieMap}, merging
|
||||
* will not ensure that there are no other `Set-Cookie` headers from other
|
||||
* sources, it will simply append the various headers together. */
|
||||
export function mergeHeaders(
|
||||
...sources: (Headered | HeadersInit | Mergeable)[]
|
||||
): Headers {
|
||||
const headers = new Headers();
|
||||
for (const source of sources) {
|
||||
let entries: Iterable<[string, string]>;
|
||||
if (source instanceof Headers) {
|
||||
entries = source;
|
||||
} else if ("headers" in source && source.headers instanceof Headers) {
|
||||
entries = source.headers;
|
||||
} else if (isMergeable(source)) {
|
||||
entries = source[cookieMapHeadersInitSymbol]();
|
||||
} else if (Array.isArray(source)) {
|
||||
entries = source as [string, string][];
|
||||
} else {
|
||||
entries = Object.entries(source);
|
||||
}
|
||||
for (const [key, value] of entries) {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
const keys = Symbol("#keys");
|
||||
const requestHeaders = Symbol("#requestHeaders");
|
||||
const responseHeaders = Symbol("#responseHeaders");
|
||||
const isSecure = Symbol("#secure");
|
||||
const requestKeys = Symbol("#requestKeys");
|
||||
|
||||
/** An internal abstract class which provides common functionality for
|
||||
* {@link CookieMap} and {@link SecureCookieMap}. */
|
||||
abstract class CookieMapBase implements Mergeable {
|
||||
[keys]?: string[];
|
||||
[requestHeaders]: Headers;
|
||||
[responseHeaders]: Headers;
|
||||
[isSecure]: boolean;
|
||||
|
||||
[requestKeys](): string[] {
|
||||
if (this[keys]) {
|
||||
return this[keys];
|
||||
}
|
||||
const result = this[keys] = [] as string[];
|
||||
const header = this[requestHeaders].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: Headers | Headered, options: CookieMapOptions) {
|
||||
this[requestHeaders] = "headers" in request ? request.headers : request;
|
||||
const { secure = false, response = new Headers() } = options;
|
||||
this[responseHeaders] = "headers" in response ? response.headers : response;
|
||||
this[isSecure] = secure;
|
||||
}
|
||||
|
||||
/** A method used by {@linkcode mergeHeaders} to be able to merge
|
||||
* headers from various sources when forming a {@linkcode Response}. */
|
||||
[cookieMapHeadersInitSymbol](): [string, string][] {
|
||||
const init: [string, string][] = [];
|
||||
for (const [key, value] of this[responseHeaders]) {
|
||||
if (key === "set-cookie") {
|
||||
init.push([key, value]);
|
||||
}
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
return `${this.constructor.name} []`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect([], newOptions)
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a way to manage cookies in a request and response on the server
|
||||
* as a single iterable collection.
|
||||
*
|
||||
* The methods and properties align to {@linkcode Map}. When constructing a
|
||||
* {@linkcode Request} or {@linkcode Headers} from the request need to be
|
||||
* provided, as well as optionally the {@linkcode Response} or `Headers` for the
|
||||
* response can be provided. Alternatively the {@linkcode mergeHeaders}
|
||||
* function can be used to generate a final set of headers for sending in the
|
||||
* response. */
|
||||
export class CookieMap extends CookieMapBase {
|
||||
/** Contains the number of valid cookies in the request headers. */
|
||||
get size(): number {
|
||||
return [...this].length;
|
||||
}
|
||||
|
||||
constructor(request: Headers | Headered, options: CookieMapOptions = {}) {
|
||||
super(request, options);
|
||||
}
|
||||
|
||||
/** Deletes all the cookies from the {@linkcode Request} in the response. */
|
||||
clear(options: CookieMapSetDeleteOptions = {}) {
|
||||
for (const key of this.keys()) {
|
||||
this.set(key, null, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a cookie to be deleted in the response.
|
||||
*
|
||||
* This is a convenience function for `set(key, null, options?)`.
|
||||
*/
|
||||
delete(key: string, options: CookieMapSetDeleteOptions = {}): boolean {
|
||||
this.set(key, null, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return the value of a matching key present in the {@linkcode Request}. If
|
||||
* the key is not present `undefined` is returned. */
|
||||
get(key: string): string | undefined {
|
||||
const headerValue = this[requestHeaders].get("cookie");
|
||||
if (!headerValue) {
|
||||
return undefined;
|
||||
}
|
||||
const match = headerValue.match(getPattern(key));
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, value] = match;
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Returns `true` if the matching key is present in the {@linkcode Request},
|
||||
* otherwise `false`. */
|
||||
has(key: string): boolean {
|
||||
const headerValue = this[requestHeaders].get("cookie");
|
||||
if (!headerValue) {
|
||||
return false;
|
||||
}
|
||||
return getPattern(key).test(headerValue);
|
||||
}
|
||||
|
||||
/** Set a named cookie in the response. The optional
|
||||
* {@linkcode CookieMapSetDeleteOptions} are applied to the cookie being set.
|
||||
*/
|
||||
set(
|
||||
key: string,
|
||||
value: string | null,
|
||||
options: CookieMapSetDeleteOptions = {},
|
||||
): this {
|
||||
const resHeaders = this[responseHeaders];
|
||||
const values: string[] = [];
|
||||
for (const [key, value] of resHeaders) {
|
||||
if (key === "set-cookie") {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
const secure = this[isSecure];
|
||||
|
||||
if (!secure && options.secure && !options.ignoreInsecure) {
|
||||
throw new TypeError(
|
||||
"Cannot send secure cookie over unencrypted connection.",
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = new Cookie(key, value, options);
|
||||
cookie.secure = options.secure ?? secure;
|
||||
pushCookie(values, cookie);
|
||||
|
||||
resHeaders.delete("set-cookie");
|
||||
for (const value of values) {
|
||||
resHeaders.append("set-cookie", value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys and values that are present in the
|
||||
* {@linkcode Request}. This is an alias of the `[Symbol.iterator]` method
|
||||
* present on the class. */
|
||||
entries(): IterableIterator<[string, string]> {
|
||||
return this[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*keys(): IterableIterator<string> {
|
||||
for (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the cookie values that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*values(): IterableIterator<string> {
|
||||
for (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys and values that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*[Symbol.iterator](): IterableIterator<[string, string]> {
|
||||
const keys = this[requestKeys]();
|
||||
for (const key of keys) {
|
||||
const value = this.get(key);
|
||||
if (value) {
|
||||
yield [key, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Types of data that can be signed cryptographically. */
|
||||
export type Data = string | number[] | ArrayBuffer | Uint8Array;
|
||||
|
||||
/** An interface which describes the methods that {@linkcode SecureCookieMap}
|
||||
* uses to sign and verify cookies. */
|
||||
export interface KeyRing {
|
||||
/** Given a set of data and a digest, return the key index of the key used
|
||||
* to sign the data. The index is 0 based. A non-negative number indices the
|
||||
* digest is valid and a key was found. */
|
||||
indexOf(data: Data, digest: string): Promise<number> | number;
|
||||
/** Sign the data, returning a string based digest of the data. */
|
||||
sign(data: Data): Promise<string> | string;
|
||||
/** Verifies the digest matches the provided data, indicating the data was
|
||||
* signed by the keyring and has not been tampered with. */
|
||||
verify(data: Data, digest: string): Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
/** Provides an way to manage cookies in a request and response on the server
|
||||
* as a single iterable collection, as well as the ability to sign and verify
|
||||
* cookies to prevent tampering.
|
||||
*
|
||||
* The methods and properties align to {@linkcode Map}, but due to the need to
|
||||
* support asynchronous cryptographic keys, all the APIs operate async. When
|
||||
* constructing a {@linkcode Request} or {@linkcode Headers} from the request
|
||||
* need to be provided, as well as optionally the {@linkcode Response} or
|
||||
* `Headers` for the response can be provided. Alternatively the
|
||||
* {@linkcode mergeHeaders} function can be used to generate a final set
|
||||
* of headers for sending in the response.
|
||||
*
|
||||
* On construction, the optional set of keys implementing the
|
||||
* {@linkcode KeyRing} interface. While it is optional, if you don't plan to use
|
||||
* keys, you might want to consider using just the {@linkcode CookieMap}.
|
||||
*
|
||||
* @example
|
||||
*/
|
||||
export class SecureCookieMap extends CookieMapBase {
|
||||
#keyRing?: KeyRing;
|
||||
|
||||
/** Is set to a promise which resolves with the number of cookies in the
|
||||
* {@linkcode Request}. */
|
||||
get size(): Promise<number> {
|
||||
return (async () => {
|
||||
let size = 0;
|
||||
for await (const _ of this) {
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
})();
|
||||
}
|
||||
|
||||
constructor(
|
||||
request: Headers | Headered,
|
||||
options: SecureCookieMapOptions = {},
|
||||
) {
|
||||
super(request, options);
|
||||
const { keys } = options;
|
||||
this.#keyRing = keys;
|
||||
}
|
||||
|
||||
/** Sets all cookies in the {@linkcode Request} to be deleted in the
|
||||
* response. */
|
||||
async clear(options: SecureCookieMapSetDeleteOptions) {
|
||||
const promises = [];
|
||||
for await (const key of this.keys()) {
|
||||
promises.push(this.set(key, null, options));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/** Set a cookie to be deleted in the response.
|
||||
*
|
||||
* This is a convenience function for `set(key, null, options?)`. */
|
||||
async delete(
|
||||
key: string,
|
||||
options: SecureCookieMapSetDeleteOptions = {},
|
||||
): Promise<boolean> {
|
||||
await this.set(key, null, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Get the value of a cookie from the {@linkcode Request}.
|
||||
*
|
||||
* If the cookie is signed, and the signature is invalid, `undefined` will be
|
||||
* returned and the cookie will be set to be deleted in the response. If the
|
||||
* cookie is using an "old" key from the keyring, the cookie will be re-signed
|
||||
* with the current key and be added to the response to be updated. */
|
||||
async get(
|
||||
key: string,
|
||||
options: SecureCookieMapGetOptions = {},
|
||||
): Promise<string | undefined> {
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
const nameSig = `${key}.sig`;
|
||||
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
const match = header.match(getPattern(key));
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const [, value] = match;
|
||||
if (!signed) {
|
||||
return value;
|
||||
}
|
||||
const digest = await this.get(nameSig, { signed: false });
|
||||
if (!digest) {
|
||||
return;
|
||||
}
|
||||
const data = `${key}=${value}`;
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("key ring required for signed cookies");
|
||||
}
|
||||
const index = await this.#keyRing.indexOf(data, digest);
|
||||
|
||||
if (index < 0) {
|
||||
await this.delete(nameSig, { path: "/", signed: false });
|
||||
} else {
|
||||
if (index) {
|
||||
await this.set(nameSig, await this.#keyRing.sign(data), {
|
||||
signed: false,
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if the key is in the {@linkcode Request}.
|
||||
*
|
||||
* If the cookie is signed, and the signature is invalid, `false` will be
|
||||
* returned and the cookie will be set to be deleted in the response. If the
|
||||
* cookie is using an "old" key from the keyring, the cookie will be re-signed
|
||||
* with the current key and be added to the response to be updated. */
|
||||
async has(
|
||||
key: string,
|
||||
options: SecureCookieMapGetOptions = {},
|
||||
): Promise<boolean> {
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
const nameSig = `${key}.sig`;
|
||||
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return false;
|
||||
}
|
||||
const match = header.match(getPattern(key));
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
if (!signed) {
|
||||
return true;
|
||||
}
|
||||
const digest = await this.get(nameSig, { signed: false });
|
||||
if (!digest) {
|
||||
return false;
|
||||
}
|
||||
const [, value] = match;
|
||||
const data = `${key}=${value}`;
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("key ring required for signed cookies");
|
||||
}
|
||||
const index = await this.#keyRing.indexOf(data, digest);
|
||||
|
||||
if (index < 0) {
|
||||
await this.delete(nameSig, { path: "/", signed: false });
|
||||
return false;
|
||||
} else {
|
||||
if (index) {
|
||||
await this.set(nameSig, await this.#keyRing.sign(data), {
|
||||
signed: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a cookie in the response headers.
|
||||
*
|
||||
* If there was a keyring set, cookies will be automatically signed, unless
|
||||
* overridden by the passed options. Cookies can be deleted by setting the
|
||||
* value to `null`. */
|
||||
async set(
|
||||
key: string,
|
||||
value: string | null,
|
||||
options: SecureCookieMapSetDeleteOptions = {},
|
||||
): Promise<this> {
|
||||
const resHeaders = this[responseHeaders];
|
||||
const headers: string[] = [];
|
||||
for (const [key, value] of resHeaders.entries()) {
|
||||
if (key === "set-cookie") {
|
||||
headers.push(value);
|
||||
}
|
||||
}
|
||||
const secure = this[isSecure];
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
|
||||
if (!secure && options.secure && !options.ignoreInsecure) {
|
||||
throw new TypeError(
|
||||
"Cannot send secure cookie over unencrypted connection.",
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = new Cookie(key, value, options);
|
||||
cookie.secure = options.secure ?? secure;
|
||||
pushCookie(headers, cookie);
|
||||
|
||||
if (signed) {
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("keys required for signed cookies.");
|
||||
}
|
||||
cookie.value = await this.#keyRing.sign(cookie.toString());
|
||||
cookie.name += ".sig";
|
||||
pushCookie(headers, cookie);
|
||||
}
|
||||
|
||||
resHeaders.delete("set-cookie");
|
||||
for (const header of headers) {
|
||||
resHeaders.append("set-cookie", header);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Iterate over the {@linkcode Request} cookies, yielding up a tuple
|
||||
* containing the key and value of each cookie.
|
||||
*
|
||||
* If a key ring was provided, only properly signed cookie keys and values are
|
||||
* returned. */
|
||||
entries(): AsyncIterableIterator<[string, string]> {
|
||||
return this[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
/** Iterate over the request's cookies, yielding up the key of each cookie.
|
||||
*
|
||||
* If a keyring was provided, only properly signed cookie keys are
|
||||
* returned. */
|
||||
async *keys(): AsyncIterableIterator<string> {
|
||||
for await (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the request's cookies, yielding up the value of each cookie.
|
||||
*
|
||||
* If a keyring was provided, only properly signed cookie values are
|
||||
* returned. */
|
||||
async *values(): AsyncIterableIterator<string> {
|
||||
for await (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the {@linkcode Request} cookies, yielding up a tuple
|
||||
* containing the key and value of each cookie.
|
||||
*
|
||||
* If a key ring was provided, only properly signed cookie keys and values are
|
||||
* returned. */
|
||||
async *[Symbol.asyncIterator](): AsyncIterableIterator<[string, string]> {
|
||||
const keys = this[requestKeys]();
|
||||
for (const key of keys) {
|
||||
const value = await this.get(key);
|
||||
if (value) {
|
||||
yield [key, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ import {
|
||||
cookieMapHeadersInitSymbol,
|
||||
mergeHeaders,
|
||||
SecureCookieMap,
|
||||
} from "./cookie_map.ts";
|
||||
} from "./unstable_cookie_map.ts";
|
||||
|
||||
function isNode(): boolean {
|
||||
return "process" in globalThis && "global" in globalThis;
|
205
http/unstable_errors.ts
Normal file
205
http/unstable_errors.ts
Normal file
@ -0,0 +1,205 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** A collection of HTTP errors and utilities.
|
||||
*
|
||||
* The export {@linkcode errors} contains an individual class that extends
|
||||
* {@linkcode HttpError} which makes handling HTTP errors in a structured way.
|
||||
*
|
||||
* The function {@linkcode createHttpError} provides a way to create instances
|
||||
* of errors in a factory pattern.
|
||||
*
|
||||
* The function {@linkcode isHttpError} is a type guard that will narrow a value
|
||||
* to an `HttpError` instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { errors, isHttpError } from "https://deno.land/std@$STD_VERSION/http/unstable_errors.ts";
|
||||
*
|
||||
* try {
|
||||
* throw new errors.NotFound();
|
||||
* } catch (e) {
|
||||
* if (isHttpError(e)) {
|
||||
* const response = new Response(e.message, { status: e.status });
|
||||
* } else {
|
||||
* throw e;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createHttpError } from "https://deno.land/std@$STD_VERSION/http/unstable_errors.ts";
|
||||
* import { Status } from "https://deno.land/std@$STD_VERSION/http/status.ts";
|
||||
*
|
||||
* try {
|
||||
* throw createHttpError(
|
||||
* Status.BadRequest,
|
||||
* "The request was bad.",
|
||||
* { expose: false }
|
||||
* );
|
||||
* } catch (e) {
|
||||
* // handle errors
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
type ErrorStatus,
|
||||
isClientErrorStatus,
|
||||
Status,
|
||||
STATUS_TEXT,
|
||||
} from "./status.ts";
|
||||
|
||||
const ERROR_STATUS_MAP = {
|
||||
"BadRequest": 400,
|
||||
"Unauthorized": 401,
|
||||
"PaymentRequired": 402,
|
||||
"Forbidden": 403,
|
||||
"NotFound": 404,
|
||||
"MethodNotAllowed": 405,
|
||||
"NotAcceptable": 406,
|
||||
"ProxyAuthRequired": 407,
|
||||
"RequestTimeout": 408,
|
||||
"Conflict": 409,
|
||||
"Gone": 410,
|
||||
"LengthRequired": 411,
|
||||
"PreconditionFailed": 412,
|
||||
"RequestEntityTooLarge": 413,
|
||||
"RequestURITooLong": 414,
|
||||
"UnsupportedMediaType": 415,
|
||||
"RequestedRangeNotSatisfiable": 416,
|
||||
"ExpectationFailed": 417,
|
||||
"Teapot": 418,
|
||||
"MisdirectedRequest": 421,
|
||||
"UnprocessableEntity": 422,
|
||||
"Locked": 423,
|
||||
"FailedDependency": 424,
|
||||
"UpgradeRequired": 426,
|
||||
"PreconditionRequired": 428,
|
||||
"TooManyRequests": 429,
|
||||
"RequestHeaderFieldsTooLarge": 431,
|
||||
"UnavailableForLegalReasons": 451,
|
||||
"InternalServerError": 500,
|
||||
"NotImplemented": 501,
|
||||
"BadGateway": 502,
|
||||
"ServiceUnavailable": 503,
|
||||
"GatewayTimeout": 504,
|
||||
"HTTPVersionNotSupported": 505,
|
||||
"VariantAlsoNegotiates": 506,
|
||||
"InsufficientStorage": 507,
|
||||
"LoopDetected": 508,
|
||||
"NotExtended": 510,
|
||||
"NetworkAuthenticationRequired": 511,
|
||||
} as const;
|
||||
|
||||
export type ErrorStatusKeys = keyof typeof ERROR_STATUS_MAP;
|
||||
|
||||
export interface HttpErrorOptions extends ErrorOptions {
|
||||
expose?: boolean;
|
||||
headers?: HeadersInit;
|
||||
}
|
||||
|
||||
/** The base class that all derivative HTTP extend, providing a `status` and an
|
||||
* `expose` property. */
|
||||
export class HttpError extends Error {
|
||||
#status: ErrorStatus = Status.InternalServerError;
|
||||
#expose: boolean;
|
||||
#headers?: Headers;
|
||||
constructor(
|
||||
message = "Http Error",
|
||||
options?: HttpErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
this.#expose = options?.expose === undefined
|
||||
? isClientErrorStatus(this.status)
|
||||
: options.expose;
|
||||
if (options?.headers) {
|
||||
this.#headers = new Headers(options.headers);
|
||||
}
|
||||
}
|
||||
/** A flag to indicate if the internals of the error, like the stack, should
|
||||
* be exposed to a client, or if they are "private" and should not be leaked.
|
||||
* By default, all client errors are `true` and all server errors are
|
||||
* `false`. */
|
||||
get expose(): boolean {
|
||||
return this.#expose;
|
||||
}
|
||||
/** The optional headers object that is set on the error. */
|
||||
get headers(): Headers | undefined {
|
||||
return this.#headers;
|
||||
}
|
||||
/** The error status that is set on the error. */
|
||||
get status(): ErrorStatus {
|
||||
return this.#status;
|
||||
}
|
||||
}
|
||||
|
||||
function createHttpErrorConstructor(status: ErrorStatus): typeof HttpError {
|
||||
const name = `${Status[status]}Error`;
|
||||
const ErrorCtor = class extends HttpError {
|
||||
constructor(
|
||||
message = STATUS_TEXT[status],
|
||||
options?: HttpErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
Object.defineProperty(this, "name", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: name,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
override get status() {
|
||||
return status;
|
||||
}
|
||||
};
|
||||
return ErrorCtor;
|
||||
}
|
||||
|
||||
/**
|
||||
* A namespace that contains each error constructor. Each error extends
|
||||
* `HTTPError` and provides `.status` and `.expose` properties, where the
|
||||
* `.status` will be an error `Status` value and `.expose` indicates if
|
||||
* information, like a stack trace, should be shared in the response.
|
||||
*
|
||||
* By default, `.expose` is set to false in server errors, and true for client
|
||||
* errors.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { errors } from "https://deno.land/std@$STD_VERSION/http/unstable_errors.ts";
|
||||
*
|
||||
* throw new errors.InternalServerError("Ooops!");
|
||||
* ```
|
||||
*/
|
||||
export const errors: Record<ErrorStatusKeys, typeof HttpError> = {} as Record<
|
||||
ErrorStatusKeys,
|
||||
typeof HttpError
|
||||
>;
|
||||
|
||||
for (const [key, value] of Object.entries(ERROR_STATUS_MAP)) {
|
||||
errors[key as ErrorStatusKeys] = createHttpErrorConstructor(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function which provides a way to create errors. It takes up to 3
|
||||
* arguments, the error `Status`, an message, which defaults to the status text
|
||||
* and error options, which includes the `expose` property to set the `.expose`
|
||||
* value on the error.
|
||||
*/
|
||||
export function createHttpError(
|
||||
status: ErrorStatus = Status.InternalServerError,
|
||||
message?: string,
|
||||
options?: HttpErrorOptions,
|
||||
): HttpError {
|
||||
return new errors[Status[status] as ErrorStatusKeys](message, options);
|
||||
}
|
||||
|
||||
/** A type guard that determines if the value is an HttpError or not. */
|
||||
export function isHttpError(value: unknown): value is HttpError {
|
||||
return value instanceof HttpError;
|
||||
}
|
@ -2,14 +2,14 @@
|
||||
|
||||
import { assert, assertEquals, assertInstanceOf } from "../assert/mod.ts";
|
||||
|
||||
import { type ErrorStatus, Status, STATUS_TEXT } from "./http_status.ts";
|
||||
import { type ErrorStatus, Status, STATUS_TEXT } from "./status.ts";
|
||||
|
||||
import {
|
||||
createHttpError,
|
||||
errors,
|
||||
type ErrorStatusKeys,
|
||||
HttpError,
|
||||
} from "./http_errors.ts";
|
||||
} from "./unstable_errors.ts";
|
||||
|
||||
const clientErrorStatus: ErrorStatus[] = [
|
||||
Status.BadRequest,
|
59
http/unstable_method.ts
Normal file
59
http/unstable_method.ts
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Contains the constant {@linkcode HTTP_METHODS} and the type
|
||||
* {@linkcode HttpMethod} and the type guard {@linkcode isHttpMethod} for
|
||||
* working with HTTP methods with type safety.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** A constant array of common HTTP methods.
|
||||
*
|
||||
* This list is compatible with Node.js `http` module.
|
||||
*/
|
||||
export const HTTP_METHODS = [
|
||||
"ACL",
|
||||
"BIND",
|
||||
"CHECKOUT",
|
||||
"CONNECT",
|
||||
"COPY",
|
||||
"DELETE",
|
||||
"GET",
|
||||
"HEAD",
|
||||
"LINK",
|
||||
"LOCK",
|
||||
"M-SEARCH",
|
||||
"MERGE",
|
||||
"MKACTIVITY",
|
||||
"MKCALENDAR",
|
||||
"MKCOL",
|
||||
"MOVE",
|
||||
"NOTIFY",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PROPFIND",
|
||||
"PROPPATCH",
|
||||
"PURGE",
|
||||
"PUT",
|
||||
"REBIND",
|
||||
"REPORT",
|
||||
"SEARCH",
|
||||
"SOURCE",
|
||||
"SUBSCRIBE",
|
||||
"TRACE",
|
||||
"UNBIND",
|
||||
"UNLINK",
|
||||
"UNLOCK",
|
||||
"UNSUBSCRIBE",
|
||||
] as const;
|
||||
|
||||
/** A type representing string literals of each of the common HTTP method. */
|
||||
export type HttpMethod = typeof HTTP_METHODS[number];
|
||||
|
||||
/** A type guard that determines if a value is a valid HTTP method. */
|
||||
export function isHttpMethod(value: unknown): value is HttpMethod {
|
||||
return HTTP_METHODS.includes(value as HttpMethod);
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
import { assert, assertEquals } from "../assert/mod.ts";
|
||||
|
||||
import { HTTP_METHODS, isHttpMethod } from "./method.ts";
|
||||
import { HTTP_METHODS, isHttpMethod } from "./unstable_method.ts";
|
||||
|
||||
Deno.test({
|
||||
name: "HTTP_METHODS",
|
405
http/unstable_server_sent_event.ts
Normal file
405
http/unstable_server_sent_event.ts
Normal file
@ -0,0 +1,405 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Provides {@linkcode ServerSentEvent} and
|
||||
* {@linkcode ServerSentEventStreamTarget} which provides an interface to send
|
||||
* server sent events to a browser using the DOM event model.
|
||||
*
|
||||
* The {@linkcode ServerSentEventStreamTarget} provides the `.asResponse()` or
|
||||
* `.asResponseInit()` to provide a body and headers to the client to establish
|
||||
* the event connection. This is accomplished by keeping a connection open to
|
||||
* the client by not closing the body, which allows events to be sent down the
|
||||
* connection and processed by the client browser.
|
||||
*
|
||||
* See more about Server-sent events on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/unstable_server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* let counter = 0;
|
||||
*
|
||||
* // Sends an event every 2 seconds, incrementing the ID
|
||||
* const id = setInterval(() => {
|
||||
* const evt = new ServerSentEvent(
|
||||
* "message",
|
||||
* { data: { hello: "world" }, id: counter++ },
|
||||
* );
|
||||
* target.dispatchEvent(evt);
|
||||
* }, 2000);
|
||||
*
|
||||
* target.addEventListener("close", () => clearInterval(id));
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { assert } from "../assert/assert.ts";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const DEFAULT_KEEP_ALIVE_INTERVAL = 30_000;
|
||||
|
||||
export interface ServerSentEventInit extends EventInit {
|
||||
/** Optional arbitrary data to send to the client, data this is a string will
|
||||
* be sent unmodified, otherwise `JSON.parse()` will be used to serialize the
|
||||
* value. */
|
||||
data?: unknown;
|
||||
|
||||
/** An optional `id` which will be sent with the event and exposed in the
|
||||
* client `EventSource`. */
|
||||
id?: number;
|
||||
|
||||
/** The replacer is passed to `JSON.stringify` when converting the `data`
|
||||
* property to a JSON string. */
|
||||
replacer?:
|
||||
| (string | number)[]
|
||||
// deno-lint-ignore no-explicit-any
|
||||
| ((this: any, key: string, value: any) => any);
|
||||
|
||||
/** Space is passed to `JSON.stringify` when converting the `data` property
|
||||
* to a JSON string. */
|
||||
space?: string | number;
|
||||
}
|
||||
|
||||
export interface ServerSentEventTargetOptions {
|
||||
/** Keep client connections alive by sending a comment event to the client
|
||||
* at a specified interval. If `true`, then it polls every 30000 milliseconds
|
||||
* (30 seconds). If set to a number, then it polls that number of
|
||||
* milliseconds. The feature is disabled if set to `false`. It defaults to
|
||||
* `false`. */
|
||||
keepAlive?: boolean | number;
|
||||
}
|
||||
|
||||
class CloseEvent extends Event {
|
||||
constructor(eventInit: EventInit) {
|
||||
super("close", eventInit);
|
||||
}
|
||||
}
|
||||
|
||||
/** An event which contains information which will be sent to the remote
|
||||
* connection and be made available in an `EventSource` as an event. A server
|
||||
* creates new events and dispatches them on the target which will then be
|
||||
* sent to a client.
|
||||
*
|
||||
* See more about Server-sent events on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("message", {
|
||||
* data: { hello: "world" },
|
||||
* id: 1
|
||||
* });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class ServerSentEvent extends Event {
|
||||
#data: string;
|
||||
#id?: number;
|
||||
#type: string;
|
||||
|
||||
/**
|
||||
* @param type the event type that will be available on the client. The type
|
||||
* of `"message"` will be handled specifically as a message
|
||||
* server-side event.
|
||||
* @param eventInit initialization options for the event
|
||||
*/
|
||||
constructor(type: string, eventInit: ServerSentEventInit = {}) {
|
||||
super(type, eventInit);
|
||||
const { data, replacer, space } = eventInit;
|
||||
this.#type = type;
|
||||
try {
|
||||
this.#data = typeof data === "string"
|
||||
? data
|
||||
: data !== undefined
|
||||
? JSON.stringify(data, replacer as (string | number)[], space)
|
||||
: "";
|
||||
} catch (e) {
|
||||
assert(e instanceof Error);
|
||||
throw new TypeError(
|
||||
`data could not be coerced into a serialized string.\n ${e.message}`,
|
||||
);
|
||||
}
|
||||
const { id } = eventInit;
|
||||
this.#id = id;
|
||||
}
|
||||
|
||||
/** The data associated with the event, which will be sent to the client and
|
||||
* be made available in the `EventSource`. */
|
||||
get data(): string {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
/** The optional ID associated with the event that will be sent to the client
|
||||
* and be made available in the `EventSource`. */
|
||||
get id(): number | undefined {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
const data = `data: ${this.#data.split("\n").join("\ndata: ")}\n`;
|
||||
return `${this.#type === "__message" ? "" : `event: ${this.#type}\n`}${
|
||||
this.#id ? `id: ${String(this.#id)}\n` : ""
|
||||
}${data}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const RESPONSE_HEADERS = [
|
||||
["Connection", "Keep-Alive"],
|
||||
["Content-Type", "text/event-stream"],
|
||||
["Cache-Control", "no-cache"],
|
||||
["Keep-Alive", `timeout=${Number.MAX_SAFE_INTEGER}`],
|
||||
] as const;
|
||||
|
||||
export interface ServerSentEventTarget extends EventTarget {
|
||||
/** Is set to `true` if events cannot be sent to the remote connection.
|
||||
* Otherwise it is set to `false`.
|
||||
*
|
||||
* *Note*: This flag is lazily set, and might not reflect a closed state until
|
||||
* another event, comment or message is attempted to be processed. */
|
||||
readonly closed: boolean;
|
||||
|
||||
/** Close the target, refusing to accept any more events. */
|
||||
close(): Promise<void>;
|
||||
|
||||
/** Send a comment to the remote connection. Comments are not exposed to the
|
||||
* client `EventSource` but are used for diagnostics and helping ensure a
|
||||
* connection is kept alive.
|
||||
*
|
||||
* ```ts
|
||||
* import { ServerSentEventStreamTarget } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* target.dispatchComment("this is a comment");
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
dispatchComment(comment: string): boolean;
|
||||
|
||||
/** Dispatch a message to the client. This message will contain `data: ` only
|
||||
* and be available on the client `EventSource` on the `onmessage` or an event
|
||||
* listener of type `"message"`. */
|
||||
dispatchMessage(data: unknown): boolean;
|
||||
|
||||
/** Dispatch a server sent event to the client. The event `type` will be
|
||||
* sent as `event: ` to the client which will be raised as a `MessageEvent`
|
||||
* on the `EventSource` in the client.
|
||||
*
|
||||
* Any local event handlers will be dispatched to first, and if the event
|
||||
* is cancelled, it will not be sent to the client.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("ping", { data: "hello" });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
dispatchEvent(event: ServerSentEvent): boolean;
|
||||
|
||||
/** Dispatch a server sent event to the client. The event `type` will be
|
||||
* sent as `event: ` to the client which will be raised as a `MessageEvent`
|
||||
* on the `EventSource` in the client.
|
||||
*
|
||||
* Any local event handlers will be dispatched to first, and if the event
|
||||
* is cancelled, it will not be sent to the client.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
*
|
||||
* Deno.serve({ port: 8000 }, (request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("ping", { data: "hello" });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
dispatchEvent(event: CloseEvent | ErrorEvent): boolean;
|
||||
}
|
||||
|
||||
/** An implementation of {@linkcode ServerSentEventTarget} that provides a
|
||||
* readable stream as a body of a response to establish a connection to a
|
||||
* client. */
|
||||
export class ServerSentEventStreamTarget extends EventTarget
|
||||
implements ServerSentEventTarget {
|
||||
#bodyInit: ReadableStream<Uint8Array>;
|
||||
#closed = false;
|
||||
#controller?: ReadableStreamDefaultController<Uint8Array>;
|
||||
// we are ignoring any here, because when exporting to npm/Node.js, the timer
|
||||
// handle isn't a number.
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#keepAliveId?: any;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#error(error: any) {
|
||||
this.dispatchEvent(new CloseEvent({ cancelable: false }));
|
||||
const errorEvent = new ErrorEvent("error", { error });
|
||||
this.dispatchEvent(errorEvent);
|
||||
}
|
||||
|
||||
#push(payload: string) {
|
||||
if (!this.#controller) {
|
||||
this.#error(new Error("The controller has not been set."));
|
||||
return;
|
||||
}
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
this.#controller.enqueue(encoder.encode(payload));
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
return this.#closed;
|
||||
}
|
||||
|
||||
constructor({ keepAlive = false }: ServerSentEventTargetOptions = {}) {
|
||||
super();
|
||||
|
||||
this.#bodyInit = new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
this.#controller = controller;
|
||||
},
|
||||
cancel: (error) => {
|
||||
// connections closing are considered "normal" for SSE events and just
|
||||
// mean the far side has closed.
|
||||
if (
|
||||
error instanceof Error && error.message.includes("connection closed")
|
||||
) {
|
||||
this.close();
|
||||
} else {
|
||||
this.#error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addEventListener("close", () => {
|
||||
this.#closed = true;
|
||||
if (this.#keepAliveId !== null && this.#keepAliveId !== undefined) {
|
||||
clearInterval(this.#keepAliveId);
|
||||
this.#keepAliveId = undefined;
|
||||
}
|
||||
if (this.#controller) {
|
||||
try {
|
||||
this.#controller.close();
|
||||
} catch {
|
||||
// we ignore any errors here, as it is likely that the controller
|
||||
// is already closed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (keepAlive) {
|
||||
const interval = typeof keepAlive === "number"
|
||||
? keepAlive
|
||||
: DEFAULT_KEEP_ALIVE_INTERVAL;
|
||||
this.#keepAliveId = setInterval(() => {
|
||||
this.dispatchComment("keep-alive comment");
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a {@linkcode Response} which contains the body and headers needed
|
||||
* to initiate a SSE connection with the client. */
|
||||
asResponse(responseInit?: ResponseInit): Response {
|
||||
return new Response(...this.asResponseInit(responseInit));
|
||||
}
|
||||
|
||||
/** Returns a tuple which contains the {@linkcode BodyInit} and
|
||||
* {@linkcode ResponseInit} needed to create a response that will establish
|
||||
* a SSE connection with the client. */
|
||||
asResponseInit(responseInit: ResponseInit = {}): [BodyInit, ResponseInit] {
|
||||
const headers = new Headers(responseInit.headers);
|
||||
for (const [key, value] of RESPONSE_HEADERS) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
responseInit.headers = headers;
|
||||
return [this.#bodyInit, responseInit];
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.dispatchEvent(new CloseEvent({ cancelable: false }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dispatchComment(comment: string): boolean {
|
||||
this.#push(`: ${comment.split("\n").join("\n: ")}\n\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
dispatchMessage(data: any): boolean {
|
||||
const event = new ServerSentEvent("__message", { data });
|
||||
return this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
override dispatchEvent(event: ServerSentEvent): boolean;
|
||||
override dispatchEvent(event: CloseEvent | ErrorEvent): boolean;
|
||||
override dispatchEvent(
|
||||
event: ServerSentEvent | CloseEvent | ErrorEvent,
|
||||
): boolean {
|
||||
const dispatched = super.dispatchEvent(event);
|
||||
if (dispatched && event instanceof ServerSentEvent) {
|
||||
this.#push(String(event));
|
||||
}
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) {
|
||||
return `${this.constructor.name} ${
|
||||
inspect({ "#bodyInit": this.#bodyInit, "#closed": this.#closed })
|
||||
}`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect(
|
||||
{ "#bodyInit": this.#bodyInit, "#closed": this.#closed },
|
||||
newOptions,
|
||||
)
|
||||
}`;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { assert, assertEquals } from "../assert/mod.ts";
|
||||
import {
|
||||
ServerSentEvent,
|
||||
ServerSentEventStreamTarget,
|
||||
} from "./server_sent_event.ts";
|
||||
} from "./unstable_server_sent_event.ts";
|
||||
|
||||
Deno.test({
|
||||
name: "ServerSentEvent - construction",
|
@ -1,10 +1,12 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { Status, STATUS_TEXT } from "./http_status.ts";
|
||||
import { Status, STATUS_TEXT } from "./status.ts";
|
||||
import { deepMerge } from "../collections/deep_merge.ts";
|
||||
|
||||
/**
|
||||
* @deprecated (will be removed after 0.210.0)
|
||||
*
|
||||
* Internal utility for returning a standardized response, automatically defining the body, status code and status text, according to the response type.
|
||||
*/
|
||||
export function createCommonResponse(
|
||||
|
Loading…
Reference in New Issue
Block a user