co-maintainer 0.4.4 → 0.4.6
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/dist/package.json +1 -1
- package/dist/src/cli/commands/serve.d.ts +1 -0
- package/dist/src/cli/commands/serve.js +8 -0
- package/dist/src/server/app.d.ts +3 -0
- package/dist/src/server/app.js +9 -3
- package/dist/src/server/proxy_headers.d.ts +4 -0
- package/dist/src/server/proxy_headers.js +16 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { type Platform } from "../../util/runtime.ts";
|
|
|
4
4
|
/** `undefined` on Linux, otherwise one line naming the platform (a pure
|
|
5
5
|
* function so it is testable without actually being off Linux). */
|
|
6
6
|
export declare function platformWarning(os: Platform): string | undefined;
|
|
7
|
+
export declare function resolveTrustProxy(args: string[], env?: (name: string) => string | undefined): boolean;
|
|
7
8
|
/** `--password=` replaces the stored password. With no flag the stored one is
|
|
8
9
|
* kept, and a first start generates one, returned so the caller can print it. */
|
|
9
10
|
export declare function ensureDashboardPassword(args: string[], hasStoredPassword: boolean, store: PasswordStore): Promise<string | undefined>;
|
|
@@ -27,6 +27,9 @@ function generatePassword() {
|
|
|
27
27
|
crypto.getRandomValues(bytes);
|
|
28
28
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
29
|
}
|
|
30
|
+
export function resolveTrustProxy(args, env = getEnv) {
|
|
31
|
+
return args.includes("--trust-proxy") || env("CM_TRUST_PROXY") === "1";
|
|
32
|
+
}
|
|
30
33
|
/** `--password=` replaces the stored password. With no flag the stored one is
|
|
31
34
|
* kept, and a first start generates one, returned so the caller can print it. */
|
|
32
35
|
export async function ensureDashboardPassword(args, hasStoredPassword, store) {
|
|
@@ -168,6 +171,10 @@ export async function runServe(args) {
|
|
|
168
171
|
startRemakeScheduler();
|
|
169
172
|
if (auth.github)
|
|
170
173
|
console.log(`[serve] GitHub sign-in enabled for ${githubOAuth.allowedUser}`);
|
|
174
|
+
const trustProxy = resolveTrustProxy(args);
|
|
175
|
+
if (trustProxy) {
|
|
176
|
+
console.log("[serve] trusting x-forwarded-for and x-forwarded-proto from a reverse proxy");
|
|
177
|
+
}
|
|
171
178
|
const inject500 = args.includes("--inject-500") || getEnv("CM_INJECT_500") === "1";
|
|
172
179
|
if (inject500) {
|
|
173
180
|
console.log("[serve] --inject-500 is on. Mutating /api requests return 500.");
|
|
@@ -177,6 +184,7 @@ export async function runServe(args) {
|
|
|
177
184
|
passwordStore,
|
|
178
185
|
webhookUrl,
|
|
179
186
|
inject500,
|
|
187
|
+
trustProxy,
|
|
180
188
|
auth,
|
|
181
189
|
githubOAuth,
|
|
182
190
|
});
|
package/dist/src/server/app.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export type AppDeps = {
|
|
|
6
6
|
passwordStore?: PasswordStore;
|
|
7
7
|
webhookUrl?: string;
|
|
8
8
|
secureCookie?: boolean;
|
|
9
|
+
/** Behind a reverse proxy this process trusts, the client address and the
|
|
10
|
+
* scheme come from `x-forwarded-for` and `x-forwarded-proto`. */
|
|
11
|
+
trustProxy?: boolean;
|
|
9
12
|
inject500?: boolean;
|
|
10
13
|
/** Defaults to password-only when omitted, matching every caller that
|
|
11
14
|
* pre-dates GitHub sign-in. */
|
package/dist/src/server/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { VERSION } from "../version.js";
|
|
|
4
4
|
import { errorResponse } from "./errors.js";
|
|
5
5
|
import { hasCsrfHeader, login, logout, readSessionToken, sessionCookieHeader, verifySession, } from "./auth.js";
|
|
6
6
|
import { memoryPasswordStore } from "./auth.js";
|
|
7
|
+
import { clientAddress, forwardedHttps } from "./proxy_headers.js";
|
|
7
8
|
import { readConfig } from "../config.js";
|
|
8
9
|
import { listJobs } from "../store/jobs.js";
|
|
9
10
|
import { handleJobsRoute } from "./api/jobs.js";
|
|
@@ -96,8 +97,13 @@ function liveGithubConfig() {
|
|
|
96
97
|
export function createApp(deps) {
|
|
97
98
|
const passwords = deps.passwordStore ?? memoryPasswordStore(deps.password);
|
|
98
99
|
return {
|
|
99
|
-
async fetch(request,
|
|
100
|
+
async fetch(request, socketAddress = "unknown") {
|
|
100
101
|
const url = new URL(request.url);
|
|
102
|
+
const remoteAddr = deps.trustProxy
|
|
103
|
+
? clientAddress(request, socketAddress)
|
|
104
|
+
: socketAddress;
|
|
105
|
+
const secureCookie = Boolean(deps.secureCookie) ||
|
|
106
|
+
(Boolean(deps.trustProxy) && forwardedHttps(request));
|
|
101
107
|
const { githubApp, webhookSecret } = liveGithubConfig();
|
|
102
108
|
const mutating = ["POST", "PATCH", "PUT", "DELETE"].includes(request.method);
|
|
103
109
|
if (url.pathname === "/api/health" && request.method === "GET") {
|
|
@@ -106,7 +112,7 @@ export function createApp(deps) {
|
|
|
106
112
|
if (url.pathname === "/github/webhook" && request.method === "POST") {
|
|
107
113
|
return await handleWebhookRequest(request, webhookSecret);
|
|
108
114
|
}
|
|
109
|
-
const page = await handlePageRequest(request, { ...deps, githubApp, passwordStore: passwords }, remoteAddr);
|
|
115
|
+
const page = await handlePageRequest(request, { ...deps, githubApp, passwordStore: passwords, secureCookie }, remoteAddr);
|
|
110
116
|
if (page)
|
|
111
117
|
return page;
|
|
112
118
|
if (url.pathname.startsWith("/api/remote/")) {
|
|
@@ -117,7 +123,7 @@ export function createApp(deps) {
|
|
|
117
123
|
return errorResponse(403, "csrf", `every mutating request needs the X-Requested-With header`);
|
|
118
124
|
}
|
|
119
125
|
if (url.pathname === "/api/login" && request.method === "POST") {
|
|
120
|
-
return await handleLogin(request, deps, passwords, remoteAddr);
|
|
126
|
+
return await handleLogin(request, { ...deps, secureCookie }, passwords, remoteAddr);
|
|
121
127
|
}
|
|
122
128
|
const session = await requireSession(request);
|
|
123
129
|
if (!session) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The rightmost entry is the one the nearest trusted proxy appended, so a
|
|
2
|
+
* client cannot forge it the way it can the entries to its left. */
|
|
3
|
+
export declare function clientAddress(request: Request, socketAddress: string): string;
|
|
4
|
+
export declare function forwardedHttps(request: Request): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
/** The rightmost entry is the one the nearest trusted proxy appended, so a
|
|
3
|
+
* client cannot forge it the way it can the entries to its left. */
|
|
4
|
+
export function clientAddress(request, socketAddress) {
|
|
5
|
+
const last = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim();
|
|
6
|
+
return last && isIP(last) ? last : socketAddress;
|
|
7
|
+
}
|
|
8
|
+
export function forwardedHttps(request) {
|
|
9
|
+
const last = request.headers
|
|
10
|
+
.get("x-forwarded-proto")
|
|
11
|
+
?.split(",")
|
|
12
|
+
.pop()
|
|
13
|
+
?.trim()
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
return last === "https";
|
|
16
|
+
}
|