Fix Windows static asset serving (#456)

This commit is contained in:
Graham Neubig
2026-05-15 01:24:11 -04:00
committed by GitHub
parent ed216c38aa
commit 5b05321483
3 changed files with 114 additions and 4 deletions
+25 -2
View File
@@ -19,9 +19,29 @@ permissions:
contents: read
jobs:
test-and-build:
prepare-test-matrix:
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Build test matrix
id: matrix
run: |
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo 'matrix={"include":[{"name":"ubuntu","os":"ubuntu-24.04","full_checks":true},{"name":"windows","os":"windows-latest","full_checks":false}]}' >> "$GITHUB_OUTPUT"
else
echo 'matrix={"include":[{"name":"ubuntu","os":"ubuntu-24.04","full_checks":true}]}' >> "$GITHUB_OUTPUT"
fi
test-and-build:
needs: prepare-test-matrix
name: test-and-build (${{ matrix.name }})
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.prepare-test-matrix.outputs.matrix) }}
steps:
- name: Check out repository
@@ -37,6 +57,7 @@ jobs:
run: npm ci
- name: Lint
if: matrix.full_checks
run: npm run lint
- name: Test
@@ -46,9 +67,11 @@ jobs:
run: npm run build
- name: Build library
if: matrix.full_checks
run: npm run build:lib
- name: Verify package contents
if: matrix.full_checks
run: npm pack --dry-run
live-e2e:
+79
View File
@@ -0,0 +1,79 @@
import type { Server } from "node:http";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { startStaticServer } from "../../scripts/static-server.mjs";
describe("static-server.mjs", () => {
const servers: Server[] = [];
const tempDirs: string[] = [];
afterEach(async () => {
await Promise.all(
servers.splice(0).map(
(server) =>
new Promise<void>((resolve) => {
server.close(() => resolve());
}),
),
);
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
async function startServer(dir: string) {
const server = await startStaticServer({
port: 0,
host: "127.0.0.1",
dir,
routes: {},
});
servers.push(server);
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Static server did not bind to a TCP port");
}
return `http://127.0.0.1:${address.port}`;
}
it("serves nested build assets on all platforms", async () => {
const buildDir = mkdtempSync(path.join(tmpdir(), "agent-canvas-build-"));
tempDirs.push(buildDir);
mkdirSync(path.join(buildDir, "assets"));
writeFileSync(path.join(buildDir, "index.html"), "<main>app</main>");
writeFileSync(
path.join(buildDir, "assets", "entry.client-test.js"),
"export const loaded = true;\n",
);
const origin = await startServer(buildDir);
const response = await fetch(`${origin}/assets/entry.client-test.js`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/javascript",
);
await expect(response.text()).resolves.toContain("loaded = true");
});
it("keeps paths confined to the static directory", async () => {
const parentDir = mkdtempSync(path.join(tmpdir(), "agent-canvas-parent-"));
tempDirs.push(parentDir);
const buildDir = path.join(parentDir, "build");
mkdirSync(buildDir);
writeFileSync(path.join(buildDir, "index.html"), "<main>app</main>");
writeFileSync(path.join(parentDir, "secret.txt"), "secret\n");
const origin = await startServer(buildDir);
const response = await fetch(`${origin}/../secret.txt`);
expect(response.status).not.toBe(200);
await expect(response.text()).resolves.not.toContain("secret");
});
});
+10 -2
View File
@@ -29,7 +29,7 @@
import { createServer, request as httpRequest } from "node:http";
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { extname, normalize, resolve } from "node:path";
import { extname, isAbsolute, normalize, relative, resolve } from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
@@ -286,6 +286,14 @@ function looksLikeAssetRequest(urlPath) {
return Boolean(ext) && ext in MIME;
}
function isPathInsideDir(dirAbs, filePath) {
const relativePath = relative(dirAbs, filePath);
return (
relativePath === "" ||
(!relativePath.startsWith("..") && !isAbsolute(relativePath))
);
}
async function serveFile(req, res, filePath, urlPath) {
const stats = await tryStat(filePath);
if (!stats) return false;
@@ -326,7 +334,7 @@ async function handleStatic(req, res, dirAbs) {
const safe = normalize(urlPath);
let filePath = resolve(dirAbs, "." + safe);
if (filePath !== dirAbs && !filePath.startsWith(dirAbs + "/")) {
if (!isPathInsideDir(dirAbs, filePath)) {
res.writeHead(403);
res.end("Forbidden");
return;