diff --git a/electron/ipc/register/cloudShare.ts b/electron/ipc/register/cloudShare.ts index da40a517..a46afcbf 100644 --- a/electron/ipc/register/cloudShare.ts +++ b/electron/ipc/register/cloudShare.ts @@ -140,7 +140,9 @@ async function uploadMultipart(options: { callback(null, chunk); }, }); - const body = createReadStream(options.filePath, { start, end }).pipe(progress); + const source = createReadStream(options.filePath, { start, end }); + const body = source.pipe(progress); + source.on("error", (error) => body.destroy(error)); try { const partResponse = await fetch( new URL( @@ -197,6 +199,7 @@ async function uploadMultipart(options: { } } finally { body.destroy(); + source.destroy(); } options.onProgress(confirmedBytes); @@ -358,7 +361,9 @@ export function registerCloudShareHandlers() { callback(null, chunk); }, }); - const body = createReadStream(resolvedPath).pipe(progress); + const source = createReadStream(resolvedPath); + const body = source.pipe(progress); + source.on("error", (error) => body.destroy(error)); const uploadHeaders: Record = { "content-type": contentType, "content-length": String(stat.size), @@ -383,6 +388,7 @@ export function registerCloudShareHandlers() { } } finally { body.destroy(); + source.destroy(); } } diff --git a/services/recordly-share/worker/migrations/0007_comment_accounts.sql b/services/recordly-share/worker/migrations/0007_comment_accounts.sql index 38b97c6a..8c60eeb9 100644 --- a/services/recordly-share/worker/migrations/0007_comment_accounts.sql +++ b/services/recordly-share/worker/migrations/0007_comment_accounts.sql @@ -1,4 +1,4 @@ --- Require an authenticated Recordly viewer account before posting comments. +-- Optional viewer accounts; posting comments does not require an account. CREATE TABLE IF NOT EXISTS comment_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/services/recordly-share/worker/src/index.js b/services/recordly-share/worker/src/index.js index 754c3f7d..56a3eb08 100644 --- a/services/recordly-share/worker/src/index.js +++ b/services/recordly-share/worker/src/index.js @@ -442,6 +442,7 @@ function dashboardPassword(env) { async function expectedSessionToken(env) { const password = dashboardPassword(env); + if (!password) throw new Error('Dashboard sign-in is not configured'); const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', encoder.encode(password), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] @@ -455,6 +456,7 @@ async function isDashboardAuthed(request, env) { } async function dashboardCookieAuthed(request, env) { + if (!dashboardPassword(env)) return false; const cookies = parseCookies(request.headers.get('Cookie') || ''); const sessionToken = cookies['voom_session']; if (!sessionToken) return false; @@ -1087,7 +1089,7 @@ async function handleVideoStream(request, env, shareCode) { const totalSize = object.size; // R2Object.size is the full stored object size const start = suffix ? Math.max(0, totalSize - r2Range.suffix) : r2Range.offset; - const actualEnd = !suffix && r2Range.length !== undefined ? start + r2Range.length - 1 : totalSize - 1; + const actualEnd = !suffix && r2Range.length !== undefined ? Math.min(totalSize - 1, start + r2Range.length - 1) : totalSize - 1; return new Response(object.body, { status: 206, diff --git a/services/recordly-share/worker/test/api.test.js b/services/recordly-share/worker/test/api.test.js index cd137558..51614f3f 100644 --- a/services/recordly-share/worker/test/api.test.js +++ b/services/recordly-share/worker/test/api.test.js @@ -544,3 +544,21 @@ it('rejects creating protected shares without a signing secret', async () => { await response.arrayBuffer(); } finally { lookup.mockRestore(); } }); + +it('rejects dashboard cookies without configured secrets instead of crashing', async () => { + const config = { ...env, API_SECRET: undefined, DASHBOARD_PASSWORD: undefined }; + const response = await worker.fetch(new Request(`${BASE}/api/videos`, { + headers: { Cookie: 'voom_session=untrusted' }, + }), config, {}); + expect(response.status).toBe(401); + await response.arrayBuffer(); +}); +it('clamps a bounded video range to the actual object size', async () => { + const { shareCode } = await createShare(); + await completeUpload(shareCode); + const response = await SELF.fetch(`${BASE}/v/${shareCode}`, { headers: { Range: 'bytes=6-200' } }); + expect(response.status).toBe(206); + expect(response.headers.get('Content-Range')).toBe('bytes 6-7/8'); + expect(response.headers.get('Content-Length')).toBe('2'); + expect(Array.from(new Uint8Array(await response.arrayBuffer()))).toEqual([6, 7]); +});