Ready for testing

This commit is contained in:
Julian van der Horst
2026-05-27 20:37:56 +02:00
parent e2441ce284
commit 9804c0db28
30 changed files with 308 additions and 145 deletions
+2 -1
View File
@@ -144,7 +144,8 @@ export const resources = pgTable("resources", {
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
onDelete: "set null"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
requestHeaders: text("requestHeaders"),
responseHeaders: text("responseHeaders"),
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
+2 -1
View File
@@ -161,7 +161,8 @@ export const resources = sqliteTable("resources", {
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
onDelete: "set null"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
requestHeaders: text("requestHeaders"),
responseHeaders: text("responseHeaders"),
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
.notNull()
.default(false),
+37 -14
View File
@@ -65,7 +65,8 @@ export async function getTraefikConfig(
tlsServerName: resources.tlsServerName,
setHostHeader: resources.setHostHeader,
enableProxy: resources.enableProxy,
headers: resources.headers,
requestHeaders: resources.requestHeaders,
responseHeaders: resources.responseHeaders,
proxyProtocol: resources.proxyProtocol,
proxyProtocolVersion: resources.proxyProtocolVersion,
@@ -177,7 +178,8 @@ export async function getTraefikConfig(
setHostHeader: row.setHostHeader,
enableProxy: row.enableProxy,
targets: [],
headers: row.headers,
requestHeaders: row.requestHeaders,
responseHeaders: row.responseHeaders,
proxyProtocol: row.proxyProtocol,
proxyProtocolVersion: row.proxyProtocolVersion ?? 1,
path: row.path, // the targets will all have the same path
@@ -364,38 +366,59 @@ export async function getTraefikConfig(
}
// Handle custom headers middleware
if (resource.headers || resource.setHostHeader) {
const headersObj: { [key: string]: string } = {};
if (resource.requestHeaders || resource.responseHeaders || resource.setHostHeader) {
const requestHeadersObj: { [key: string]: string } = {};
const responseHeadersObj: { [key: string]: string } = {};
if (resource.headers) {
let headersArr: { name: string; value: string }[] = [];
if (resource.requestHeaders) {
let requestHeadersArr: { name: string; value: string }[] = [];
try {
headersArr = JSON.parse(resource.headers) as {
requestHeadersArr = JSON.parse(resource.requestHeaders) as {
name: string;
value: string;
}[];
} catch (e) {
logger.warn(
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
`Failed to parse requestHeaders for resource ${resource.resourceId}: ${e}`
);
}
headersArr.forEach((header) => {
headersObj[header.name] = header.value;
requestHeadersArr.forEach((header) => {
requestHeadersObj[header.name] = header.value;
});
}
if (resource.setHostHeader) {
headersObj["Host"] = resource.setHostHeader;
requestHeadersObj["Host"] = resource.setHostHeader;
}
if (Object.keys(headersObj).length > 0) {
if (resource.responseHeaders) {
let responseHeadersArr: { name: string; value: string }[] = [];
try {
responseHeadersArr = JSON.parse(resource.responseHeaders) as {
name: string;
value: string;
}[];
} catch (e) {
logger.warn(
`Failed to parse responseHeaders for resource ${resource.resourceId}: ${e}`
);
}
responseHeadersArr.forEach((header) => {
responseHeadersObj[header.name] = header.value;
});
}
const hasRequestHeaders = Object.keys(requestHeadersObj).length > 0;
const hasResponseHeaders = Object.keys(responseHeadersObj).length > 0;
if (hasRequestHeaders || hasResponseHeaders) {
if (!config_output.http.middlewares) {
config_output.http.middlewares = {};
}
config_output.http.middlewares[headersMiddlewareName] = {
headers: {
customRequestHeaders: headersObj
...(hasRequestHeaders && { customRequestHeaders: requestHeadersObj }),
...(hasResponseHeaders && { customResponseHeaders: responseHeadersObj })
}
};
+38 -17
View File
@@ -105,7 +105,8 @@ export async function getTraefikConfig(
tlsServerName: resources.tlsServerName,
setHostHeader: resources.setHostHeader,
enableProxy: resources.enableProxy,
headers: resources.headers,
requestHeaders: resources.requestHeaders,
responseHeaders: resources.responseHeaders,
proxyProtocol: resources.proxyProtocol,
proxyProtocolVersion: resources.proxyProtocolVersion,
wildcard: resources.wildcard,
@@ -237,7 +238,8 @@ export async function getTraefikConfig(
setHostHeader: row.setHostHeader,
enableProxy: row.enableProxy,
targets: [],
headers: row.headers,
requestHeaders: row.requestHeaders,
responseHeaders: row.responseHeaders,
proxyProtocol: row.proxyProtocol,
proxyProtocolVersion: row.proxyProtocolVersion ?? 1,
path: row.path, // the targets will all have the same path
@@ -648,40 +650,59 @@ export async function getTraefikConfig(
}
}
if (resource.headers || resource.setHostHeader) {
// if there are headers, parse them into an object
const headersObj: { [key: string]: string } = {};
if (resource.headers) {
let headersArr: { name: string; value: string }[] = [];
if (resource.requestHeaders || resource.responseHeaders || resource.setHostHeader) {
const requestHeadersObj: { [key: string]: string } = {};
const responseHeadersObj: { [key: string]: string } = {};
if (resource.requestHeaders) {
let requestHeadersArr: { name: string; value: string }[] = [];
try {
headersArr = JSON.parse(resource.headers) as {
requestHeadersArr = JSON.parse(resource.requestHeaders) as {
name: string;
value: string;
}[];
} catch (e) {
logger.warn(
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
`Failed to parse requestHeaders for resource ${resource.resourceId}: ${e}`
);
}
headersArr.forEach((header) => {
headersObj[header.name] = header.value;
requestHeadersArr.forEach((header) => {
requestHeadersObj[header.name] = header.value;
});
}
if (resource.setHostHeader) {
headersObj["Host"] = resource.setHostHeader;
requestHeadersObj["Host"] = resource.setHostHeader;
}
// check if the object is not empty
if (Object.keys(headersObj).length > 0) {
// Add the headers middleware
if (resource.responseHeaders) {
let responseHeadersArr: { name: string; value: string }[] = [];
try {
responseHeadersArr = JSON.parse(resource.responseHeaders) as {
name: string;
value: string;
}[];
} catch (e) {
logger.warn(
`Failed to parse responseHeaders for resource ${resource.resourceId}: ${e}`
);
}
responseHeadersArr.forEach((header) => {
responseHeadersObj[header.name] = header.value;
});
}
const hasRequestHeaders = Object.keys(requestHeadersObj).length > 0;
const hasResponseHeaders = Object.keys(responseHeadersObj).length > 0;
if (hasRequestHeaders || hasResponseHeaders) {
if (!config_output.http.middlewares) {
config_output.http.middlewares = {};
}
config_output.http.middlewares[headersMiddlewareName] = {
headers: {
customRequestHeaders: headersObj
...(hasRequestHeaders && { customRequestHeaders: requestHeadersObj }),
...(hasResponseHeaders && { customResponseHeaders: responseHeadersObj })
}
};
+6 -3
View File
@@ -105,9 +105,12 @@ export async function getResource(
return response<GetResourceResponse>(res, {
data: {
...resource,
headers: resource.headers
? JSON.parse(resource.headers)
: resource.headers
requestHeaders: resource.requestHeaders
? JSON.parse(resource.requestHeaders)
: resource.requestHeaders,
responseHeaders: resource.responseHeaders
? JSON.parse(resource.responseHeaders)
: resource.responseHeaders
},
success: true,
error: false,
+40 -31
View File
@@ -58,7 +58,11 @@ const updateHttpResourceBodySchema = z
tlsServerName: z.string().nullable().optional(),
setHostHeader: z.string().nullable().optional(),
skipToIdpId: z.int().positive().nullable().optional(),
headers: z
requestHeaders: z
.array(z.strictObject({ name: z.string(), value: z.string() }))
.nullable()
.optional(),
responseHeaders: z
.array(z.strictObject({ name: z.string(), value: z.string() }))
.nullable()
.optional(),
@@ -111,12 +115,12 @@ const updateHttpResourceBodySchema = z
)
.refine(
(data) => {
if (data.headers) {
// HTTP header names must be valid token characters (RFC 7230)
const validHeaderName = /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/;
return data.headers.every((h) => validHeaderName.test(h.name));
}
return true;
const validHeaderName = /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/;
const allHeaders = [
...(data.requestHeaders ?? []),
...(data.responseHeaders ?? [])
];
return allHeaders.every((h) => validHeaderName.test(h.name));
},
{
error: "Header names may only contain valid HTTP token characters (letters, digits, and !#$%&'*+-.^_`|~)."
@@ -124,14 +128,12 @@ const updateHttpResourceBodySchema = z
)
.refine(
(data) => {
if (data.headers) {
// HTTP header values must be visible ASCII or horizontal whitespace, no control chars (RFC 7230)
const validHeaderValue = /^[\t\x20-\x7E]*$/;
return data.headers.every((h) =>
validHeaderValue.test(h.value)
);
}
return true;
const validHeaderValue = /^[\t\x20-\x7E]*$/;
const allHeaders = [
...(data.requestHeaders ?? []),
...(data.responseHeaders ?? [])
];
return allHeaders.every((h) => validHeaderValue.test(h.value));
},
{
error: "Header values may only contain printable ASCII characters and horizontal whitespace."
@@ -139,16 +141,16 @@ const updateHttpResourceBodySchema = z
)
.refine(
(data) => {
if (data.headers) {
// Reject Traefik template syntax {{word}} in names or values
const templatePattern = /\{\{[^}]+\}\}/;
return data.headers.every(
(h) =>
!templatePattern.test(h.name) &&
!templatePattern.test(h.value)
);
}
return true;
const templatePattern = /\{\{[^}]+\}\}/;
const allHeaders = [
...(data.requestHeaders ?? []),
...(data.responseHeaders ?? [])
];
return allHeaders.every(
(h) =>
!templatePattern.test(h.name) &&
!templatePattern.test(h.value)
);
},
{
error: "Header names and values must not contain template expressions such as {{value}}."
@@ -467,11 +469,18 @@ async function updateHttpResource(
}
}
let headers = undefined;
if (updateData.headers) {
headers = JSON.stringify(updateData.headers);
} else if (updateData.headers === null) {
headers = null;
let requestHeaders = undefined;
if (updateData.requestHeaders) {
requestHeaders = JSON.stringify(updateData.requestHeaders);
} else if (updateData.requestHeaders === null) {
requestHeaders = null;
}
let responseHeaders = undefined;
if (updateData.responseHeaders) {
responseHeaders = JSON.stringify(updateData.responseHeaders);
} else if (updateData.responseHeaders === null) {
responseHeaders = null;
}
const isLicensed = await isLicensedOrSubscribed(
@@ -488,7 +497,7 @@ async function updateHttpResource(
const updatedResource = await db
.update(resources)
.set({ ...updateData, headers })
.set({ ...updateData, requestHeaders, responseHeaders })
.where(eq(resources.resourceId, resource.resourceId))
.returning();
+3 -1
View File
@@ -25,6 +25,7 @@ import m16 from "./scriptsPg/1.17.0";
import m17 from "./scriptsPg/1.18.0";
import m18 from "./scriptsPg/1.18.3";
import m19 from "./scriptsPg/1.18.4";
import m20 from "./scriptsPg/1.18.5";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -49,7 +50,8 @@ const migrations = [
{ version: "1.17.0", run: m16 },
{ version: "1.18.0", run: m17 },
{ version: "1.18.3", run: m18 },
{ version: "1.18.4", run: m19 }
{ version: "1.18.4", run: m19 },
{ version: "1.18.5", run: m20 }
// Add new migrations here as they are created
] as {
version: string;
+3 -1
View File
@@ -43,6 +43,7 @@ import m37 from "./scriptsSqlite/1.17.0";
import m38 from "./scriptsSqlite/1.18.0";
import m39 from "./scriptsSqlite/1.18.3";
import m40 from "./scriptsSqlite/1.18.4";
import m41 from "./scriptsSqlite/1.18.5";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -83,7 +84,8 @@ const migrations = [
{ version: "1.17.0", run: m37 },
{ version: "1.18.0", run: m38 },
{ version: "1.18.3", run: m39 },
{ version: "1.18.4", run: m40 }
{ version: "1.18.4", run: m40 },
{ version: "1.18.5", run: m41 }
// Add new migrations here as they are created
] as const;
+30
View File
@@ -0,0 +1,30 @@
import { db } from "@server/db/pg/driver";
import { sql } from "drizzle-orm";
const version = "1.18.5";
export default async function migration() {
console.log(`Running setup script ${version}...`);
try {
await db.execute(sql`BEGIN`);
await db.execute(sql`
ALTER TABLE "resources" RENAME COLUMN "headers" TO "requestHeaders";
`);
await db.execute(sql`
ALTER TABLE "resources" ADD COLUMN "responseHeaders" text;
`);
await db.execute(sql`COMMIT`);
console.log("Migrated database");
} catch (e) {
await db.execute(sql`ROLLBACK`);
console.log("Unable to migrate database");
console.log(e);
throw e;
}
console.log(`${version} migration complete`);
}
+34
View File
@@ -0,0 +1,34 @@
import { APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import path from "path";
const version = "1.18.5";
export default async function migration() {
console.log(`Running setup script ${version}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
try {
db.pragma("foreign_keys = OFF");
db.transaction(() => {
db.prepare(
`ALTER TABLE 'resources' RENAME COLUMN 'headers' TO 'requestHeaders';`
).run();
db.prepare(
`ALTER TABLE 'resources' ADD 'responseHeaders' text;`
).run();
})();
db.pragma("foreign_keys = ON");
console.log("Migrated database");
} catch (e) {
console.log("Failed to migrate db:", e);
throw e;
}
console.log(`${version} migration complete`);
}