const crypto = require("crypto"); async function createPlugin(ctx) { const tickets = new Map(); return { async execute(action, input) { if (action === "issueAccess") { return issueAccess(ctx, tickets, input || {}); } if (action === "verifyAccess") { return verifyAccess(tickets, input || {}); } if (action === "revokeOwner") { return revokeOwner(tickets, input || {}); } if (action === "serviceStart") { return controlService(ctx, true); } if (action === "serviceStop") { return controlService(ctx, false); } if (action === "ensureSdrPath") { return ensureSdrPath(ctx); } throw new Error(`Unknown action: ${action}`); }, async getStatus() { pruneExpired(tickets); return { activeTickets: tickets.size }; }, async health() { return { ok: true }; } }; } function issueAccess(ctx, tickets, input) { pruneExpired(tickets); const userId = String(input.userId || "").trim(); const ownerUserId = String(input.ownerUserId || "").trim(); if (!userId || !ownerUserId || userId !== ownerUserId) { throw new Error("OpenWebRX Zugriff nur fuer aktiven Besitzer"); } const ttlSec = Number(ctx.getSetting("ticketTtlSec", ctx.env.OPENWEBRX_TICKET_TTL_SEC || 3600)); const ttlExpiresAtMs = Date.now() + Math.max(10, ttlSec) * 1000; const stationEndsAtMs = Date.parse(String(input.stationEndsAt || "")); const expiresAtMs = Number.isFinite(stationEndsAtMs) ? Math.min(ttlExpiresAtMs, stationEndsAtMs) : ttlExpiresAtMs; const ticket = crypto.randomBytes(24).toString("base64url"); tickets.set(ticket, { userId, ownerUserId, createdAt: new Date().toISOString(), expiresAtMs }); const openWebRxPath = String(ctx.getSetting("upstreamPath", ctx.env.OPENWEBRX_PATH || "/openwebrx/")).trim() || "/openwebrx/"; const separator = openWebRxPath.includes("?") ? "&" : "?"; const iframeUrl = `${openWebRxPath}${separator}ticket=${encodeURIComponent(ticket)}`; return { ticket, expiresAt: new Date(expiresAtMs).toISOString(), iframeUrl, userId }; } function verifyAccess(tickets, input) { pruneExpired(tickets); const ticket = String(input.ticket || "").trim(); if (!ticket || !tickets.has(ticket)) { return { ok: false }; } const entry = tickets.get(ticket); if (!entry || entry.expiresAtMs <= Date.now()) { tickets.delete(ticket); return { ok: false }; } return { ok: true, userId: entry.userId, ownerUserId: entry.ownerUserId, expiresAt: new Date(entry.expiresAtMs).toISOString() }; } function revokeOwner(tickets, input) { const ownerUserId = String(input.ownerUserId || "").trim(); if (!ownerUserId) { return { ok: true, revoked: 0 }; } let revoked = 0; for (const [ticket, entry] of tickets.entries()) { if (entry.ownerUserId === ownerUserId) { tickets.delete(ticket); revoked += 1; } } return { ok: true, revoked }; } async function controlService(ctx, start) { const command = String(ctx.getSetting(start ? "startCommand" : "stopCommand", start ? (ctx.env.OPENWEBRX_START_CMD || "") : (ctx.env.OPENWEBRX_STOP_CMD || ""))).trim(); if (!command) { return { ok: true, skipped: true, message: "Kein OpenWebRX Service-Kommando gesetzt" }; } const simulate = typeof ctx.simulateHardware === "boolean" ? ctx.simulateHardware : !(process.platform === "linux" || String(ctx.env.ALLOW_NON_LINUX_CMDS || "false") === "true"); if (simulate) { return { ok: true, skipped: true, message: `OpenWebRX ${start ? "start" : "stop"} simuliert (${ctx.execMode || "dev"})` }; } const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.OPENWEBRX_CTRL_TIMEOUT_MS || 20000)); const result = await ctx.commandRunner(command, { timeoutMs }); if (!result.ok) { throw new Error(result.stderr || result.error || `OpenWebRX ${start ? "start" : "stop"} failed`); } return { ok: true, message: `OpenWebRX ${start ? "gestartet" : "gestoppt"}` }; } async function ensureSdrPath(ctx) { const command = String(ctx.getSetting("ensureSdrCommand", ctx.env.OPENWEBRX_ENSURE_SDR_CMD || "")).trim(); if (!command) { return { ok: true, skipped: true, message: "Kein OpenWebRX SDR-Pfad-Kommando gesetzt" }; } const simulate = typeof ctx.simulateHardware === "boolean" ? ctx.simulateHardware : !(process.platform === "linux" || String(ctx.env.ALLOW_NON_LINUX_CMDS || "false") === "true"); if (simulate) { return { ok: true, skipped: true, message: `OpenWebRX SDR-Pfad nur simuliert (${ctx.execMode || "dev"})` }; } const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.OPENWEBRX_CTRL_TIMEOUT_MS || 20000)); const result = await ctx.commandRunner(command, { timeoutMs }); if (!result.ok) { throw new Error(result.stderr || result.error || "OpenWebRX SDR-Pfad setzen fehlgeschlagen"); } return { ok: true, message: "OpenWebRX SDR-Pfad auf SDR gesetzt" }; } function pruneExpired(tickets) { const now = Date.now(); for (const [ticket, entry] of tickets.entries()) { if (!entry || entry.expiresAtMs <= now) { tickets.delete(ticket); } } } module.exports = { createPlugin };