mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-04-11 13:37:44 +03:00
* fix: improve reverse proxy support for subdomain deployments (#63) - Add X-Accel-Buffering: no header to SSE endpoint to prevent Nginx from buffering the event stream - Auto-detect trusted origin from Host/X-Forwarded-* request headers so the app works behind a proxy without manual env var configuration - Add prominent reverse proxy documentation to advanced docs page explaining BETTER_AUTH_URL, PUBLIC_BETTER_AUTH_URL, and BETTER_AUTH_TRUSTED_ORIGINS are mandatory for proxy deployments - Add reverse proxy env var comments and entries to both docker-compose.yml and docker-compose.alt.yml - Add dedicated reverse proxy configuration section to .env.example * fix: address review findings for reverse proxy origin detection - Fix x-forwarded-proto multi-value handling: take first value only and validate it is "http" or "https" before using - Update comment to accurately describe auto-detection scope: helps with per-request CSRF checks but not callback URL validation - Restore startup logging of static trusted origins for debugging * fix: handle multi-value x-forwarded-host in chained proxy setups x-forwarded-host can be comma-separated (e.g. "proxy1.example.com, proxy2.example.com") in chained proxy setups. Take only the first value, matching the same handling already applied to x-forwarded-proto. * test: add unit tests for reverse proxy origin detection Extract resolveTrustedOrigins into a testable exported function and add 11 tests covering: - Default localhost origins - BETTER_AUTH_URL and BETTER_AUTH_TRUSTED_ORIGINS env vars - Invalid URL handling - Auto-detection from x-forwarded-host + x-forwarded-proto - Multi-value header handling (chained proxy setups) - Invalid proto rejection (only http/https allowed) - Deduplication - Fallback to host header when x-forwarded-host absent
120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
import { resolveTrustedOrigins } from "./auth";
|
|
|
|
// Helper to create a mock Request with specific headers
|
|
function mockRequest(headers: Record<string, string>): Request {
|
|
return new Request("http://localhost:4321/api/auth/sign-in", {
|
|
headers: new Headers(headers),
|
|
});
|
|
}
|
|
|
|
describe("resolveTrustedOrigins", () => {
|
|
const savedEnv: Record<string, string | undefined> = {};
|
|
|
|
beforeEach(() => {
|
|
// Save and clear relevant env vars
|
|
for (const key of ["BETTER_AUTH_URL", "BETTER_AUTH_TRUSTED_ORIGINS"]) {
|
|
savedEnv[key] = process.env[key];
|
|
delete process.env[key];
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore env vars
|
|
for (const [key, val] of Object.entries(savedEnv)) {
|
|
if (val === undefined) delete process.env[key];
|
|
else process.env[key] = val;
|
|
}
|
|
});
|
|
|
|
test("includes localhost defaults when called without request", async () => {
|
|
const origins = await resolveTrustedOrigins();
|
|
expect(origins).toContain("http://localhost:4321");
|
|
expect(origins).toContain("http://localhost:8080");
|
|
});
|
|
|
|
test("includes BETTER_AUTH_URL from env", async () => {
|
|
process.env.BETTER_AUTH_URL = "https://gitea-mirror.example.com";
|
|
const origins = await resolveTrustedOrigins();
|
|
expect(origins).toContain("https://gitea-mirror.example.com");
|
|
});
|
|
|
|
test("includes BETTER_AUTH_TRUSTED_ORIGINS (comma-separated)", async () => {
|
|
process.env.BETTER_AUTH_TRUSTED_ORIGINS = "https://a.example.com, https://b.example.com";
|
|
const origins = await resolveTrustedOrigins();
|
|
expect(origins).toContain("https://a.example.com");
|
|
expect(origins).toContain("https://b.example.com");
|
|
});
|
|
|
|
test("skips invalid URLs in env vars", async () => {
|
|
process.env.BETTER_AUTH_URL = "not-a-url";
|
|
process.env.BETTER_AUTH_TRUSTED_ORIGINS = "also-invalid, https://valid.example.com";
|
|
const origins = await resolveTrustedOrigins();
|
|
expect(origins).not.toContain("not-a-url");
|
|
expect(origins).not.toContain("also-invalid");
|
|
expect(origins).toContain("https://valid.example.com");
|
|
});
|
|
|
|
test("auto-detects origin from x-forwarded-host + x-forwarded-proto", async () => {
|
|
const req = mockRequest({
|
|
"x-forwarded-host": "gitea-mirror.mydomain.tld",
|
|
"x-forwarded-proto": "https",
|
|
});
|
|
const origins = await resolveTrustedOrigins(req);
|
|
expect(origins).toContain("https://gitea-mirror.mydomain.tld");
|
|
});
|
|
|
|
test("falls back to host header when x-forwarded-host is absent", async () => {
|
|
const req = mockRequest({
|
|
host: "myserver.local:4321",
|
|
});
|
|
const origins = await resolveTrustedOrigins(req);
|
|
expect(origins).toContain("http://myserver.local:4321");
|
|
});
|
|
|
|
test("handles multi-value x-forwarded-host (chained proxies)", async () => {
|
|
const req = mockRequest({
|
|
"x-forwarded-host": "external.example.com, internal.proxy.local",
|
|
"x-forwarded-proto": "https",
|
|
});
|
|
const origins = await resolveTrustedOrigins(req);
|
|
expect(origins).toContain("https://external.example.com");
|
|
expect(origins).not.toContain("https://internal.proxy.local");
|
|
});
|
|
|
|
test("handles multi-value x-forwarded-proto (chained proxies)", async () => {
|
|
const req = mockRequest({
|
|
"x-forwarded-host": "gitea.example.com",
|
|
"x-forwarded-proto": "https, http",
|
|
});
|
|
const origins = await resolveTrustedOrigins(req);
|
|
expect(origins).toContain("https://gitea.example.com");
|
|
// Should NOT create an origin with "https, http" as proto
|
|
expect(origins).not.toContain("https, http://gitea.example.com");
|
|
});
|
|
|
|
test("rejects invalid x-forwarded-proto values", async () => {
|
|
const req = mockRequest({
|
|
"x-forwarded-host": "gitea.example.com",
|
|
"x-forwarded-proto": "ftp",
|
|
});
|
|
const origins = await resolveTrustedOrigins(req);
|
|
expect(origins).not.toContain("ftp://gitea.example.com");
|
|
});
|
|
|
|
test("deduplicates origins", async () => {
|
|
process.env.BETTER_AUTH_URL = "http://localhost:4321";
|
|
const origins = await resolveTrustedOrigins();
|
|
const count = origins.filter(o => o === "http://localhost:4321").length;
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
test("defaults proto to http when x-forwarded-proto is absent", async () => {
|
|
const req = mockRequest({
|
|
"x-forwarded-host": "gitea.internal",
|
|
});
|
|
const origins = await resolveTrustedOrigins(req);
|
|
expect(origins).toContain("http://gitea.internal");
|
|
});
|
|
});
|