From d45cf57ec90d64e439549bfb909ac70bf3306d25 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Sat, 19 Jul 2025 18:17:41 -0500 Subject: [PATCH] Support range requests --- .changeset/proud-bobcats-trade.md | 5 +++++ package.json | 2 ++ pnpm-lock.yaml | 26 ++++++++++++++++++++++++++ src/blossom.ts | 11 ++++++++--- src/helpers/http.ts | 3 ++- src/index.ts | 19 +++++++++++++++++-- 6 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 .changeset/proud-bobcats-trade.md diff --git a/.changeset/proud-bobcats-trade.md b/.changeset/proud-bobcats-trade.md new file mode 100644 index 0000000..0cac334 --- /dev/null +++ b/.changeset/proud-bobcats-trade.md @@ -0,0 +1,5 @@ +--- +"nsite-gateway": minor +--- + +Support range requests diff --git a/package.json b/package.json index 2fadff1..30f7450 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "keyv": "^5.4.0", "koa": "^2.16.1", "koa-morgan": "^1.0.1", + "koa-range": "^0.3.0", "koa-send": "^5.0.1", "koa-static": "^5.0.0", "mime": "^4.0.7", @@ -46,6 +47,7 @@ "@types/follow-redirects": "^1.14.4", "@types/koa": "^2.15.0", "@types/koa-morgan": "^1.0.8", + "@types/koa-range": "^0.3.5", "@types/koa-send": "^4.1.6", "@types/koa-static": "^4.0.4", "@types/koa__cors": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43e7ec8..519775e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: koa-morgan: specifier: ^1.0.1 version: 1.0.1 + koa-range: + specifier: ^0.3.0 + version: 0.3.0 koa-send: specifier: ^5.0.1 version: 5.0.1 @@ -87,6 +90,9 @@ importers: '@types/koa-morgan': specifier: ^1.0.8 version: 1.0.8 + '@types/koa-range': + specifier: ^0.3.5 + version: 0.3.5 '@types/koa-send': specifier: ^4.1.6 version: 4.1.6 @@ -639,6 +645,9 @@ packages: '@types/koa-morgan@1.0.8': resolution: {integrity: sha512-2GredUi+iA3V0XrbzdsOAYgwj4F6+FnN+f5YjoKjessIE2lrMkqnc06YQQnzbMG75hRsXjyD+p6d5vlI70s1vg==} + '@types/koa-range@0.3.5': + resolution: {integrity: sha512-DvbLgWVctu3k0dnuQsb2If7ROdYvUK+oGOJTxFviQjfrMjaewZYM1IPYoH1yZ2zhcpD+hKzBiGi5DMOj0kLAQg==} + '@types/koa-send@4.1.6': resolution: {integrity: sha512-vgnNGoOJkx7FrF0Jl6rbK1f8bBecqAchKpXtKuXzqIEdXTDO6dsSTjr+eZ5m7ltSjH4K/E7auNJEQCAd0McUPA==} @@ -1287,6 +1296,10 @@ packages: koa-morgan@1.0.1: resolution: {integrity: sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==} + koa-range@0.3.0: + resolution: {integrity: sha512-Ich3pCz6RhtbajYXRWjIl6O5wtrLs6kE3nkXc9XmaWe+MysJyZO7K4L3oce1Jpg/iMgCbj+5UCiMm/rqVtcDIg==} + engines: {node: '>=7'} + koa-send@5.0.1: resolution: {integrity: sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==} engines: {node: '>= 8'} @@ -1751,6 +1764,9 @@ packages: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} + stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2415,6 +2431,10 @@ snapshots: '@types/koa': 2.15.0 '@types/morgan': 1.9.10 + '@types/koa-range@0.3.5': + dependencies: + '@types/koa': 2.15.0 + '@types/koa-send@4.1.6': dependencies: '@types/koa': 2.15.0 @@ -3130,6 +3150,10 @@ snapshots: transitivePeerDependencies: - supports-color + koa-range@0.3.0: + dependencies: + stream-slice: 0.1.2 + koa-send@5.0.1: dependencies: debug: 4.4.1(supports-color@5.5.0) @@ -3671,6 +3695,8 @@ snapshots: statuses@1.5.0: {} + stream-slice@0.1.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 diff --git a/src/blossom.ts b/src/blossom.ts index cb26c3f..ac977bc 100644 --- a/src/blossom.ts +++ b/src/blossom.ts @@ -29,8 +29,12 @@ export async function findBlobURLs(sha256: string, servers: string[]): Promise { +/** Downloads a file from multiple servers with optional range support */ +export async function streamBlob( + sha256: string, + servers: string[], + headers?: Record, +): Promise { if (servers.length === 0) return undefined; // First find all available URLs @@ -49,7 +53,7 @@ export async function streamBlob(sha256: string, servers: string[]): Promise MAX_FILE_SIZE) throw new Error("File too large"); + // Accept both 200 (full content) and 206 (partial content) status codes if (response.statusCode >= 200 && response.statusCode < 300) return response; } catch (error) { if (res) res.resume(); diff --git a/src/helpers/http.ts b/src/helpers/http.ts index aa79849..adce8ad 100644 --- a/src/helpers/http.ts +++ b/src/helpers/http.ts @@ -4,7 +4,7 @@ const { http, https } = followRedirects; import agent from "../proxy.js"; -export function makeRequestWithAbort(url: URL, controller: AbortController) { +export function makeRequestWithAbort(url: URL, controller: AbortController, headers?: Record) { return new Promise((res, rej) => { controller.signal.addEventListener("abort", () => rej(new Error("Aborted"))); @@ -13,6 +13,7 @@ export function makeRequestWithAbort(url: URL, controller: AbortController) { { signal: controller.signal, agent, + headers, }, (response) => { res(response); diff --git a/src/index.ts b/src/index.ts index ed8f384..85b8164 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import fs from "node:fs"; import { fileURLToPath } from "node:url"; import mime from "mime"; import morgan from "koa-morgan"; +import range from "koa-range"; import { npubEncode } from "nostr-tools/nip19"; import { nip19 } from "nostr-tools"; @@ -38,6 +39,9 @@ morgan.token("host", (req) => req.headers.host ?? ""); app.use(morgan(":method :host:url :status :response-time ms - :res[content-length]")); +// add range request support +app.use(range); + // set CORS headers app.use( cors({ @@ -114,7 +118,13 @@ app.use(async (ctx, next) => { if (servers.length === 0) throw new Error("Failed to find blossom servers"); try { - const res = await streamBlob(event.sha256, servers); + // Prepare headers for range requests + const requestHeaders: Record = {}; + if (ctx.headers.range) { + requestHeaders.range = ctx.headers.range; + } + + const res = await streamBlob(event.sha256, servers, requestHeaders); if (!res) { ctx.status = 502; ctx.body = `Failed to find blob\npath: ${event.path}\nsha256: ${event.sha256}\nservers: ${servers.join(", ")}`; @@ -128,6 +138,10 @@ app.use(async (ctx, next) => { // pass headers along if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]); + // handle range response headers + if (res.headers["accept-ranges"]) ctx.set("accept-ranges", res.headers["accept-ranges"]); + if (res.headers["content-range"]) ctx.set("content-range", res.headers["content-range"]); + // set Onion-Location header if (ONION_HOST) { const url = new URL(ONION_HOST); @@ -140,7 +154,8 @@ app.use(async (ctx, next) => { ctx.set("Cache-Control", "public, max-age=3600"); ctx.set("Last-Modified", res.headers["last-modified"] || new Date(event.created_at * 1000).toUTCString()); - ctx.status = 200; + // set appropriate status code (206 for partial content, 200 for full content) + ctx.status = res.statusCode === 206 ? 206 : 200; ctx.body = res; return; } catch (error) {