136 lines
4.7 KiB
TypeScript
136 lines
4.7 KiB
TypeScript
// 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: <uid>.dns-staging.rebreak.org | Prod: <uid>.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 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadDisplayName</key>
|
|
<string>ReBreak Schutz</string>
|
|
<key>PayloadDescription</key>
|
|
<string>${description}</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>org.rebreak.protection</string>
|
|
<key>PayloadUUID</key>
|
|
<string>${profileUUID}</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>PayloadDisplayName</key>
|
|
<string>ReBreak Schutz</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>org.rebreak.protection.payload</string>
|
|
<key>PayloadUUID</key>
|
|
<string>${payloadUUID}</string>
|
|
<key>PayloadType</key>
|
|
<string>com.apple.dnsSettings.managed</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>DNSSettings</key>
|
|
<dict>
|
|
<key>DNSProtocol</key>
|
|
<string>HTTPS</string>
|
|
<key>ServerURL</key>
|
|
<string>${dnsServerUrl}</string>
|
|
</dict>
|
|
</dict>
|
|
</array>
|
|
</dict>
|
|
</plist>`;
|
|
|
|
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;
|
|
});
|