mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 15:01:48 +03:00
refactor(canvas-host): share static file resolver
This commit is contained in:
+2
-46
@@ -2,8 +2,8 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { SafeOpenError, openFileWithinRoot, type SafeOpenResult } from "../infra/fs-safe.js";
|
|
||||||
import { detectMime } from "../media/mime.js";
|
import { detectMime } from "../media/mime.js";
|
||||||
|
import { resolveFileWithinRoot } from "./file-resolver.js";
|
||||||
|
|
||||||
export const A2UI_PATH = "/__openclaw__/a2ui";
|
export const A2UI_PATH = "/__openclaw__/a2ui";
|
||||||
|
|
||||||
@@ -57,50 +57,6 @@ async function resolveA2uiRootReal(): Promise<string | null> {
|
|||||||
return resolvingA2uiRoot;
|
return resolvingA2uiRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrlPath(rawPath: string): string {
|
|
||||||
const decoded = decodeURIComponent(rawPath || "/");
|
|
||||||
const normalized = path.posix.normalize(decoded);
|
|
||||||
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveA2uiFile(rootReal: string, urlPath: string): Promise<SafeOpenResult | null> {
|
|
||||||
const normalized = normalizeUrlPath(urlPath);
|
|
||||||
const rel = normalized.replace(/^\/+/, "");
|
|
||||||
if (rel.split("/").some((p) => p === "..")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tryOpen = async (relative: string) => {
|
|
||||||
try {
|
|
||||||
return await openFileWithinRoot({ rootDir: rootReal, relativePath: relative });
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof SafeOpenError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (normalized.endsWith("/")) {
|
|
||||||
return await tryOpen(path.posix.join(rel, "index.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidate = path.join(rootReal, rel);
|
|
||||||
try {
|
|
||||||
const st = await fs.lstat(candidate);
|
|
||||||
if (st.isSymbolicLink()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (st.isDirectory()) {
|
|
||||||
return await tryOpen(path.posix.join(rel, "index.html"));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
return await tryOpen(rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function injectCanvasLiveReload(html: string): string {
|
export function injectCanvasLiveReload(html: string): string {
|
||||||
const snippet = `
|
const snippet = `
|
||||||
<script>
|
<script>
|
||||||
@@ -192,7 +148,7 @@ export async function handleA2uiHttpRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rel = url.pathname.slice(basePath.length);
|
const rel = url.pathname.slice(basePath.length);
|
||||||
const result = await resolveA2uiFile(a2uiRootReal, rel || "/");
|
const result = await resolveFileWithinRoot(a2uiRootReal, rel || "/");
|
||||||
if (!result) {
|
if (!result) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { SafeOpenError, openFileWithinRoot, type SafeOpenResult } from "../infra/fs-safe.js";
|
||||||
|
|
||||||
|
export function normalizeUrlPath(rawPath: string): string {
|
||||||
|
const decoded = decodeURIComponent(rawPath || "/");
|
||||||
|
const normalized = path.posix.normalize(decoded);
|
||||||
|
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveFileWithinRoot(
|
||||||
|
rootReal: string,
|
||||||
|
urlPath: string,
|
||||||
|
): Promise<SafeOpenResult | null> {
|
||||||
|
const normalized = normalizeUrlPath(urlPath);
|
||||||
|
const rel = normalized.replace(/^\/+/, "");
|
||||||
|
if (rel.split("/").some((p) => p === "..")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryOpen = async (relative: string) => {
|
||||||
|
try {
|
||||||
|
return await openFileWithinRoot({ rootDir: rootReal, relativePath: relative });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof SafeOpenError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (normalized.endsWith("/")) {
|
||||||
|
return await tryOpen(path.posix.join(rel, "index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = path.join(rootReal, rel);
|
||||||
|
try {
|
||||||
|
const st = await fs.lstat(candidate);
|
||||||
|
if (st.isSymbolicLink()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
return await tryOpen(path.posix.join(rel, "index.html"));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return await tryOpen(rel);
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import { type WebSocket, WebSocketServer } from "ws";
|
|||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { isTruthyEnvValue } from "../infra/env.js";
|
import { isTruthyEnvValue } from "../infra/env.js";
|
||||||
import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js";
|
|
||||||
import { detectMime } from "../media/mime.js";
|
import { detectMime } from "../media/mime.js";
|
||||||
import { ensureDir, resolveUserPath } from "../utils.js";
|
import { ensureDir, resolveUserPath } from "../utils.js";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
handleA2uiHttpRequest,
|
handleA2uiHttpRequest,
|
||||||
injectCanvasLiveReload,
|
injectCanvasLiveReload,
|
||||||
} from "./a2ui.js";
|
} from "./a2ui.js";
|
||||||
|
import { normalizeUrlPath, resolveFileWithinRoot } from "./file-resolver.js";
|
||||||
|
|
||||||
export type CanvasHostOpts = {
|
export type CanvasHostOpts = {
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
@@ -149,50 +149,6 @@ function defaultIndexHTML() {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrlPath(rawPath: string): string {
|
|
||||||
const decoded = decodeURIComponent(rawPath || "/");
|
|
||||||
const normalized = path.posix.normalize(decoded);
|
|
||||||
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveFilePath(rootReal: string, urlPath: string) {
|
|
||||||
const normalized = normalizeUrlPath(urlPath);
|
|
||||||
const rel = normalized.replace(/^\/+/, "");
|
|
||||||
if (rel.split("/").some((p) => p === "..")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tryOpen = async (relative: string) => {
|
|
||||||
try {
|
|
||||||
return await openFileWithinRoot({ rootDir: rootReal, relativePath: relative });
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof SafeOpenError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (normalized.endsWith("/")) {
|
|
||||||
return await tryOpen(path.posix.join(rel, "index.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidate = path.join(rootReal, rel);
|
|
||||||
try {
|
|
||||||
const st = await fs.lstat(candidate);
|
|
||||||
if (st.isSymbolicLink()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (st.isDirectory()) {
|
|
||||||
return await tryOpen(path.posix.join(rel, "index.html"));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
return await tryOpen(rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDisabledByEnv() {
|
function isDisabledByEnv() {
|
||||||
if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) {
|
if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -372,7 +328,7 @@ export async function createCanvasHostHandler(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const opened = await resolveFilePath(rootReal, urlPath);
|
const opened = await resolveFileWithinRoot(rootReal, urlPath);
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
if (urlPath === "/" || urlPath.endsWith("/")) {
|
if (urlPath === "/" || urlPath.endsWith("/")) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
|
|||||||
Reference in New Issue
Block a user