feat(host-agent): Auto-Update fuer den Android-Agent ueber den RVS
Agent fragt beim Verbinden und alle ~6 h per agent_update_check nach; der RVS antwortet mit agent_update_available, wenn in rvs/updates/agent/ eine neuere APK liegt (eigener Ordner — die Cockpit-App bekommt sie so nicht angeboten). Download im Hintergrund per HTTP (/update/agent/latest.apk, Raum-Token im Header, 3 Versuche, Groessen-Check), dann Benachrichtigung "Tippen zum Installieren" + Knopf in der App. Installieren per FileProvider und Android-Installer (REQUEST_INSTALL_PACKAGES; ein Tippen bleibt Pflicht). host_hello meldet jetzt die Version. release_agent.sh kopiert die APK auf den RVS, wenn RVS_UPDATE_HOST gesetzt ist. RVS: APK-Auslieferung als serveApk() fuer beide Apps zusammengefasst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+67
-40
@@ -11,6 +11,9 @@ const path = require("path");
|
||||
const PORT = parseInt(process.env.PORT || "3000", 10);
|
||||
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
|
||||
const UPDATES_DIR = process.env.UPDATES_DIR || "/updates";
|
||||
// Host-Agent (Android) hat einen EIGENEN Ordner — sonst bekaeme die Cockpit-App
|
||||
// die Agent-APK angeboten (die Versions-Erkennung schaut nur auf den Dateinamen).
|
||||
const AGENT_UPDATES_DIR = path.join(UPDATES_DIR, "agent");
|
||||
// HTTPS statt HTTP auf PORT — fuer die Strecke externer Reverse-Proxy → RVS.
|
||||
// Das oeffentliche Zertifikat hat der Proxy; hier reicht ein selbstsigniertes
|
||||
// (wird beim ersten Start erzeugt). NICHT zusammen mit dem Caddy im Stack
|
||||
@@ -148,6 +151,8 @@ const ALLOWED_TYPES = new Set([
|
||||
// Wer ist im Raum? client_hello: Client stellt sich vor (bleibt beim RVS);
|
||||
// room_clients_query → room_clients: Liste NUR des eigenen Raums.
|
||||
"client_hello", "room_clients_query", "room_clients",
|
||||
// Host-Agent (Android): Update-Pruefung, beantwortet der RVS selbst
|
||||
"agent_update_check", "agent_update_available",
|
||||
"node_stats", "node_stats_stream_start", "node_stats_stream_stop",
|
||||
"node_stats_history_request", "node_stats_history",
|
||||
"node_stats_reset", "node_stats_reset_done",
|
||||
@@ -403,48 +408,20 @@ function handleHttpRequest(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
// App-Update als normaler HTTP-Download (statt eines riesigen WS-Blocks):
|
||||
// Content-Length fuer die Fortschrittsanzeige, Range zum Fortsetzen.
|
||||
// Nur fuer Geraete mit gueltigem Raum-Token (Header X-RVS-Token).
|
||||
if ((req.method === "GET" || req.method === "HEAD") && pathname === "/update/latest.apk") {
|
||||
// Updates als normaler HTTP-Download (statt eines riesigen WS-Blocks):
|
||||
// Cockpit-App unter /update/latest.apk, Host-Agent (Android) unter
|
||||
// /update/agent/latest.apk. Nur mit gueltigem Raum-Token (X-RVS-Token).
|
||||
if ((req.method === "GET" || req.method === "HEAD")
|
||||
&& (pathname === "/update/latest.apk" || pathname === "/update/agent/latest.apk")) {
|
||||
const tok = String(req.headers["x-rvs-token"] || "");
|
||||
if (!tok || !rooms.has(tok)) {
|
||||
res.writeHead(403, { "Content-Type": "text/plain" });
|
||||
res.end("Forbidden\n");
|
||||
return;
|
||||
}
|
||||
const apk = getLatestAPK();
|
||||
if (!apk) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Keine APK verfuegbar\n");
|
||||
return;
|
||||
}
|
||||
const size = fs.statSync(apk.path).size;
|
||||
const headers = {
|
||||
"Content-Type": "application/vnd.android.package-archive",
|
||||
"Content-Disposition": `attachment; filename="ARIA-v${apk.version}.apk"`,
|
||||
"Accept-Ranges": "bytes",
|
||||
"Cache-Control": "no-store",
|
||||
"X-APK-Version": apk.version,
|
||||
};
|
||||
let start = 0, end = size - 1, status = 200;
|
||||
const m = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range || ""));
|
||||
if (m && (m[1] || m[2])) {
|
||||
if (m[1]) { start = parseInt(m[1], 10); if (m[2]) end = Math.min(parseInt(m[2], 10), size - 1); }
|
||||
else { start = Math.max(0, size - parseInt(m[2], 10)); }
|
||||
if (start > end || start >= size) {
|
||||
res.writeHead(416, { "Content-Range": `bytes */${size}` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
status = 206;
|
||||
headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
|
||||
}
|
||||
headers["Content-Length"] = end - start + 1;
|
||||
res.writeHead(status, headers);
|
||||
if (req.method === "HEAD") { res.end(); return; }
|
||||
log(`APK-Download (HTTP) v${apk.version}${status === 206 ? ` ab Byte ${start}` : ""}`);
|
||||
fs.createReadStream(apk.path, { start, end }).pipe(res);
|
||||
const isAgent = pathname.startsWith("/update/agent/");
|
||||
serveApk(req, res, isAgent ? getLatestAPK(AGENT_UPDATES_DIR) : getLatestAPK(),
|
||||
isAgent ? "aria-host-agent" : "ARIA");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -641,6 +618,21 @@ function registerClient(ws, token) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Host-Agent (Android) fragt nach einem Update — direkt antworten
|
||||
if (msg.type === "agent_update_check") {
|
||||
const have = String(msg.payload?.version || "0.0.0.0");
|
||||
const apk = getLatestAPK(AGENT_UPDATES_DIR);
|
||||
if (apk && compareVersions(apk.version, have) > 0) {
|
||||
ws.send(JSON.stringify({
|
||||
type: "agent_update_available",
|
||||
payload: { version: apk.version, downloadUrl: "/update/agent/latest.apk",
|
||||
size: fs.statSync(apk.path).size },
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Raum-Diagnose: direkt an den anfragenden Client antworten (nicht relay'en).
|
||||
// Zeigt ALLE Raeume mit token8 (wie im Log), einem laengeren Fingerprint und
|
||||
// der Token-Laenge — so werden Prefix-Kollisionen (2 Raeume, gleicher 8-Zeichen-
|
||||
@@ -781,15 +773,50 @@ wss.on("close", () => {
|
||||
|
||||
let latestVersion = null;
|
||||
|
||||
function getLatestAPK() {
|
||||
/** APK per HTTP ausliefern: Content-Length (Fortschritt), Range (Fortsetzen). */
|
||||
function serveApk(req, res, apk, label) {
|
||||
if (!apk) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Keine APK verfuegbar\n");
|
||||
return;
|
||||
}
|
||||
const size = fs.statSync(apk.path).size;
|
||||
const headers = {
|
||||
"Content-Type": "application/vnd.android.package-archive",
|
||||
"Content-Disposition": `attachment; filename="${label}-v${apk.version}.apk"`,
|
||||
"Accept-Ranges": "bytes",
|
||||
"Cache-Control": "no-store",
|
||||
"X-APK-Version": apk.version,
|
||||
};
|
||||
let start = 0, end = size - 1, status = 200;
|
||||
const m = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range || ""));
|
||||
if (m && (m[1] || m[2])) {
|
||||
if (m[1]) { start = parseInt(m[1], 10); if (m[2]) end = Math.min(parseInt(m[2], 10), size - 1); }
|
||||
else { start = Math.max(0, size - parseInt(m[2], 10)); }
|
||||
if (start > end || start >= size) {
|
||||
res.writeHead(416, { "Content-Range": `bytes */${size}` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
status = 206;
|
||||
headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
|
||||
}
|
||||
headers["Content-Length"] = end - start + 1;
|
||||
res.writeHead(status, headers);
|
||||
if (req.method === "HEAD") { res.end(); return; }
|
||||
log(`APK-Download (HTTP) ${label} v${apk.version}${status === 206 ? ` ab Byte ${start}` : ""}`);
|
||||
fs.createReadStream(apk.path, { start, end }).pipe(res);
|
||||
}
|
||||
|
||||
function getLatestAPK(dir = UPDATES_DIR) {
|
||||
try {
|
||||
if (!fs.existsSync(UPDATES_DIR)) return null;
|
||||
const files = fs.readdirSync(UPDATES_DIR)
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir)
|
||||
.filter(f => f.endsWith(".apk"))
|
||||
.map(f => {
|
||||
// ARIA-v0.0.2.3.apk oder ARIA-Cockpit-release.apk
|
||||
const match = f.match(/(\d+(?:\.\d+){2,})/);
|
||||
return { file: f, path: path.join(UPDATES_DIR, f), version: match ? match[1] : null };
|
||||
return { file: f, path: path.join(dir, f), version: match ? match[1] : null };
|
||||
})
|
||||
.filter(f => f.version)
|
||||
.sort((a, b) => compareVersions(b.version, a.version)); // Neueste zuerst
|
||||
|
||||
Reference in New Issue
Block a user