Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
This commit is contained in:
@@ -1,6 +1,96 @@
|
||||
<?php
|
||||
// Receives Sonarr/Radarr/Lidarr Download events. Fires upgrade_webhook_handler.sh
|
||||
// in the background and returns 200 immediately — arr does not wait on the push.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Arr download webhook. Receives Sonarr, Radarr and Lidarr Download events and fires
|
||||
// upgrade_webhook_handler.sh against the affected folder so the partner learns about the
|
||||
// new file immediately rather than at the next scheduled sync.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Accept, dispatch, return. The handler is backgrounded and the response goes out at once,
|
||||
// because an arr that is made to wait on a cross-host push will time the webhook out and
|
||||
// log it as a failure — and it retries on a schedule that would compound the problem.
|
||||
//
|
||||
// 200 therefore means accepted, not propagated. The handler's own log is the record of what
|
||||
// happened; this endpoint cannot report it and does not pretend to.
|
||||
//
|
||||
// The reason for immediacy is search suppression: until the partner knows a file exists, it
|
||||
// will keep searching for it. The scheduled sync would close that gap eventually; the
|
||||
// webhook closes it in seconds.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// The arr type is inferred from the payload's shape, not from a parameter.
|
||||
// series.path means Sonarr, movie.folderPath means Radarr, artist.path means Lidarr.
|
||||
// Each app names its own field, so the structure identifies the sender without a query
|
||||
// string the user could get wrong when configuring three separate applications.
|
||||
//
|
||||
// Test events are answered with instructions.
|
||||
// Sonarr's "Test" button gets a message telling the user to configure On Download,
|
||||
// because a bare success there is exactly what leads to a webhook that is connected and
|
||||
// wired to nothing.
|
||||
//
|
||||
// Every non-Download event is acknowledged and skipped.
|
||||
// ok:true with the event name, never an error. An arr that receives an error status
|
||||
// retries and eventually disables the webhook, so events this endpoint does not care
|
||||
// about have to be accepted rather than rejected.
|
||||
//
|
||||
// Fires on all Download events, not just upgrades.
|
||||
// New grabs and upgrades both need propagating; distinguishing them would suppress
|
||||
// exactly the first-time grabs the partner is most likely to duplicate.
|
||||
//
|
||||
// The kill switch is read per request.
|
||||
// DOWNLOAD_WEBHOOK_ENABLED is checked from conf on every call, so turning it off takes
|
||||
// effect without touching the arr configuration — and the response says it was skipped
|
||||
// rather than silently doing nothing.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// POST only, with a real status code.
|
||||
// 405 for the wrong method, 400 for unparseable or unrecognised bodies. Status codes
|
||||
// matter more here than elsewhere in this layer: the caller is a machine that changes
|
||||
// its retry behaviour based on them.
|
||||
//
|
||||
// The path is validated and then escaped.
|
||||
// Must be absolute, no '..', no null bytes or newlines — then passed through
|
||||
// escapeshellarg() into the handler invocation. The arr type is likewise escaped even
|
||||
// though it is one of three literals this file chose itself.
|
||||
//
|
||||
// Both the empty-path and unrecognised-structure cases are handled explicitly.
|
||||
// A payload with a recognised key holding an empty value is rejected separately from
|
||||
// one whose structure is unknown, because those are different misconfigurations.
|
||||
//
|
||||
// A missing handler is reported as a server error, not swallowed.
|
||||
// 500 with a named error, so a partial deploy is visible in the arr's own webhook log
|
||||
// rather than appearing to succeed forever.
|
||||
//
|
||||
// Output is appended to a dedicated log with stdin detached, so a backgrounded handler
|
||||
// cannot hold the request's file descriptors open.
|
||||
//
|
||||
// Known gap: this endpoint authenticates nothing.
|
||||
// master.conf carries WEBHOOK_SECRET, and the standalone Node listener on WEBHOOK_PORT
|
||||
// validates it — this WebGUI-hosted path does not. Anyone who can reach the URL can
|
||||
// make it run the handler against any absolute path that passes validation. Injection
|
||||
// is not the risk (the path is escaped); triggering work is. Left as-is deliberately
|
||||
// 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
|
||||
// with the arr configs open, not as part of a documentation pass. See also the CSRF
|
||||
// note in README-unraid.md.
|
||||
//
|
||||
// REQUEST
|
||||
// POST <arr webhook JSON body>
|
||||
// eventType=Test → connection acknowledgement
|
||||
// eventType=Download → dispatches the handler
|
||||
// any other eventType → acknowledged and skipped
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":true,"arr":"sonarr|radarr|lidarr","path":"…"} dispatched
|
||||
// {"ok":true,"message":"Webhook connected — …"} Test
|
||||
// {"ok":true,"skipped":"<event>|DOWNLOAD_WEBHOOK_ENABLED=false"}
|
||||
// {"ok":false,"error":…} with 405 / 400 / 500 as appropriate
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/config.php vv_conf_vars(), SCRIPTS_DIR
|
||||
// Media/upgrade_webhook_handler.sh the backgrounded handler
|
||||
// master.conf DOWNLOAD_WEBHOOK_ENABLED
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user