This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
// Keep managed artifacts within the host agent's MAX_FILE limit.
|
||||
export const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
|
||||
@@ -2,6 +2,7 @@ import { putFileBlob, blobContent } from '../db/file-blobs.js';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { db } from '../db/index.js';
|
||||
import { configFileForGame, type ArtifactKind } from '../config.js';
|
||||
import { MAX_ARTIFACT_BYTES } from '../../artifact-limits.js';
|
||||
|
||||
export type { ArtifactKind };
|
||||
|
||||
@@ -72,6 +73,7 @@ export class ScriptError extends Error {
|
||||
| 'baseline-exists'
|
||||
| 'empty-overlay'
|
||||
| 'invalid-release-version'
|
||||
| 'too-large'
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
@@ -218,6 +220,8 @@ export function createScript(input: CreateScriptInput): {
|
||||
script: ScriptRow;
|
||||
version: ScriptVersionRow;
|
||||
} {
|
||||
if (input.content.byteLength > MAX_ARTIFACT_BYTES)
|
||||
throw new ScriptError('Artifacts must not exceed 32 MiB.', 'too-large');
|
||||
if (!input.name.trim()) throw new ScriptError('script name is required', 'name-empty');
|
||||
const kind: ArtifactKind = input.kind ?? 'script';
|
||||
if (!isArtifactKind(kind)) throw new ScriptError(`invalid kind: ${input.kind}`, 'invalid-kind');
|
||||
@@ -316,6 +320,8 @@ export function uploadNewVersion(
|
||||
notes?: string,
|
||||
releaseVersion?: string
|
||||
): ScriptVersionRow {
|
||||
if (content.byteLength > MAX_ARTIFACT_BYTES)
|
||||
throw new ScriptError('Artifacts must not exceed 32 MiB.', 'too-large');
|
||||
const script = getScript(scriptId);
|
||||
if (!script) throw new ScriptError(`no such script (id=${scriptId})`, 'no-such-script');
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { GithubPluginAsset, GithubPluginRelease } from '$lib/plugin-github.js';
|
||||
import { MAX_ARTIFACT_BYTES } from '../../artifact-limits.js';
|
||||
|
||||
const MAX_DLL_BYTES = 64 * 1024 * 1024;
|
||||
const CACHE_MS = 5 * 60 * 1000;
|
||||
const cache = new Map<string, { expires: number; release: Promise<GithubPluginRelease> }>();
|
||||
|
||||
@@ -157,7 +157,7 @@ async function fetchRelease(repository: string, tag: string | null): Promise<Git
|
||||
a.state === 'uploaded' &&
|
||||
Number.isSafeInteger(a.size) &&
|
||||
a.size > 0 &&
|
||||
a.size <= MAX_DLL_BYTES
|
||||
a.size <= MAX_ARTIFACT_BYTES
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The preceding filter validates the decoded asset fields.
|
||||
.map((a: any) => ({
|
||||
@@ -238,7 +238,7 @@ export async function downloadGithubAsset(
|
||||
502
|
||||
);
|
||||
}
|
||||
const content = await limitedBody(response, MAX_DLL_BYTES);
|
||||
const content = await limitedBody(response, MAX_ARTIFACT_BYTES);
|
||||
if (content.length !== asset.size)
|
||||
throw new GithubReleaseError(
|
||||
'Downloaded size differs from the release asset. Preview the release again.',
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { MAX_ARTIFACT_BYTES } from '../artifact-limits.js';
|
||||
|
||||
export const JSON_BODY_LIMIT = 1024 * 1024;
|
||||
// Leave room for form fields and multipart framing around a maximum-size file.
|
||||
export const ARTIFACT_FORM_LIMIT = MAX_ARTIFACT_BYTES + 1024 * 1024;
|
||||
export const BETA_REVISION_FORM_LIMIT = 2 * MAX_ARTIFACT_BYTES + 1024 * 1024;
|
||||
|
||||
/** Enforce the limit before parsing, including absent or understated lengths. */
|
||||
export async function boundedBody(request: Request, maxBytes: number) {
|
||||
if (Number(request.headers.get('content-length')) > maxBytes) {
|
||||
void request.body?.cancel().catch(() => {});
|
||||
error(413, 'Request is too large.');
|
||||
}
|
||||
const reader = request.body?.getReader();
|
||||
if (!reader) return Buffer.alloc(0);
|
||||
const chunks: Buffer[] = [];
|
||||
let block = Buffer.alloc(0);
|
||||
let used = 0;
|
||||
let received = 0;
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
received += value.byteLength;
|
||||
if (received > maxBytes) {
|
||||
// Cancellation may wait for another branch of a cloned request.
|
||||
void reader.cancel().catch(() => {});
|
||||
error(413, 'Request is too large.');
|
||||
}
|
||||
// Coalesce tiny chunks so bookkeeping cannot outgrow the byte limit.
|
||||
let offset = 0;
|
||||
while (offset < value.byteLength) {
|
||||
if (used === block.length) {
|
||||
block = Buffer.allocUnsafe(Math.min(maxBytes, 64 * 1024));
|
||||
chunks.push(block);
|
||||
used = 0;
|
||||
}
|
||||
const length = Math.min(value.byteLength - offset, block.length - used);
|
||||
block.set(value.subarray(offset, offset + length), used);
|
||||
used += length;
|
||||
offset += length;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
return Buffer.concat(chunks, received);
|
||||
}
|
||||
|
||||
// Match Request.json()'s result type; each route retains its input validation.
|
||||
export async function jsonBody(
|
||||
request: Request,
|
||||
maxBytes = JSON_BODY_LIMIT
|
||||
): ReturnType<Request['json']> {
|
||||
return JSON.parse((await boundedBody(request, maxBytes)).toString('utf8'));
|
||||
}
|
||||
|
||||
export async function artifactFormBody(
|
||||
request: Request,
|
||||
maxBytes = ARTIFACT_FORM_LIMIT
|
||||
): Promise<FormData> {
|
||||
const body = await boundedBody(request, maxBytes);
|
||||
let form: FormData;
|
||||
try {
|
||||
form = await new Response(body, {
|
||||
headers: { 'Content-Type': request.headers.get('content-type') ?? '' }
|
||||
}).formData();
|
||||
} catch {
|
||||
error(400, 'Request body must be valid form data.');
|
||||
}
|
||||
// Validate every part before any handler copies file bytes or writes versions.
|
||||
for (const value of form.values()) {
|
||||
const size = typeof value === 'string' ? Buffer.byteLength(value, 'utf8') : value.size;
|
||||
if (size > MAX_ARTIFACT_BYTES) error(413, 'Artifacts must not exceed 32 MiB.');
|
||||
}
|
||||
return form;
|
||||
}
|
||||
Reference in New Issue
Block a user