add traefik settings to config and use fullDomain

This commit is contained in:
Milo Schwartz 2024-10-22 00:09:27 -04:00
parent 1c4608fbf4
commit 6d9731f071
No known key found for this signature in database
5 changed files with 45 additions and 52 deletions

View file

@ -10,6 +10,11 @@ server:
internal_hostname: localhost internal_hostname: localhost
secure_cookies: "false" secure_cookies: "false"
traefik:
cert_resolver: "letsencrypt"
http_entrypoint: "http"
https_entrypoint: "https"
rate_limit: rate_limit:
window_minutes: "1" window_minutes: "1"
max_requests: "100" max_requests: "100"

View file

@ -10,7 +10,6 @@ const environmentSchema = z.object({
app: z.object({ app: z.object({
name: z.string(), name: z.string(),
base_url: z.string().url(), base_url: z.string().url(),
base_domain: z.string(),
log_level: z.enum(["debug", "info", "warn", "error"]), log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.string().transform((val) => val === "true"), save_logs: z.string().transform((val) => val === "true"),
}), }),
@ -26,6 +25,11 @@ const environmentSchema = z.object({
internal_hostname: z.string(), internal_hostname: z.string(),
secure_cookies: z.string().transform((val) => val === "true"), 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({ rate_limit: z.object({
window_minutes: z window_minutes: z
.string() .string()

View file

@ -24,7 +24,8 @@ export const sites = sqliteTable("sites", {
}); });
export const resources = sqliteTable("resources", { 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, { siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
@ -45,6 +46,7 @@ export const targets = sqliteTable("targets", {
port: integer("port").notNull(), port: integer("port").notNull(),
protocol: text("protocol"), protocol: text("protocol"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
}); });
export const exitNodes = sqliteTable("exitNodes", { export const exitNodes = sqliteTable("exitNodes", {

View file

@ -39,8 +39,8 @@ app.prepare().then(() => {
if (!dev) { if (!dev) {
externalServer.use( externalServer.use(
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: 1, windowMin: config.rate_limit.window_minutes,
max: 100, max: config.rate_limit.max_requests,
type: "IP_ONLY", type: "IP_ONLY",
}), }),
); );
@ -88,7 +88,7 @@ app.prepare().then(() => {
internalServer.use(errorHandlerMiddleware); internalServer.use(errorHandlerMiddleware);
}); });
declare global { declare global { // TODO: eventually make seperate types that extend express.Request
namespace Express { namespace Express {
interface Request { interface Request {
user?: User; user?: User;
@ -97,4 +97,4 @@ declare global {
userOrgIds?: string[]; userOrgIds?: string[];
} }
} }
} }

View file

@ -2,10 +2,9 @@ import { Request, Response } from "express";
import db from "@server/db"; import db from "@server/db";
import * as schema from "@server/db/schema"; import * as schema from "@server/db/schema";
import { DynamicTraefikConfig } from "./configSchema"; 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 logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import env from "@server/config";
import config from "@server/config"; import config from "@server/config";
export async function traefikConfigProvider(_: Request, res: Response) { export async function traefikConfigProvider(_: Request, res: Response) {
@ -22,46 +21,36 @@ export async function traefikConfigProvider(_: Request, res: Response) {
} }
export function buildTraefikConfig( export function buildTraefikConfig(
targets: schema.Target[], targets: schema.Target[]
): DynamicTraefikConfig { ): DynamicTraefikConfig {
if (!targets.length) {
return { http: {} } as DynamicTraefikConfig;
}
const middlewareName = "badger"; 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 = { const http: any = {
routers: { routers: {},
main: { services: {},
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}`,
},
],
},
},
},
middlewares: { middlewares: {
[middlewareName]: { [middlewareName]: {
plugin: { plugin: {
[middlewareName]: { [middlewareName]: {
apiBaseUrl: new URL( apiBaseUrl: new URL(
"/api/v1", "/api/v1",
`http://${config.server.internal_hostname}:${config.server.internal_port}`, `http://${config.server.internal_hostname}:${config.server.internal_port}`
).href, ).href,
appBaseUrl: config.app.base_url, appBaseUrl: config.app.base_url,
}, },
@ -74,19 +63,11 @@ export function buildTraefikConfig(
const serviceName = `service-${target.targetId}`; const serviceName = `service-${target.targetId}`;
http.routers![routerName] = { http.routers![routerName] = {
entryPoints: ["https"], entryPoints: [target.ssl ? config.traefik.https_entrypoint : config.traefik.https_entrypoint],
middlewares: [middlewareName], middlewares: [middlewareName],
service: serviceName, service: serviceName,
rule: `Host(\`${target.resourceId}\`)`, // assuming resourceId is a valid full hostname rule: `Host(\`${target.resourceId}\`)`, // assuming resourceId is a valid full hostname
tls: { ...(target.ssl ? { tls } : {}),
certResolver: "letsencrypt",
domains: [
{
main: "fossorial.io",
sans: ["*.fossorial.io"],
},
],
},
}; };
http.services![serviceName] = { http.services![serviceName] = {
@ -105,11 +86,12 @@ export async function getAllTargets(): Promise<schema.Target[]> {
const all = await db const all = await db
.select() .select()
.from(schema.targets) .from(schema.targets)
.innerJoin(schema.resources, eq(schema.targets.resourceId, schema.resources.resourceId))
.where( .where(
and( and(
eq(schema.targets.enabled, true), eq(schema.targets.enabled, true),
like(schema.targets.resourceId, "%.%"), isNotNull(schema.resources.fullDomain)
), )
); // any resourceId with a dot is a valid hostname; otherwise it's a UUID placeholder );
return all; return all.map((row) => row.targets);
} }