anbaric-cloud-hosting 1.16.1 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric-cloud-hosting",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
4
4
  "description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -18,17 +18,17 @@
18
18
  "@aws-sdk/client-s3": "^3.1110.0",
19
19
  "@aws-sdk/client-secrets-manager": "^3.700.0",
20
20
  "@aws-sdk/client-servicediscovery": "^3.1110.0",
21
- "anbaric-data-store": "^1.16.1",
22
- "anbaric-plugins": "^1.16.1",
23
- "anbaric-tsapi": "^1.16.1",
21
+ "anbaric-data-store": "^1.16.2",
22
+ "anbaric-plugins": "^1.16.2",
23
+ "anbaric-tsapi": "^1.16.2",
24
24
  "esbuild": "^0.28.2",
25
25
  "pg": "^8.16.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.2.0",
29
29
  "@types/pg": "^8.15.0",
30
- "anbaric-impl-cloud": "^1.16.1",
31
- "anbaric-state-machine": "^1.16.1",
30
+ "anbaric-impl-cloud": "^1.16.2",
31
+ "anbaric-state-machine": "^1.16.2",
32
32
  "tsx": "^4.20.0",
33
33
  "typescript": "^7.0.2"
34
34
  },
@@ -1,9 +1,21 @@
1
+ import {IncomingHttpHeaders, OutgoingHttpHeaders, request as httpRequest} from "node:http";
1
2
  import {BuildLayer} from "../../app-management/BuildLayer";
2
3
  import {Request} from "../Request";
3
4
  import {RequestHandler} from "../RequestHandler";
4
5
 
5
- /* The router's fallback: any unregistered top-level path naming a running
6
- app is forwarded to it. */
6
+ /* Headers that describe a single hop and must not be forwarded across the
7
+ proxy in either direction. */
8
+ const HOP_BY_HOP = new Set([
9
+ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
10
+ "te", "trailer", "transfer-encoding", "upgrade",
11
+ ]);
12
+
13
+ /* The router's fallback: any unregistered top-level path naming a running app
14
+ is reverse-proxied to it. The app is exposed at /<appName>; that prefix is
15
+ stripped before forwarding (the app sees the sub-path) and surfaced to the
16
+ app as X-Forwarded-Prefix so it can rebuild public URLs. Request and response
17
+ headers pass through both ways - notably cookies, Set-Cookie and Location - so
18
+ sessions and redirects work from app-served HTML. */
7
19
  class AppProxyHandler implements RequestHandler {
8
20
 
9
21
  constructor(private buildLayer : BuildLayer) {}
@@ -16,17 +28,41 @@ class AppProxyHandler implements RequestHandler {
16
28
  const appPath = request.url.pathname.slice(`/${appName}`.length) || "/";
17
29
  const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.rawBody();
18
30
 
19
- const upstream = await fetch(`http://${app.appHost}:${app.appPort}${appPath}${request.url.search}`, {
20
- method: request.method,
21
- headers: { "content-type": request.header("content-type") ?? "application/json" },
22
- body: body && body.length > 0 ? new Uint8Array(body) : undefined,
31
+ await new Promise<void>((resolve, reject) => {
32
+ const upstream = httpRequest({
33
+ host: app.appHost,
34
+ port: app.appPort,
35
+ method: request.method,
36
+ path: `${appPath}${request.url.search}`,
37
+ headers: this.forwardHeaders(request, appName),
38
+ }, response => {
39
+ request.rawResponse.writeHead(response.statusCode ?? 502, this.passThrough(response.headers));
40
+ response.pipe(request.rawResponse);
41
+ response.on("end", resolve);
42
+ response.on("error", reject);
43
+ });
44
+ upstream.on("error", reject);
45
+ if (body && body.length > 0) upstream.write(body);
46
+ upstream.end();
23
47
  });
48
+ }
24
49
 
25
- const payload = Buffer.from(await upstream.arrayBuffer());
26
- request.rawResponse.writeHead(upstream.status, {
27
- "content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
28
- });
29
- request.rawResponse.end(payload);
50
+ private forwardHeaders(request : Request, appName : string) : OutgoingHttpHeaders {
51
+ const headers = this.passThrough(request.raw.headers);
52
+ delete headers.host;
53
+ headers["x-forwarded-prefix"] = `/${appName}`;
54
+ headers["x-forwarded-host"] = request.raw.headers.host;
55
+ headers["x-forwarded-proto"] = "https";
56
+ return headers;
57
+ }
58
+
59
+ private passThrough(headers : IncomingHttpHeaders) : OutgoingHttpHeaders {
60
+ const kept : OutgoingHttpHeaders = {};
61
+ for (const [name, value] of Object.entries(headers)) {
62
+ if (value === undefined || HOP_BY_HOP.has(name)) continue;
63
+ kept[name] = value;
64
+ }
65
+ return kept;
30
66
  }
31
67
 
32
68
  }
@@ -27,7 +27,7 @@ class AppsHandler implements RequestHandler {
27
27
  case "POST": {
28
28
  const appPort = Number(request.query("port"));
29
29
  if (!Number.isInteger(appPort) || appPort <= 0) {
30
- return request.reply(400, { error: "Expected a numeric port query parameter" });
30
+ return request.reply(400, { error: "Expected a numeric ?port query parameter - the app's internal port (\"internalPort\" in .anbaric/app-config.json)" });
31
31
  }
32
32
  const tarball = await request.rawBody();
33
33
  if (tarball.length === 0) return request.reply(400, { error: "Expected a gzipped tarball body" });