import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import type { Db } from '@evofw/db' import { repos } from '@evofw/db' import { AppError } from '../plugins/error-handler.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const scriptsDir = join(__dirname, '../agent-scripts') const ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' export function randomToken(length: number): string { const bytes = crypto.getRandomValues(new Uint8Array(length)) let out = '' for (const b of bytes) { out += ID_ALPHABET[b % ID_ALPHABET.length] } return out } /** Root path segments that must never be treated as install slugs. */ export const RESERVED_ROOT_SEGMENTS = new Set([ 'api', 'v1', 'assets', 'health', 'ready', 'favicon.ico', 'index.html', 'robots.txt', 'agent-install', 'static', ]) const SLUG_RE = /^[A-Za-z0-9_-]{8,64}$/ export function isValidInstallSlug(segment: string): boolean { if (!segment || segment.includes('/') || segment.includes('.')) return false if (RESERVED_ROOT_SEGMENTS.has(segment.toLowerCase())) return false return SLUG_RE.test(segment) } export type InstallPlatform = 'linux' | 'mikrotik' function mikrotikFetchImport(url: string): string { return `/tool fetch url="${url}" dst-path=evofw-install.rsc; /import file-name=evofw-install.rsc` } export function buildInstallUrls( baseUrl: string, id: string, slug: string, platform: InstallPlatform = 'linux', ) { const base = baseUrl.replace(/\/$/, '') const byId = `${base}/agent-install/${id}` const bySlug = `${base}/${slug}` if (platform === 'mikrotik') { return { by_id: byId, by_slug: bySlug, curl: { by_id: mikrotikFetchImport(byId), by_slug: mikrotikFetchImport(bySlug), }, } } return { by_id: byId, by_slug: bySlug, curl: { by_id: `curl -fsSL ${byId} | bash`, by_slug: `curl -fsSL ${bySlug} | bash`, }, } } export function mapInstallLink( row: NonNullable>, baseUrl: string, ) { const platform = (row.platform === 'mikrotik' ? 'mikrotik' : 'linux') as InstallPlatform const urls = buildInstallUrls(baseUrl, row.id, row.slug, platform) return { id: row.id, slug: row.slug, client_name: row.clientName, platform, agent_id: row.agentId ?? null, created_at: row.createdAt, revoked_at: row.revokedAt, last_used_at: row.lastUsedAt, use_count: row.useCount, urls: { by_id: urls.by_id, by_slug: urls.by_slug }, curl: urls.curl, } } function loadInstallSh(): string { return readFileSync(join(scriptsDir, 'install.sh'), 'utf-8') } function loadMikrotikInstallRsc(): string { return readFileSync(join(scriptsDir, 'mikrotik-install.rsc'), 'utf-8') } function escapeRosString(s: string): string { return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') } /** * Self-contained install script: env exports + full install.sh body. */ export function renderInstallScript(opts: { cpUrl: string seed: string clientName: string platform: string installLinkId: string }): string { const cp = opts.cpUrl.replace(/\/$/, '') const escape = (s: string) => s.replace(/'/g, `'\\''`) const header = [ '#!/usr/bin/env bash', '# EvoFirewall short install link — env pre-set', 'set -euo pipefail', `export EVOFW_CP_URL='${escape(cp)}'`, `export EVOFW_SEED='${escape(opts.seed)}'`, `export EVOFW_CLIENT_NAME='${escape(opts.clientName)}'`, `export EVOFW_PLATFORM='${escape(opts.platform)}'`, `export EVOFW_INSTALL_LINK_ID='${escape(opts.installLinkId)}'`, '', ].join('\n') // Drop the shebang from install.sh to avoid double shebang. const body = loadInstallSh().replace(/^#!\/usr\/bin\/env bash\r?\n/, '') return `${header}${body}` } /** * Personalized MikroTik RSC: globals + mikrotik-install.rsc body. */ export function renderMikrotikInstallScript(opts: { cpUrl: string seed: string clientName: string installLinkId: string }): string { const cp = opts.cpUrl.replace(/\/$/, '') const header = [ '# EvoFirewall short install link — globals pre-set (RouterOS 7.21+)', `:global EvofwCpUrl "${escapeRosString(cp)}"`, `:global EvofwSeed "${escapeRosString(opts.seed)}"`, `:global EvofwName "${escapeRosString(opts.clientName)}"`, `:global EvofwInstallLinkId "${escapeRosString(opts.installLinkId)}"`, '', ].join('\n') return `${header}${loadMikrotikInstallRsc()}` } export type ResolvedInstall = { body: string contentType: string } /** @deprecated use resolveAndRenderInstall */ export function resolveAndRenderInstallScript( db: Db, link: NonNullable>, publicBaseUrl: string, enrollSeedFallback: string, ): string { return resolveAndRenderInstall(db, link, publicBaseUrl, enrollSeedFallback) .body } export function resolveAndRenderInstall( db: Db, link: NonNullable>, publicBaseUrl: string, enrollSeedFallback: string, ): ResolvedInstall { if (link.revokedAt) { throw new AppError('GONE', 'Install link revoked', 410) } const seed = repos.getSetting(db, 'enroll_seed') || enrollSeedFallback repos.touchInstallLink(db, link.id) if (link.platform === 'mikrotik') { return { body: renderMikrotikInstallScript({ cpUrl: publicBaseUrl, seed, clientName: link.clientName, installLinkId: link.id, }), contentType: 'text/plain', } } return { body: renderInstallScript({ cpUrl: publicBaseUrl, seed, clientName: link.clientName, platform: link.platform, installLinkId: link.id, }), contentType: 'text/x-shellscript', } }