Skip to content

Commit

Permalink
feat(server): support authorize (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Mar 7, 2023
1 parent 3cc2c48 commit fd57eed
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 8 deletions.
12 changes: 9 additions & 3 deletions docs/content/4.http-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
45 changes: 41 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { RequestListener } from "node:http";
import {
createApp,
createError,
isError,
readBody,
eventHandler,
toNodeListener,
Expand All @@ -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<void>;
}

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") {
Expand Down
17 changes: 16 additions & 1 deletion test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
Expand All @@ -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();
});
});

0 comments on commit fd57eed

Please sign in to comment.