From 65b42b33b0148b151b8efee10c1b11ed714fefdb Mon Sep 17 00:00:00 2001 From: florian <> Date: Sun, 24 Mar 2024 00:28:19 +0100 Subject: [PATCH] chore: split code to a few files --- src/auth.ts | 71 +++++++++++++++++++++++++++++++++ src/index.ts | 110 ++------------------------------------------------- src/types.ts | 19 +++++++++ src/utils.ts | 18 +++++++++ 4 files changed, 111 insertions(+), 107 deletions(-) create mode 100644 src/auth.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..4e950f9 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,71 @@ +import { HTTPException } from 'hono/http-exception'; +import type { AuthType } from './types'; +import { nip19, validateEvent, verifyEvent } from 'nostr-tools'; +import type { Event } from 'nostr-tools'; +import dayjs from 'dayjs'; + +export const getAuth = (authHeader?: string) => { + if (!authHeader?.startsWith('Nostr ')) throw new HTTPException(401, { message: 'Missing Nostr: Auth Scheme.' }); + + const authEvent = authHeader ? (JSON.parse(atob(authHeader.replace(/^Nostr\s/i, ''))) as Event) : undefined; + + if (!authEvent) { + throw new HTTPException(400, { message: 'Nostr-Auth Token is invalid.' }); + } + + if (!validateEvent(authEvent)) { + throw new HTTPException(400, { message: 'Auth event is not valid.' }); + } + if (!verifyEvent(authEvent)) { + throw new HTTPException(400, { + message: 'Auth event signature is not valid.', + }); + } + + if (authEvent.kind !== 24242) { + throw new HTTPException(400, { message: 'Unexpected auth kind' }); + } + + const type = authEvent.tags.find((t) => t[0] === 't')?.[1] as AuthType; + if (!type) { + throw new HTTPException(400, { message: 'Auth missing type' }); + } + + const now = dayjs().unix(); + const expiration = authEvent.tags.find((t) => t[0] === 'expiration')?.[1]; + if (!expiration) { + throw new HTTPException(400, { message: 'Auth missing expiration' }); + } + + if (parseInt(expiration) < now) { + throw new HTTPException(400, { message: 'Auth expired' }); + } + + return { + event: authEvent, + type, + expiration, + }; +}; + +export const checkAuth = async ( + auth: { event: Event; type: AuthType }, + expectedType: AuthType, + allowedNubs: string, + kv: KVNamespace +) => { + if (auth.type !== expectedType) { + throw new HTTPException(400, { message: 'Auth type not allowed.' }); + } + + const key = `auth:${auth.type}:${auth.event.id}`; + if ((await kv.get(key)) != null) { + throw new HTTPException(400, { message: 'Token was used before.' }); + } + await kv.put(key, 'used', { expirationTtl: 60 * 60 * 24 }); // 24h TTL for expired tokens + + const allowedPubKeys = allowedNubs.split(',').map((n) => nip19.decode(n).data as string); + if (allowedPubKeys.length > 0 && !allowedPubKeys.some((pk) => pk == auth.event.pubkey)) { + throw new HTTPException(403, { message: 'Public key not authorized.' }); + } +}; diff --git a/src/index.ts b/src/index.ts index e70eed9..57af802 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,102 +5,10 @@ import { html } from 'hono/html'; import { HTTPException } from 'hono/http-exception'; import { logger } from 'hono/logger'; import { stream } from 'hono/streaming'; -import { nip19, validateEvent, verifyEvent } from 'nostr-tools'; import dayjs from 'dayjs'; -import type { Event } from 'nostr-tools'; - -type Bindings = { - BLOSSOM_BUCKET: R2Bucket; - KV_BLOSSOM: KVNamespace; - PUBLIC_URL: string; - ALLOWED_NPUBS: string; -}; - -type BlobData = { - created: number; - type?: string; - size: number; -}; - -type BlobDescriptor = BlobData & { - sha256: string; - url: string; -}; - -type AuthType = 'upload' | 'delete'; - -const getAuth = (authHeader?: string) => { - if (!authHeader?.startsWith('Nostr ')) throw new HTTPException(401, { message: 'Missing Nostr: Auth Scheme.' }); - - const authEvent = authHeader ? (JSON.parse(atob(authHeader.replace(/^Nostr\s/i, ''))) as Event) : undefined; - - if (!authEvent) { - throw new HTTPException(400, { message: 'Nostr-Auth Token is invalid.' }); - } - - if (!validateEvent(authEvent)) { - throw new HTTPException(400, { message: 'Auth event is not valid.' }); - } - if (!verifyEvent(authEvent)) { - throw new HTTPException(400, { - message: 'Auth event signature is not valid.', - }); - } - - if (authEvent.kind !== 24242) { - throw new HTTPException(400, { message: 'Unexpected auth kind' }); - } - - const type = authEvent.tags.find((t) => t[0] === 't')?.[1] as AuthType; - if (!type) { - throw new HTTPException(400, { message: 'Auth missing type' }); - } - - const now = dayjs().unix(); - const expiration = authEvent.tags.find((t) => t[0] === 'expiration')?.[1]; - if (!expiration) { - throw new HTTPException(400, { message: 'Auth missing expiration' }); - } - - if (parseInt(expiration) < now) { - throw new HTTPException(400, { message: 'Auth expired' }); - } - - return { - event: authEvent, - type, - expiration, - }; -}; - -const checkAuth = async ( - auth: { event: Event; type: AuthType }, - expectedType: AuthType, - allowedNubs: string, - kv: KVNamespace -) => { - if (auth.type !== expectedType) { - throw new HTTPException(400, { message: 'Auth type not allowed.' }); - } - - const key = `auth:${auth.type}:${auth.event.id}`; - if ((await kv.get(key)) != null) { - throw new HTTPException(400, { message: 'Token was used before.' }); - } - await kv.put(key, 'used', { expirationTtl: 60 * 60 * 24 }); // 24h TTL for expired tokens - - const allowedPubKeys = allowedNubs.split(',').map((n) => nip19.decode(n).data as string); - if (allowedPubKeys.length > 0 && !allowedPubKeys.some((pk) => pk == auth.event.pubkey)) { - throw new HTTPException(403, { message: 'Public key not authorized.' }); - } -}; - -const parseHashFromPath = (path: string) => { - const match = path.toLowerCase().match(/\/([0-9a-f]{64})(\.[a-z]+)?/); - const hash = match && match[1]; - const ext = match && match[2]; - return { hash, ext }; -}; +import type { Bindings, BlobData, BlobDescriptor } from './types'; +import { checkAuth, getAuth } from './auth'; +import { computeContentRange, parseHashFromPath } from './utils'; const app = new Hono<{ Bindings: Bindings }>(); app.use(cors()); @@ -143,18 +51,6 @@ app.get('/list/:pubkey', async (c) => { app.get('/list', cache({ cacheName: 'blossom', cacheControl: 'max-age=60' })); -function computeContentRange(range: R2Range, size: number) { - const offset = 'offset' in range ? range.offset : undefined; - const length = 'length' in range ? range.length : undefined; - const suffix = 'suffix' in range ? range.suffix : undefined; - - const startOffset = typeof suffix === 'number' ? size - suffix : typeof offset === 'number' ? offset : 0; - const endOffset = typeof suffix === 'number' ? size : typeof length === 'number' ? startOffset + length : size; - - //console.log({ offset, length, suffix, startOffset, endOffset }, `bytes ${startOffset}-${endOffset - 1}/${size}`); - return `bytes ${startOffset}-${endOffset - 1}/${size}`; -} - app.put('/upload', async (c) => { const auth = getAuth(c.req.header('authorization') as string); await checkAuth(auth, 'upload', c.env.ALLOWED_NPUBS, c.env.KV_BLOSSOM); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..70d66bd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,19 @@ +export type Bindings = { + BLOSSOM_BUCKET: R2Bucket; + KV_BLOSSOM: KVNamespace; + PUBLIC_URL: string; + ALLOWED_NPUBS: string; +}; + +export type BlobData = { + created: number; + type?: string; + size: number; +}; + +export type BlobDescriptor = BlobData & { + sha256: string; + url: string; +}; + +export type AuthType = 'upload' | 'delete'; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..63a2c34 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +export const parseHashFromPath = (path: string) => { + const match = path.toLowerCase().match(/\/([0-9a-f]{64})(\.[a-z]+)?/); + const hash = match && match[1]; + const ext = match && match[2]; + return { hash, ext }; +}; + +export const computeContentRange = (range: R2Range, size: number) => { + const offset = 'offset' in range ? range.offset : undefined; + const length = 'length' in range ? range.length : undefined; + const suffix = 'suffix' in range ? range.suffix : undefined; + + const startOffset = typeof suffix === 'number' ? size - suffix : typeof offset === 'number' ? offset : 0; + const endOffset = typeof suffix === 'number' ? size : typeof length === 'number' ? startOffset + length : size; + + //console.log({ offset, length, suffix, startOffset, endOffset }, `bytes ${startOffset}-${endOffset - 1}/${size}`); + return `bytes ${startOffset}-${endOffset - 1}/${size}`; +};