feat: Added hash check (x tag) for delete and upload

This commit is contained in:
florian 2024-05-19 12:03:47 +02:00
parent d7caabe33c
commit dedea80914
4 changed files with 49 additions and 10 deletions

View File

@ -4,12 +4,12 @@
"deploy": "wrangler deploy --minify src/index.ts"
},
"dependencies": {
"dayjs": "^1.11.10",
"hono": "^4.1.3",
"nostr-tools": "^2.3.2"
"dayjs": "^1.11.11",
"hono": "^4.3.7",
"nostr-tools": "^2.5.2"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0",
"wrangler": "^3.32.0"
"@cloudflare/workers-types": "^4.20240512.0",
"wrangler": "^3.57.0"
}
}

View File

@ -31,6 +31,8 @@ export const getAuth = (authHeader?: string) => {
throw new HTTPException(400, { message: 'Auth missing type' });
}
const objectHash = authEvent.tags.find((t) => t[0] === 'x')?.[1] as AuthType;
const now = dayjs().unix();
const expiration = authEvent.tags.find((t) => t[0] === 'expiration')?.[1];
if (!expiration) {
@ -45,6 +47,7 @@ export const getAuth = (authHeader?: string) => {
event: authEvent,
type,
expiration,
objectHash
};
};

View File

@ -1,6 +1,6 @@
import { cache } from 'hono/cache';
import { cors } from 'hono/cors';
import { Hono } from 'hono';
import { Hono, type Next } from 'hono';
import { html } from 'hono/html';
import { HTTPException } from 'hono/http-exception';
import { logger } from 'hono/logger';
@ -24,8 +24,15 @@ app.get('/', (c) =>
);
app.get('/list/:pubkey', async (c) => {
const auth = getAuth(c.req.header('authorization') as string);
await checkAuth(auth, 'list', c.env.ALLOWED_NPUBS, c.env.KV_BLOSSOM);
const pubkey = c.req.param('pubkey');
if (auth.event.pubkey != pubkey) {
throw new HTTPException(403, { message: 'pubkey from URL does not match the auth event.' });
}
const keyList = await c.env.KV_BLOSSOM.list({ prefix: pubkey + ':' });
// TODO paging is needed for >1000 blobs
@ -80,6 +87,15 @@ app.put('/upload', async (c) => {
const hash = Array.from(new Uint8Array(await digestStream.digest))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
if (auth.objectHash) {
// currently not all clients support x tag in auth
if (auth.objectHash.toLocaleLowerCase() !== hash.toLocaleLowerCase()) {
c.env.BLOSSOM_BUCKET.delete(blobId); // delete the blob if hash does not match
throw new HTTPException(400, { message: 'Object hash does not match the uploaded blob.' });
}
}
await c.env.KV_BLOSSOM.put('blob:' + hash, blobId); // R2 IDs for each blob hash
// Record the blob descriptors for the list operation
@ -109,6 +125,10 @@ app.delete('*', async (c) => {
throw new HTTPException(400, { message: 'Invalid path, hash missing' });
}
if (hash != auth.objectHash) {
throw new HTTPException(400, { message: 'Object hash is not signed in auth event' });
}
if (!(await c.env.KV_BLOSSOM.get(pubkey + ':' + hash, { cacheTtl: 3600 }))) {
throw new HTTPException(403, { message: 'Not allowed to delete the blob.' });
}
@ -130,7 +150,18 @@ app.delete('*', async (c) => {
}
});
app.get(cache({ cacheName: 'blossom', cacheControl: 'max-age=604800' }));
const getCache = cache({
cacheName: 'blossom4',
cacheControl: 'max-age=604800',
vary: ['Access-Control-Request-Headers', 'Accept-Encoding', 'Content-Range'],
});
app.use(async (c, next: Next) => {
if (c.req.method == 'GET') return getCache(c, next);
// HEAD, PUT, POST
return next();
});
app.get('*', async (c) => {
const { hash } = parseHashFromPath(c.req.path);
@ -147,7 +178,12 @@ app.get('*', async (c) => {
if (!blob) {
throw new HTTPException(404, { message: 'Blob hash not found.' });
}
// c.header('Accept-Ranges', 'bytes');
c.header('Accept-Ranges', 'bytes');
c.header('Content-Length', `${blob.size}`);
c.header('Last-Modified', blob.uploaded.toUTCString());
c.header('Etag', blob.etag);
c.header('Content-Type', blob.httpMetadata?.contentType || 'application/octet-stream');
return c.body(null, 204);
} else {
const blob = await c.env.BLOSSOM_BUCKET.get(key, {
@ -161,7 +197,7 @@ app.get('*', async (c) => {
c.header('Content-Encoding', blob.httpMetadata?.contentEncoding);
c.header('Content-Disposition', blob.httpMetadata?.contentDisposition);
c.header('X-Content-Digest', `SHA-256=${hash}`);
// c.header('Accept-Ranges', 'bytes');
c.header('Accept-Ranges', 'bytes');
c.header('ETag', blob.httpEtag);
if (c.req.header('Range') && blob.range) {
c.header('Content-Range', computeContentRange(blob.range, blob.size));

View File

@ -16,4 +16,4 @@ export type BlobDescriptor = BlobData & {
url: string;
};
export type AuthType = 'upload' | 'delete';
export type AuthType = 'upload' | 'delete' | 'list';