浏览代码

optionally generate traefik files, set cors in config, and set trust proxy in config

Milo Schwartz 6 月之前
父节点
当前提交
1aec431c36

+ 2 - 0
Dockerfile

@@ -27,6 +27,8 @@ COPY --from=builder /app/dist ./dist
 COPY --from=builder /app/init ./dist/init
 
 COPY config/config.example.yml ./dist/config.example.yml
+COPY config/traefik/traefik_config.example.yml ./dist/traefik_config.example.yml
+COPY config/traefik/dynamic_config.example.yml ./dist/dynamic_config.example.yml
 COPY server/db/names.json ./dist/names.json
 
 COPY public ./public

+ 1 - 1
config/config.example.yml

@@ -38,6 +38,6 @@ users:
         password: Password123!
 
 flags:
-    require_email_verification: true
+    require_email_verification: false
     disable_signup_without_invite: true
     disable_user_create_org: true

+ 54 - 0
config/traefik/dynamic_config.example.yml

@@ -0,0 +1,54 @@
+http:
+  middlewares:
+    redirect-to-https:
+      redirectScheme:
+        scheme: https
+        permanent: true
+
+  routers:
+    # HTTP to HTTPS redirect router
+    main-app-router-redirect:
+      rule: "Host(`{{.DashboardDomain}}`)"
+      service: next-service
+      entryPoints:
+        - web
+      middlewares:
+        - redirect-to-https
+
+    # Next.js router (handles everything except API and WebSocket paths)
+    next-router:
+      rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
+      service: next-service
+      entryPoints:
+        - websecure
+      tls:
+        certResolver: letsencrypt
+
+    # API router (handles /api/v1 paths)
+    api-router:
+      rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
+      service: api-service
+      entryPoints:
+        - websecure
+      tls:
+        certResolver: letsencrypt
+
+    # WebSocket router
+    ws-router:
+      rule: "Host(`{{.DashboardDomain}}`)"
+      service: api-service
+      entryPoints:
+        - websecure
+      tls:
+        certResolver: letsencrypt
+
+  services:
+    next-service:
+      loadBalancer:
+        servers:
+          - url: "http://pangolin:3002"  # Next.js server
+
+    api-service:
+      loadBalancer:
+        servers:
+          - url: "http://pangolin:3000"  # API/WebSocket server

+ 41 - 0
config/traefik/traefik_config.example.yml

@@ -0,0 +1,41 @@
+api:
+  insecure: true
+  dashboard: true
+
+providers:
+  http:
+    endpoint: "http://pangolin:3001/api/v1/traefik-config"
+    pollInterval: "5s"
+  file:
+    filename: "/etc/traefik/dynamic_config.yml"
+
+experimental:
+  plugins:
+    badger:
+      moduleName: "github.com/fosrl/badger"
+      version: "v1.0.0-beta.2"
+
+log:
+  level: "INFO"
+  format: "common"
+
+certificatesResolvers:
+  letsencrypt:
+    acme:
+      httpChallenge:
+        entryPoint: web
+      email: "{{.LetsEncryptEmail}}"
+      storage: "/letsencrypt/acme.json"
+      caServer: "https://acme-v02.api.letsencrypt.org/directory"
+
+entryPoints:
+  web:
+    address: ":80"
+  websecure:
+    address: ":443"
+    http:
+      tls:
+        certResolver: "letsencrypt"
+
+serversTransport:
+  insecureSkipVerify: true

+ 5 - 0
install/fs/config.yml

@@ -13,6 +13,11 @@ server:
     session_cookie_name: p_session
     resource_session_cookie_name: p_resource_session
     resource_access_token_param: p_token
+    cors:
+        origins: ["https://{{.DashboardDomain}}"]
+        methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
+        headers: ["X-CSRF-Token", "Content-Type"]
+        credentials: false
 
 traefik:
     cert_resolver: letsencrypt

+ 2 - 24
install/fs/traefik/dynamic_config.yml

@@ -4,21 +4,6 @@ http:
       redirectScheme:
         scheme: https
         permanent: true
-    cors:
-      headers:
-        accessControlAllowMethods:
-          - GET
-          - PUT
-          - POST
-          - DELETE
-          - PATCH
-        accessControlAllowHeaders:
-          - Content-Type
-          - X-CSRF-Token
-        accessControlAllowOriginList:
-          - https://{{.DashboardDomain}}
-        accessControlAllowCredentials: false
-
 
   routers:
     # HTTP to HTTPS redirect router
@@ -29,7 +14,6 @@ http:
         - web
       middlewares:
         - redirect-to-https
-        - cors
 
     # Next.js router (handles everything except API and WebSocket paths)
     next-router:
@@ -37,8 +21,6 @@ http:
       service: next-service
       entryPoints:
         - websecure
-      middlewares:
-        - cors
       tls:
         certResolver: letsencrypt
 
@@ -48,8 +30,6 @@ http:
       service: api-service
       entryPoints:
         - websecure
-      middlewares:
-        - cors
       tls:
         certResolver: letsencrypt
 
@@ -59,8 +39,6 @@ http:
       service: api-service
       entryPoints:
         - websecure
-      middlewares:
-        - cors
       tls:
         certResolver: letsencrypt
 
@@ -68,9 +46,9 @@ http:
     next-service:
       loadBalancer:
         servers:
-          - url: "http://pangolin:3002"  # Next.js server
+          - url: "http://pangolin:{{.NEXT_PORT}}"  # Next.js server
 
     api-service:
       loadBalancer:
         servers:
-          - url: "http://pangolin:3000"  # API/WebSocket server
+          - url: "http://pangolin:{{.EXTERNAL_PORT}}"  # API/WebSocket server

+ 1 - 1
install/fs/traefik/traefik_config.yml

@@ -4,7 +4,7 @@ api:
 
 providers:
   http:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config"
+    endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config"
     pollInterval: "5s"
   file:
     filename: "/etc/traefik/dynamic_config.yml"

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
     "name": "@fosrl/pangolin",
-    "version": "1.0.0-beta.5",
+    "version": "1.0.0-beta.6",
     "private": true,
     "type": "module",
     "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",

+ 3 - 1
server/apiServer.ts

@@ -20,7 +20,9 @@ const externalPort = config.getRawConfig().server.external_port;
 export function createApiServer() {
     const apiServer = express();
 
-    apiServer.set("trust proxy", 1);
+    if (config.getRawConfig().server.trust_proxy) {
+        apiServer.set("trust proxy", 1);
+    }
 
     const corsConfig = config.getRawConfig().server.cors;
 

+ 131 - 16
server/lib/config.ts

@@ -11,6 +11,7 @@ import {
 } from "@server/lib/consts";
 import { loadAppVersion } from "@server/lib/loadAppVersion";
 import { passwordSchema } from "@server/auth/passwordSchema";
+import stoi from "./stoi";
 
 const portSchema = z.number().positive().gt(0).lte(65535);
 const hostnameSchema = z
@@ -20,31 +21,56 @@ const hostnameSchema = z
     )
     .or(z.literal("localhost"));
 
-const environmentSchema = z.object({
+const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
+    return process.env[envVar] ?? valFromYaml;
+};
+
+const configSchema = z.object({
     app: z.object({
         dashboard_url: z
             .string()
             .url()
+            .optional()
+            .transform(getEnvOrYaml("APP_DASHBOARDURL"))
+            .pipe(z.string().url())
             .transform((url) => url.toLowerCase()),
-        base_domain: hostnameSchema,
+        base_domain: hostnameSchema
+            .optional()
+            .transform(getEnvOrYaml("APP_BASEDOMAIN"))
+            .pipe(hostnameSchema),
         log_level: z.enum(["debug", "info", "warn", "error"]),
         save_logs: z.boolean()
     }),
     server: z.object({
-        external_port: portSchema,
-        internal_port: portSchema,
-        next_port: portSchema,
+        external_port: portSchema
+            .optional()
+            .transform(getEnvOrYaml("SERVER_EXTERNALPORT"))
+            .transform(stoi)
+            .pipe(portSchema),
+        internal_port: portSchema
+            .optional()
+            .transform(getEnvOrYaml("SERVER_INTERNALPORT"))
+            .transform(stoi)
+            .pipe(portSchema),
+        next_port: portSchema
+            .optional()
+            .transform(getEnvOrYaml("SERVER_NEXTPORT"))
+            .transform(stoi)
+            .pipe(portSchema),
         internal_hostname: z.string().transform((url) => url.toLowerCase()),
         secure_cookies: z.boolean(),
         session_cookie_name: z.string(),
         resource_session_cookie_name: z.string(),
         resource_access_token_param: z.string(),
-        cors: z.object({
-            origins: z.array(z.string()).optional(),
-            methods: z.array(z.string()).optional(),
-            allowed_headers: z.array(z.string()).optional(),
-            credentials: z.boolean().optional(),
-        }).optional()
+        cors: z
+            .object({
+                origins: z.array(z.string()).optional(),
+                methods: z.array(z.string()).optional(),
+                allowed_headers: z.array(z.string()).optional(),
+                credentials: z.boolean().optional()
+            })
+            .optional(),
+        trust_proxy: z.boolean().optional().default(true)
     }),
     traefik: z.object({
         http_entrypoint: z.string(),
@@ -53,8 +79,17 @@ const environmentSchema = z.object({
         prefer_wildcard_cert: z.boolean().optional()
     }),
     gerbil: z.object({
-        start_port: portSchema,
-        base_endpoint: z.string().transform((url) => url.toLowerCase()),
+        start_port: portSchema
+            .optional()
+            .transform(getEnvOrYaml("GERBIL_STARTPORT"))
+            .transform(stoi)
+            .pipe(portSchema),
+        base_endpoint: z
+            .string()
+            .optional()
+            .transform(getEnvOrYaml("GERBIL_BASEENDPOINT"))
+            .pipe(z.string())
+            .transform((url) => url.toLowerCase()),
         use_subdomain: z.boolean(),
         subnet_group: z.string(),
         block_size: z.number().positive().gt(0),
@@ -83,8 +118,16 @@ const environmentSchema = z.object({
         .optional(),
     users: z.object({
         server_admin: z.object({
-            email: z.string().email(),
+            email: z
+                .string()
+                .email()
+                .optional()
+                .transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
+                .pipe(z.string().email()),
             password: passwordSchema
+                .optional()
+                .transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
+                .pipe(passwordSchema)
         })
     }),
     flags: z
@@ -97,12 +140,18 @@ const environmentSchema = z.object({
 });
 
 export class Config {
-    private rawConfig!: z.infer<typeof environmentSchema>;
+    private rawConfig!: z.infer<typeof configSchema>;
 
     constructor() {
         this.loadConfig();
+
+        if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
+            this.createTraefikConfig();
+        }
     }
 
+    public loadEnvironment() {}
+
     public loadConfig() {
         const loadConfig = (configPath: string) => {
             try {
@@ -166,7 +215,7 @@ export class Config {
             throw new Error("No configuration file found");
         }
 
-        const parsedConfig = environmentSchema.safeParse(environment);
+        const parsedConfig = configSchema.safeParse(environment);
 
         if (!parsedConfig.success) {
             const errors = fromError(parsedConfig.error);
@@ -214,6 +263,72 @@ export class Config {
     public getBaseDomain(): string {
         return this.rawConfig.app.base_domain;
     }
+
+    private createTraefikConfig() {
+        try {
+            // check if traefik_config.yml and dynamic_config.yml exists in APP_PATH/traefik
+            const defaultTraefikConfigPath = path.join(
+                __DIRNAME,
+                "traefik_config.example.yml"
+            );
+            const defaultDynamicConfigPath = path.join(
+                __DIRNAME,
+                "dynamic_config.example.yml"
+            );
+
+            const traefikPath = path.join(APP_PATH, "traefik");
+            if (!fs.existsSync(traefikPath)) {
+                return;
+            }
+
+            // load default configs
+            let traefikConfig = fs.readFileSync(
+                defaultTraefikConfigPath,
+                "utf8"
+            );
+            let dynamicConfig = fs.readFileSync(
+                defaultDynamicConfigPath,
+                "utf8"
+            );
+
+            traefikConfig = traefikConfig
+                .split("{{.LetsEncryptEmail}}")
+                .join(this.rawConfig.users.server_admin.email);
+            traefikConfig = traefikConfig
+                .split("{{.INTERNAL_PORT}}")
+                .join(this.rawConfig.server.internal_port.toString());
+
+            dynamicConfig = dynamicConfig
+                .split("{{.DashboardDomain}}")
+                .join(new URL(this.rawConfig.app.dashboard_url).hostname);
+            dynamicConfig = dynamicConfig
+                .split("{{.NEXT_PORT}}")
+                .join(this.rawConfig.server.next_port.toString());
+            dynamicConfig = dynamicConfig
+                .split("{{.EXTERNAL_PORT}}")
+                .join(this.rawConfig.server.external_port.toString());
+
+            // write thiese to the traefik directory
+            const traefikConfigPath = path.join(
+                traefikPath,
+                "traefik_config.yml"
+            );
+            const dynamicConfigPath = path.join(
+                traefikPath,
+                "dynamic_config.yml"
+            );
+
+            fs.writeFileSync(traefikConfigPath, traefikConfig, "utf8");
+            fs.writeFileSync(dynamicConfigPath, dynamicConfig, "utf8");
+
+            console.log("Traefik configuration files created");
+        } catch (e) {
+            console.log(
+                "Failed to generate the Traefik configuration files. Please create them manually."
+            );
+            console.error(e);
+        }
+    }
 }
 
 export const config = new Config();

+ 3 - 1
server/setup/migrations.ts

@@ -10,6 +10,7 @@ import m1 from "./scripts/1.0.0-beta1";
 import m2 from "./scripts/1.0.0-beta2";
 import m3 from "./scripts/1.0.0-beta3";
 import m4 from "./scripts/1.0.0-beta5";
+import m5 from "./scripts/1.0.0-beta6";
 import { existsSync, mkdirSync } from "fs";
 
 // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
@@ -20,7 +21,8 @@ const migrations = [
     { version: "1.0.0-beta.1", run: m1 },
     { version: "1.0.0-beta.2", run: m2 },
     { version: "1.0.0-beta.3", run: m3 },
-    { version: "1.0.0-beta.5", run: m4 }
+    { version: "1.0.0-beta.5", run: m4 },
+    { version: "1.0.0-beta.6", run: m5 }
     // Add new migrations here as they are created
 ] as const;
 

+ 52 - 0
server/setup/scripts/1.0.0-beta6.ts

@@ -0,0 +1,52 @@
+import { configFilePath1, configFilePath2 } from "@server/lib/consts";
+import fs from "fs";
+import yaml from "js-yaml";
+
+export default async function migration() {
+    console.log("Running setup script 1.0.0-beta.6...");
+
+    try {
+        // Determine which config file exists
+        const filePaths = [configFilePath1, configFilePath2];
+        let filePath = "";
+        for (const path of filePaths) {
+            if (fs.existsSync(path)) {
+                filePath = path;
+                break;
+            }
+        }
+
+        if (!filePath) {
+            throw new Error(
+                `No config file found (expected config.yml or config.yaml).`
+            );
+        }
+
+        // Read and parse the YAML file
+        let rawConfig: any;
+        const fileContents = fs.readFileSync(filePath, "utf8");
+        rawConfig = yaml.load(fileContents);
+
+        // Validate the structure
+        if (!rawConfig.server) {
+            throw new Error(`Invalid config file: server is missing.`);
+        }
+
+        // Update the config
+        rawConfig.server.cors = {
+            origins: [rawConfig.app.dashboard_url],
+            methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
+            headers: ["X-CSRF-Token", "Content-Type"],
+            credentials: false
+        };
+
+        // Write the updated YAML back to the file
+        const updatedYaml = yaml.dump(rawConfig);
+        fs.writeFileSync(filePath, updatedYaml, "utf8");
+    } catch (error) {
+        console.log("We were unable to add CORS to your config file. Please add it manually.")
+        console.error(error)
+    }
+
+    console.log("Done.");
+}

+ 4 - 4
src/app/[orgId]/settings/sites/CreateSiteForm.tsx

@@ -235,10 +235,10 @@ PersistentKeepalive = 5`
             : "";
 
     // am I at http or https?
-    let proto = "http:";
-    if (typeof window !== "undefined") {
-        proto = window.location.protocol;
-    }
+    let proto = "https:";
+    // if (typeof window !== "undefined") {
+    //     proto = window.location.protocol;
+    // }
 
     const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;