/**
* Entity tags and conditional requests (specification ยง7).
*/
import { createHash } from "node:crypto";
import { LinoHttpError } from "./problem.js";
/**
* Compute the strong entity tag of an encoded representation.
*
* @param {string} body - Encoded representation
* @returns {string} Quoted hexadecimal SHA-256
*/
export function computeETag(body) {
const digest = createHash("sha256").update(body, "utf-8").digest("hex");
return `"${digest}"`;
}
/**
* Split an `If-Match` or `If-None-Match` header into entity tags.
*
* @param {string} [headerValue] - Raw header value
* @returns {string[]} Entity tags, with any weak prefix removed
*/
export function parseETagList(headerValue) {
if (!headerValue) {
return [];
}
return headerValue
.split(",")
.map((tag) => tag.trim().replace(/^W\//, ""))
.filter(Boolean);
}
/**
* Test whether an entity tag list matches the current tag.
*
* @param {string} [headerValue] - Raw `If-Match` / `If-None-Match` value
* @param {string} currentETag - Entity tag of the current representation
* @returns {boolean} True when the list matches
*/
export function etagMatches(headerValue, currentETag) {
const tags = parseETagList(headerValue);
return tags.includes("*") || tags.includes(currentETag);
}
/**
* Evaluate the conditional request headers of a request.
*
* @param {object} headers - Request headers, lower-cased names
* @param {string} method - HTTP method
* @param {string} currentETag - Entity tag of the current representation
* @param {object} [options] - Behaviour switches
* @param {boolean} [options.requirePrecondition] - Demand `If-Match` on unsafe methods
* @returns {{notModified: boolean}} Whether the response should be `304`
* @throws {LinoHttpError} `412` on a failed `If-Match`, `428` when one is required
*/
export function evaluatePreconditions(
headers,
method,
currentETag,
options = {},
) {
const safe = method === "GET" || method === "HEAD";
const ifMatch = headers["if-match"];
const ifNoneMatch = headers["if-none-match"];
if (!safe) {
if (ifMatch !== undefined) {
if (!etagMatches(ifMatch, currentETag)) {
throw new LinoHttpError(
412,
"The entity tag in If-Match does not match the current representation",
);
}
} else if (options.requirePrecondition) {
throw new LinoHttpError(
428,
"This request requires an If-Match precondition",
);
}
}
if (ifNoneMatch !== undefined && etagMatches(ifNoneMatch, currentETag)) {
if (safe) {
return { notModified: true };
}
throw new LinoHttpError(
412,
"The entity tag in If-None-Match matches the current representation",
);
}
return { notModified: false };
}