fix: proxy cloud web read-only API
This commit is contained in:
@@ -308,6 +308,13 @@ const environment = process.env.HWLAB_ENVIRONMENT || "dev";
|
||||
const runtimeKind = process.env.HWLAB_ARTIFACT_KIND || "health-placeholder";
|
||||
const entrypoint = process.env.HWLAB_SERVICE_ENTRYPOINT || "";
|
||||
const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "8080", 10);
|
||||
const cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || "";
|
||||
const readOnlyRpcMethods = new Set([
|
||||
"system.health",
|
||||
"cloud.adapter.describe",
|
||||
"audit.event.query",
|
||||
"evidence.record.query"
|
||||
]);
|
||||
|
||||
function sendJson(response, statusCode, body) {
|
||||
const payload = JSON.stringify(body, null, 2);
|
||||
@@ -340,6 +347,76 @@ function runNodeEntrypoint(file) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readRequestBody(request) {
|
||||
let body = "";
|
||||
for await (const chunk of request) {
|
||||
body += chunk;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function copyHeaders(headers) {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (value === undefined) continue;
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function proxyCloudApi(request, response, url) {
|
||||
if (!cloudApiBaseUrl) {
|
||||
sendJson(response, 503, {
|
||||
error: "cloud_api_base_url_missing",
|
||||
serviceId,
|
||||
path: url.pathname
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let body = "";
|
||||
if (request.method !== "GET") {
|
||||
body = await readRequestBody(request);
|
||||
}
|
||||
|
||||
if (url.pathname === "/json-rpc") {
|
||||
try {
|
||||
const envelope = body ? JSON.parse(body) : {};
|
||||
if (!readOnlyRpcMethods.has(envelope.method)) {
|
||||
sendJson(response, 403, {
|
||||
error: "readonly_rpc_required",
|
||||
serviceId,
|
||||
method: envelope.method || null
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
sendJson(response, 400, {
|
||||
error: "invalid_json",
|
||||
serviceId,
|
||||
reason: error.message
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const target = new URL(url.pathname + url.search, cloudApiBaseUrl);
|
||||
const upstream = await fetch(target, {
|
||||
method: request.method,
|
||||
headers: {
|
||||
accept: request.headers.accept || "application/json",
|
||||
"content-type": request.headers["content-type"] || "application/json"
|
||||
},
|
||||
body: request.method === "GET" ? undefined : body
|
||||
});
|
||||
const payload = Buffer.from(await upstream.arrayBuffer());
|
||||
response.writeHead(upstream.status, {
|
||||
...copyHeaders(Object.fromEntries(upstream.headers.entries())),
|
||||
"content-length": payload.length
|
||||
});
|
||||
response.end(payload);
|
||||
}
|
||||
|
||||
async function serveCloudWeb() {
|
||||
const roots = ["/app/web/hwlab-cloud-web/dist", "/app/web/hwlab-cloud-web"];
|
||||
const server = createServer(async (request, response) => {
|
||||
@@ -349,6 +426,14 @@ async function serveCloudWeb() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(request.method === "GET" && (url.pathname === "/v1" || url.pathname.startsWith("/v1/"))) ||
|
||||
(request.method === "POST" && url.pathname === "/json-rpc")
|
||||
) {
|
||||
await proxyCloudApi(request, response, url);
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
||||
for (const root of roots) {
|
||||
const candidate = path.resolve(root, relativePath);
|
||||
|
||||
@@ -19,7 +19,8 @@ for (const file of requiredFiles) {
|
||||
|
||||
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
|
||||
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
|
||||
const frontendSource = `${html}\n${app}`;
|
||||
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8");
|
||||
const frontendSource = `${html}\n${app}\n${artifactPublisher}`;
|
||||
|
||||
assert.match(html, /HWLAB DEV Console/);
|
||||
assert.match(html, /M3 Diagnostics Console/);
|
||||
@@ -48,6 +49,13 @@ assert.match(app, /callRpc\("system\.health"\)/);
|
||||
assert.match(app, /callRpc\("cloud\.adapter\.describe"\)/);
|
||||
assert.match(app, /callRpc\("audit\.event\.query"/);
|
||||
assert.match(app, /callRpc\("evidence\.record\.query"/);
|
||||
assert.match(artifactPublisher, /HWLAB_API_BASE_URL/);
|
||||
assert.match(artifactPublisher, /readOnlyRpcMethods/);
|
||||
assert.match(artifactPublisher, /"system\.health"/);
|
||||
assert.match(artifactPublisher, /"cloud\.adapter\.describe"/);
|
||||
assert.match(artifactPublisher, /"audit\.event\.query"/);
|
||||
assert.match(artifactPublisher, /"evidence\.record\.query"/);
|
||||
assert.match(artifactPublisher, /readonly_rpc_required/);
|
||||
assert.match(app, /degraded/);
|
||||
assert.match(app, /runtime\.serviceRoute\.join\(" -> "\)/);
|
||||
assert.doesNotMatch(app, /callRpc\("hardware\./);
|
||||
|
||||
Reference in New Issue
Block a user