initialize generic rms-software repository
Add the reusable RMS core application (server, web UI, plugins, tests, tools) with generic defaults, GPL licensing, and maintainer context documentation so deployments can consume this repo as software source independent of station-specific overlays.
This commit is contained in:
160
plugins/rms.openwebrx.guard/index.js
Normal file
160
plugins/rms.openwebrx.guard/index.js
Normal file
@@ -0,0 +1,160 @@
|
||||
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/";
|
||||
return {
|
||||
ticket,
|
||||
expiresAt: new Date(expiresAtMs).toISOString(),
|
||||
iframeUrl: openWebRxPath,
|
||||
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
|
||||
};
|
||||
24
plugins/rms.openwebrx.guard/manifest.json
Normal file
24
plugins/rms.openwebrx.guard/manifest.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "rms.openwebrx.guard",
|
||||
"name": "OpenWebRX Guard",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0",
|
||||
"capabilities": [
|
||||
"openwebrx.access.issue",
|
||||
"openwebrx.access.verify",
|
||||
"openwebrx.service.control"
|
||||
],
|
||||
"settingsSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticketTtlSec": { "type": "integer", "minimum": 10, "maximum": 3600 },
|
||||
"upstreamPath": { "type": "string" },
|
||||
"ensureSdrCommand": { "type": "string" },
|
||||
"startCommand": { "type": "string" },
|
||||
"stopCommand": { "type": "string" },
|
||||
"timeoutMs": { "type": "integer", "minimum": 1000 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"uiControls": []
|
||||
}
|
||||
Reference in New Issue
Block a user