diff --git a/lib/http.ts b/lib/http.ts new file mode 100644 index 0000000000000000000000000000000000000000..1802a440af89e9d1a3de68bff553467210fc46bd --- /dev/null +++ b/lib/http.ts @@ -0,0 +1,42 @@ +/** + * parses the [`Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests) + * header, returning `null` if it is invalid. the `fileSize` + */ +export function parseRangeHeader(rangeHeader: string, fileSize: number): Range[] | null { + const [unit, ranges] = rangeHeader.split("="); + if (!ranges) return null; + if (unit !== "bytes") return null; + + const result: Range[] = []; + const rangeParts = ranges.split(","); + + for (const range of rangeParts) { + const split = range.split("-"); + const [start, end] = split; + if (!start || !end) return null; + if (start === "" && end === "") return null; + result.push({ + start: start === "" ? fileSize - +end : +start, + end: end === "" ? fileSize - 1 : +end, + }); + } + + // validate that ranges do not intersect + result.sort((a, b) => a.start - b.start); + for (let i = 1; i < result.length; i++) { + if (UNWRAP(result[i]).start <= UNWRAP(result[i - 1]).end) { + return null; + } + } + + return result; +} + +export interface Range { + /** inclusive */ + start: number; + /** inclusive */ + end: number; +} + +import { UNWRAP } from "./assert.ts"; diff --git a/src/file-viewer/backend.tsx b/src/file-viewer/backend.tsx index cb84e7d1109156182990b797a24385fcb4d7ace6..32c9028e12e9a1aa1752506c37a4bc8ecf12c9ae 100644 --- a/src/file-viewer/backend.tsx +++ b/src/file-viewer/backend.tsx @@ -161,7 +161,7 @@ app.get("/file/*", async (c, next) => { // Range requests // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests if (rangeHeader) { - const ranges = parseRange(rangeHeader, file.size); + const ranges = http.parseRangeHeader(rangeHeader, file.size); if (ranges?.[0] && ranges.length === 1) { start = ranges[0][0]; end = ranges[0][1]; @@ -216,39 +216,6 @@ function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) { return new Date(ifRangeHeader) < file.date; } -/** The end is inclusive */ -type Ranges = Array<[start: number, end: number]>; - -function parseRange(rangeHeader: string, fileSize: number): Ranges | null { - const [unit, ranges] = rangeHeader.split("="); - if (!ranges) return null; - if (unit !== "bytes") return null; - - const result: Array<[start: number, end: number]> = []; - const rangeParts = ranges.split(","); - - for (const range of rangeParts) { - const split = range.split("-"); - const [start, end] = split; - if (!start || !end) return null; - if (start === "" && end === "") return null; - const parsedRange: [number, number] = [ - start === "" ? fileSize - +end : +start, - end === "" ? fileSize - 1 : +end, - ]; - result.push(parsedRange); - } - - // Validate that ranges do not intersect - result.sort((a, b) => a[0] - b[0]); - for (let i = 1; i < result.length; i++) { - if (UNWRAP(result[i])[0] <= UNWRAP(result[i - 1])[1]) { - return null; - } - } - - return result; -} function checkCotyledonCookie(c: Context) { const cookie = c.req.header("Cookie"); @@ -317,6 +284,7 @@ import { type Context, Hono } from "hono"; import * as render from "@clo/lib/render"; import { etagMatches, serveAsset } from "#sitegen/assets"; import * as view from "#sitegen/view"; +import * as http from "@clo/lib/http"; import * as mime from "@clo/lib/mime"; import { getForFile, requireFriendAuth } from "#src/friend-auth.ts";