mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:19:05 +08:00
Fix Windows static asset serving (#456)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user