diff --git a/src/server/Cloudflare.ts b/src/server/Cloudflare.ts deleted file mode 100644 index a9bc132e4..000000000 --- a/src/server/Cloudflare.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { spawn } from "child_process"; -import { promises as fs } from "fs"; -import yaml from "js-yaml"; -import { logger } from "./Logger"; - -const log = logger.child({ - module: "cloudflare", -}); - -export interface TunnelConfig { - domain: string; - subdomain: string; - subdomainToService: Map; -} - -interface TunnelResponse { - result: { - id: string; - token: string; - }; -} - -interface ZoneResponse { - result: Array<{ - id: string; - }>; -} - -interface DNSRecordResponse { - result: Array<{ - id: string; - }>; -} - -interface CloudflaredConfig { - tunnel: string; - "credentials-file": string; - ingress: Array<{ - hostname?: string; - service: string; - }>; -} - -export class Cloudflare { - private baseUrl = "https://api.cloudflare.com/client/v4"; - - constructor( - private accountId: string, - private apiToken: string, - private configPath: string, - private credsPath: string, - ) { - log.info(`Using config: ${this.configPath}`); - log.info(`Using credentials: ${this.credsPath}`); - } - - private async makeRequest( - url: string, - method: string = "GET", - data?: any, - ): Promise { - const response = await fetch(url, { - method, - headers: { - Authorization: `Bearer ${this.apiToken}`, - "Content-Type": "application/json", - }, - body: data ? JSON.stringify(data) : undefined, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `Cloudflare API error: url ${url} ${response.status} - ${errorText}`, - ); - } - - return response.json() as Promise; - } - - public async configAlreadyExists(): Promise { - try { - await fs.access(this.configPath); - return true; - } catch { - return false; - } - } - - public async createTunnel(config: TunnelConfig): Promise<{ - tunnelId: string; - tunnelToken: string; - tunnelUrl: string; - }> { - const { domain, subdomain, subdomainToService } = config; - - // Generate unique tunnel name - const timestamp = new Date().toISOString().replace(/[-:.]/g, ""); - const tunnelName = `${subdomain}-tunnel-${timestamp}`; - - log.info(`Creating tunnel with name: ${tunnelName}`); - - // Create tunnel via API to get official tunnel ID and token - const tunnelResponse = await this.makeRequest( - `${this.baseUrl}/accounts/${this.accountId}/cfd_tunnel`, - "POST", - { name: tunnelName }, - ); - - const tunnelId = tunnelResponse.result.id; - const tunnelToken = tunnelResponse.result.token; - - if (!tunnelId) { - throw new Error("Failed to create tunnel"); - } - - log.info(`Tunnel created with ID: ${tunnelId}`); - - // Create local config file instead of using API configuration - await this.writeTunnelConfig( - tunnelId, - tunnelToken, - subdomain, - domain, - subdomainToService, - tunnelName, - ); - - // Get zone ID - const zoneResponse = await this.makeRequest( - `${this.baseUrl}/zones?name=${domain}`, - ); - - const zoneId = zoneResponse.result[0]?.id; - if (!zoneId) { - throw new Error(`Could not find zone ID for domain ${domain}`); - } - - await Promise.all( - Array.from(subdomainToService.entries()).map(([subdomain, _]) => - this.updateDNSRecord(zoneId, tunnelId, subdomain, domain), - ), - ); - - const tunnelUrl = `https://${subdomain}.${domain}`; - log.info(`Tunnel is set up! Site will be available at: ${tunnelUrl}`); - - return { tunnelId, tunnelToken, tunnelUrl }; - } - - private async writeTunnelConfig( - tunnelId: string, - tunnelToken: string, - subdomain: string, - domain: string, - subdomainToService: Map, - tunnelName: string, - ): Promise { - log.info(`Creating local config for tunnel ${subdomain}.${domain}...`); - const tokenData = JSON.parse( - Buffer.from(tunnelToken, "base64").toString("utf8"), - ); - - const credentials = { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - AccountTag: tokenData.a || this.accountId, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - TunnelID: tokenData.t || tunnelId, - TunnelName: tunnelName, - TunnelSecret: tokenData.s, - }; - - await fs.writeFile( - this.credsPath, - JSON.stringify(credentials, null, 2), - "utf8", - ); - log.info(`Created credentials file at: ${this.credsPath}`); - - const tunnelConfig: CloudflaredConfig = { - tunnel: tunnelId, - "credentials-file": this.credsPath, - ingress: [ - ...Array.from(subdomainToService.entries()).map( - ([subdomain, service]) => ({ - hostname: `${subdomain}.${domain}`, - service: service, - }), - ), - { - service: "http_status:404", - }, - ], - }; - - // Write config file - await fs.writeFile(this.configPath, yaml.dump(tunnelConfig), "utf8"); - log.info(`Created config file at: ${this.configPath}`); - } - - private async updateDNSRecord( - zoneId: string, - tunnelId: string, - subdomain: string, - domain: string, - ): Promise { - const existingRecords = await this.makeRequest( - `${this.baseUrl}/zones/${zoneId}/dns_records?name=${subdomain}.${domain}`, - ); - - const recordId = existingRecords.result[0]?.id; - const dnsData = { - type: "CNAME", - name: subdomain, - content: `${tunnelId}.cfargotunnel.com`, - ttl: 1, - proxied: true, - }; - - if (recordId) { - log.info(`Updating existing DNS record for ${subdomain}.${domain}...`); - await this.makeRequest( - `${this.baseUrl}/zones/${zoneId}/dns_records/${recordId}`, - "PUT", - dnsData, - ); - } else { - log.info(`Creating new DNS record for ${subdomain}.${domain}...`); - await this.makeRequest( - `${this.baseUrl}/zones/${zoneId}/dns_records`, - "POST", - dnsData, - ); - } - } - - public async startCloudflared() { - const cloudflared = spawn( - "cloudflared", - [ - "tunnel", - "--config", - this.configPath, - "--loglevel", - "error", - "--protocol", - "http2", - "--retries", - "15", - "--no-autoupdate", - "run", - ], - - { - detached: true, - stdio: ["ignore", "pipe", "pipe"], - env: { - ...process.env, - // Set this to bypass origin cert requirement for named tunnels - TUNNEL_ORIGIN_CERT: "/dev/null", - }, - }, - ); - - cloudflared.stdout?.on("data", (data) => { - log.info(data.toString().trim()); - }); - cloudflared.stderr?.on("data", (data) => { - log.error(data.toString().trim()); - }); - - cloudflared.on("error", (error) => { - log.error("Failed to start cloudflared", { - error: error.message, - }); - }); - - cloudflared.on("exit", (code, signal) => { - if (code !== null) { - log.error(`Cloudflared exited with code ${code}`, { - exitCode: code, - signal, - }); - } - }); - - cloudflared.unref(); - } -} diff --git a/src/server/Server.ts b/src/server/Server.ts index c3d91d295..e83205acf 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -1,22 +1,15 @@ import cluster from "cluster"; import * as dotenv from "dotenv"; -import { GameEnv } from "../core/configuration/Config"; -import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; -import { Cloudflare, TunnelConfig } from "./Cloudflare"; import { startMaster } from "./Master"; import { startWorker } from "./Worker"; // Load environment variables before we read configuration values derived from them. dotenv.config(); -const config = getServerConfigFromServer(); // Main entry point of the application async function main() { // Check if this is the primary (master) process if (cluster.isPrimary) { - if (config.env() !== GameEnv.Dev) { - await setupTunnels(); - } console.log("Starting master process..."); await startMaster(); } else { @@ -31,37 +24,3 @@ main().catch((error) => { console.error("Failed to start server:", error); process.exit(1); }); - -async function setupTunnels() { - const cloudflare = new Cloudflare( - config.cloudflareAccountId(), - config.cloudflareApiToken(), - config.cloudflareConfigPath(), - config.cloudflareCredsPath(), - ); - - const domainToService = new Map().set( - config.subdomain(), - // TODO: change to 3000 when we have a proper tunnel setup. - `http://localhost:80`, - ); - - for (let i = 0; i < config.numWorkers(); i++) { - domainToService.set( - `w${i}-${config.subdomain()}`, - `http://localhost:${3000 + i + 1}`, - ); - } - - if (!(await cloudflare.configAlreadyExists())) { - await cloudflare.createTunnel({ - subdomain: config.subdomain(), - domain: config.domain(), - subdomainToService: domainToService, - } as TunnelConfig); - } else { - console.log("Config already exists, skipping tunnel creation"); - } - - await cloudflare.startCloudflared(); -}