From a013d155029fa82649fb6351080b89a450eb177e Mon Sep 17 00:00:00 2001 From: Oleksandr Piskun Date: Fri, 23 Oct 2026 12:09:28 +0000 Subject: [PATCH] docs: HaRP checks the route verb Signed-off-by: Oleksandr Piskun --- .../references/exapp-contract.md | 33 ++++++++++++------- skills/harp-operations/SKILL.md | 4 +-- .../references/harp-operations.md | 8 +++-- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/skills/exapp-development/references/exapp-contract.md b/skills/exapp-development/references/exapp-contract.md index 7ce60c5..63baf84 100644 --- a/skills/exapp-development/references/exapp-contract.md +++ b/skills/exapp-development/references/exapp-contract.md @@ -78,7 +78,7 @@ Every HTTP surface the ExApp exposes must be covered by a ``; unmatched r | Field | Meaning | |---|---| -| `url` | Case-insensitive regex matched against the request path (e.g. `^/api/.*`) | +| `url` | Regex matched against the request path including its leading `/` (e.g. `^/api/.*`); the PHP proxy ignores case, HaRP does not | | `verb` | Comma-separated HTTP methods the route accepts | | `access_level` | `PUBLIC` (0) anyone, `USER` (1) any logged-in user, `ADMIN` (2) admins only | | `bruteforce_protection` | JSON array of response status codes that count as a bruteforce attempt (e.g. `[401,429]`) | @@ -86,19 +86,28 @@ Every HTTP surface the ExApp exposes must be covered by a ``; unmatched r Enforcement happens in two places, depending on the path a request takes: -- **HaRP path** (browser to `/exapps/...`): AppAPI hands HaRP the route table (url + access_level + +- **HaRP path** (browser to `/exapps/...`): AppAPI hands HaRP the route table (url + verb + access_level + bruteforce_protection) via the ExApp metadata endpoint, and HaRP resolves the caller's level per request through the user-info endpoint (no/disabled user = PUBLIC, admin = ADMIN, else USER), enforcing the - comparison itself. `verb` and `headers_to_exclude` are not part of the HaRP checks. Observed replies: an - anonymous request to a `USER` or `ADMIN` route gets `403` (an admin session or basic auth gets through), an - undeclared path gets `404`, and the lifecycle endpoints (`/heartbeat`, `/init`, `/enabled`) get `502`. -- **PHP proxy path** (`/index.php/apps/app_api/proxy/...`): `ExAppProxyController` matches `url` (regex) and - `verb`, enforces the access level, strips `headers_to_exclude`, and applies bruteforce throttling on the - listed status codes. - -Authoring notes: an empty element (``) is fine and means "none", but -nested sub-elements (`401`) are rejected; use -the JSON-in-text form shown above. + comparison itself. Since HaRP 0.4.8 the first route whose `url` matches the path and whose `verb` lists the + method decides, as in the PHP proxy; `HEAD` is accepted wherever `GET` is listed, and a method that no route + of the path lists gets `405` with an `Allow` header. HaRP 0.4.7 and older, and HaRP with an AppAPI that does + not send the verbs yet, match by `url` only: the first matching route decides for every method. + `headers_to_exclude` is not part of the HaRP checks. Observed replies: an anonymous request to a `USER` or + `ADMIN` route gets `403` (an admin session or basic auth gets through), an undeclared path gets `404`, and the + lifecycle endpoints (`/heartbeat`, `/init`, `/enabled`) get `502`. +- **PHP proxy path** (`/index.php/apps/app_api/proxy/...`): `ExAppProxyController` takes the first route whose + `url` (regex) matches and whose `verb` lists the method (`HEAD` also where `GET` is listed, since + nextcloud/app_api#1144), enforces the access level, strips `headers_to_exclude`, and applies bruteforce + throttling on the listed status codes. A request no route lets through gets `404`. Only `GET`, `HEAD`, `POST`, + `PUT` and `DELETE` reach the proxy at all: Nextcloud answers other methods, `PATCH` and `OPTIONS` included, + with `405`. + +Authoring notes: list every method a route serves in its `verb` (`GET,POST`), and give methods that need +different access levels different URLs, as older HaRP versions decide by URL only. An empty element +(``) is fine and means "none", but nested sub-elements +(`401`) are rejected; use the JSON-in-text form +shown above. ## Environment variables: a declared allow-list diff --git a/skills/harp-operations/SKILL.md b/skills/harp-operations/SKILL.md index fb82807..31c088b 100644 --- a/skills/harp-operations/SKILL.md +++ b/skills/harp-operations/SKILL.md @@ -34,8 +34,8 @@ This skill is HaRP itself. Registering daemons, installing ExApps and the occ su ## Facts that save hours - Read the status code first, it names the plane: `401` shared key, `403` route access level, `404` unknown - route or an app not on this daemon, `502` an infrastructure route or a proxy rule pointing nowhere, `503` - the agent under load. + route or an app not on this daemon, `405` a method the route's `verb` does not list (HaRP 0.4.8+), `502` an + infrastructure route or a proxy rule pointing nowhere, `503` the agent under load. - `GET /` on the HTTP frontend answers `404` by design. Never use it as a health probe; use `/exapps/app_api/info` with the `harp-shared-key` header. - One HaRP serves one Nextcloud. `NC_INSTANCE_URL` is a single value and every request is authorised against diff --git a/skills/harp-operations/references/harp-operations.md b/skills/harp-operations/references/harp-operations.md index 5e55007..8dc2c58 100644 --- a/skills/harp-operations/references/harp-operations.md +++ b/skills/harp-operations/references/harp-operations.md @@ -142,6 +142,7 @@ cause. | `401` | Shared key missing or wrong on a control route, or an unsigned request to `/exapps/app_api/...` | | `403` | The caller's level is below the route's `access_level` (anonymous on a USER or ADMIN route) | | `404` | Path not declared by the ExApp, or the app is not routable here (``), or `GET /` | +| `405` | No route of that path lists the method in its `verb` (HaRP 0.4.8+); the `Allow` header names the listed ones | | `502` | An infrastructure route (`/heartbeat`, `/init`, `/enabled`) requested from outside, which is blocked by design, or a reverse-proxy rule pointing at nothing | | `503` | No healthy backend: the agent is stalled or overloaded | @@ -150,8 +151,10 @@ cause. - Tags, from HaRP's publish workflow: `:release` on every GitHub release, `:vX.Y.Z` alongside it, `:latest` and `:main` on every push to `main`. Production wants `:release` or a pinned `:vX.Y.Z`; `:latest` is a development build. -- AppAPI enforces a floor of `0.3.0` (`Application::MINIMUM_HARP_VERSION`, same in NC33, NC34 and NC35) and - reports it through the `HarpVersionCheck` setup check. +- AppAPI enforces a floor (`Application::MINIMUM_HARP_VERSION`) and reports it through the `HarpVersionCheck` + setup check: `0.4.8` with nextcloud/app_api#1149 and its backports to NC33, NC34 and NC35, `0.3.0` before. + With #1149 the check caches the version it reads for an hour, so the warning can outlive a HaRP update by + that long. - Before 0.4.3, `/info` reported the version as a float, which Nextcloud read as `0.4.0` whatever the real patch level was. On 0.4.3 and later it is a real `x.y.z` string, so the admin overview shows the truth. - Upgrading is `docker pull` plus recreate; ExApps reconnect their tunnels on their own. Verify with the @@ -193,6 +196,7 @@ least 0.4.3 before investigating `503` bursts, and check host CPU pressure at th | Every ExApp request is `401` | Shared key mismatch between `HP_SHARED_KEY` and the daemon's `--harp_shared_key`, or a non-ASCII key. Re-register the daemon with the exact value | | One app is `404` with `` in the log | That app is not deployed on this HaRP daemon (manual-install apps are served at `/index.php/apps/app_api/proxy//...`) | | A route answers `403` for normal users | The route declares `access_level` USER or ADMIN; anonymous callers get `403` | +| A request that used to work gets `405` | HaRP 0.4.8+ with an AppAPI that sends the route verbs checks the method: no route of that path lists it, and the reply's `Allow` header names the ones listed. The ExApp's `info.xml` must add the method to that route's ``; update the ExApp once it does | | Browser gets `502` on `/heartbeat`, `/init` or `/enabled` | Working as intended: infrastructure routes are not reachable from outside | | Every ExApp install ends in "heartbeat check failed" | The `/exapps/` rule on the Nextcloud reverse proxy is missing or wrong; AppAPI cannot reach `/exapps//heartbeat` | | Bursts of `503` across all ExApps under load | Agent stall. Upgrade to 0.4.3+, then look for host CPU starvation at those timestamps |