diff --git a/docs/content/4.http-server.md b/docs/content/4.http-server.md index 04c4956a..adae38b1 100644 --- a/docs/content/4.http-server.md +++ b/docs/content/4.http-server.md @@ -14,7 +14,14 @@ import { createStorage } from "unstorage"; import { createStorageServer } from "unstorage/server"; const storage = createStorage(); -const storageServer = createStorageServer(storage); +const storageServer = createStorageServer(storage, { + authorize(req) { + // req: { key, type, event } + if (req.type === "read" && req.key.startsWith("private:")) { + throw new Error("Unauthorized Read"); + } + }, +}); // Alternatively we can use `storageServer.handle` as a middleware await listen(storageServer.handle); @@ -23,8 +30,7 @@ await listen(storageServer.handle); The `storageServer` is an [h3](https://github.com/unjs/h3) instance. Checkout also [listhen](https://github.com/unjs/listhen) for an elegant HTTP listener. ::alert{type="primary"} -**🛡️ Security Note:** The server is unprotected by default. You need to add your own authentication/security middleware like basic authentication. -Also consider that even with authentication, `unstorage` should not be exposed to untrusted users since it has no protection for abuse (DDOS, Filesystem escalation, etc) +**🛡️ Security Note:** Make sure to always implement `authorize` in order to protect server when it is exposed to a production environemnt. :: ## Storage Client diff --git a/src/server.ts b/src/server.ts index df27dad2..3c8767e5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,6 +2,7 @@ import type { RequestListener } from "node:http"; import { createApp, createError, + isError, readBody, eventHandler, toNodeListener, @@ -10,22 +11,58 @@ import { setResponseHeader, readRawBody, EventHandler, + H3Event, } from "h3"; import { Storage } from "./types"; import { stringify } from "./_utils"; -import { normalizeKey } from "./utils"; +import { normalizeKey, normalizeBaseKey } from "./utils"; -export interface StorageServerOptions {} +export type StorageServerRequest = { + event: H3Event; + key: string; + type: "read" | "write"; +}; + +const MethodToTypeMap = { + GET: "read", + HEAD: "read", + PUT: "write", + DELETE: "write", +} as const; + +export interface StorageServerOptions { + authorize?: (request: StorageServerRequest) => void | Promise; +} export function createH3StorageHandler( storage: Storage, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _options: StorageServerOptions = {} + opts: StorageServerOptions = {} ): EventHandler { return eventHandler(async (event) => { const method = getMethod(event); const isBaseKey = event.path.endsWith(":") || event.path.endsWith("/"); - const key = normalizeKey(event.path); + const key = isBaseKey + ? normalizeBaseKey(event.path) + : normalizeKey(event.path); + + // Authorize Request + try { + await opts.authorize?.({ + type: MethodToTypeMap[method], + event, + key, + }); + } catch (error) { + const _httpError = isError(error) + ? error + : createError({ + statusMessage: error.message, + statusCode: 401, + ...error, + }); + throw _httpError; + } // GET => getItem if (method === "GET") { diff --git a/test/server.test.ts b/test/server.test.ts index 479d796d..adc30c1a 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -7,7 +7,13 @@ import { createStorageServer } from "../src/server"; describe("server", () => { it("basic", async () => { const storage = createStorage(); - const storageServer = createStorageServer(storage); + const storageServer = createStorageServer(storage, { + authorize(req) { + if (req.type === "read" && req.key.startsWith("private:")) { + throw new Error("Unauthorized Read"); + } + }, + }); const { close, url: serverURL } = await listen(storageServer.handle, { port: { random: true }, }); @@ -30,6 +36,15 @@ describe("server", () => { expect(await fetchStorage("foo/bar", { method: "DELETE" })).toBe("OK"); expect(await fetchStorage("foo/bar/", {})).toMatchObject([]); + await expect( + fetchStorage("private/foo/bar", { method: "GET" }).catch((error) => { + throw error.data; + }) + ).rejects.toMatchObject({ + statusCode: 401, + statusMessage: "Unauthorized Read", + }); + await close(); }); });