2024-01-01 21:11:32 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2024-04-10 02:43:44 +00:00
|
|
|
// This module is browser compatible.
|
2023-04-12 11:51:19 +00:00
|
|
|
|
2024-01-16 06:17:44 +00:00
|
|
|
/**
|
|
|
|
* Provides functions for dealing with and matching ETags, including
|
2024-07-10 06:27:34 +00:00
|
|
|
* {@linkcode eTag} to calculate an etag for a given entity,
|
2023-04-12 11:51:19 +00:00
|
|
|
* {@linkcode ifMatch} for validating if an ETag matches against a `If-Match`
|
|
|
|
* header and {@linkcode ifNoneMatch} for validating an Etag against an
|
|
|
|
* `If-None-Match` header.
|
|
|
|
*
|
|
|
|
* See further information on the `ETag` header on
|
2024-01-16 06:17:44 +00:00
|
|
|
* {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag | MDN}.
|
2023-04-12 11:51:19 +00:00
|
|
|
*
|
|
|
|
* @module
|
|
|
|
*/
|
|
|
|
|
2024-04-29 02:57:30 +00:00
|
|
|
import { encodeBase64 as base64Encode } from "@std/encoding/base64";
|
2023-04-12 11:51:19 +00:00
|
|
|
|
2024-01-16 06:17:44 +00:00
|
|
|
/**
|
|
|
|
* Just the part of {@linkcode Deno.FileInfo} that is required to calculate an `ETag`,
|
|
|
|
* so partial or user generated file information can be passed.
|
|
|
|
*/
|
2023-04-12 11:51:19 +00:00
|
|
|
export interface FileInfo {
|
2024-01-16 06:17:44 +00:00
|
|
|
/** The last modification time of the file. This corresponds to the `mtime`
|
|
|
|
* field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
|
|
|
|
* may not be available on all platforms. */
|
2023-04-12 11:51:19 +00:00
|
|
|
mtime: Date | null;
|
2024-01-16 06:17:44 +00:00
|
|
|
/** The size of the file, in bytes. */
|
2023-04-12 11:51:19 +00:00
|
|
|
size: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
|
|
|
const DEFAULT_ALGORITHM: AlgorithmIdentifier = "SHA-256";
|
|
|
|
|
2024-07-10 06:27:34 +00:00
|
|
|
/** Options for {@linkcode eTag}. */
|
2023-04-12 11:51:19 +00:00
|
|
|
export interface ETagOptions {
|
2024-01-16 06:17:44 +00:00
|
|
|
/**
|
|
|
|
* A digest algorithm to use to calculate the etag.
|
|
|
|
*
|
2024-03-25 08:52:22 +00:00
|
|
|
* @default {"SHA-256"}
|
2024-01-16 06:17:44 +00:00
|
|
|
*/
|
2023-04-12 11:51:19 +00:00
|
|
|
algorithm?: AlgorithmIdentifier;
|
|
|
|
|
2024-07-11 09:21:37 +00:00
|
|
|
/**
|
|
|
|
* Override the default behavior of calculating the `ETag`, either forcing
|
|
|
|
* a tag to be labelled weak or not.
|
|
|
|
*
|
|
|
|
* Defaults to `true` when the entity is a `FileInfo` and `false` otherwise.
|
|
|
|
*/
|
2023-04-12 11:51:19 +00:00
|
|
|
weak?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isFileInfo(value: unknown): value is FileInfo {
|
|
|
|
return Boolean(
|
|
|
|
value && typeof value === "object" && "mtime" in value && "size" in value,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function calcEntity(
|
|
|
|
entity: string | Uint8Array,
|
|
|
|
{ algorithm = DEFAULT_ALGORITHM }: ETagOptions,
|
|
|
|
) {
|
|
|
|
// a short circuit for zero length entities
|
|
|
|
if (entity.length === 0) {
|
|
|
|
return `0-47DEQpj8HBSa+/TImW+5JCeuQeR`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof entity === "string") {
|
|
|
|
entity = encoder.encode(entity);
|
|
|
|
}
|
|
|
|
|
|
|
|
const hash = base64Encode(await crypto.subtle.digest(algorithm, entity))
|
|
|
|
.substring(0, 27);
|
|
|
|
|
|
|
|
return `${entity.length.toString(16)}-${hash}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function calcFileInfo(
|
|
|
|
fileInfo: FileInfo,
|
|
|
|
{ algorithm = DEFAULT_ALGORITHM }: ETagOptions,
|
|
|
|
) {
|
|
|
|
if (fileInfo.mtime) {
|
|
|
|
const hash = base64Encode(
|
|
|
|
await crypto.subtle.digest(
|
|
|
|
algorithm,
|
|
|
|
encoder.encode(fileInfo.mtime.toJSON()),
|
|
|
|
),
|
|
|
|
).substring(0, 27);
|
|
|
|
return `${fileInfo.size.toString(16)}-${hash}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-16 06:17:44 +00:00
|
|
|
/**
|
2024-07-30 03:10:53 +00:00
|
|
|
* Calculate an ETag for string or `Uint8Array` entities. This returns a
|
|
|
|
* {@linkcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value | strong tag}
|
|
|
|
* of the form `"<ascii chars>"`, which guarantees the byte-for-byte equality of the resource.
|
|
|
|
*
|
|
|
|
* You can optionally set true to the `weak` option to get a weak tag.
|
2023-04-12 11:51:19 +00:00
|
|
|
*
|
2024-05-22 19:09:08 +00:00
|
|
|
* @example Usage
|
2023-04-12 11:51:19 +00:00
|
|
|
* ```ts
|
2024-07-10 06:27:34 +00:00
|
|
|
* import { eTag } from "@std/http/etag";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assert } from "@std/assert";
|
2023-04-12 11:51:19 +00:00
|
|
|
*
|
|
|
|
* const body = "hello deno!";
|
|
|
|
*
|
2024-07-10 06:27:34 +00:00
|
|
|
* const etag = await eTag(body);
|
2023-04-12 11:51:19 +00:00
|
|
|
* assert(etag);
|
|
|
|
*
|
|
|
|
* const res = new Response(body, { headers: { etag } });
|
|
|
|
* ```
|
2024-05-22 19:09:08 +00:00
|
|
|
*
|
|
|
|
* @param entity The entity to get the ETag for.
|
|
|
|
* @param options Various additional options.
|
|
|
|
* @returns The calculated ETag.
|
2023-04-12 11:51:19 +00:00
|
|
|
*/
|
2024-07-30 03:10:53 +00:00
|
|
|
export async function eTag(
|
|
|
|
entity: string | Uint8Array,
|
|
|
|
options?: ETagOptions,
|
|
|
|
): Promise<string>;
|
|
|
|
/**
|
|
|
|
* Calculate an ETag for file information entity. This returns a
|
|
|
|
* {@linkcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#w | weak tag}
|
|
|
|
* of the form `W\"<ascii chars>"`, which guarantees the equivalence of the resource,
|
|
|
|
* not the byte-for-byte equality.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { eTag } from "@std/http/etag";
|
|
|
|
* import { assert } from "@std/assert";
|
|
|
|
*
|
|
|
|
* const fileInfo = await Deno.stat("README.md");
|
|
|
|
*
|
|
|
|
* const etag = await eTag(fileInfo);
|
|
|
|
* assert(etag);
|
|
|
|
*
|
|
|
|
* const body = (await Deno.open("README.md")).readable;
|
|
|
|
*
|
|
|
|
* const res = new Response(body, { headers: { etag } });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param entity The entity to get the ETag for.
|
|
|
|
* @param options Various additional options.
|
|
|
|
* @returns The calculated ETag.
|
|
|
|
*/
|
|
|
|
export async function eTag(
|
|
|
|
entity: FileInfo,
|
|
|
|
options?: ETagOptions,
|
|
|
|
): Promise<string | undefined>;
|
2024-07-10 06:27:34 +00:00
|
|
|
export async function eTag(
|
2024-07-30 03:40:08 +00:00
|
|
|
entity: string | Uint8Array | FileInfo,
|
2023-04-12 11:51:19 +00:00
|
|
|
options: ETagOptions = {},
|
|
|
|
): Promise<string | undefined> {
|
|
|
|
const weak = options.weak ?? isFileInfo(entity);
|
|
|
|
const tag =
|
|
|
|
await (isFileInfo(entity)
|
|
|
|
? calcFileInfo(entity, options)
|
|
|
|
: calcEntity(entity, options));
|
|
|
|
|
|
|
|
return tag ? weak ? `W/"${tag}"` : `"${tag}"` : undefined;
|
|
|
|
}
|
|
|
|
|
2024-08-21 04:50:18 +00:00
|
|
|
const STAR_REGEXP = /^\s*\*\s*$/;
|
|
|
|
const COMMA_REGEXP = /\s*,\s*/;
|
|
|
|
|
2023-04-12 11:51:19 +00:00
|
|
|
/** A helper function that takes the value from the `If-Match` header and a
|
|
|
|
* calculated etag for the target. By using strong comparison, return `true` if
|
|
|
|
* the values match, otherwise `false`.
|
|
|
|
*
|
|
|
|
* See MDN's [`If-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match)
|
|
|
|
* article for more information on how to use this function.
|
|
|
|
*
|
2024-05-22 19:09:08 +00:00
|
|
|
* @example Usage
|
|
|
|
* ```ts no-eval
|
2023-04-12 11:51:19 +00:00
|
|
|
* import {
|
2024-07-10 06:27:34 +00:00
|
|
|
* eTag,
|
2023-04-12 11:51:19 +00:00
|
|
|
* ifMatch,
|
2024-04-29 02:57:30 +00:00
|
|
|
* } from "@std/http/etag";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assert } from "@std/assert";
|
2023-04-12 11:51:19 +00:00
|
|
|
*
|
|
|
|
* const body = "hello deno!";
|
|
|
|
*
|
2023-07-21 03:29:01 +00:00
|
|
|
* Deno.serve(async (req) => {
|
2023-04-12 11:51:19 +00:00
|
|
|
* const ifMatchValue = req.headers.get("if-match");
|
2024-07-10 06:27:34 +00:00
|
|
|
* const etag = await eTag(body);
|
2023-04-12 11:51:19 +00:00
|
|
|
* assert(etag);
|
|
|
|
* if (!ifMatchValue || ifMatch(ifMatchValue, etag)) {
|
|
|
|
* return new Response(body, { status: 200, headers: { etag } });
|
|
|
|
* } else {
|
|
|
|
* return new Response(null, { status: 412, statusText: "Precondition Failed"});
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
* ```
|
2024-05-22 19:09:08 +00:00
|
|
|
*
|
|
|
|
* @param value the If-Match header value.
|
|
|
|
* @param etag the ETag to check against.
|
|
|
|
* @returns whether or not the parameters match.
|
2023-04-12 11:51:19 +00:00
|
|
|
*/
|
|
|
|
export function ifMatch(
|
|
|
|
value: string | null,
|
|
|
|
etag: string | undefined,
|
|
|
|
): boolean {
|
|
|
|
// Weak tags cannot be matched and return false.
|
|
|
|
if (!value || !etag || etag.startsWith("W/")) {
|
|
|
|
return false;
|
|
|
|
}
|
2024-08-21 04:50:18 +00:00
|
|
|
if (STAR_REGEXP.test(value)) {
|
2023-04-12 11:51:19 +00:00
|
|
|
return true;
|
|
|
|
}
|
2024-08-21 04:50:18 +00:00
|
|
|
const tags = value.split(COMMA_REGEXP);
|
2023-04-12 11:51:19 +00:00
|
|
|
return tags.includes(etag);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** A helper function that takes the value from the `If-None-Match` header and
|
|
|
|
* a calculated etag for the target entity and returns `false` if the etag for
|
|
|
|
* the entity matches the supplied value, otherwise `true`.
|
|
|
|
*
|
|
|
|
* See MDN's [`If-None-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
|
|
|
|
* article for more information on how to use this function.
|
|
|
|
*
|
2024-05-22 19:09:08 +00:00
|
|
|
* @example Usage
|
|
|
|
* ```ts no-eval
|
2023-04-12 11:51:19 +00:00
|
|
|
* import {
|
2024-07-10 06:27:34 +00:00
|
|
|
* eTag,
|
2023-04-12 11:51:19 +00:00
|
|
|
* ifNoneMatch,
|
2024-04-29 02:57:30 +00:00
|
|
|
* } from "@std/http/etag";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assert } from "@std/assert";
|
2023-04-12 11:51:19 +00:00
|
|
|
*
|
|
|
|
* const body = "hello deno!";
|
|
|
|
*
|
2023-07-21 03:29:01 +00:00
|
|
|
* Deno.serve(async (req) => {
|
2023-04-12 11:51:19 +00:00
|
|
|
* const ifNoneMatchValue = req.headers.get("if-none-match");
|
2024-07-10 06:27:34 +00:00
|
|
|
* const etag = await eTag(body);
|
2023-04-12 11:51:19 +00:00
|
|
|
* assert(etag);
|
|
|
|
* if (!ifNoneMatch(ifNoneMatchValue, etag)) {
|
|
|
|
* return new Response(null, { status: 304, headers: { etag } });
|
|
|
|
* } else {
|
|
|
|
* return new Response(body, { status: 200, headers: { etag } });
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
* ```
|
2024-05-22 19:09:08 +00:00
|
|
|
*
|
|
|
|
* @param value the If-None-Match header value.
|
|
|
|
* @param etag the ETag to check against.
|
|
|
|
* @returns whether or not the parameters do not match.
|
2023-04-12 11:51:19 +00:00
|
|
|
*/
|
|
|
|
export function ifNoneMatch(
|
|
|
|
value: string | null,
|
|
|
|
etag: string | undefined,
|
|
|
|
): boolean {
|
|
|
|
if (!value || !etag) {
|
|
|
|
return true;
|
|
|
|
}
|
2024-08-21 04:50:18 +00:00
|
|
|
if (STAR_REGEXP.test(value)) {
|
2023-04-12 11:51:19 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
etag = etag.startsWith("W/") ? etag.slice(2) : etag;
|
2024-08-21 04:50:18 +00:00
|
|
|
const tags = value.split(COMMA_REGEXP).map((tag) =>
|
2023-04-12 11:51:19 +00:00
|
|
|
tag.startsWith("W/") ? tag.slice(2) : tag
|
|
|
|
);
|
|
|
|
return !tags.includes(etag);
|
|
|
|
}
|