1+ import { createHmac } from 'node:crypto'
12import { createReadStream , createWriteStream , promises as fs } from 'node:fs'
23import http from 'node:http'
34import { tmpdir } from 'node:os'
@@ -7,6 +8,9 @@ import { ListResponse } from './backend/list.ts'
78import { decodeMetadata , encodeMetadata , METADATA_HEADER_INTERNAL } from './metadata.ts'
89import { isNodeError , Logger } from './util.ts'
910
11+ const API_URL_PATH = / \/ a p i \/ v 1 \/ s i t e s \/ (?< site_id > [ ^ / ] + ) \/ b l o b s \/ ? (?< key > [ ^ ? ] * ) /
12+ const DEFAULT_STORE = 'production'
13+
1014interface BlobsServerOptions {
1115 /**
1216 * Whether debug-level information should be logged, such as internal errors
@@ -44,6 +48,7 @@ export class BlobsServer {
4448 private port : number
4549 private server ?: http . Server
4650 private token ?: string
51+ private tokenHash : string
4752
4853 constructor ( { debug, directory, logger, port, token } : BlobsServerOptions ) {
4954 this . address = ''
@@ -52,6 +57,9 @@ export class BlobsServer {
5257 this . logger = logger ?? console . log
5358 this . port = port || 0
5459 this . token = token
60+ this . tokenHash = createHmac ( 'sha256' , Math . random . toString ( ) )
61+ . update ( token ?? Math . random . toString ( ) )
62+ . digest ( 'hex' )
5563 }
5664
5765 logDebug ( ...message : unknown [ ] ) {
@@ -222,10 +230,23 @@ export class BlobsServer {
222230 }
223231
224232 handleRequest ( req : http . IncomingMessage , res : http . ServerResponse ) {
225- if ( ! this . validateAccess ( req ) ) {
233+ if ( ! req . url || ! this . validateAccess ( req ) ) {
226234 return this . sendResponse ( req , res , 403 )
227235 }
228236
237+ const apiURLMatch = req . url . match ( API_URL_PATH )
238+
239+ // If this matches an API URL, return a signed URL.
240+ if ( apiURLMatch ) {
241+ const fullURL = new URL ( req . url , this . address )
242+ const storeName = fullURL . searchParams . get ( 'context' ) ?? DEFAULT_STORE
243+ const key = apiURLMatch . groups ?. key as string
244+ const siteID = apiURLMatch . groups ?. site_id as string
245+ const url = `${ this . address } /${ siteID } /${ storeName } /${ key } ?signature=${ this . tokenHash } `
246+
247+ return this . sendResponse ( req , res , 200 , JSON . stringify ( { url } ) )
248+ }
249+
229250 switch ( req . method ) {
230251 case 'DELETE' :
231252 return this . delete ( req , res )
@@ -294,11 +315,22 @@ export class BlobsServer {
294315 const { authorization = '' } = req . headers
295316 const parts = authorization . split ( ' ' )
296317
297- if ( parts . length !== 2 || parts [ 0 ] . toLowerCase ( ) !== 'bearer' ) {
318+ if ( parts . length === 2 || ( parts [ 0 ] . toLowerCase ( ) === 'bearer' && parts [ 1 ] === this . token ) ) {
319+ return true
320+ }
321+
322+ if ( ! req . url ) {
298323 return false
299324 }
300325
301- return parts [ 1 ] === this . token
326+ const url = new URL ( req . url , this . address )
327+ const signature = url . searchParams . get ( 'signature' )
328+
329+ if ( signature === this . tokenHash ) {
330+ return true
331+ }
332+
333+ return false
302334 }
303335
304336 /**
0 commit comments