From 674316aa46014179e358cc39a9dc6b7cb81e7cb6 Mon Sep 17 00:00:00 2001 From: Matthias Palmetshofer Date: Wed, 9 Apr 2025 23:42:50 +0200 Subject: [PATCH] add option to set TLS Server Name --- server/db/schemas/schema.ts | 3 +- server/lib/schemas.ts | 7 +++ server/routers/resource/listResources.ts | 6 ++- server/routers/resource/updateResource.ts | 14 +++++- server/routers/traefik/getTraefikConfig.ts | 19 +++++++- .../resources/[resourceId]/general/page.tsx | 46 +++++++++++++++++-- 6 files changed, 84 insertions(+), 11 deletions(-) diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index a8627553..2fe5ac2b 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -77,7 +77,8 @@ export const resources = sqliteTable("resources", { applyRules: integer("applyRules", { mode: "boolean" }) .notNull() .default(false), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + tlsServerName: text("tlsServerName").notNull().default("") }); export const targets = sqliteTable("targets", { diff --git a/server/lib/schemas.ts b/server/lib/schemas.ts index f4b7daf3..cf1b40c8 100644 --- a/server/lib/schemas.ts +++ b/server/lib/schemas.ts @@ -9,3 +9,10 @@ export const subdomainSchema = z .min(1, "Subdomain must be at least 1 character long") .transform((val) => val.toLowerCase()); +export const tlsNameSchema = z + .string() + .regex( + /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$|^$/, + "Invalid subdomain format" + ) + .transform((val) => val.toLowerCase()); \ No newline at end of file diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 1dba4119..56df9128 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -68,7 +68,8 @@ function queryResources( http: resources.http, protocol: resources.protocol, proxyPort: resources.proxyPort, - enabled: resources.enabled + enabled: resources.enabled, + tlsServerName: resources.tlsServerName }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) @@ -102,7 +103,8 @@ function queryResources( http: resources.http, protocol: resources.protocol, proxyPort: resources.proxyPort, - enabled: resources.enabled + enabled: resources.enabled, + tlsServerName: resources.tlsServerName }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 121b34ed..54802ccc 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -16,7 +16,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import config from "@server/lib/config"; -import { subdomainSchema } from "@server/lib/schemas"; +import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; const updateResourceParamsSchema = z .object({ @@ -40,7 +40,8 @@ const updateHttpResourceBodySchema = z isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), domainId: z.string().optional(), - enabled: z.boolean().optional() + enabled: z.boolean().optional(), + tlsServerName: z.string().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -67,6 +68,15 @@ const updateHttpResourceBodySchema = z { message: "Base domain resources are not allowed" } + ) + .refine( + (data) => { + if (data.tlsServerName) { + return tlsNameSchema.safeParse(data.tlsServerName).success; + } + return true; + }, + { message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." } ); export type UpdateResourceResponse = Resource; diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 17e385ed..42a47940 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -40,7 +40,8 @@ export async function traefikConfigProvider( org: { orgId: orgs.orgId }, - enabled: resources.enabled + enabled: resources.enabled, + tlsServerName: resources.tlsServerName }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -139,6 +140,7 @@ export async function traefikConfigProvider( const routerName = `${resource.resourceId}-router`; const serviceName = `${resource.resourceId}-service`; const fullDomain = `${resource.fullDomain}`; + const transportName = `${resource.resourceId}-transport`; if (!resource.enabled) { continue; @@ -278,6 +280,21 @@ export async function traefikConfigProvider( }) } }; + + // Add the serversTransport if TLS server name is provided + if (resource.tlsServerName) { + if (!config_output.http.serversTransports) { + config_output.http.serversTransports = {}; + } + config_output.http.serversTransports![transportName] = { + serverName: resource.tlsServerName, + //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings + // if defined in the static config and here. if not set, self-signed certs won't work + insecureSkipVerify: true + }; + config_output.http.services![serviceName].loadBalancer.serversTransport = transportName; + } + } else { // Non-HTTP (TCP/UDP) configuration const protocol = resource.protocol.toLowerCase(); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 5d6cc81e..a3fccf26 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -48,7 +48,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext"; import CustomDomainInput from "../CustomDomainInput"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { subdomainSchema } from "@server/lib/schemas"; +import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Label } from "@app/components/ui/label"; @@ -73,7 +73,8 @@ const GeneralFormSchema = z proxyPort: z.number().optional(), http: z.boolean(), isBaseDomain: z.boolean().optional(), - domainId: z.string().optional() + domainId: z.string().optional(), + tlsServerName: z.string().optional() }) .refine( (data) => { @@ -103,6 +104,18 @@ const GeneralFormSchema = z message: "Invalid subdomain", path: ["subdomain"] } + ) + .refine( + (data) => { + if (data.tlsServerName) { + return tlsNameSchema.safeParse(data.tlsServerName).success; + } + return true; + }, + { + message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", + path: ["tlsServerName"] + } ); const TransferFormSchema = z.object({ @@ -146,7 +159,8 @@ export default function GeneralForm() { proxyPort: resource.proxyPort ? resource.proxyPort : undefined, http: resource.http, isBaseDomain: resource.isBaseDomain ? true : false, - domainId: resource.domainId || undefined + domainId: resource.domainId || undefined, + tlsServerName: resource.http ? resource.tlsServerName || "" : undefined }, mode: "onChange" }); @@ -210,7 +224,8 @@ export default function GeneralForm() { subdomain: data.http ? data.subdomain : undefined, proxyPort: data.proxyPort, isBaseDomain: data.http ? data.isBaseDomain : undefined, - domainId: data.http ? data.domainId : undefined + domainId: data.http ? data.domainId : undefined, + tlsServerName: data.http ? data.tlsServerName : undefined } ) .catch((e) => { @@ -237,7 +252,8 @@ export default function GeneralForm() { subdomain: data.subdomain, proxyPort: data.proxyPort, isBaseDomain: data.isBaseDomain, - fullDomain: resource.fullDomain + fullDomain: resource.fullDomain, + tlsServerName: data.tlsServerName }); router.refresh(); @@ -545,7 +561,27 @@ export default function GeneralForm() { )} /> )} + {/* New TLS Server Name Field */} +
+ + TLS Server Name + + ( + + + + + + + )} + /> +
)}