// DNS profile generator — DoH URL uses per-user subdomain for iOS compatibility import { randomUUID } from "node:crypto"; import { execSync } from "node:child_process"; import { existsSync, writeFileSync, readFileSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { getProfile } from "../../db/profile"; /** * Generiert ein iOS/macOS .mobileconfig DNS-Profil. * * Das Profil zeigt auf unseren eigenen DNS-Server (dns.rebreak.de), * der sowohl globale Blocklist als auch User-Custom-Domains blockiert. * * Free-User: Nur eigene Custom Domains werden blockiert * Pro-User: Globale Blocklist + Custom Domains */ export default defineEventHandler(async (event) => { const user = await requireUser(event); // Get user plan const profile = await getProfile(user.id); const isPro = profile?.plan === "pro" || profile?.plan === "legend"; const profileUUID = randomUUID().toUpperCase(); const payloadUUID = randomUUID().toUpperCase(); // DNS Server URL - points to our DNS proxy via per-user subdomain // Subdomain carries userId so iOS DoH profile has user context without query params // Staging: .dns-staging.rebreak.org | Prod: .rebreak.org const config = useRuntimeConfig(); const isStaging = config.public.appUrl.includes("staging"); const dnsServerUrl = isStaging ? `https://${user.id}.dns-staging.rebreak.org/dns-query` : `https://${user.id}.rebreak.org/dns-query`; // Get counts for description const { getActiveBlocklistCount, getUserCustomDomains } = await import("../../db/domains"); const [globalCount, customDomains] = await Promise.all([ isPro ? getActiveBlocklistCount() : Promise.resolve(0), getUserCustomDomains(user.id), ]); const customCount = customDomains.length; const totalCount = globalCount + customCount; const description = isPro ? `Aktiviert automatisches Blocking von Glücksspielseiten auf deinem Gerät. ${totalCount.toLocaleString()} Domains werden blockiert.` : `Aktiviert automatisches Blocking deiner personalisierten Sperrliste auf deinem Gerät. ${customCount} Domains werden blockiert.`; const xml = ` PayloadDisplayName ReBreak Schutz PayloadDescription ${description} PayloadIdentifier org.rebreak.protection PayloadUUID ${profileUUID} PayloadType Configuration PayloadVersion 1 PayloadContent PayloadDisplayName ReBreak Schutz PayloadIdentifier org.rebreak.protection.payload PayloadUUID ${payloadUUID} PayloadType com.apple.dnsSettings.managed PayloadVersion 1 ProhibitDisablement DNSSettings DNSProtocol HTTPS ServerURL ${dnsServerUrl} `; setResponseHeaders(event, { "Content-Type": "application/x-apple-aspen-config", "Content-Disposition": 'attachment; filename="rebreak-protection.mobileconfig"', }); // Cert base path based on environment const certBase = isStaging ? "/etc/letsencrypt/live/staging.rebreak.org" : "/etc/letsencrypt/live/rebreak.org"; const certFile = `${certBase}/cert.pem`; const keyFile = `${certBase}/privkey.pem`; const chainFile = `${certBase}/chain.pem`; if (existsSync(certFile) && existsSync(keyFile)) { const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const tmpIn = join(tmpdir(), `rebreak-profile-${id}.xml`); const tmpOut = join(tmpdir(), `rebreak-profile-${id}.der`); try { writeFileSync(tmpIn, xml, "utf8"); execSync( `openssl smime -sign -signer ${certFile} -inkey ${keyFile}${existsSync(chainFile) ? ` -certfile ${chainFile}` : ""} -nodetach -in ${tmpIn} -out ${tmpOut} -outform DER`, { stdio: "pipe", timeout: 5000 }, ); const signed = readFileSync(tmpOut); return signed; } catch { // Fallback: unsigniert (funktioniert noch, zeigt nur Warnung in iOS) } finally { try { unlinkSync(tmpIn); } catch { /* ignore */ } try { unlinkSync(tmpOut); } catch { /* ignore */ } } } // Unsigned fallback (Entwicklung / Zertifikat nicht verfügbar) return xml; });