mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 02:36:38 +00:00
Store headers as json
This commit is contained in:
@@ -42,7 +42,9 @@ async function query(resourceId?: number, niceId?: string, orgId?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetResourceResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
export type GetResourceResponse = Omit<NonNullable<Awaited<ReturnType<typeof query>>>, 'headers'> & {
|
||||||
|
headers: { name: string; value: string }[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
@@ -99,7 +101,10 @@ export async function getResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response<GetResourceResponse>(res, {
|
return response<GetResourceResponse>(res, {
|
||||||
data: resource,
|
data: {
|
||||||
|
...resource,
|
||||||
|
headers: resource.headers ? JSON.parse(resource.headers) : resource.headers
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Resource retrieved successfully",
|
message: "Resource retrieved successfully",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const updateHttpResourceBodySchema = z
|
|||||||
tlsServerName: z.string().nullable().optional(),
|
tlsServerName: z.string().nullable().optional(),
|
||||||
setHostHeader: z.string().nullable().optional(),
|
setHostHeader: z.string().nullable().optional(),
|
||||||
skipToIdpId: z.number().int().positive().nullable().optional(),
|
skipToIdpId: z.number().int().positive().nullable().optional(),
|
||||||
headers: z.string().nullable().optional()
|
headers: z.array(z.object({ name: z.string(), value: z.string() })).optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
@@ -86,18 +86,6 @@ const updateHttpResourceBodySchema = z
|
|||||||
"Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header."
|
"Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.headers) {
|
|
||||||
return validateHeaders(data.headers);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"Invalid headers format. Use comma-separated format: 'Header-Name: value, Another-Header: another-value'. Header values cannot contain colons."
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type UpdateResourceResponse = Resource;
|
export type UpdateResourceResponse = Resource;
|
||||||
|
|
||||||
@@ -292,9 +280,14 @@ async function updateHttpResource(
|
|||||||
updateData.subdomain = finalSubdomain;
|
updateData.subdomain = finalSubdomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headers = null;
|
||||||
|
if (updateData.headers) {
|
||||||
|
headers = JSON.stringify(updateData.headers);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedResource = await db
|
const updatedResource = await db
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set({ ...updateData })
|
.set({ ...updateData, headers })
|
||||||
.where(eq(resources.resourceId, resource.resourceId))
|
.where(eq(resources.resourceId, resource.resourceId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -306,22 +306,25 @@ export async function getTraefikConfig(
|
|||||||
...additionalMiddlewares
|
...additionalMiddlewares
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (resource.headers || resource.setHostHeader) {
|
||||||
resource.headers ||
|
|
||||||
resource.setHostHeader
|
|
||||||
) {
|
|
||||||
// if there are headers, parse them into an object
|
// if there are headers, parse them into an object
|
||||||
const headersObj: { [key: string]: string } = {};
|
const headersObj: { [key: string]: string } = {};
|
||||||
const headersArr = resource.headers?.split(",");
|
if (resource.headers) {
|
||||||
if (headersArr && headersArr.length > 0) {
|
let headersArr: { name: string; value: string }[] = [];
|
||||||
for (const header of headersArr) {
|
try {
|
||||||
const [key, value] = header
|
headersArr = JSON.parse(resource.headers) as {
|
||||||
.split(":")
|
name: string;
|
||||||
.map((s: string) => s.trim());
|
value: string;
|
||||||
if (key && value) {
|
}[];
|
||||||
headersObj[key] = value;
|
} catch (e) {
|
||||||
}
|
logger.warn(
|
||||||
|
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headersArr.forEach((header) => {
|
||||||
|
headersObj[header.name] = header.value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.setHostHeader) {
|
if (resource.setHostHeader) {
|
||||||
|
|||||||
49
server/setup/scriptsPg/1.10.1.ts
Normal file
49
server/setup/scriptsPg/1.10.1.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import path, { join } from "path";
|
||||||
|
|
||||||
|
const version = "1.10.1";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resources = await db.execute(sql`
|
||||||
|
SELECT * FROM "resources"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
for (const resource of resources.rows) {
|
||||||
|
const headers = resource.headers as string | null;
|
||||||
|
if (headers && headers !== "") {
|
||||||
|
// lets convert it to json
|
||||||
|
// fist split at commas
|
||||||
|
const headersArray = headers
|
||||||
|
.split(",")
|
||||||
|
.map((header: string) => {
|
||||||
|
const [name, ...valueParts] = header.split(":");
|
||||||
|
const value = valueParts.join(":").trim();
|
||||||
|
return { name: name.trim(), value };
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "resources" SET "headers" = ${JSON.stringify(headersArray)} WHERE "resourceId" = ${resource.resourceId}
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Updated resource ${resource.resourceId} headers to JSON format`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,16 @@ import path from "path";
|
|||||||
const version = "1.10.1";
|
const version = "1.10.1";
|
||||||
|
|
||||||
export default async function migration() {
|
export default async function migration() {
|
||||||
console.log(`Running setup script ${version}...`);
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
const db = new Database(location);
|
const db = new Database(location);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.pragma("foreign_keys = OFF");
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.exec(`ALTER TABLE "targets" RENAME TO "targets_old";
|
db.exec(`ALTER TABLE "targets" RENAME TO "targets_old";
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "targets" (
|
CREATE TABLE "targets" (
|
||||||
"targetId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
"targetId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -57,13 +57,43 @@ SELECT
|
|||||||
FROM "targets_old";
|
FROM "targets_old";
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
DROP TABLE "targets_old";`);
|
DROP TABLE "targets_old";`);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
db.pragma("foreign_keys = ON");
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
console.log(`Migrated database`);
|
const resources = db.prepare("SELECT * FROM resources").all() as Array<{
|
||||||
} catch (e) {
|
resourceId: number;
|
||||||
console.log("Failed to migrate db:", e);
|
headers: string | null;
|
||||||
throw e;
|
}>;
|
||||||
}
|
|
||||||
|
for (const resource of resources) {
|
||||||
|
const headers = resource.headers;
|
||||||
|
if (headers && headers !== "") {
|
||||||
|
// lets convert it to json
|
||||||
|
// fist split at commas
|
||||||
|
const headersArray = headers
|
||||||
|
.split(",")
|
||||||
|
.map((header: string) => {
|
||||||
|
const [name, ...valueParts] = header.split(":");
|
||||||
|
const value = valueParts.join(":").trim();
|
||||||
|
return { name: name.trim(), value };
|
||||||
|
});
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
UPDATE "resources" SET "headers" = ? WHERE "resourceId" = ?
|
||||||
|
`
|
||||||
|
).run(JSON.stringify(headersArray), resource.resourceId);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Updated resource ${resource.resourceId} headers to JSON format`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
message: t("proxyErrorInvalidHeader")
|
message: t("proxyErrorInvalidHeader")
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
headers: z.string().optional()
|
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
const tlsSettingsSchema = z.object({
|
const tlsSettingsSchema = z.object({
|
||||||
@@ -286,7 +286,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
resolver: zodResolver(proxySettingsSchema),
|
resolver: zodResolver(proxySettingsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
setHostHeader: resource.setHostHeader || "",
|
setHostHeader: resource.setHostHeader || "",
|
||||||
headers: resource.headers || ""
|
headers: resource.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1479,7 +1479,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<HeadersInput
|
<HeadersInput
|
||||||
value={
|
value={
|
||||||
field.value || ""
|
field.value
|
||||||
}
|
}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
field.onChange(
|
field.onChange(
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
|
|
||||||
interface HeadersInputProps {
|
interface HeadersInputProps {
|
||||||
value?: string;
|
value?: { name: string, value: string }[] | null;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: { name: string, value: string }[] | null) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeadersInput({
|
export function HeadersInput({
|
||||||
value = "",
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = `X-Example-Header: example-value
|
placeholder = `X-Example-Header: example-value
|
||||||
X-Another-Header: another-value`,
|
X-Another-Header: another-value`,
|
||||||
@@ -21,26 +22,35 @@ X-Another-Header: another-value`,
|
|||||||
}: HeadersInputProps) {
|
}: HeadersInputProps) {
|
||||||
const [internalValue, setInternalValue] = useState("");
|
const [internalValue, setInternalValue] = useState("");
|
||||||
|
|
||||||
// Convert comma-separated to newline-separated for display
|
// Convert header objects array to newline-separated string for display
|
||||||
const convertToNewlineSeparated = (commaSeparated: string): string => {
|
const convertToNewlineSeparated = (headers: { name: string, value: string }[] | null): string => {
|
||||||
if (!commaSeparated || commaSeparated.trim() === "") return "";
|
if (!headers || headers.length === 0) return "";
|
||||||
|
|
||||||
return commaSeparated
|
return headers
|
||||||
.split(',')
|
.map(header => `${header.name}: ${header.value}`)
|
||||||
.map(header => header.trim())
|
|
||||||
.filter(header => header.length > 0)
|
|
||||||
.join('\n');
|
.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert newline-separated to comma-separated for output
|
// Convert newline-separated string to header objects array
|
||||||
const convertToCommaSeparated = (newlineSeparated: string): string => {
|
const convertToHeadersArray = (newlineSeparated: string): { name: string, value: string }[] | null => {
|
||||||
if (!newlineSeparated || newlineSeparated.trim() === "") return "";
|
if (!newlineSeparated || newlineSeparated.trim() === "") return [];
|
||||||
|
|
||||||
return newlineSeparated
|
return newlineSeparated
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(header => header.trim())
|
.map(line => line.trim())
|
||||||
.filter(header => header.length > 0)
|
.filter(line => line.length > 0 && line.includes(':'))
|
||||||
.join(', ');
|
.map(line => {
|
||||||
|
const colonIndex = line.indexOf(':');
|
||||||
|
const name = line.substring(0, colonIndex).trim();
|
||||||
|
const value = line.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
// Ensure header name conforms to HTTP header requirements
|
||||||
|
// Header names should be case-insensitive, contain only ASCII letters, digits, and hyphens
|
||||||
|
const normalizedName = name.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase();
|
||||||
|
|
||||||
|
return { name: normalizedName, value };
|
||||||
|
})
|
||||||
|
.filter(header => header.name.length > 0); // Filter out headers with invalid names
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update internal value when external value changes
|
// Update internal value when external value changes
|
||||||
@@ -52,9 +62,9 @@ X-Another-Header: another-value`,
|
|||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setInternalValue(newValue);
|
setInternalValue(newValue);
|
||||||
|
|
||||||
// Convert back to comma-separated format for the parent
|
// Convert back to header objects array for the parent
|
||||||
const commaSeparatedValue = convertToCommaSeparated(newValue);
|
const headersArray = convertToHeadersArray(newValue);
|
||||||
onChange(commaSeparatedValue);
|
onChange(headersArray);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user