diff --git a/README.md b/README.md index 6c6906b..24e3ef3 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ server { server_name nextcloud.com; location /exapps/ { - proxy_pass http://127.0.0.1:8780; + proxy_pass http://127.0.0.1:8780/exapps/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -515,13 +515,13 @@ docker run \ 4. Create folder `dev` at the root of repository, extract there content of the desired archive with the [FRP](https://github.com/fatedier/frp/releases/latest) archive which is located at `exapps_dev` folder of this repo. 5. Edit the `data/nginx/vhost.d/nextcloud.local_location` file from the `nextcloud-docker-dev` to point `/exapps/` web route to the host: ``` - proxy_pass http://172.17.0.1:8780; + proxy_pass http://172.17.0.1:8780/exapps/; ``` > **Note:** my original content from my dev machine of file `nextcloud.local_location`: > ```nginx > location /exapps/ { - > proxy_pass http://172.17.0.1:8780; + > proxy_pass http://172.17.0.1:8780/exapps/; > } > ``` 6. Use `docker compose up -d --force-recreate proxy` command from Julius `nextcloud-docker-dev` to recreate the proxy container. diff --git a/haproxy_agent.py b/haproxy_agent.py index 11d44a7..e511aeb 100644 --- a/haproxy_agent.py +++ b/haproxy_agent.py @@ -310,6 +310,20 @@ async def record_ip_failure(ip_address: str | IPv4Address | IPv6Address) -> None LOGGER.warning("Recorded failure for IP %s. Failures in window: %d", ip_str, len(attempts)) +async def record_failure_unless_trusted( + ip_address: str | IPv4Address | IPv6Address, request_headers: dict[str, str] +) -> None: + """Record a blacklist failure unless the request is authenticated with a valid harp-shared-key. + + Requests from Nextcloud/AppAPI carry `harp-shared-key` matching `SHARED_KEY`; a misconfiguration + on that trusted path (e.g. reverse-proxy stripping `/exapps/`) would otherwise cause the caller + to self-DoS once the blacklist window fills up. + """ + if request_headers.get("harp-shared-key") == SHARED_KEY: + return + await record_ip_failure(ip_address) + + async def is_ip_banned(ip_address: str | IPv4Address | IPv6Address) -> bool: """Return True if IP has exceeded the maximum allowed failures in the request window.""" ip_str = str(ip_address) @@ -375,7 +389,7 @@ async def exapps_msg( match = APPID_PATTERN.search(path) if not match: LOGGER.error("Invalid request path, cannot find AppID: %s", path) - await record_ip_failure(client_ip_str) + await record_failure_unless_trusted(client_ip_str, request_headers) return reply.set_txn_var("not_found", 1) exapp_id = match.group(1) exapp_id_lower = exapp_id.lower() @@ -424,7 +438,7 @@ async def exapps_msg( exapp_record = await _get_or_fetch_exapp(exapp_id_lower) if not exapp_record: LOGGER.error("No such ExApp enabled: %s", exapp_id) - await record_ip_failure(client_ip_str) + await record_failure_unless_trusted(client_ip_str, request_headers) return reply.set_txn_var("not_found", 1) except ValidationError as e: LOGGER.error("Invalid ExApp metadata from Nextcloud: %s", e)