diff --git a/config/config.example.yml b/config/config.example.yml index 3f2d0b60..bf37dc82 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -10,6 +10,11 @@ server: internal_hostname: localhost secure_cookies: "false" +traefik: + cert_resolver: "letsencrypt" + http_entrypoint: "http" + https_entrypoint: "https" + rate_limit: window_minutes: "1" max_requests: "100" diff --git a/server/config.ts b/server/config.ts index 93fbeccb..37b5e2cd 100644 --- a/server/config.ts +++ b/server/config.ts @@ -10,7 +10,6 @@ const environmentSchema = z.object({ app: z.object({ name: z.string(), base_url: z.string().url(), - base_domain: z.string(), log_level: z.enum(["debug", "info", "warn", "error"]), save_logs: z.string().transform((val) => val === "true"), }), @@ -26,6 +25,11 @@ const environmentSchema = z.object({ internal_hostname: z.string(), secure_cookies: z.string().transform((val) => val === "true"), }), + traefik: z.object({ + http_entrypoint: z.string(), + https_entrypoint: z.string().optional(), + cert_resolver: z.string().optional(), + }), rate_limit: z.object({ window_minutes: z .string() diff --git a/server/db/schema.ts b/server/db/schema.ts index 38daccb5..244192d6 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -24,7 +24,8 @@ export const sites = sqliteTable("sites", { }); export const resources = sqliteTable("resources", { - resourceId: text("resourceId", { length: 2048 }).primaryKey(), + resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), + fullDomain: text("fullDomain", { length: 2048 }), siteId: integer("siteId").references(() => sites.siteId, { onDelete: "cascade", }), @@ -45,6 +46,7 @@ export const targets = sqliteTable("targets", { port: integer("port").notNull(), protocol: text("protocol"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), }); export const exitNodes = sqliteTable("exitNodes", { diff --git a/server/index.ts b/server/index.ts index bce7352b..9bfd1ccc 100644 --- a/server/index.ts +++ b/server/index.ts @@ -39,8 +39,8 @@ app.prepare().then(() => { if (!dev) { externalServer.use( rateLimitMiddleware({ - windowMin: 1, - max: 100, + windowMin: config.rate_limit.window_minutes, + max: config.rate_limit.max_requests, type: "IP_ONLY", }), ); @@ -88,7 +88,7 @@ app.prepare().then(() => { internalServer.use(errorHandlerMiddleware); }); -declare global { +declare global { // TODO: eventually make seperate types that extend express.Request namespace Express { interface Request { user?: User; @@ -97,4 +97,4 @@ declare global { userOrgIds?: string[]; } } -} +} \ No newline at end of file diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 44dea0e5..d83e8fad 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -2,10 +2,9 @@ import { Request, Response } from "express"; import db from "@server/db"; import * as schema from "@server/db/schema"; import { DynamicTraefikConfig } from "./configSchema"; -import { and, like, eq } from "drizzle-orm"; +import { and, like, eq, isNotNull } from "drizzle-orm"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; -import env from "@server/config"; import config from "@server/config"; export async function traefikConfigProvider(_: Request, res: Response) { @@ -22,46 +21,36 @@ export async function traefikConfigProvider(_: Request, res: Response) { } export function buildTraefikConfig( - targets: schema.Target[], + targets: schema.Target[] ): DynamicTraefikConfig { + if (!targets.length) { + return { http: {} } as DynamicTraefikConfig; + } + const middlewareName = "badger"; + const baseDomain = new URL(config.app.base_url).hostname; + + const tls = { + certResolver: config.traefik.cert_resolver, + // domains: [ // TODO: figure out if this is neccessary + // { + // main: baseDomain, + // sans: ["*." + baseDomain], + // }, + // ], + }; + const http: any = { - routers: { - main: { - entryPoints: ["https"], - middlewares: [], - service: "service-main", - rule: "Host(`fossorial.io`)", - tls: { - certResolver: "letsencrypt", - domains: [ - { - main: "fossorial.io", - sans: ["*.fossorial.io"], - }, - ], - }, - }, - }, - services: { - "service-main": { - loadBalancer: { - servers: [ - { - url: `http://${config.server.internal_hostname}:${config.server.external_port}`, - }, - ], - }, - }, - }, + routers: {}, + services: {}, middlewares: { [middlewareName]: { plugin: { [middlewareName]: { apiBaseUrl: new URL( "/api/v1", - `http://${config.server.internal_hostname}:${config.server.internal_port}`, + `http://${config.server.internal_hostname}:${config.server.internal_port}` ).href, appBaseUrl: config.app.base_url, }, @@ -74,19 +63,11 @@ export function buildTraefikConfig( const serviceName = `service-${target.targetId}`; http.routers![routerName] = { - entryPoints: ["https"], + entryPoints: [target.ssl ? config.traefik.https_entrypoint : config.traefik.https_entrypoint], middlewares: [middlewareName], service: serviceName, rule: `Host(\`${target.resourceId}\`)`, // assuming resourceId is a valid full hostname - tls: { - certResolver: "letsencrypt", - domains: [ - { - main: "fossorial.io", - sans: ["*.fossorial.io"], - }, - ], - }, + ...(target.ssl ? { tls } : {}), }; http.services![serviceName] = { @@ -105,11 +86,12 @@ export async function getAllTargets(): Promise { const all = await db .select() .from(schema.targets) + .innerJoin(schema.resources, eq(schema.targets.resourceId, schema.resources.resourceId)) .where( and( eq(schema.targets.enabled, true), - like(schema.targets.resourceId, "%.%"), - ), - ); // any resourceId with a dot is a valid hostname; otherwise it's a UUID placeholder - return all; + isNotNull(schema.resources.fullDomain) + ) + ); + return all.map((row) => row.targets); }