From ce6b9a1d29e0443570138d831823ac9af83ed938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 6 Mar 2024 14:26:09 +0000 Subject: [PATCH 1/3] feat: respect `accept` header in server --- src/client.ts | 4 ++- src/server.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++++ src/server.ts | 10 ++++---- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/client.ts b/src/client.ts index 619f8be..ffcf94c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,6 +4,8 @@ import { encodeMetadata, Metadata, METADATA_HEADER_EXTERNAL, METADATA_HEADER_INT import { fetchAndRetry } from './retry.ts' import { BlobInput, Fetcher, HTTPMethod } from './types.ts' +export const SIGNED_URL_ACCEPT_HEADER = 'application/json;type=signed-url' + interface MakeStoreRequestOptions { body?: BlobInput | null consistency?: ConsistencyMode @@ -135,7 +137,7 @@ export class Client { } const res = await this.fetch(url.toString(), { - headers: { ...apiHeaders, accept: `application/json;type=signed-url` }, + headers: { ...apiHeaders, accept: SIGNED_URL_ACCEPT_HEADER }, method, }) diff --git a/src/server.test.ts b/src/server.test.ts index 68692c7..966ec68 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -366,3 +366,66 @@ test('Lists site stores', async () => { expect(stores).toStrictEqual(['coldplay', 'phoenix']) }) + +test('Returns a signed URL or the blob directly based on the request parameters', async () => { + const siteID = '9a003659-aaaa-0000-aaaa-63d3720d8621' + const token = 'some token' + const value = 'value 1' + const server1Ops: string[] = [] + const directory = await tmp.dir() + const server = new BlobsServer({ + directory: directory.path, + onRequest: ({ type }) => server1Ops.push(type), + token, + }) + + const { port } = await server.start() + const store = getStore({ + edgeURL: `http://localhost:${port}`, + name: 'my-store', + token, + siteID, + }) + + await store.set('key-1', value) + + // When reading through a legacy API endpoint, we should get a signed URL. + const res1 = await fetch(`http://localhost:${port}/api/v1/sites/${siteID}/blobs/key-1?context=site:my-store`, { + headers: { + authorization: `Bearer ${token}`, + }, + }) + const { url: url1 } = await res1.json() + const data1 = await fetch(url1) + + expect(await data1.text()).toBe(value) + + // When reading through a new API endpoint, we should get the blob data by + // default. + const res2 = await fetch(`http://localhost:${port}/api/v1/blobs/${siteID}/site:my-store/key-1`, { + headers: { + accept: 'application/json;type=signed-url', + authorization: `Bearer ${token}`, + }, + }) + const { url: url2 } = await res2.json() + const data2 = await fetch(url2) + + expect(await data2.text()).toBe(value) + + // When reading through a new API endpoint and requesting a signed URL, we + // should get one. + const res3 = await fetch(`http://localhost:${port}/api/v1/blobs/${siteID}/site:my-store/key-1`, { + headers: { + accept: 'application/json;type=signed-url', + authorization: `Bearer ${token}`, + }, + }) + const { url: url3 } = await res3.json() + const data3 = await fetch(url3) + + expect(await data3.text()).toBe(value) + + await server.stop() + await fs.rm(directory.path, { force: true, recursive: true }) +}) diff --git a/src/server.ts b/src/server.ts index d55c2f2..91a479f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,6 +8,7 @@ import stream from 'node:stream' import { promisify } from 'node:util' import { ListResponse } from './backend/list.ts' +import { SIGNED_URL_ACCEPT_HEADER } from './client.ts' import { decodeMetadata, encodeMetadata, METADATA_HEADER_INTERNAL } from './metadata.ts' import { HTTPMethod } from './types.ts' import { isNodeError, Logger } from './util.ts' @@ -137,11 +138,11 @@ export class BlobsServer { const apiMatch = this.parseAPIRequest(req) const url = apiMatch?.url ?? new URL(req.url ?? '', this.address) - if (apiMatch?.key) { + if (apiMatch?.key && apiMatch?.useSignedURL) { return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })) } - const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(url) + const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(apiMatch?.url ?? url) // If there's no root path, the request is invalid. if (!rootPath) { @@ -404,7 +405,7 @@ export class BlobsServer { siteID, storeName, url, - useSignedURL: req.headers.accept === 'application/json;type=signed-url', + useSignedURL: req.headers.accept === SIGNED_URL_ACCEPT_HEADER, } } @@ -481,9 +482,8 @@ export class BlobsServer { } const { authorization = '' } = req.headers - const parts = authorization.split(' ') - if (parts.length === 2 || (parts[0].toLowerCase() === 'bearer' && parts[1] === this.token)) { + if (authorization.toLowerCase().startsWith('bearer ') && authorization.slice('bearer '.length) === this.token) { return true } From c29cd843f6713f3bf91ced427380d37d0ba718e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 6 Mar 2024 14:26:59 +0000 Subject: [PATCH 2/3] chore: simplify test --- src/server.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server.test.ts b/src/server.test.ts index 966ec68..6ac8279 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -371,11 +371,9 @@ test('Returns a signed URL or the blob directly based on the request parameters' const siteID = '9a003659-aaaa-0000-aaaa-63d3720d8621' const token = 'some token' const value = 'value 1' - const server1Ops: string[] = [] const directory = await tmp.dir() const server = new BlobsServer({ directory: directory.path, - onRequest: ({ type }) => server1Ops.push(type), token, }) From 3222b709ed269902c3e86e2f9efaa6e090d10d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 6 Mar 2024 14:30:00 +0000 Subject: [PATCH 3/3] chore: fix test --- src/server.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/server.test.ts b/src/server.test.ts index 6ac8279..5d1dd3e 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -402,14 +402,10 @@ test('Returns a signed URL or the blob directly based on the request parameters' // default. const res2 = await fetch(`http://localhost:${port}/api/v1/blobs/${siteID}/site:my-store/key-1`, { headers: { - accept: 'application/json;type=signed-url', authorization: `Bearer ${token}`, }, }) - const { url: url2 } = await res2.json() - const data2 = await fetch(url2) - - expect(await data2.text()).toBe(value) + expect(await res2.text()).toBe(value) // When reading through a new API endpoint and requesting a signed URL, we // should get one.