Add arr upgrade webhook listener and setup — closes propagation window without manual arr config
This commit is contained in:
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ======================= Upgrade Webhook Listener (continuous) ================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Starts a standalone PHP HTTP server that receives Sonarr/Radarr/Lidarr
|
||||
# OnUpgrade webhooks and dispatches upgrade_webhook_handler.sh.
|
||||
#
|
||||
# Runs outside Unraid nginx — no session auth required. The shared secret in
|
||||
# the webhook URL is the only gate. Arrs on this host call:
|
||||
#
|
||||
# http://<HOST_LAN_IP>:<WEBHOOK_PORT>/webhook?key=<WEBHOOK_SECRET>
|
||||
#
|
||||
# Runs as a continuous script started by array_started.sh. Execs php -S which
|
||||
# replaces this process — the PID stays the same for array_started.sh's check.
|
||||
#
|
||||
# If WEBHOOK_SECRET is empty in master.conf: generates and saves one, then starts.
|
||||
# If WEBHOOK_PORT is 0: exits cleanly (disables the listener).
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ECOSYSTEM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
source "$ECOSYSTEM_ROOT/load_config.sh"
|
||||
|
||||
[[ "${WEBHOOK_PORT:-0}" -eq 0 ]] && {
|
||||
echo "[webhook] WEBHOOK_PORT=0 — listener disabled"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Auto-generate secret if not yet set ─────────────────────────────────────
|
||||
if [[ -z "${WEBHOOK_SECRET:-}" ]]; then
|
||||
GENERATED=$(openssl rand -hex 32)
|
||||
MASTER_CONF="$ECOSYSTEM_ROOT/Configurations/master.conf"
|
||||
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$GENERATED\"/" "$MASTER_CONF"
|
||||
WEBHOOK_SECRET="$GENERATED"
|
||||
echo "[webhook] Generated WEBHOOK_SECRET — run Tools/webhook_setup.sh to register in arrs"
|
||||
fi
|
||||
|
||||
mkdir -p /var/log/varaverk
|
||||
|
||||
exec node "$ECOSYSTEM_ROOT/Media/webhook_listener.js" \
|
||||
"$WEBHOOK_PORT" "$WEBHOOK_SECRET" "$ECOSYSTEM_ROOT" \
|
||||
>> /var/log/varaverk/upgrade_webhook.log 2>&1
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
// Standalone arr upgrade webhook listener.
|
||||
// Started by start_webhook_listener.sh via `node webhook_listener.js`.
|
||||
// Lives outside Unraid nginx — no session auth. Secret in URL is the only gate.
|
||||
//
|
||||
// Usage: node webhook_listener.js <port> <secret> <scripts_dir>
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const [,, port, secret, scriptsDir] = process.argv;
|
||||
|
||||
if (!port || !secret || !scriptsDir) {
|
||||
process.stderr.write('Usage: webhook_listener.js <port> <secret> <scripts_dir>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const LOG_FILE = '/var/log/varaverk/upgrade_webhook.log';
|
||||
const HANDLER = path.join(scriptsDir, 'Media', 'upgrade_webhook_handler.sh');
|
||||
|
||||
function log(msg) {
|
||||
const ts = new Date().toTimeString().slice(0, 8);
|
||||
const line = `[${ts}] ${msg}\n`;
|
||||
fs.appendFile(LOG_FILE, line, () => {});
|
||||
}
|
||||
|
||||
function send(res, code, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, `http://localhost`);
|
||||
const key = url.searchParams.get('key');
|
||||
|
||||
if (key !== secret) {
|
||||
send(res, 403, { ok: false, error: 'Forbidden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
send(res, 405, { ok: false, error: 'POST only' });
|
||||
return;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
let payload;
|
||||
try { payload = JSON.parse(body); }
|
||||
catch (_) { send(res, 400, { ok: false, error: 'Invalid JSON' }); return; }
|
||||
|
||||
const event = payload.eventType ?? '';
|
||||
const isUpgrade = payload.isUpgrade ?? false;
|
||||
|
||||
if (event === 'Test') {
|
||||
send(res, 200, { ok: true, message: 'Webhook connected — upgrade propagation active' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (event !== 'Download' || !isUpgrade) {
|
||||
send(res, 200, { ok: true, skipped: event });
|
||||
return;
|
||||
}
|
||||
|
||||
let arrType, itemPath;
|
||||
if (payload.series?.path) {
|
||||
arrType = 'sonarr';
|
||||
itemPath = payload.series.path;
|
||||
} else if (payload.movie?.folderPath) {
|
||||
arrType = 'radarr';
|
||||
itemPath = payload.movie.folderPath;
|
||||
} else if (payload.artist?.path) {
|
||||
arrType = 'lidarr';
|
||||
itemPath = payload.artist.path;
|
||||
} else {
|
||||
send(res, 400, { ok: false, error: 'Unrecognised payload structure' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!itemPath || !itemPath.startsWith('/') || itemPath.includes('..') || /[\x00\n\r]/.test(itemPath)) {
|
||||
send(res, 400, { ok: false, error: 'Unsafe path' });
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Upgrade: ${arrType} — ${path.basename(itemPath)}`);
|
||||
send(res, 200, { ok: true, arr: arrType, path: itemPath });
|
||||
|
||||
const cmd = `bash ${JSON.stringify(HANDLER)} ${JSON.stringify(arrType)} ${JSON.stringify(itemPath)}`;
|
||||
const out = fs.createWriteStream(LOG_FILE, { flags: 'a' });
|
||||
exec(cmd, { stdio: ['ignore', out, out] });
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(parseInt(port, 10), '0.0.0.0', () => {
|
||||
log(`Webhook listener started on port ${port}`);
|
||||
process.stdout.write(`[webhook] Listening on port ${port}\n`);
|
||||
});
|
||||
|
||||
server.on('error', err => {
|
||||
process.stderr.write(`[webhook] Server error: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user