diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index 2fe5ac2..5263161 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -78,7 +78,8 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - tlsServerName: text("tlsServerName").notNull().default("") + tlsServerName: text("tlsServerName").notNull().default(""), + setHostHeader: text("setHostHeader").notNull().default("") }); export const targets = sqliteTable("targets", { diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 56df912..72788bf 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -69,7 +69,8 @@ function queryResources( protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, - tlsServerName: resources.tlsServerName + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) @@ -104,7 +105,8 @@ function queryResources( protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, - tlsServerName: resources.tlsServerName + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 23dea61..7ceb565 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -42,7 +42,8 @@ const updateHttpResourceBodySchema = z applyRules: z.boolean().optional(), domainId: z.string().optional(), enabled: z.boolean().optional(), - tlsServerName: z.string().optional() + tlsServerName: z.string().optional(), + setHostHeader: z.string().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -78,6 +79,15 @@ const updateHttpResourceBodySchema = z return true; }, { message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." } + ) + .refine( + (data) => { + if (data.setHostHeader) { + return tlsNameSchema.safeParse(data.setHostHeader).success; + } + return true; + }, + { message: "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." } ); export type UpdateResourceResponse = Resource; diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 42a4794..8a95254 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -41,7 +41,8 @@ export async function traefikConfigProvider( orgId: orgs.orgId }, enabled: resources.enabled, - tlsServerName: resources.tlsServerName + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -141,6 +142,7 @@ export async function traefikConfigProvider( const serviceName = `${resource.resourceId}-service`; const fullDomain = `${resource.fullDomain}`; const transportName = `${resource.resourceId}-transport`; + const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; if (!resource.enabled) { continue; @@ -295,6 +297,28 @@ export async function traefikConfigProvider( config_output.http.services![serviceName].loadBalancer.serversTransport = transportName; } + // Add the host header middleware + if (resource.setHostHeader) { + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + config_output.http.middlewares[hostHeaderMiddlewareName] = + { + headers: { + customRequestHeaders: { + Host: resource.setHostHeader + } + } + }; + if (!config_output.http.routers![routerName].middlewares) { + config_output.http.routers![routerName].middlewares = []; + } + config_output.http.routers![routerName].middlewares = [ + ...config_output.http.routers![routerName].middlewares, + hostHeaderMiddlewareName + ]; + } + } else { // Non-HTTP (TCP/UDP) configuration const protocol = resource.protocol.toLowerCase(); diff --git a/server/setup/scripts/1.3.0.ts b/server/setup/scripts/1.3.0.ts index f0b481f..692dacb4 100644 --- a/server/setup/scripts/1.3.0.ts +++ b/server/setup/scripts/1.3.0.ts @@ -11,6 +11,9 @@ export default async function migration() { trx.run( sql`ALTER TABLE 'resources' ADD 'tlsServerName' text DEFAULT '' NOT NULL;` ); + trx.run( + sql`ALTER TABLE 'resources' ADD 'setHostHeader' text DEFAULT '' NOT NULL;` + ); }); console.log(`Migrated database schema`); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 529b5b9..05d263e 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -73,8 +73,7 @@ const GeneralFormSchema = z proxyPort: z.number().optional(), http: z.boolean(), isBaseDomain: z.boolean().optional(), - domainId: z.string().optional(), - tlsServerName: z.string().optional() + domainId: z.string().optional() }) .refine( (data) => { @@ -104,7 +103,18 @@ const GeneralFormSchema = z message: "Invalid subdomain", path: ["subdomain"] } - ) + ); + +const TransferFormSchema = z.object({ + siteId: z.number() +}); + +const AdvancedFormSchema = z + .object({ + http: z.boolean(), + tlsServerName: z.string().optional(), + setHostHeader: z.string().optional() + }) .refine( (data) => { if (data.tlsServerName) { @@ -116,14 +126,23 @@ const GeneralFormSchema = z message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", path: ["tlsServerName"] } + ) + .refine( + (data) => { + if (data.setHostHeader) { + return tlsNameSchema.safeParse(data.setHostHeader).success; + } + return true; + }, + { + message: "Invalid custom Host Header value. Use domain name format, or save empty to unset the custom Host Header", + path: ["tlsServerName"] + } ); -const TransferFormSchema = z.object({ - siteId: z.number() -}); - type GeneralFormValues = z.infer; type TransferFormValues = z.infer; +type AdvancedFormValues = z.infer; export default function GeneralForm() { const [formKey, setFormKey] = useState(0); @@ -159,8 +178,17 @@ export default function GeneralForm() { proxyPort: resource.proxyPort ? resource.proxyPort : undefined, http: resource.http, isBaseDomain: resource.isBaseDomain ? true : false, - domainId: resource.domainId || undefined, - tlsServerName: resource.http ? resource.tlsServerName || "" : undefined + domainId: resource.domainId || undefined + }, + mode: "onChange" + }); + + const advancedForm = useForm({ + resolver: zodResolver(AdvancedFormSchema), + defaultValues: { + http: resource.http, + tlsServerName: resource.http ? resource.tlsServerName || "" : undefined, + setHostHeader: resource.http ? resource.setHostHeader || "" : undefined }, mode: "onChange" }); @@ -224,8 +252,7 @@ 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, - tlsServerName: data.http ? data.tlsServerName : undefined + domainId: data.http ? data.domainId : undefined } ) .catch((e) => { @@ -252,8 +279,7 @@ export default function GeneralForm() { subdomain: data.subdomain, proxyPort: data.proxyPort, isBaseDomain: data.isBaseDomain, - fullDomain: resource.fullDomain, - tlsServerName: data.tlsServerName + fullDomain: resource.fullDomain }); router.refresh(); @@ -295,6 +321,46 @@ export default function GeneralForm() { setTransferLoading(false); } + async function onSubmitAdvanced(data: AdvancedFormValues) { + setSaveLoading(true); + + const res = await api + .post>( + `resource/${resource?.resourceId}`, + { + tlsServerName: data.http ? data.tlsServerName : undefined, + setHostHeader: data.http ? data.setHostHeader : undefined + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to update resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: "Resource updated", + description: "The resource has been updated successfully" + }); + + const resource = res.data.data; + + updateResource({ + tlsServerName: data.tlsServerName, + setHostHeader: data.setHostHeader + }); + + router.refresh(); + } + setSaveLoading(false); + } + async function toggleResourceEnabled(val: boolean) { const res = await api .post>( @@ -561,27 +627,7 @@ export default function GeneralForm() { )} /> )} - {/* New TLS Server Name Field */} -
- - TLS Server Name (optional) - - ( - - - - - - - )} - /> -
)} @@ -637,6 +683,81 @@ export default function GeneralForm() { + {resource.http && ( + <> + + + Advanced + + Adjust advanced settings for the resource, like customize the Host Header or set a TLS Server Name for SNI based routing. + + + + +
+ + {/* New TLS Server Name Field */} +
+ + TLS Server Name (optional) + + ( + + + + + + + )} + /> +
+ {/* New Custom Host Header Field */} +
+ + Custom Host Header (optional) + + ( + + + + + + + )} + /> +
+
+ +
+
+ + + + +
+ + )}