Carry the CSRF token on fetch requests and put mutations behind POST

Unraid already enforces CSRF on every POST via auto_prepend, but its
injector is jQuery-only — the plugin's native fetch() calls carried no
token and were being terminated before the endpoint ran, silently,
because csrf_terminate exits with an empty body that r.json() swallows.
This commit is contained in:
Gmer4Lfe
2026-08-02 10:28:53 -04:00
parent 987313e7dc
commit c34224effa
16 changed files with 198 additions and 67 deletions
+54 -24
View File
@@ -114,34 +114,64 @@ be the reason a page fails to load.
## ━━━ WHAT GUARDS THE API LAYER ━━━ ## ━━━ WHAT GUARDS THE API LAYER ━━━
Every endpoint under `api/` is protected by exactly one thing: **the Unraid WebGUI session.** Two layers, both supplied by the platform, neither implemented in this plugin.
Anything that can reach `/plugins/varaverk/api/*.php` with a valid session can do everything
this plugin can do — stop the array, power off the host, write `master.conf` and push it to
every partner, create an lldap user, write and schedule a root-run script.
That is the same trust level as the rest of the WebGUI, and it is the intended model. It is **1. nginx session auth.** `/etc/nginx/nginx.conf` applies `satisfy any; allow unix:; deny all;
written down here because two things about it are easy to assume and wrong. auth_request /auth-request.php;` to everything it serves, `/plugins/` included. A request
without a valid WebGUI session never reaches PHP — it is redirected to the login page.
**No endpoint validates a CSRF token.** `pages/partnership.php` and `pages/scheduler.php` send **2. Unraid's CSRF prepend.** `/etc/php.ini` sets
Unraid's `csrf_token` with their POSTs, which reads like the token is checked somewhere. It is `auto_prepend_file=/usr/local/emhttp/webGui/include/local_prepend.php`, which runs before the
not — no file in `api/` looks at it. A request that arrives with a logged-in session cookie is first line of any endpoint and, for **every POST**, requires a valid token as either a
honoured whatever caused the browser to send it. Adding validation is a worthwhile hardening `csrf_token` body field or an `X-CSRF-Token` header. On a mismatch it logs to syslog and
pass, but it is a real change: every caller has to send the token before any endpoint requires `exit`s. php-fpm inherits this — there is no pool override in `/etc/php-fpm.d/www.conf`.
it, or the UI breaks silently on whichever page was missed. Do it deliberately, in one pass,
with the pages open — not opportunistically while touching one endpoint.
**`api/webhook.php` is the exception that authenticates nothing at all.** It exists to receive Anything that clears both can do everything this plugin can do: stop the array, power off the
Sonarr/Radarr/Lidarr download events, which arrive from a container rather than a browser. host, write `master.conf` and push it to every partner, create an lldap user, write and
`master.conf` carries a `WEBHOOK_SECRET` and the standalone Node listener on `WEBHOOK_PORT` schedule a root-run script. That is the same trust level as the rest of the WebGUI, and it is
validates it — this WebGUI-hosted path does not. Injection is not the risk (the path is the intended model.
validated and escaped); triggering work is. Closing it means adding a secret check here **and**
updating the webhook URL in each arr's settings, in that order.
Two further endpoints are worth knowing about because they read wider than the rest: ### The two consequences that actually matter
`api/api_test.php` returns an API key prefix and the live GraphQL schema, and
`api/import_script.php`'s browse action and `api/manual_sync.php`'s browse actions list **GET is not covered.** The prepend inspects POSTs only. So any action that *changes something*
directory names anywhere on either host. All three are read-only and none return file contents, must be POST — over GET it would run with no token check at all. Four actions were GET and have
but they are the ones to look at first if the session boundary ever moves. been converted: `create_api_key.php`, `arrs.php?action=refresh_remote`, `cert.php?action=run`
and `setup.php?action=ssh_generate`. **When adding an endpoint the rule is simply: if it changes
state, it is POST.** Read-only GETs (`monitor`, `board`, `checklist`, the browse pickers,
`api_test.php`) are bounded by the session alone, which is correct for reads.
**The webGUI's token injector is jQuery-only.** `$.ajaxPrefilter` in dynamix's
`HeadInlineJS.php` appends `csrf_token` to jQuery POSTs. Varaverk's pages use native `fetch()`,
which it does not touch — so a `fetch()` POST carries no token and the prepend kills it
silently: `csrf_terminate()` exits with an empty body, `r.json()` throws on the empty response,
and the page's own `.catch()` swallows it. There is no error anywhere except a syslog line.
`Varaverk.page` therefore installs a small `window.fetch` shim, inline and above the tab
content, that attaches `X-CSRF-Token` to same-origin `/plugins/varaverk/` requests. The header
form is deliberate: it works for `FormData`, `URLSearchParams` and raw JSON bodies alike, so no
call site has to know about it and a new one cannot forget it. A cross-origin page cannot set
a custom header without a preflight it will fail, which is what makes it a real defence rather
than a formality.
**Do not construct POSTs outside `fetch()`** — a raw `XMLHttpRequest` or a generated form —
without adding the token yourself; the shim only wraps `fetch`. A plain `<form method="POST">`
is already covered by a different platform mechanism: dynamix's `BodyInlineJS.php` appends a
hidden `csrf_token` input to every form on the page, which is what makes `VaraverkSettings.page`
work without any of this.
### api/webhook.php is dead code
It receives arr download events — but an arr has no WebGUI session and no CSRF token, so it is
blocked by *both* layers above and cannot be called at all. The live path is
`Arrs_Stack/webhook_listener.js` on `WEBHOOK_PORT`, running outside nginx entirely and
validating `WEBHOOK_SECRET` itself. `api/webhook.php` cannot be made to work by adding a secret
check — it would have to be served from outside `/plugins/` first. Removing it is the other
reasonable option.
Two endpoints read wider than the rest and are worth knowing about if the session boundary ever
moves: `api/api_test.php` returns an API key prefix and the live GraphQL schema, and the browse
actions in `api/import_script.php` and `api/manual_sync.php` list directory names anywhere on
either host. All are read-only and none return file contents.
--- ---
+48
View File
@@ -8,7 +8,55 @@ $docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$pluginDir = "$docroot/plugins/$plugin"; $pluginDir = "$docroot/plugins/$plugin";
require_once "$pluginDir/include/config.php"; require_once "$pluginDir/include/config.php";
?>
<script>
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// CSRF token propagation — must run before any page JS.
//
// Unraid enforces CSRF centrally: /etc/php.ini sets auto_prepend_file to webGui's
// local_prepend.php, which terminates *every* POST that does not carry a valid token, before a
// single line of endpoint code runs. It accepts the token as a `csrf_token` POST field or as an
// X-CSRF-Token header.
//
// The webGUI's own injector is jQuery-only ($.ajaxPrefilter in HeadInlineJS.php). Varaverk's
// pages use native fetch(), which that prefilter does not touch — so without this shim every
// mutating request in the plugin is silently killed by the platform. Silently, because
// csrf_terminate() exits with no body: the fetch resolves, r.json() throws on the empty
// response, and the page's own .catch() swallows it.
//
// Setting the header rather than appending a body field is deliberate. It works identically for
// FormData, URLSearchParams and raw JSON bodies, so no call site has to know about it and a new
// endpoint cannot forget to include it. The header is only ever attached to same-origin
// Varaverk URLs; a cross-origin page cannot set a custom header without a preflight it will
// fail, which is precisely what makes this a CSRF defence rather than a formality.
//
// Inline, and above the tab content, because pages/*.php carry their own inline fetch calls and
// some fire on load — an external script could not be guaranteed to install first.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
(function () {
if (window.__vvCsrfInstalled) return;
window.__vvCsrfInstalled = true;
var nativeFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
var url = (typeof input === 'string') ? input : (input && input.url) || '';
if (url.indexOf('/plugins/varaverk/') !== -1 &&
typeof csrf_token !== 'undefined' && csrf_token) {
init = init || {};
var headers = new Headers(init.headers || (typeof input === 'object' && input.headers) || {});
if (!headers.has('X-CSRF-Token')) headers.set('X-CSRF-Token', csrf_token);
init = Object.assign({}, init, { headers: headers });
}
return nativeFetch(input, init);
};
})();
</script>
<?php
// First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing // First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing
$_master = vv_read_conf_raw('master.conf'); $_master = vv_read_conf_raw('master.conf');
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m); preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
+2 -2
View File
@@ -59,8 +59,8 @@
// A key prefix, the cached API debug log, and the live schema are all visible to anyone // A key prefix, the cached API debug log, and the live schema are all visible to anyone
// with a WebGUI session. That is acceptable for a diagnostic reachable only by typing // with a WebGUI session. That is acceptable for a diagnostic reachable only by typing
// its URL, but it is the reason it is not linked from any page and should not be // its URL, but it is the reason it is not linked from any page and should not be
// wrapped in one. See the CSRF note in README-unraid.md for the session-only guard this // wrapped in one. It is a pure GET read, so Unraid's POST-only CSRF guard does not
// shares with the rest of the api layer. // apply to it — the WebGUI session is its whole boundary. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET, no parameters // GET, no parameters
+12 -3
View File
@@ -50,7 +50,8 @@
// //
// REQUEST // REQUEST
// GET cached (300s) full payload // GET cached (300s) full payload
// GET ?action=refresh_remote&host=host<n> re-run the partner cache writer for one host // POST action=refresh_remote host=host<n> re-run the partner cache writer for one host
// (POST, so Unraid's CSRF guard applies)
// //
// RESPONSE // RESPONSE
// normal vv_arrs_all() verbatim — local node live, remote nodes cached with cache_age // normal vv_arrs_all() verbatim — local node live, remote nodes cached with cache_age
@@ -70,9 +71,17 @@ require_once dirname(__DIR__) . '/include/config.php';
define('VV_ARRS_REFRESH_TIMEOUT', 120); define('VV_ARRS_REFRESH_TIMEOUT', 120);
// ── Manual remote refresh — runs remote_arr_cache_writer for one host ───────── // ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
$_action = trim($_GET['action'] ?? ''); // POST only. The refresh executes a script, and Unraid's CSRF prepend validates POSTs while
// ignoring GETs entirely — so reaching this over GET would mean running it with no token
// check. A GET naming the action is refused rather than falling through to the cached read,
// so a stale caller fails visibly instead of silently appearing to succeed.
if (trim($_GET['action'] ?? '') === 'refresh_remote') {
http_response_code(405);
echo json_encode(['ok' => false, 'error' => 'POST only']); exit;
}
$_action = ($_SERVER['REQUEST_METHOD'] === 'POST') ? trim($_POST['action'] ?? '') : '';
if ($_action === 'refresh_remote') { if ($_action === 'refresh_remote') {
$host = strtolower(trim($_GET['host'] ?? '')); $host = strtolower(trim($_POST['host'] ?? ''));
if (!preg_match('/^host\d+$/', $host)) { if (!preg_match('/^host\d+$/', $host)) {
echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit; echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
} }
+6 -4
View File
@@ -58,10 +58,12 @@
// authoring a fresh config — a config it created would carry no rules and a default // authoring a fresh config — a config it created would carry no rules and a default
// policy, which is an accidental open door. // policy, which is an accidental open door.
// //
// Known gap: no CSRF token is validated on the POST actions. // The POST actions are CSRF-guarded by the platform, not by this file.
// The endpoint is guarded by the Unraid WebGUI session alone, and this is the highest- // Unraid's auto_prepend (webGui/local_prepend.php) validates a token on every POST and
// value endpoint in the plugin to reach — it can create a user and open a proxy host. // terminates the request before any code here runs. That guard is the reason every
// Shared with the rest of the api layer; see the CSRF note in README-unraid.md. // mutating action lives in the POST arm — this is the highest-value endpoint in the
// plugin to reach, since it can create a user and open a proxy host. The five GET
// actions are reads and are deliberately outside it. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET ?action=npm_proxies | npm_certs | lldap_users | lldap_groups | authelia_rules // GET ?action=npm_proxies | npm_certs | lldap_users | lldap_groups | authelia_rules
+13 -4
View File
@@ -67,15 +67,17 @@
// response is treated as a certificate list — so an unreachable NPM is reported as such // response is treated as a certificate list — so an unreachable NPM is reported as such
// instead of rendering as zero certificates. // instead of rendering as zero certificates.
// //
// Accepted: the run action is reachable over GET. // The run action is POST only, which is what places it behind Unraid's CSRF guard.
// It re-runs a read-only monitor and writes only its own cache, so repeating it is // The platform prepend validates the token on every POST and inspects no GET at all, so
// harmless. Guarded by the Unraid WebGUI session; see the CSRF note in README-unraid.md. // an action that executes a script must not be reachable by GET. The three read actions
// stay GET-reachable because they change nothing.
// //
// REQUEST // REQUEST
// GET cached status, or the configured domains when no cache exists // GET cached status, or the configured domains when no cache exists
// GET|POST ?action=npm live certificate list from NPM's API // GET|POST ?action=npm live certificate list from NPM's API
// GET|POST ?action=domains configured domains and thresholds, no checks run // GET|POST ?action=domains configured domains and thresholds, no checks run
// GET|POST ?action=run re-run cert_monitor.sh, then return its fresh cache // POST action=run re-run cert_monitor.sh, then return its fresh cache
// (POST, so Unraid's CSRF guard applies)
// //
// RESPONSE // RESPONSE
// default {"ok":true,"checked_at","host","warn_days","crit_days","domains":[…]} // default {"ok":true,"checked_at","host","warn_days","crit_days","domains":[…]}
@@ -167,6 +169,13 @@ if ($action === 'domains') {
// ── Run cert_monitor.sh now ─────────────────────────────────────────────────── // ── Run cert_monitor.sh now ───────────────────────────────────────────────────
if ($action === 'run') { if ($action === 'run') {
// POST only. This executes a script, and Unraid's CSRF prepend validates POSTs while
// ignoring GETs entirely — over GET it would run with no token check at all.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$script = SCRIPTS_DIR . '/Monitors/cert_monitor.sh'; $script = SCRIPTS_DIR . '/Monitors/cert_monitor.sh';
if (!file_exists($script)) { if (!file_exists($script)) {
echo json_encode(['ok' => false, 'error' => 'cert_monitor.sh not found']); echo json_encode(['ok' => false, 'error' => 'cert_monitor.sh not found']);
+14 -6
View File
@@ -46,14 +46,14 @@
// Cache-Control: no-store, no-cache. A cached provisioning response would report an old // Cache-Control: no-store, no-cache. A cached provisioning response would report an old
// key preview after a genuine renewal — the one moment the preview matters. // key preview after a genuine renewal — the one moment the preview matters.
// //
// Known gap: this is a state-changing action served over GET. // POST only, which is what places it behind Unraid's CSRF guard.
// Both callers (pages/setup.php, pages/partnership.php) fetch it, and POST bodies are // The platform prepend validates the token on every POST and inspects no GET at all, so
// unreliable on this nginx/PHP setup. It is guarded by the Unraid WebGUI session rather // a state-changing action reachable by GET is a state-changing action with no CSRF
// than by method or token. Documented rather than silently accepted — see the CSRF note // protection. This was previously a GET; the callers now POST and the token travels as
// in README-unraid.md. // an X-CSRF-Token header set by the shim in Varaverk.page. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET, no parameters (the target host is the local host, by construction) // POST, no parameters (the target host is the local host, by construction)
// //
// RESPONSE // RESPONSE
// {"ok":true,"key_preview":"abcd1234...wxyz"} or "registered" when the key is not readable // {"ok":true,"key_preview":"abcd1234...wxyz"} or "registered" when the key is not readable
@@ -67,6 +67,14 @@ header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache'); header('Cache-Control: no-store, no-cache');
require_once dirname(__DIR__) . '/include/config.php'; require_once dirname(__DIR__) . '/include/config.php';
// POST is what puts this behind Unraid's CSRF guard — the platform prepend validates every
// POST and ignores every GET, so a state-changing action must not be reachable by GET.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$host = vv_detect_host(); $host = vv_detect_host();
if (!preg_match('/^host\d+$/', $host)) { if (!preg_match('/^host\d+$/', $host)) {
echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']); echo json_encode(['ok' => false, 'error' => 'Cannot detect local host']);
+3 -2
View File
@@ -67,8 +67,9 @@
// //
// Accepted exposure: the browser can list any directory on the host. // Accepted exposure: the browser can list any directory on the host.
// It returns directory names and .sh filenames only — no file contents, and nothing // It returns directory names and .sh filenames only — no file contents, and nothing
// outside those two types. That is the minimum a file picker rooted at / can do, and it // outside those two types. That is the minimum a file picker rooted at / can do. browse
// is guarded by the Unraid WebGUI session; see the CSRF note in README-unraid.md. // is a GET read, so the WebGUI session is its whole boundary; the import itself is POST
// and is CSRF-guarded by Unraid's auto_prepend. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET ?action=browse&path=/absolute/dir list subdirectories and .sh files // GET ?action=browse&path=/absolute/dir list subdirectories and .sh files
+3 -2
View File
@@ -93,8 +93,9 @@
// output in /tmp indefinitely. /tmp is tmpfs, so an abandoned one clears at reboot anyway. // output in /tmp indefinitely. /tmp is tmpfs, so an abandoned one clears at reboot anyway.
// //
// Accepted exposure: browse can list any directory on either host. // Accepted exposure: browse can list any directory on either host.
// Directory names only — no file contents, no file names. Guarded by the Unraid WebGUI // Directory names only — no file contents, no file names. browse and poll are GET reads
// session; see the CSRF note in README-unraid.md. // bounded by the WebGUI session; run and stop are POST and are CSRF-guarded by Unraid's
// auto_prepend before any code here runs. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET ?action=hosts partners, Tailscale state, key presence // GET ?action=hosts partners, Tailscale state, key presence
+2 -1
View File
@@ -43,7 +43,8 @@
// Credential fields are returned as they appear in the conf. // Credential fields are returned as they appear in the conf.
// The partnership sections carry SSH key paths and API keys, and this endpoint returns // The partnership sections carry SSH key paths and API keys, and this endpoint returns
// them for editing. That is the same exposure the raw conf editor has, over the same // them for editing. That is the same exposure the raw conf editor has, over the same
// WebGUI session; see the CSRF note in README-unraid.md. // WebGUI session. This is a GET read, so Unraid's POST-only CSRF guard does not apply —
// the session is its whole boundary. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET, no parameters // GET, no parameters
+3 -2
View File
@@ -54,8 +54,9 @@
// //
// Accepted by design: this endpoint writes an executable root-run script from a browser. // Accepted by design: this endpoint writes an executable root-run script from a browser.
// That is the entire feature, and it is why it is confined to a directory outside the // That is the entire feature, and it is why it is confined to a directory outside the
// repo with a flat namespace and a strict name pattern. It is guarded by the Unraid // repo with a flat namespace and a strict name pattern. The save and delete paths are
// WebGUI session; see the CSRF note in README-unraid.md. // POST, so Unraid's auto_prepend validates a CSRF token before any of this runs; the
// WebGUI session is the outer boundary. See README-unraid.md.
// //
// REQUEST // REQUEST
// GET ?id=Custom/<name>.sh read (empty content when absent) // GET ?id=Custom/<name>.sh read (empty content when absent)
+15 -4
View File
@@ -82,13 +82,17 @@
// The SCP target is the local conf path, composed here — no part of the request names a // The SCP target is the local conf path, composed here — no part of the request names a
// destination file. // destination file.
// //
// Accepted: this endpoint writes credentials and runs setup scripts as root. // Every action that changes anything is POST only, which is what places them behind
// It is the setup wizard; that is its function. It is guarded by the Unraid WebGUI // Unraid's CSRF guard.
// session; see the CSRF note in README-unraid.md. // The platform prepend validates the token on every POST and inspects no GET at all.
// Only `detect` remains GET-reachable, and it is a pure read. This endpoint writes
// credentials and runs setup scripts as root — that is its function, and it is why the
// method boundary is the one that matters here.
// //
// REQUEST // REQUEST
// GET ?action=detect hostname, Unraid version, boot transport, suggested mode // GET ?action=detect hostname, Unraid version, boot transport, suggested mode
// GET|POST action=ssh_generate generate the local keypair, return the public key // POST action=ssh_generate generate the local keypair, return the public key
// (POST, so Unraid's CSRF guard applies)
// POST action=populate run conf_populate.sh --no-push // POST action=populate run conf_populate.sh --no-push
// POST action=pull partner: SCP master.conf from HOST1, create host conf // POST action=pull partner: SCP master.conf from HOST1, create host conf
// [my_slot] [my_hostname] [host1_hostname] // [my_slot] [my_hostname] [host1_hostname]
@@ -149,6 +153,13 @@ if ($action === 'detect') {
// ── GET/POST: generate local SSH keypair ────────────────────────────────────────────────────── // ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
if ($action === 'ssh_generate') { if ($action === 'ssh_generate') {
// POST only. This generates a keypair, and Unraid's CSRF prepend validates POSTs while
// ignoring GETs entirely — over GET it would run with no token check at all.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh'; $script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
if (!file_exists($script)) { if (!file_exists($script)) {
echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']); echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
+16 -9
View File
@@ -64,15 +64,22 @@
// Output is appended to a dedicated log with stdin detached, so a backgrounded handler // Output is appended to a dedicated log with stdin detached, so a backgrounded handler
// cannot hold the request's file descriptors open. // cannot hold the request's file descriptors open.
// //
// Known gap: this endpoint authenticates nothing. // UNREACHABLE — this endpoint cannot currently be called by an arr at all.
// master.conf carries WEBHOOK_SECRET, and the standalone Node listener on WEBHOOK_PORT // Two platform layers stand in front of it, and an arr satisfies neither. nginx applies
// validates it — this WebGUI-hosted path does not. Anyone who can reach the URL can // `auth_request` to everything under /plugins/, so a request without a WebGUI session is
// make it run the handler against any absolute path that passes validation. Injection // redirected to the login page. And Unraid's auto_prepend terminates every POST that
// is not the risk (the path is escaped); triggering work is. Left as-is deliberately // does not carry a valid CSRF token — which an arr has no way to obtain.
// rather than fixed in passing: adding a secret check here would break whichever arr //
// instances are currently configured against this URL, and that is a change to make // The live path is the standalone Node listener, Arrs_Stack/webhook_listener.js, running
// with the arr configs open, not as part of a documentation pass. See also the CSRF // on WEBHOOK_PORT outside nginx entirely and validating WEBHOOK_SECRET itself. That is
// note in README-unraid.md. // what the arrs are pointed at, and it is why it exists.
//
// This file is therefore dead code kept for reference, not a second live entry point.
// Nothing here authenticates, because nothing here can be reached. Do not "fix" it by
// adding a secret check and re-pointing an arr at it — it would still be blocked by the
// two layers above. If it is ever wanted as a real entry point it needs to be served
// from outside /plugins/, at which point it needs WEBHOOK_SECRET validation first.
// Removing it outright is the other reasonable option. See README-unraid.md.
// //
// REQUEST // REQUEST
// POST <arr webhook JSON body> // POST <arr webhook JSON body>
+4 -1
View File
@@ -435,7 +435,10 @@ setInterval(vvArrsLoad, 60000);
function vvArrsRefreshRemote(host) { function vvArrsRefreshRemote(host) {
const btn = document.getElementById('vv-arr-rfsh-' + host); const btn = document.getElementById('vv-arr-rfsh-' + host);
if (btn) { btn.disabled = true; btn.textContent = '↻…'; } if (btn) { btn.disabled = true; btn.textContent = '↻…'; }
fetch(`/plugins/varaverk/api/arrs.php?action=refresh_remote&host=${host}&_=` + Date.now()) const fd = new FormData();
fd.append('action', 'refresh_remote');
fd.append('host', host);
fetch('/plugins/varaverk/api/arrs.php', { method: 'POST', body: fd })
.then(r => r.json()) .then(r => r.json())
.then(d => { .then(d => {
if (btn) { btn.disabled = false; btn.textContent = d.ok ? '↻' : '✗'; } if (btn) { btn.disabled = false; btn.textContent = d.ok ? '↻' : '✗'; }
+1 -1
View File
@@ -202,7 +202,7 @@ function vvApiKey(btn) {
const origText = btn.textContent; const origText = btn.textContent;
btn.disabled = true; btn.disabled = true;
btn.textContent = '⟳ Working…'; btn.textContent = '⟳ Working…';
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now()) fetch('/plugins/varaverk/api/create_api_key.php', { method: 'POST' })
.then(r => { .then(r => {
if (!r.ok || r.status === 0) throw new Error('HTTP ' + r.status + ' ' + r.statusText); if (!r.ok || r.status === 0) throw new Error('HTTP ' + r.status + ' ' + r.statusText);
return r.text(); return r.text();
+1 -1
View File
@@ -397,7 +397,7 @@ function vvPullMaster(btn) {
function vvCreateKey(btn) { function vvCreateKey(btn) {
const status = document.getElementById('vv-key-status'); const status = document.getElementById('vv-key-status');
btn.disabled = true; btn.textContent = '⟳ Creating…'; btn.disabled = true; btn.textContent = '⟳ Creating…';
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now()) fetch('/plugins/varaverk/api/create_api_key.php', { method: 'POST' })
.then(r => r.json()).then(d => { .then(r => r.json()).then(d => {
if (d.ok) { if (d.ok) {
status.textContent = '✓ Key created — ' + d.key_preview; status.textContent = '✓ Key created — ' + d.key_preview;