feat: Added hash check (x tag) for delete and upload
This commit is contained in:
parent
d7caabe33c
commit
dedea80914
10
package.json
10
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
44
src/index.ts
44
src/index.ts
@ -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));
|
||||
|
@ -16,4 +16,4 @@ export type BlobDescriptor = BlobData & {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type AuthType = 'upload' | 'delete';
|
||||
export type AuthType = 'upload' | 'delete' | 'list';
|
||||
|
Loading…
Reference in New Issue
Block a user