From 149a4b916b37ff0eb1f5fa496a9f6dccdb2cddcf Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 28 Sep 2025 11:25:11 +0530 Subject: [PATCH 01/12] basic setup for rewriting requests to another path --- messages/en-US.json | 6 +- server/db/pg/schema.ts | 4 +- server/db/sqlite/schema.ts | 4 +- server/routers/target/createTarget.ts | 4 +- server/routers/target/listTargets.ts | 4 +- server/routers/target/updateTarget.ts | 4 +- server/routers/traefik/getTraefikConfig.ts | 392 +++++++++++++++--- .../resources/[niceId]/proxy/page.tsx | 284 +++++++++---- 8 files changed, 557 insertions(+), 145 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index d9a60837..f4e063ce 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1522,5 +1522,7 @@ "resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml", "resourceExposePortsEditFile": "Edit file: docker-compose.yml", "emailVerificationRequired": "Email verification is required. Please log in again via {dashboardUrl}/auth/login complete this step. Then, come back here.", - "twoFactorSetupRequired": "Two-factor authentication setup is required. Please log in again via {dashboardUrl}/auth/login complete this step. Then, come back here." -} \ No newline at end of file + "twoFactorSetupRequired": "Two-factor authentication setup is required. Please log in again via {dashboardUrl}/auth/login complete this step. Then, come back here.", + "rewritePath": "Rewrite Path", + "rewritePathDescription": "Optionally rewrite the path before forwarding to the target." +} diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 94e9d98a..18b29f35 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -123,7 +123,9 @@ export const targets = pgTable("targets", { internalPort: integer("internalPort"), enabled: boolean("enabled").notNull().default(true), path: text("path"), - pathMatchType: text("pathMatchType") // exact, prefix, regex + pathMatchType: text("pathMatchType"), // exact, prefix, regex + rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target + rewritePathType: text("rewritePathType") // exact, prefix, regex, stripPrefix }); export const exitNodes = pgTable("exitNodes", { diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 0fb65d03..e38d6b67 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -135,7 +135,9 @@ export const targets = sqliteTable("targets", { internalPort: integer("internalPort"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), path: text("path"), - pathMatchType: text("pathMatchType") // exact, prefix, regex + pathMatchType: text("pathMatchType"), // exact, prefix, regex + rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target + rewritePathType: text("rewritePathType") // exact, prefix, regex, stripPrefix }); export const exitNodes = sqliteTable("exitNodes", { diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index fb85f566..6ed9d9aa 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -32,7 +32,9 @@ const createTargetSchema = z port: z.number().int().min(1).max(65535), enabled: z.boolean().default(true), path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), + rewritePath: z.string().optional().nullable(), + rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() // NEW: rewrite path type }) .strict(); diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index ca1159d2..4a1d99a0 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -46,7 +46,9 @@ function queryTargets(resourceId: number) { siteId: targets.siteId, siteType: sites.type, path: targets.path, - pathMatchType: targets.pathMatchType + pathMatchType: targets.pathMatchType, + rewritePath: targets.rewritePath, + rewritePathType: targets.rewritePathType }) .from(targets) .leftJoin(sites, eq(sites.siteId, targets.siteId)) diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 928a1a55..1e47ce96 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -28,7 +28,9 @@ const updateTargetBodySchema = z port: z.number().int().min(1).max(65535).optional(), enabled: z.boolean().optional(), path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), + rewritePath: z.string().optional().nullable(), + rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 4bbb13d3..b713f98a 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -90,6 +90,217 @@ export async function traefikConfigProvider( } } + +function validatePathRewriteConfig( + path: string | null, + pathMatchType: string | null, + rewritePath: string | null, + rewritePathType: string | null +): { isValid: boolean; error?: string } { + // If no path matching is configured, no rewriting is possible + if (!path || !pathMatchType) { + if (rewritePath || rewritePathType) { + return { + isValid: false, + error: "Path rewriting requires path matching to be configured" + }; + } + return { isValid: true }; + } + + if ((rewritePath && !rewritePathType) || (!rewritePath && rewritePathType)) { + return { + isValid: false, + error: "Both rewritePath and rewritePathType must be specified together" + }; + } + + if (!rewritePath || !rewritePathType) { + return { isValid: true }; + } + + const validPathMatchTypes = ["exact", "prefix", "regex"]; + if (!validPathMatchTypes.includes(pathMatchType)) { + return { + isValid: false, + error: `Invalid pathMatchType: ${pathMatchType}. Must be one of: ${validPathMatchTypes.join(", ")}` + }; + } + + const validRewritePathTypes = ["exact", "prefix", "regex", "stripPrefix"]; + if (!validRewritePathTypes.includes(rewritePathType)) { + return { + isValid: false, + error: `Invalid rewritePathType: ${rewritePathType}. Must be one of: ${validRewritePathTypes.join(", ")}` + }; + } + + if (pathMatchType === "regex") { + try { + new RegExp(path); + } catch (e) { + return { + isValid: false, + error: `Invalid regex pattern in path: ${path}` + }; + } + } + + if (rewritePathType === "regex") { + // For regex rewrite type, we don't validate the replacement pattern + // as it may contain capture groups like $1, $2, etc. + // The regex engine will handle validation at runtime + } + + // Validate path formats for non-regex types + if (pathMatchType !== "regex" && !path.startsWith("/")) { + return { + isValid: false, + error: "Path must start with '/' for exact and prefix matching" + }; + } + + // Additional validation for stripPrefix + if (rewritePathType === "stripPrefix") { + if (pathMatchType !== "prefix") { + logger.warn(`stripPrefix rewrite type is most effective with prefix path matching. Current match type: ${pathMatchType}`); + } + // For stripPrefix, rewritePath is optional (can be empty to just strip) + if (rewritePath && !rewritePath.startsWith("/") && rewritePath !== "") { + return { + isValid: false, + error: "stripPrefix rewritePath must start with '/' or be empty" + }; + } + } + + return { isValid: true }; +} + + +function createPathRewriteMiddleware( + middlewareName: string, + path: string, + pathMatchType: string, + rewritePath: string, + rewritePathType: string +): { [key: string]: any } { + const middlewares: { [key: string]: any } = {}; + + switch (rewritePathType) { + case "exact": + // Replace the entire path with the exact rewrite path + middlewares[middlewareName] = { + replacePathRegex: { + regex: "^.*$", + replacement: rewritePath + } + }; + break; + + case "prefix": + // Replace matched prefix with new prefix, preserve the rest + switch (pathMatchType) { + case "prefix": + middlewares[middlewareName] = { + replacePathRegex: { + regex: `^${escapeRegex(path)}(.*)`, + replacement: `${rewritePath}$1` + } + }; + break; + case "exact": + middlewares[middlewareName] = { + replacePathRegex: { + regex: `^${escapeRegex(path)}$`, + replacement: rewritePath + } + }; + break; + case "regex": + // For regex path matching with prefix rewrite, we assume the regex has capture groups + middlewares[middlewareName] = { + replacePathRegex: { + regex: path, + replacement: rewritePath + } + }; + break; + } + break; + + case "regex": + // Use advanced regex replacement - works with any match type + let regexPattern: string; + if (pathMatchType === "regex") { + regexPattern = path; + } else if (pathMatchType === "prefix") { + regexPattern = `^${escapeRegex(path)}(.*)`; + } else { // exact + regexPattern = `^${escapeRegex(path)}$`; + } + + middlewares[middlewareName] = { + replacePathRegex: { + regex: regexPattern, + replacement: rewritePath + } + }; + break; + + case "stripPrefix": + // Strip the matched prefix and optionally add new path + if (pathMatchType === "prefix") { + middlewares[middlewareName] = { + stripPrefix: { + prefixes: [path] + } + }; + + // If rewritePath is provided and not empty, add it as a prefix after stripping + if (rewritePath && rewritePath !== "" && rewritePath !== "/") { + const addPrefixMiddlewareName = `${middlewareName.replace('-rewrite', '')}-add-prefix-middleware`; + middlewares[addPrefixMiddlewareName] = { + addPrefix: { + prefix: rewritePath + } + }; + // Return both middlewares with a special flag to indicate chaining + return { + middlewares, + chain: [middlewareName, addPrefixMiddlewareName] + }; + } + } else { + // For exact and regex matches, use replacePathRegex to strip + let regexPattern: string; + if (pathMatchType === "exact") { + regexPattern = `^${escapeRegex(path)}$`; + } else if (pathMatchType === "regex") { + regexPattern = path; + } else { + // This shouldn't happen due to earlier validation, but handle gracefully + regexPattern = `^${escapeRegex(path)}`; + } + + const replacement = rewritePath || "/"; + middlewares[middlewareName] = { + replacePathRegex: { + regex: regexPattern, + replacement: replacement + } + }; + } + break; + + default: + logger.error(`Unknown rewritePathType: ${rewritePathType}`); + throw new Error(`Unknown rewritePathType: ${rewritePathType}`); + } + + return { middlewares }; +} + export async function getTraefikConfig( exitNodeId: number, siteTypes: string[] @@ -133,7 +344,8 @@ export async function getTraefikConfig( internalPort: targets.internalPort, path: targets.path, pathMatchType: targets.pathMatchType, - + rewritePath: targets.rewritePath, + rewritePathType: targets.rewritePathType, // Site fields siteId: sites.siteId, siteType: sites.type, @@ -163,12 +375,28 @@ export async function getTraefikConfig( const resourceId = row.resourceId; const targetPath = sanitizePath(row.path) || ""; // Handle null/undefined paths const pathMatchType = row.pathMatchType || ""; + const rewritePath = row.rewritePath || ""; + const rewritePathType = row.rewritePathType || ""; - // Create a unique key combining resourceId and path+pathMatchType - const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-"); + // Create a unique key combining resourceId, path config, and rewrite config + const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType] + .filter(Boolean) + .join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); if (!resourcesMap.has(mapKey)) { + const validation = validatePathRewriteConfig( + row.path, + row.pathMatchType, + row.rewritePath, + row.rewritePathType + ); + + if (!validation.isValid) { + logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`); + return; + } + resourcesMap.set(mapKey, { resourceId: row.resourceId, fullDomain: row.fullDomain, @@ -186,7 +414,9 @@ export async function getTraefikConfig( targets: [], headers: row.headers, path: row.path, // the targets will all have the same path - pathMatchType: row.pathMatchType // the targets will all have the same pathMatchType + pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType + rewritePath: row.rewritePath, + rewritePathType: row.rewritePathType }); } @@ -199,6 +429,8 @@ export async function getTraefikConfig( port: row.port, internalPort: row.internalPort, enabled: row.targetEnabled, + rewritePath: row.rewritePath, + rewritePathType: row.rewritePathType, site: { siteId: row.siteId, type: row.siteType, @@ -241,19 +473,14 @@ export async function getTraefikConfig( } if (resource.http) { - if (!resource.domainId) { + if (!resource.domainId || !resource.fullDomain) { continue; } - if (!resource.fullDomain) { - continue; - } - - // add routers and services empty objects if they don't exist + // Initialize routers and services if they don't exist if (!config_output.http.routers) { config_output.http.routers = {}; } - if (!config_output.http.services) { config_output.http.services = {}; } @@ -288,24 +515,66 @@ export async function getTraefikConfig( certResolver: certResolver, ...(preferWildcardCert ? { - domains: [ - { - main: wildCard - } - ] - } + domains: [ + { + main: wildCard + } + ] + } : {}) }; } - const additionalMiddlewares = - config.getRawConfig().traefik.additional_middlewares || []; + const additionalMiddlewares = + config.getRawConfig().traefik.additional_middlewares || []; const routerMiddlewares = [ badgerMiddlewareName, ...additionalMiddlewares ]; + // Handle path rewriting middleware + if (resource.rewritePath && + resource.path && + resource.pathMatchType && + resource.rewritePathType) { + + const rewriteMiddlewareName = `${resource.id}-${key}-rewrite`; + + try { + const rewriteResult = createPathRewriteMiddleware( + rewriteMiddlewareName, + resource.path, + resource.pathMatchType, + resource.rewritePath, + resource.rewritePathType + ); + + // Initialize middlewares object if it doesn't exist + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + + // Add the middleware(s) to the config + Object.assign(config_output.http.middlewares, rewriteResult.middlewares); + + // Add middleware(s) to the router middleware chain + if (rewriteResult.chain) { + // For chained middlewares (like stripPrefix + addPrefix) + routerMiddlewares.push(...rewriteResult.chain); + } else { + // Single middleware + routerMiddlewares.push(rewriteMiddlewareName); + } + + logger.info(`Created path rewrite middleware for ${key}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); + } catch (error) { + logger.error(`Failed to create path rewrite middleware for ${key}: ${error}`); + // Continue without the rewrite middleware rather than failing completely + } + } + + // Handle custom headers middleware if (resource.headers || resource.setHostHeader) { // if there are headers, parse them into an object const headersObj: { [key: string]: string } = {}; @@ -406,15 +675,15 @@ export async function getTraefikConfig( return ( (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } + .filter((target: TargetWithSite) => { + if (!target.enabled) { + return false; + } // If any sites are online, exclude offline sites - if (anySitesOnline && !target.site.online) { - return false; - } + if (anySitesOnline && !target.site.online) { + return false; + } if ( target.site.type === "local" || @@ -427,33 +696,33 @@ export async function getTraefikConfig( ) { return false; } - } else if (target.site.type === "newt") { + } else if (target.site.type === "newt") { if ( !target.internalPort || !target.method || !target.site.subnet ) { - return false; + return false; } } return true; - }) - .map((target: TargetWithSite) => { + }) + .map((target: TargetWithSite) => { if ( target.site.type === "local" || target.site.type === "wireguard" ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { + return { + url: `${target.method}://${target.ip}:${target.port}` + }; + } else if (target.site.type === "newt") { const ip = target.site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }) + return { + url: `${target.method}://${ip}:${target.internalPort}` + }; + } + }) // filter out duplicates .filter( (v, i, a) => @@ -465,14 +734,14 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - cookie: { + sticky: { + cookie: { name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } + secure: resource.ssl, + httpOnly: true + } + } + } : {}) } }; @@ -549,7 +818,7 @@ export async function getTraefikConfig( !target.internalPort || !target.site.subnet ) { - return false; + return false; } } return true; @@ -573,13 +842,13 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } + sticky: { + ipStrategy: { + depth: 0, + sourcePort: true + } + } + } : {}) } }; @@ -590,10 +859,19 @@ export async function getTraefikConfig( function sanitizePath(path: string | null | undefined): string | undefined { if (!path) return undefined; - // clean any non alphanumeric characters from the path and replace with dashes - // the path cant be too long either, so limit to 50 characters + + // For path rewriting, we need to be more careful about sanitization + // Only limit length and ensure it's a valid path structure if (path.length > 50) { path = path.substring(0, 50); + logger.warn(`Path truncated to 50 characters: ${path}`); } - return path.replace(/[^a-zA-Z0-9]/g, ""); + + // Don't remove special characters as they might be part of regex patterns + // Just ensure it's not empty after trimming + return path.trim() || undefined; } + +function escapeRegex(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 1232e542..23a55494 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -105,7 +105,9 @@ const addTargetSchema = z.object({ port: z.coerce.number().int().positive(), siteId: z.number().int().positive(), path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), + rewritePath: z.string().optional().nullable(), + rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() }).refine( (data) => { // If path is provided, pathMatchType must be provided @@ -138,7 +140,23 @@ const addTargetSchema = z.object({ { message: "Invalid path configuration" } -); +) + .refine( + (data) => { + // If rewritePath is provided, rewritePathType must be provided + if (data.rewritePath && !data.rewritePathType) { + return false; + } + // If rewritePathType is provided, rewritePath must be provided + if (data.rewritePathType && !data.rewritePath) { + return false; + } + return true; + }, + { + message: "Invalid rewrite path configuration" + } + ); const targetsSettingsSchema = z.object({ stickySession: z.boolean() @@ -259,8 +277,10 @@ export default function ReverseProxyTargets(props: { method: resource.http ? "http" : null, port: "" as any as number, path: null, - pathMatchType: null - } + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + } as z.infer }); const watchedIp = addTargetForm.watch("ip"); @@ -436,6 +456,8 @@ export default function ReverseProxyTargets(props: { ...data, path: data.path || null, pathMatchType: data.pathMatchType || null, + rewritePath: data.rewritePath || null, + rewritePathType: data.rewritePathType || null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), @@ -449,7 +471,9 @@ export default function ReverseProxyTargets(props: { method: resource.http ? "http" : null, port: "" as any as number, path: null, - pathMatchType: null + pathMatchType: null, + rewritePath: null, + rewritePathType: null, }); } @@ -469,11 +493,11 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...data, - updated: true, - siteType: site?.type || null - } + ...target, + ...data, + updated: true, + siteType: site?.type || null + } : target ) ); @@ -494,7 +518,9 @@ export default function ReverseProxyTargets(props: { enabled: target.enabled, siteId: target.siteId, path: target.path, - pathMatchType: target.pathMatchType + pathMatchType: target.pathMatchType, + rewritePath: target.rewritePath, + rewritePathType: target.rewritePathType }; if (target.new) { @@ -590,7 +616,7 @@ export default function ReverseProxyTargets(props: { } }} > - + {t("matchPath")} + + {t("matchPath")} ); } @@ -693,7 +719,7 @@ export default function ReverseProxyTargets(props: { className={cn( "justify-between flex-1", !row.original.siteId && - "text-muted-foreground" + "text-muted-foreground" )} > {row.original.siteId @@ -772,31 +798,31 @@ export default function ReverseProxyTargets(props: { }, ...(resource.http ? [ - { - accessorKey: "method", - header: t("method"), - cell: ({ row }: { row: Row }) => ( - - ) - } - ] + { + accessorKey: "method", + header: t("method"), + cell: ({ row }: { row: Row }) => ( + + ) + } + ] : []), { accessorKey: "ip", @@ -856,6 +882,102 @@ export default function ReverseProxyTargets(props: { /> ) }, + { + accessorKey: "rewritePath", + header: t("rewritePath"), + cell: ({ row }) => { + const [showRewritePathInput, setShowRewritePathInput] = useState( + !!(row.original.rewritePath || row.original.rewritePathType) + ); + + if (!showRewritePathInput) { + const noPathMatch = + !row.original.path && !row.original.pathMatchType; + return ( + + ); + } + + return ( +
+ + + + + { + const value = e.target.value.trim(); + if (!value) { + setShowRewritePathInput(false); + updateTarget(row.original.targetId, { + ...row.original, + rewritePath: null, + rewritePathType: null + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + rewritePath: value + }); + } + }} + /> +
+ ); + } + }, // { // accessorKey: "protocol", // header: t('targetProtocol'), @@ -968,21 +1090,21 @@ export default function ReverseProxyTargets(props: { className={cn( "justify-between flex-1", !field.value && - "text-muted-foreground" + "text-muted-foreground" )} > {field.value ? sites.find( - ( - site - ) => - site.siteId === - field.value - ) - ?.name + ( + site + ) => + site.siteId === + field.value + ) + ?.name : t( - "siteSelect" - )} + "siteSelect" + )} @@ -1048,34 +1170,34 @@ export default function ReverseProxyTargets(props: { ); return selectedSite && selectedSite.type === - "newt" + "newt" ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() + const dockerState = + getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })() : null; })()} @@ -1303,12 +1425,12 @@ export default function ReverseProxyTargets(props: { {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} From 90d3ac07a9f1c490746a8fe72a97b1de1ddbbee9 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 28 Sep 2025 11:45:49 +0530 Subject: [PATCH 02/12] add rewrite path to create resource page --- .../settings/resources/create/page.tsx | 134 +++++++++++++++++- 1 file changed, 128 insertions(+), 6 deletions(-) diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index f551e418..e8e7d68c 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -116,7 +116,9 @@ const addTargetSchema = z.object({ port: z.coerce.number().int().positive(), siteId: z.number().int().positive(), path: z.string().optional().nullable(), - pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable() + pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), + rewritePath: z.string().optional().nullable(), + rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() }).refine( (data) => { // If path is provided, pathMatchType must be provided @@ -149,7 +151,23 @@ const addTargetSchema = z.object({ { message: "Invalid path configuration" } -); +) + .refine( + (data) => { + // If rewritePath is provided, rewritePathType must be provided + if (data.rewritePath && !data.rewritePathType) { + return false; + } + // If rewritePathType is provided, rewritePath must be provided + if (data.rewritePathType && !data.rewritePath) { + return false; + } + return true; + }, + { + message: "Invalid rewrite path configuration" + } + ); type BaseResourceFormValues = z.infer; type HttpResourceFormValues = z.infer; @@ -240,8 +258,10 @@ export default function Page() { method: baseForm.watch("http") ? "http" : null, port: "" as any as number, path: null, - pathMatchType: null - } + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + } as z.infer }); const watchedIp = addTargetForm.watch("ip"); @@ -313,6 +333,8 @@ export default function Page() { ...data, path: data.path || null, pathMatchType: data.pathMatchType || null, + rewritePath: data.rewritePath || null, + rewritePathType: data.rewritePathType || null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), @@ -326,7 +348,9 @@ export default function Page() { method: baseForm.watch("http") ? "http" : null, port: "" as any as number, path: null, - pathMatchType: null + pathMatchType: null, + rewritePath: null, + rewritePathType: null, }); } @@ -422,7 +446,9 @@ export default function Page() { enabled: target.enabled, siteId: target.siteId, path: target.path, - pathMatchType: target.pathMatchType + pathMatchType: target.pathMatchType, + rewritePath: target.rewritePath, + rewritePathType: target.rewritePathType }; await api.put(`/resource/${id}/target`, data); @@ -820,6 +846,102 @@ export default function Page() { /> ) }, + { + accessorKey: "rewritePath", + header: t("rewritePath"), + cell: ({ row }) => { + const [showRewritePathInput, setShowRewritePathInput] = useState( + !!(row.original.rewritePath || row.original.rewritePathType) + ); + + if (!showRewritePathInput) { + const noPathMatch = + !row.original.path && !row.original.pathMatchType; + return ( + + ); + } + + return ( +
+ + + + + { + const value = e.target.value.trim(); + if (!value) { + setShowRewritePathInput(false); + updateTarget(row.original.targetId, { + ...row.original, + rewritePath: null, + rewritePathType: null + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + rewritePath: value + }); + } + }} + /> +
+ ); + } + }, { accessorKey: "enabled", header: t("enabled"), From 218a5ec9e4f5bf2802b0d0625d498582ec76ec24 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 28 Sep 2025 12:53:37 +0530 Subject: [PATCH 03/12] fix traefik config file --- server/routers/traefik/getTraefikConfig.ts | 57 ++++++++++------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index b713f98a..49bb50fd 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -146,12 +146,6 @@ function validatePathRewriteConfig( } } - if (rewritePathType === "regex") { - // For regex rewrite type, we don't validate the replacement pattern - // as it may contain capture groups like $1, $2, etc. - // The regex engine will handle validation at runtime - } - // Validate path formats for non-regex types if (pathMatchType !== "regex" && !path.startsWith("/")) { return { @@ -184,7 +178,7 @@ function createPathRewriteMiddleware( pathMatchType: string, rewritePath: string, rewritePathType: string -): { [key: string]: any } { +): { middlewares: { [key: string]: any }; chain?: string[] } { const middlewares: { [key: string]: any } = {}; switch (rewritePathType) { @@ -259,13 +253,12 @@ function createPathRewriteMiddleware( // If rewritePath is provided and not empty, add it as a prefix after stripping if (rewritePath && rewritePath !== "" && rewritePath !== "/") { - const addPrefixMiddlewareName = `${middlewareName.replace('-rewrite', '')}-add-prefix-middleware`; + const addPrefixMiddlewareName = `addprefix-${middlewareName.replace('rewrite-', '')}`; middlewares[addPrefixMiddlewareName] = { addPrefix: { prefix: rewritePath } }; - // Return both middlewares with a special flag to indicate chaining return { middlewares, chain: [middlewareName, addPrefixMiddlewareName] @@ -279,7 +272,6 @@ function createPathRewriteMiddleware( } else if (pathMatchType === "regex") { regexPattern = path; } else { - // This shouldn't happen due to earlier validation, but handle gracefully regexPattern = `^${escapeRegex(path)}`; } @@ -538,9 +530,10 @@ export async function getTraefikConfig( resource.path && resource.pathMatchType && resource.rewritePathType) { - - const rewriteMiddlewareName = `${resource.id}-${key}-rewrite`; - + + // Create a unique middleware name + const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${sanitizeForMiddlewareName(key)}`; + try { const rewriteResult = createPathRewriteMiddleware( rewriteMiddlewareName, @@ -567,17 +560,17 @@ export async function getTraefikConfig( routerMiddlewares.push(rewriteMiddlewareName); } - logger.info(`Created path rewrite middleware for ${key}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); + logger.info(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); } catch (error) { - logger.error(`Failed to create path rewrite middleware for ${key}: ${error}`); + logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); // Continue without the rewrite middleware rather than failing completely } } // Handle custom headers middleware 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 }[] = []; try { @@ -586,9 +579,7 @@ export async function getTraefikConfig( value: string; }[]; } catch (e) { - logger.warn( - `Failed to parse headers for resource ${resource.resourceId}: ${e}` - ); + logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`); } headersArr.forEach((header) => { @@ -600,9 +591,7 @@ export async function getTraefikConfig( headersObj["Host"] = resource.setHostHeader; } - // check if the object is not empty if (Object.keys(headersObj).length > 0) { - // Add the headers middleware if (!config_output.http.middlewares) { config_output.http.middlewares = {}; } @@ -616,8 +605,10 @@ export async function getTraefikConfig( } } + // Build routing rules let rule = `Host(\`${fullDomain}\`)`; let priority = 100; + if (resource.path && resource.pathMatchType) { priority += 1; // add path to rule based on match type @@ -763,7 +754,7 @@ export async function getTraefikConfig( } } else { // Non-HTTP (TCP/UDP) configuration - if (!resource.enableProxy) { + if (!resource.enableProxy || !resource.proxyPort) { continue; } @@ -860,16 +851,22 @@ export async function getTraefikConfig( function sanitizePath(path: string | null | undefined): string | undefined { if (!path) return undefined; - // For path rewriting, we need to be more careful about sanitization - // Only limit length and ensure it's a valid path structure - if (path.length > 50) { - path = path.substring(0, 50); - logger.warn(`Path truncated to 50 characters: ${path}`); + const trimmed = path.trim(); + if (!trimmed) return undefined; + + // Preserve path structure for rewriting, only warn if very long + if (trimmed.length > 1000) { + logger.warn(`Path exceeds 1000 characters: ${trimmed.substring(0, 100)}...`); + return trimmed.substring(0, 1000); } - // Don't remove special characters as they might be part of regex patterns - // Just ensure it's not empty after trimming - return path.trim() || undefined; + return trimmed; +} + +function sanitizeForMiddlewareName(str: string): string { + // Replace any characters that aren't alphanumeric or dash with dash + // and remove consecutive dashes + return str.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); } function escapeRegex(string: string): string { From 3722b6772433a9ff597f1fe2df3c060eb6d23efb Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 28 Sep 2025 13:05:58 +0530 Subject: [PATCH 04/12] preserves the rest of the path after the matched prefix --- server/routers/traefik/getTraefikConfig.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 49bb50fd..789b2f82 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -184,9 +184,10 @@ function createPathRewriteMiddleware( switch (rewritePathType) { case "exact": // Replace the entire path with the exact rewrite path + let exactPattern = `^${escapeRegex(path)}$`; middlewares[middlewareName] = { replacePathRegex: { - regex: "^.*$", + regex: exactPattern, replacement: rewritePath } }; From a97b6efe9cc2b6d6095b76929902932692574c8e Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 29 Sep 2025 16:40:37 +0530 Subject: [PATCH 05/12] redesign path match and rewrite modal --- .../resources/[niceId]/proxy/page.tsx | 243 ++++++--------- src/components/PathMatchRenameModal.tsx | 293 ++++++++++++++++++ 2 files changed, 385 insertions(+), 151 deletions(-) create mode 100644 src/components/PathMatchRenameModal.tsx diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 23a55494..4442e150 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -73,6 +73,7 @@ import { CircleCheck, CircleX, ArrowRight, + Plus, MoveRight } from "lucide-react"; import { ContainersSelector } from "@app/components/ContainersSelector"; @@ -95,9 +96,9 @@ import { CommandItem, CommandList } from "@app/components/ui/command"; -import { Badge } from "@app/components/ui/badge"; import { parseHostTarget } from "@app/lib/parseHostTarget"; import { HeadersInput } from "@app/components/HeadersInput"; +import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), @@ -597,93 +598,64 @@ export default function ReverseProxyTargets(props: { accessorKey: "path", header: t("matchPath"), cell: ({ row }) => { - const [showPathInput, setShowPathInput] = useState( - !!(row.original.path || row.original.pathMatchType) - ); + const hasPathMatch = !!(row.original.path || row.original.pathMatchType); - if (!showPathInput) { - return ( - - ); - } - - return ( -
- - { - const value = e.target.value.trim(); - if (!value) { - setShowPathInput(false); - updateTarget(row.original.targetId, { - ...row.original, - path: null, - pathMatchType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - path: value - }); - } - }} /> - + {/* */}
+ ) : ( + updateTarget(row.original.targetId, config)} + trigger={ + + } + /> ); - } + }, }, { accessorKey: "siteId", @@ -886,98 +858,68 @@ export default function ReverseProxyTargets(props: { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { - const [showRewritePathInput, setShowRewritePathInput] = useState( - !!(row.original.rewritePath || row.original.rewritePathType) - ); + const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType); + const noPathMatch = !row.original.path && !row.original.pathMatchType; - if (!showRewritePathInput) { - const noPathMatch = - !row.original.path && !row.original.pathMatchType; - return ( - - ); - } - - return ( -
+ onChange={(config) => updateTarget(row.original.targetId, config)} + trigger={ + + } + /> - - - - { - const value = e.target.value.trim(); - if (!value) { - setShowRewritePathInput(false); - updateTarget(row.original.targetId, { - ...row.original, - rewritePath: null, - rewritePathType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - rewritePath: value - }); - } - }} - />
+ ) : ( + updateTarget(row.original.targetId, config)} + trigger={ + + } + disabled={noPathMatch} + /> ); - } + }, }, + // { // accessorKey: "protocol", // header: t('targetProtocol'), @@ -1649,7 +1591,6 @@ export default function ReverseProxyTargets(props: { } function isIPInSubnet(subnet: string, ip: string): boolean { - // Split subnet into IP and mask parts const [subnetIP, maskBits] = subnet.split("/"); const mask = parseInt(maskBits); diff --git a/src/components/PathMatchRenameModal.tsx b/src/components/PathMatchRenameModal.tsx new file mode 100644 index 00000000..574c9c70 --- /dev/null +++ b/src/components/PathMatchRenameModal.tsx @@ -0,0 +1,293 @@ +import { Pencil } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@app/components/ui/dialog"; +import { Badge } from "@app/components/ui/badge"; +import { Label } from "@app/components/ui/label"; +import { useEffect, useState } from "react"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; + + +export function PathMatchModal({ + value, + onChange, + trigger, +}: { + value: { path: string | null; pathMatchType: string | null }; + onChange: (config: { path: string | null; pathMatchType: string | null }) => void; + trigger: React.ReactNode; +}) { + const [open, setOpen] = useState(false); + const [matchType, setMatchType] = useState(value?.pathMatchType || "prefix"); + const [path, setPath] = useState(value?.path || ""); + + useEffect(() => { + if (open) { + setMatchType(value?.pathMatchType || "prefix"); + setPath(value?.path || ""); + } + }, [open, value]); + + const handleSave = () => { + onChange({ pathMatchType: matchType as any, path: path.trim() }); + setOpen(false); + }; + + const handleClear = () => { + onChange({ pathMatchType: null, path: null }); + setOpen(false); + }; + + const getPlaceholder = () => (matchType === "regex" ? "^/api/.*" : "/path"); + + const getHelpText = () => { + switch (matchType) { + case "prefix": + return "Example: /api matches /api, /api/users, etc."; + case "exact": + return "Example: /api matches only /api"; + case "regex": + return "Example: ^/api/.* matches /api/anything"; + default: + return ""; + } + }; + + return ( + + {trigger} + + + Configure Path Matching + + Set up how incoming requests should be matched based on their path. + + +
+
+ + +
+
+ + setPath(e.target.value)} + /> +

{getHelpText()}

+
+
+ + {value?.path && ( + + )} + + +
+
+ ); +} + + +export function PathRewriteModal({ + value, + onChange, + trigger, + disabled, +}: { + value: { rewritePath: string | null; rewritePathType: string | null }; + onChange: (config: { rewritePath: string | null; rewritePathType: string | null }) => void; + trigger: React.ReactNode; + disabled?: boolean; +}) { + const [open, setOpen] = useState(false); + const [rewriteType, setRewriteType] = useState(value?.rewritePathType || "prefix"); + const [rewritePath, setRewritePath] = useState(value?.rewritePath || ""); + + useEffect(() => { + if (open) { + setRewriteType(value?.rewritePathType || "prefix"); + setRewritePath(value?.rewritePath || ""); + } + }, [open, value]); + + const handleSave = () => { + onChange({ rewritePathType: rewriteType as any, rewritePath: rewritePath.trim() }); + setOpen(false); + }; + + const handleClear = () => { + onChange({ rewritePathType: null, rewritePath: null }); + setOpen(false); + }; + + const getPlaceholder = () => { + switch (rewriteType) { + case "regex": + return "/new/$1"; + case "stripPrefix": + return ""; + default: + return "/new-path"; + } + }; + + const getHelpText = () => { + switch (rewriteType) { + case "prefix": + return "Replace the matched prefix with this value"; + case "exact": + return "Replace the entire path with this value"; + case "regex": + return "Use capture groups like $1, $2 for replacement"; + case "stripPrefix": + return "Leave empty to strip prefix or provide new prefix"; + default: + return ""; + } + }; + + return ( + + + {trigger} + + + + Configure Path Rewriting + + Transform the matched path before forwarding to the target. + + +
+
+ + +
+
+ + setRewritePath(e.target.value)} + /> +

{getHelpText()}

+
+
+ + {value?.rewritePath && ( + + )} + + +
+
+ ); +} + +export function PathMatchDisplay({ + value, +}: { + value: { path: string | null; pathMatchType: string | null }; +}) { + if (!value?.path) return null; + + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: "Prefix", + exact: "Exact", + regex: "Regex", + }; + return labels[type || ""] || type; + }; + + return ( +
+ + {getTypeLabel(value.pathMatchType)} + + + {value.path} + + +
+ ); +} + + +export function PathRewriteDisplay({ + value, +}: { + value: { rewritePath: string | null; rewritePathType: string | null }; +}) { + if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix") return null; + + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: "Prefix", + exact: "Exact", + regex: "Regex", + stripPrefix: "Strip", + }; + return labels[type || ""] || type; + }; + + return ( +
+ + {getTypeLabel(value.rewritePathType)} + + + {value.rewritePath || (strip)} + + +
+ ); +} From 7b2f1dd4c654b9fed12bbcdbfc33398c319989ed Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 29 Sep 2025 16:49:26 +0530 Subject: [PATCH 06/12] button fix --- src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 4442e150..4f81ed58 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -648,7 +648,7 @@ export default function ReverseProxyTargets(props: { }} onChange={(config) => updateTarget(row.original.targetId, config)} trigger={ - @@ -909,7 +909,7 @@ export default function ReverseProxyTargets(props: { }} onChange={(config) => updateTarget(row.original.targetId, config)} trigger={ - From 1b34ee7369545ccd58f2f54ada298fb512e182c5 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Tue, 30 Sep 2025 00:19:59 +0530 Subject: [PATCH 07/12] match and rewrite path ui improve for create resource --- server/routers/target/createTarget.ts | 2 +- .../settings/resources/create/page.tsx | 243 +++++++----------- 2 files changed, 93 insertions(+), 152 deletions(-) diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 6ed9d9aa..dd85c888 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -34,7 +34,7 @@ const createTargetSchema = z path: z.string().optional().nullable(), pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), rewritePath: z.string().optional().nullable(), - rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() // NEW: rewrite path type + rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() }) .strict(); diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index e8e7d68c..efb136cb 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -58,7 +58,7 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; -import { ArrowRight, MoveRight, SquareArrowOutUpRight } from "lucide-react"; +import { ArrowRight, MoveRight, Plus, SquareArrowOutUpRight } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -92,6 +92,7 @@ import { parseHostTarget } from "@app/lib/parseHostTarget"; import { toASCII, toUnicode } from 'punycode'; import { DomainRow } from "../../../../../components/DomainsTable"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; +import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; const baseResourceFormSchema = z.object({ @@ -152,7 +153,7 @@ const addTargetSchema = z.object({ message: "Invalid path configuration" } ) - .refine( + .refine( (data) => { // If rewritePath is provided, rewritePathType must be provided if (data.rewritePath && !data.rewritePathType) { @@ -575,93 +576,64 @@ export default function Page() { accessorKey: "path", header: t("matchPath"), cell: ({ row }) => { - const [showPathInput, setShowPathInput] = useState( - !!(row.original.path || row.original.pathMatchType) - ); + const hasPathMatch = !!(row.original.path || row.original.pathMatchType); - if (!showPathInput) { - return ( - - ); - } - - return ( -
- - { - const value = e.target.value.trim(); - if (!value) { - setShowPathInput(false); - updateTarget(row.original.targetId, { - ...row.original, - path: null, - pathMatchType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - path: value - }); - } - }} /> - + {/* */}
+ ) : ( + updateTarget(row.original.targetId, config)} + trigger={ + + } + /> ); - } + }, }, { accessorKey: "siteId", @@ -850,97 +822,66 @@ export default function Page() { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { - const [showRewritePathInput, setShowRewritePathInput] = useState( - !!(row.original.rewritePath || row.original.rewritePathType) - ); + const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType); + const noPathMatch = !row.original.path && !row.original.pathMatchType; - if (!showRewritePathInput) { - const noPathMatch = - !row.original.path && !row.original.pathMatchType; - return ( - - ); - } - - return ( -
+ onChange={(config) => updateTarget(row.original.targetId, config)} + trigger={ + + } + /> - - - - { - const value = e.target.value.trim(); - if (!value) { - setShowRewritePathInput(false); - updateTarget(row.original.targetId, { - ...row.original, - rewritePath: null, - rewritePathType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - rewritePath: value - }); - } - }} - />
+ ) : ( + updateTarget(row.original.targetId, config)} + trigger={ + + } + disabled={noPathMatch} + /> ); - } + }, }, { accessorKey: "enabled", From 574cd2a754d524bf1023e051ab380093565870b7 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Tue, 30 Sep 2025 16:56:24 +0530 Subject: [PATCH 08/12] make rewrite data null if no match added --- server/routers/traefik/getTraefikConfig.ts | 7 +++---- src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx | 2 ++ src/app/[orgId]/settings/resources/create/page.tsx | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 789b2f82..7acdcabc 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -183,7 +183,7 @@ function createPathRewriteMiddleware( switch (rewritePathType) { case "exact": - // Replace the entire path with the exact rewrite path + // Replace the path with the exact rewrite path let exactPattern = `^${escapeRegex(path)}$`; middlewares[middlewareName] = { replacePathRegex: { @@ -549,10 +549,10 @@ export async function getTraefikConfig( config_output.http.middlewares = {}; } - // Add the middleware(s) to the config + // the middleware to the config Object.assign(config_output.http.middlewares, rewriteResult.middlewares); - // Add middleware(s) to the router middleware chain + // middlewares to the router middleware chain if (rewriteResult.chain) { // For chained middlewares (like stripPrefix + addPrefix) routerMiddlewares.push(...rewriteResult.chain); @@ -564,7 +564,6 @@ export async function getTraefikConfig( logger.info(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); } catch (error) { logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); - // Continue without the rewrite middleware rather than failing completely } } diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 4f81ed58..b37cfba9 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -632,6 +632,8 @@ export default function ReverseProxyTargets(props: { ...row.original, path: null, pathMatchType: null, + rewritePath: null, + rewritePathType: null }); }} > diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index efb136cb..bb19cc79 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -610,6 +610,8 @@ export default function Page() { ...row.original, path: null, pathMatchType: null, + rewritePath: null, + rewritePathType: null }); }} > From 664aa6ed2a83f174a337f30cc1544d381614b076 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Tue, 30 Sep 2025 23:20:42 +0530 Subject: [PATCH 09/12] update blueprints --- server/lib/blueprints/proxyResources.ts | 8 ++++++-- server/lib/blueprints/types.ts | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index c6ab6f40..9b349281 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -107,7 +107,9 @@ export async function updateProxyResources( enabled: targetData.enabled, internalPort: internalPortToCreate, path: targetData.path, - pathMatchType: targetData["path-match"] + pathMatchType: targetData["path-match"], + rewritePath: targetData.rewritePath, + rewritePathType: targetData["rewrite-match"] }) .returning(); @@ -327,7 +329,9 @@ export async function updateProxyResources( port: targetData.port, enabled: targetData.enabled, path: targetData.path, - pathMatchType: targetData["path-match"] + pathMatchType: targetData["path-match"], + rewritePath: targetData.rewritePath, + rewritePathType: targetData["rewrite-match"] }) .where(eq(targets.targetId, existingTarget.targetId)) .returning(); diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 9b3a7a20..dda672a6 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -14,7 +14,9 @@ export const TargetSchema = z.object({ enabled: z.boolean().optional().default(true), "internal-port": z.number().int().min(1).max(65535).optional(), path: z.string().optional(), - "path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable() + "path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(), + rewritePath: z.string().optional(), + "rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable() }); export type TargetData = z.infer; @@ -183,7 +185,7 @@ export const ClientResourceSchema = z.object({ "proxy-port": z.number().min(1).max(65535), "hostname": z.string().min(1).max(255), "internal-port": z.number().min(1).max(65535), - enabled: z.boolean().optional().default(true) + enabled: z.boolean().optional().default(true) }); // Schema for the entire configuration object From 51fad19d0ded1561c338cc1f08b5d53743157249 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 30 Sep 2025 14:32:48 -0700 Subject: [PATCH 10/12] Sanitize all keys --- server/routers/traefik/getTraefikConfig.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 7acdcabc..bf572f68 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -455,11 +455,13 @@ export async function getTraefikConfig( for (const [key, resource] of resourcesMap.entries()) { const targets = resource.targets; - const routerName = `${key}-router`; - const serviceName = `${key}-service`; + const sanatizedKey = sanitizeForMiddlewareName(key); + + const routerName = `${sanatizedKey}-router`; + const serviceName = `${sanatizedKey}-service`; const fullDomain = `${resource.fullDomain}`; - const transportName = `${key}-transport`; - const headersMiddlewareName = `${key}-headers-middleware`; + const transportName = `${sanatizedKey}-transport`; + const headersMiddlewareName = `${sanatizedKey}-headers-middleware`; if (!resource.enabled) { continue; From 4cbf3fffb17fd20e3a2795bb604cd00d68da67d1 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 30 Sep 2025 14:47:06 -0700 Subject: [PATCH 11/12] Quiet up logs --- server/routers/traefik/getTraefikConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index bf572f68..495cdaef 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -563,7 +563,7 @@ export async function getTraefikConfig( routerMiddlewares.push(rewriteMiddlewareName); } - logger.info(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); + logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); } catch (error) { logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); } From 8767d20c47d907b15ce79f5924f81c3a89c73da0 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Wed, 1 Oct 2025 13:03:26 +0530 Subject: [PATCH 12/12] add missing path / validation --- server/routers/traefik/getTraefikConfig.ts | 106 ++++++++++----------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 495cdaef..fa722ed5 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -108,13 +108,13 @@ function validatePathRewriteConfig( return { isValid: true }; } - if ((rewritePath && !rewritePathType) || (!rewritePath && rewritePathType)) { - return { - isValid: false, - error: "Both rewritePath and rewritePathType must be specified together" - }; + if (rewritePathType !== "stripPrefix") { + if ((rewritePath && !rewritePathType) || (!rewritePath && rewritePathType)) { + return { isValid: false, error: "Both rewritePath and rewritePathType must be specified together" }; + } } + if (!rewritePath || !rewritePathType) { return { isValid: true }; } @@ -146,26 +146,12 @@ function validatePathRewriteConfig( } } - // Validate path formats for non-regex types - if (pathMatchType !== "regex" && !path.startsWith("/")) { - return { - isValid: false, - error: "Path must start with '/' for exact and prefix matching" - }; - } // Additional validation for stripPrefix if (rewritePathType === "stripPrefix") { if (pathMatchType !== "prefix") { logger.warn(`stripPrefix rewrite type is most effective with prefix path matching. Current match type: ${pathMatchType}`); } - // For stripPrefix, rewritePath is optional (can be empty to just strip) - if (rewritePath && !rewritePath.startsWith("/") && rewritePath !== "") { - return { - isValid: false, - error: "stripPrefix rewritePath must start with '/' or be empty" - }; - } } return { isValid: true }; @@ -181,6 +167,14 @@ function createPathRewriteMiddleware( ): { middlewares: { [key: string]: any }; chain?: string[] } { const middlewares: { [key: string]: any } = {}; + if (pathMatchType !== "regex" && !path.startsWith("/")) { + path = `/${path}`; + } + + if (rewritePathType !== "regex" && rewritePath !== "" && !rewritePath.startsWith("/")) { + rewritePath = `/${rewritePath}`; + } + switch (rewritePathType) { case "exact": // Replace the path with the exact rewrite path @@ -260,9 +254,9 @@ function createPathRewriteMiddleware( prefix: rewritePath } }; - return { - middlewares, - chain: [middlewareName, addPrefixMiddlewareName] + return { + middlewares, + chain: [middlewareName, addPrefixMiddlewareName] }; } } else { @@ -387,7 +381,7 @@ export async function getTraefikConfig( if (!validation.isValid) { logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`); - return; + return; } resourcesMap.set(mapKey, { @@ -520,8 +514,8 @@ export async function getTraefikConfig( }; } - const additionalMiddlewares = - config.getRawConfig().traefik.additional_middlewares || []; + const additionalMiddlewares = + config.getRawConfig().traefik.additional_middlewares || []; const routerMiddlewares = [ badgerMiddlewareName, @@ -529,14 +523,14 @@ export async function getTraefikConfig( ]; // Handle path rewriting middleware - if (resource.rewritePath && - resource.path && - resource.pathMatchType && + if (resource.rewritePath && + resource.path && + resource.pathMatchType && resource.rewritePathType) { - + // Create a unique middleware name const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${sanitizeForMiddlewareName(key)}`; - + try { const rewriteResult = createPathRewriteMiddleware( rewriteMiddlewareName, @@ -572,7 +566,7 @@ export async function getTraefikConfig( // Handle custom headers middleware if (resource.headers || resource.setHostHeader) { const headersObj: { [key: string]: string } = {}; - + if (resource.headers) { let headersArr: { name: string; value: string }[] = []; try { @@ -668,15 +662,15 @@ export async function getTraefikConfig( return ( (targets as TargetWithSite[]) - .filter((target: TargetWithSite) => { - if (!target.enabled) { - return false; - } + .filter((target: TargetWithSite) => { + if (!target.enabled) { + return false; + } // If any sites are online, exclude offline sites - if (anySitesOnline && !target.site.online) { - return false; - } + if (anySitesOnline && !target.site.online) { + return false; + } if ( target.site.type === "local" || @@ -689,33 +683,33 @@ export async function getTraefikConfig( ) { return false; } - } else if (target.site.type === "newt") { + } else if (target.site.type === "newt") { if ( !target.internalPort || !target.method || !target.site.subnet ) { - return false; + return false; } } return true; - }) - .map((target: TargetWithSite) => { + }) + .map((target: TargetWithSite) => { if ( target.site.type === "local" || target.site.type === "wireguard" ) { - return { - url: `${target.method}://${target.ip}:${target.port}` - }; - } else if (target.site.type === "newt") { + return { + url: `${target.method}://${target.ip}:${target.port}` + }; + } else if (target.site.type === "newt") { const ip = target.site.subnet!.split("/")[0]; - return { - url: `${target.method}://${ip}:${target.internalPort}` - }; - } - }) + return { + url: `${target.method}://${ip}:${target.internalPort}` + }; + } + }) // filter out duplicates .filter( (v, i, a) => @@ -729,7 +723,7 @@ export async function getTraefikConfig( ? { sticky: { cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies secure: resource.ssl, httpOnly: true } @@ -811,7 +805,7 @@ export async function getTraefikConfig( !target.internalPort || !target.site.subnet ) { - return false; + return false; } } return true; @@ -852,16 +846,16 @@ export async function getTraefikConfig( function sanitizePath(path: string | null | undefined): string | undefined { if (!path) return undefined; - + const trimmed = path.trim(); if (!trimmed) return undefined; - + // Preserve path structure for rewriting, only warn if very long if (trimmed.length > 1000) { logger.warn(`Path exceeds 1000 characters: ${trimmed.substring(0, 100)}...`); return trimmed.substring(0, 1000); } - + return trimmed; }