Skip to content

Commit 012f554

Browse files
committed
fix: verify recording sizes through signed GET URLs
1 parent a14275b commit 012f554

5 files changed

Lines changed: 81 additions & 11 deletions

File tree

.github/workflows/docker-build-media-server.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ jobs:
8080
docker run --rm --network none --entrypoint bun "$MEDIA_IMAGE" test \
8181
src/__tests__/lib/recording-verification.integration.test.ts \
8282
src/__tests__/lib/job-manager.test.ts
83+
docker run --rm --network none --entrypoint bun "$MEDIA_IMAGE" test \
84+
src/__tests__/lib/media-size.test.ts \
85+
src/__tests__/lib/media-probe.integration.test.ts
8386
docker run --rm --network none --entrypoint bun "$MEDIA_IMAGE" test \
8487
src/__tests__/lib/media-transfer.test.ts
8588
docker run --rm --network none --entrypoint bun "$MEDIA_IMAGE" test \

apps/media-server/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ RUN bun install --frozen-lockfile --production
1111

1212
COPY apps/media-server/src ./src
1313

14-
RUN bun test src/__tests__/lib/recording-verification.integration.test.ts src/__tests__/lib/job-manager.test.ts \
14+
RUN bun test src/__tests__/lib/media-size.test.ts src/__tests__/lib/media-probe.integration.test.ts \
15+
&& bun test src/__tests__/lib/recording-verification.integration.test.ts src/__tests__/lib/job-manager.test.ts \
1516
&& bun test src/__tests__/lib/media-transfer.test.ts \
1617
&& bun test src/__tests__/routes/recording-verification.test.ts \
1718
&& bun test src/__tests__/routes/video.test.ts \

apps/media-server/src/__tests__/lib/media-probe.integration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ describe("mediaProbe integration tests", () => {
130130

131131
expect(metadata.duration).toBeGreaterThan(0);
132132
expect(metadata.videoCodec).toBeTruthy();
133+
expect(metadata.fileSize).toBe(videoData.length);
133134
} finally {
134135
await server.stop(true);
135136
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { getSourceSize } from "../../lib/media-common";
3+
4+
describe("remote media size", () => {
5+
for (const sample of [
6+
{
7+
status: 206,
8+
headers: { "Content-Range": "bytes 0-0/1024", "Content-Length": "1" },
9+
size: 1024,
10+
},
11+
{ status: 200, headers: { "Content-Length": "1024" }, size: 1024 },
12+
{ status: 403, headers: { "Content-Length": "1024" }, size: null },
13+
{ status: 206, headers: { "Content-Length": "1" }, size: null },
14+
{ status: 206, headers: { "Content-Range": "bytes 1-1/1024" }, size: null },
15+
{
16+
status: 206,
17+
headers: { "Content-Range": "bytes 0-0/9007199254740992" },
18+
size: null,
19+
},
20+
{ status: 200, headers: {}, size: null },
21+
]) {
22+
test(`validates size response ${JSON.stringify(sample)}`, async () => {
23+
const server = Bun.serve({
24+
port: 0,
25+
fetch(request) {
26+
expect(request.method).toBe("GET");
27+
expect(request.headers.get("range")).toBe("bytes=0-0");
28+
return new Response(
29+
sample.size === 1024 && sample.status === 200
30+
? new Uint8Array(1024)
31+
: null,
32+
{
33+
status: sample.status,
34+
headers: sample.headers,
35+
},
36+
);
37+
},
38+
});
39+
try {
40+
const result = getSourceSize(`${server.url}video.mp4`);
41+
if (sample.size === null) {
42+
await expect(result).rejects.toThrow(
43+
"Media input size is unavailable",
44+
);
45+
} else {
46+
expect(await result).toBe(sample.size);
47+
}
48+
} finally {
49+
await server.stop(true);
50+
}
51+
});
52+
}
53+
});

apps/media-server/src/lib/media-common.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,29 @@ export function normalizeLocalPath(path: string): string {
198198
}
199199

200200
export async function getSourceSize(path: string): Promise<number> {
201-
try {
202-
const url = new URL(path);
203-
if (url.protocol === "http:" || url.protocol === "https:") {
204-
const response = await mediaFetch(path, {
205-
method: "HEAD",
206-
signal: AbortSignal.timeout(10_000),
207-
});
208-
const contentLength = response.headers.get("content-length");
209-
return contentLength ? Number.parseInt(contentLength, 10) || 0 : 0;
201+
if (/^https?:\/\//.test(path)) {
202+
const response = await mediaFetch(path, {
203+
headers: { Range: "bytes=0-0" },
204+
signal: AbortSignal.timeout(10_000),
205+
});
206+
try {
207+
const length =
208+
response.status === 206
209+
? response.headers
210+
.get("content-range")
211+
?.match(/^bytes 0-0\/(\d+)$/)?.[1]
212+
: response.status === 200
213+
? response.headers.get("content-length")
214+
: undefined;
215+
const size = Number(length);
216+
if (!Number.isSafeInteger(size) || size <= 0) {
217+
throw new Error("Media input size is unavailable");
218+
}
219+
return size;
220+
} finally {
221+
await response.body?.cancel().catch(() => {});
210222
}
211-
} catch {}
223+
}
212224

213225
try {
214226
return (await stat(normalizeLocalPath(path))).size;

0 commit comments

Comments
 (0)