Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
68ab379
feat(amuleapi): standalone REST API + SSE daemon
got3nks Jun 18, 2026
50e4695
feat(amuleapi): align endpoint surface with RFC PR #132 review
got3nks Jun 18, 2026
b5c4815
fix(amuleapi): explicit standard includes + LoggerConsole link
got3nks Jun 18, 2026
c7e5613
fix(amuleapi): address PR review findings (high + medium + low)
got3nks Jun 18, 2026
a460c5f
fix(amuleapi): address second-round PR review findings
got3nks Jun 18, 2026
b4bfab8
fix(amuleapi): AmuleApiConfigTest tmp portability for mingw-w64
got3nks Jun 18, 2026
ec49ab2
fix(amuleapi): address pre-PR blockers + High/Medium review findings
got3nks Jun 18, 2026
1a2c360
fix(amuleapi): four partial-finding follow-ups
got3nks Jun 18, 2026
d5aa2b8
fix(amuleapi): ship picojson license + cap JSON nesting before parse
got3nks Jun 18, 2026
a5045e9
fix(amuleapi): sweep final pre-PR review (six batches, ~25 findings)
got3nks Jun 18, 2026
7a459f1
fix(amuleapi): correct phase3/phase4 fixture off-by-ones from review …
got3nks Jun 18, 2026
415eb82
fix(amuleapi): second pre-PR review sweep (7 findings, including 1 Se…
got3nks Jun 19, 2026
ab55f64
docs(amuleapi): add full REST + SSE reference under docs/amuleapi/
got3nks Jun 19, 2026
5286763
docs(amuleapi): rename docs/amuleapi/ → docs/api/
got3nks Jun 19, 2026
a56b4c9
docs(amuleapi): swap real ed2k server IP in examples to RFC 5737 range
got3nks Jun 19, 2026
7016701
fix(amuleapi): align SSE↔REST shapes + 6 review items + partial comme…
got3nks Jun 19, 2026
e57cbf9
fix(amuleapi): /shared ghost entries on partfile-becoming-shared tick
got3nks Jun 21, 2026
00fbbe6
feat(amuleapi): /downloads/{key} and /shared/{key} accept hash OR ECID
got3nks Jun 21, 2026
bf4c8f0
feat(amuleapi): static-frontend serve + out-of-box discovery
got3nks Jun 21, 2026
e187b72
fix(amuleapi): always include category 0 in GET /categories
got3nks Jun 21, 2026
cd94269
test(amuleapi): rename curl-test phases to descriptive names
got3nks Jun 21, 2026
9dc4f36
feat(amuleapi): refresher-driven search lifecycle + SSE events
got3nks Jun 21, 2026
2c85f7d
docs(amuleapi): declare the search SSE channel + tighten filter tests
got3nks Jun 21, 2026
03700a6
refactor(amuleapi): unify downloads + shared into one ECID-keyed File…
got3nks Jun 21, 2026
1dd35a6
refactor(amuleapi): drop file ECIDs from the public API surface
got3nks Jun 22, 2026
3dc6370
refactor(amuleapi): rename clients.ecid → client_ecid
got3nks Jun 22, 2026
898818a
perf(amuleapi): switch m_files to unordered_map with inline hash index
got3nks Jun 22, 2026
6f32a7a
docs(amuleapi): add SSE bootstrap section (open stream then snapshot)
got3nks Jun 22, 2026
7dc005b
docs(amuleapi): fill SSE doc gaps + convert § cross-refs to anchor links
got3nks Jun 22, 2026
ef0e40d
feat(ec): unambiguous search lifecycle tags on EC_OP_SEARCH_PROGRESS
got3nks Jun 22, 2026
15a02b3
refactor(amuleapi): consume EC_TAG_SEARCH_LIFECYCLE_STATE directly
got3nks Jun 22, 2026
c866b65
feat(ec): Kad search progress ramp via EC_TAG_SEARCH_LIFECYCLE_PERCENT
got3nks Jun 23, 2026
84afaa2
feat(amuleapi): search_progress SSE event + unified percent (supersed…
got3nks Jun 23, 2026
d87b0ea
docs(amuleapi): document search_progress + fix verified REST/SSE doc …
got3nks Jun 23, 2026
9e7f98e
fix(amuleapi): nest search_result_added sources to match REST + corre…
got3nks Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ccpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ env:
-DBUILD_ALC=YES \
-DBUILD_ALCC=YES \
-DBUILD_AMULECMD=YES \
-DBUILD_AMULEAPI=YES \
-DBUILD_CAS=YES \
-DBUILD_DAEMON=YES \
-DBUILD_WXCAS=YES \
Expand Down
7 changes: 7 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# aMule License

aMule is released under the GNU General Public License, version 2 or
later (the "GPLv2+"). The full text follows. Third-party components
bundled into aMule's binary distributions retain their own permissive
licenses, listed in [`docs/THIRDPARTY.md`](docs/THIRDPARTY.md).

# GNU GENERAL PUBLIC LICENSE

Version 2, June 1991
Expand Down
37 changes: 36 additions & 1 deletion cmake/options.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ option (BUILD_FILEVIEW "compile aMule file viewer for console (EXPERIMENTAL)")
option (BUILD_MONOLITHIC "enable building of the monolithic aMule app" ON)
option (BUILD_REMOTEGUI "compile aMule remote GUI")
option (BUILD_WEBSERVER "compile aMule WebServer")
option (BUILD_AMULEAPI "compile aMule REST API daemon")
option (BUILD_WXCAS "compile aMule GUI Statistics")
option (BUILD_TESTING "Build unit tests" OFF)

Expand All @@ -63,6 +64,7 @@ if (BUILD_EVERYTHING)
set (BUILD_FILEVIEW ON CACHE BOOL "compile aMule file viewer for console (EXPERIMENTAL)" FORCE)
set (BUILD_REMOTEGUI ON CACHE BOOL "compile aMule remote GUI" FORCE)
set (BUILD_WEBSERVER ON CACHE BOOL "compile aMule WebServer" FORCE)
set (BUILD_AMULEAPI ON CACHE BOOL "compile aMule REST API daemon" FORCE)
set (BUILD_WXCAS ON CACHE BOOL "compile aMule GUI Statistics" FORCE)
endif()

Expand All @@ -74,6 +76,39 @@ if (BUILD_AMULECMD)
set (NEED_ZLIB TRUE)
endif()

if (BUILD_AMULEAPI)
# Mirrors amulecmd's needs: EC connection, mulecommon helpers (Format,
# MD5Sum), socket lib for CRemoteConnect. Boost.Beast is header-only
# so we don't add a Boost component requirement; the link-side Boost
# is already wired via the project-level `Boost_LIBRARIES` lookup.
#
# Hard-fail policy. `BUILD_AMULEAPI=YES` + missing dep must fail
# at configure time, never soft-disable the target. Today the
# guarantees come from upstream wiring:
# * cryptopp — `NEED_LIB_EC` (set below) implies `NEED_LIB_CRYPTO`,
# which includes cmake/cryptopp.cmake; that file FATAL_ERRORs
# on a missing `cryptlib.h`.
# * Boost — `cmake/boost.cmake` runs unconditionally at the
# project root and uses `find_package(Boost CONFIG REQUIRED)`,
# which FATAL_ERRORs on miss.
# If a future refactor breaks either chain (e.g. moves Boost
# behind a conditional `if`), add an explicit `find_package(Boost
# CONFIG REQUIRED)` here so amuleapi keeps fail-loud.
set (NEED_LIB_EC TRUE)
set (NEED_LIB_MULECOMMON TRUE)
set (NEED_LIB_MULESOCKET TRUE)
set (wx_NEED_NET TRUE)
set (NEED_ZLIB TRUE)
# Compile-time install path the daemon falls back to when
# [Server]/StaticRoot is empty in the conf. Mirrors WEBSERVERDIR but
# uses the ABSOLUTE form so a binary running from /usr/local/bin
# (or wherever the operator put it) resolves to the matching
# /usr/local/share/amule/amuleapi-static without needing to be
# cwd'd at the install prefix.
set (AMULEAPI_STATIC_DIR
"${CMAKE_INSTALL_FULL_DATADIR}/${PACKAGE}/amuleapi-static/")
endif()

if (BUILD_CAS)
set (BUILD_UTIL TRUE)
endif()
Expand Down Expand Up @@ -196,7 +231,7 @@ endif()
# wxWidgets::NET directly in src/webserver/src/CMakeLists.txt for its
# socket code). Keep wx_NEED_NET on only when those are actually being
# built.
if (NOT (BUILD_DAEMON OR BUILD_MONOLITHIC OR BUILD_REMOTEGUI OR BUILD_WEBSERVER OR BUILD_WXCAS OR BUILD_AMULECMD))
if (NOT (BUILD_DAEMON OR BUILD_MONOLITHIC OR BUILD_REMOTEGUI OR BUILD_WEBSERVER OR BUILD_WXCAS OR BUILD_AMULECMD OR BUILD_AMULEAPI))
set (wx_NEED_NET FALSE)
endif()

Expand Down
4 changes: 4 additions & 0 deletions config.h.cm
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@
/* Where the webserver finds it's themes */
#cmakedefine WEBSERVERDIR "${WEBSERVERDIR}"

/* Compile-time install path of amuleapi's bundled static frontend.
amuleapi falls back to this when [Server]/StaticRoot is empty. */
#cmakedefine AMULEAPI_STATIC_DIR "${AMULEAPI_STATIC_DIR}"

/* Define if libpng is present is set. */
#cmakedefine WITH_LIBPNG

Expand Down
1 change: 1 addition & 0 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Common `-D` options (`YES` / `NO` unless noted otherwise):
| `BUILD_DAEMON` | NO | `amuled` — headless daemon |
| `BUILD_AMULECMD` | NO | `amulecmd` — CLI client for the daemon |
| `BUILD_WEBSERVER` | NO | `amuleweb` — HTTP interface for the daemon |
| `BUILD_AMULEAPI` | NO | `amuleapi` — REST API + SSE daemon ([docs/QUICKSTART-AMULEAPI.md](QUICKSTART-AMULEAPI.md)) |
| `BUILD_ED2K` | NO | `ed2k` — handle `ed2k://` links |
| `BUILD_CAS` | NO | `cas` — C statistics tool |
| `BUILD_WXCAS` | NO | `wxCas` — GUI statistics tool |
Expand Down
245 changes: 245 additions & 0 deletions docs/QUICKSTART-AMULEAPI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# amuleapi — quick start

amuleapi is a standalone HTTP daemon that serves a versioned JSON REST API
and a long-lived Server-Sent Events stream backed by amuled. It connects
to amuled as an EC client (same protocol amuleweb and amulecmd use) and
exposes its own HTTP surface on a separate port. amuleapi is the first
shipping REST API for aMule — there is no prior on-the-wire surface to
migrate from.

For the endpoint list see the [What ships](#what-ships) section below. Full per-endpoint contracts (methods, query params, request bodies, response shapes, error codes) live in [`docs/api/REFERENCE.md`](api/REFERENCE.md); the SSE event catalog and Last-Event-ID reconnect semantics live in [`docs/api/EVENTS.md`](api/EVENTS.md). The source of truth for routing is [`src/webapi/Api.cpp`](../src/webapi/Api.cpp).

## Requirements

- A running `amuled` (or a monolithic `aMule` with EC enabled) that
amuleapi can connect to over the EC protocol.
- The EC password from `amule.conf[ExternalConnect]/Password` (set via
`amuled --ec-config` if you've never run it).

## First-run setup

amuleapi keeps its config in the same per-platform aMule data directory
that `amuled` uses.

> **Cohabitation with amuled.** This is the same directory amuled
> keeps `amule.conf` and `*.met` in — intentionally so. amuleapi's
> three files (`amuleapi.conf`, `amuleapi-jwt-secret`,
> `amuleapi-passwords`) sit alongside amuled's without colliding,
> and operators reading both sets of configs together don't have
> to context-switch directories. amuleapi never touches amuled's
> files; amuled never touches amuleapi's.

The default location:

| Platform | Default config dir |
| -------- | --------------------------------------------------- |
| Linux | `~/.aMule/` |
| macOS | `~/Library/Application Support/aMule/` |
| Windows | `%APPDATA%\aMule\` |

Override with `amuleapi --config-dir=/path/to/dir`.

The directory holds three amuleapi-specific files, all written with mode
`0600`:

| File | Purpose |
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
| `amuleapi.conf` | INI-style runtime config (HTTP bind/port/CORS, outbound EC connection to amuled, login rate-limit knobs, SSE event-bus ring size). Full reference below. |
| `amuleapi-jwt-secret` | 32-byte HMAC signing key for issued tokens. Auto-generated on first launch if absent. |
| `amuleapi-passwords` | MD5-hashed admin and guest passwords. Plaintext is never persisted. |

Set passwords via the dedicated CLI flags. Each invocation writes the
file and exits — the HTTP server is NOT brought up, no EC connection
is attempted, and the exit code reflects success / failure (so
`amuleapi --set-admin-pass=... && systemctl restart amuleapi` actually
short-circuits if the write fails):

```sh
amuleapi --set-admin-pass=mySecret123
amuleapi --set-guest-pass=readOnlyPass
```

An empty password row means "this role is disabled" and
`POST /api/v0/auth/login` returns `login_disabled` for that role.

## Running

```sh
amuleapi --host=127.0.0.1 --port=4712 --password=$EC_PASSWORD
```

> **Two ports.** `--port=4712` is the EC port amuleapi USES to talk
> to amuled (i.e. it's a client of amuled on 4712). amuleapi's OWN
> HTTP listener is on `amuleapi.conf[Server]/Port` (default 4713)
> — that's the port REST clients hit. The example above starts a
> daemon that consumes 4712 (outbound to amuled) and serves 4713
> (inbound from REST clients).

- `--host` / `--port` / `--password` specify the EC connection to
`amuled` (default port `4712`).
- HTTP serves on `amuleapi.conf[Server]/Port` (default `4713`).
- amuleweb can run concurrently on its own port (default `4711`); the
two daemons talk to amuled independently as separate EC clients.

aMule does not ship init-system units (systemd, launchd, Windows
service) for any of its daemons. If you want one, write a downstream
unit that wraps the command above.

## Verifying

```sh
# Public — no auth.
curl -s http://127.0.0.1:4713/api/v0/version

# Login → token.
# `?type=bearer` opts into the SDK-client response shape: the JWT
# lands in the JSON body so a shell script can extract it. Browser
# clients call /auth/login WITHOUT ?type=bearer and authenticate via
# the HttpOnly session cookie set on the response — that's the
# default to keep the token out of any XSS-readable surface.
TOKEN=$(curl -s -X POST "http://127.0.0.1:4713/api/v0/auth/login?type=bearer" \
-H 'Content-Type: application/json' \
-d '{"password":"mySecret123"}' | jq -r .token)

# Authenticated GETs.
curl -s -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:4713/api/v0/status

# Live event stream — open in a separate terminal and trigger
# mutations elsewhere to watch events flow.
curl -s -N -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:4713/api/v0/events
```

## `amuleapi.conf` reference

INI-style file written with mode `0600`. The defaults file is created on first launch if absent — edits roundtrip through `wxFileConfig`, so quotes and comments are preserved across daemon restarts. The full surface:

```ini
[Server]
BindAddress=127.0.0.1
Port=4713
AllowCORS=0
CorsOriginAllowlist=
StaticRoot=

[EC]
Host=127.0.0.1
Port=4712
Password=

[Auth]
LoginFailureWindowSeconds=60
LoginFailureThreshold=5
LoginLockoutSeconds=300

[Streaming]
EventBusRingCapacity=16384
```

### `[Server]` — HTTP listener

| Key | Default | Meaning |
| --- | --- | --- |
| `BindAddress` | `127.0.0.1` | Interface the HTTP listener binds to. Non-loopback binds are rejected at startup unless at least one of admin/guest passwords is set (the "publicly listening with no password" footgun gate in `App.cpp`). Overridable with `--bind=…` on the CLI. |
| `Port` | `4713` | TCP port for inbound REST traffic. Distinct from amuled's EC port (`[EC]/Port`, default 4712). Overridable with `--http-port=…`. |
| `AllowCORS` | `0` | `1` enables CORS headers (`Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials: true`, `Vary: Origin`, preflight OPTIONS). Required for browser clients hosted on a different origin. See §CORS below. |
| `CorsOriginAllowlist` | *(empty)* | Comma-separated list of origins that may set credentialed CORS requests. Empty + `AllowCORS=1` echoes the caller's `Origin` verbatim (wildcard-equivalent that remains cookie-compatible). |
| `StaticRoot` | *(empty)* | Absolute filesystem path of a bundled web frontend. Empty (default) auto-discovers the bundled placeholder via the install-path chain (`make install` target on Linux/Windows, `aMule.app/Contents/Resources/amuleapi-static` on macOS) — same pattern amuleweb uses for templates, see [`WebInterface.cpp:146`](../src/webserver/src/WebInterface.cpp#L146). If no install is found, the daemon stays API-only and non-`/api/` paths return `404`. A non-empty `StaticRoot` overrides discovery and serves `GET`/`HEAD` requests outside `/api/` from that directory, with `index.html` SPA fallback for extension-less misses. Reads are containment-checked (symlinks pointing outside the root are rejected on POSIX; lexical `..`-rejection on Windows where symlinks require elevation), capped at 16 MiB per asset, and emit a mtime-size `ETag` so subsequent loads short-circuit to `304` via `If-None-Match`. |

### `[EC]` — outbound connection to amuled

| Key | Default | Meaning |
| --- | --- | --- |
| `Host` | `127.0.0.1` | Hostname or IP of the running amuled daemon. amuleapi is a long-lived EC client; CLI `--host=…` overrides. |
| `Port` | `4712` | amuled's EC listener port (matches amuled's `[ExternalConn]/ECPort`). CLI `--port=…` overrides. |
| `Password` | *(empty)* | Plaintext EC password matching amuled's `[ExternalConn]/ECPassword`. Stored cleartext because the base class wants a hashable plaintext — the `0600` file mode matches `amuleapi-jwt-secret` and `amuleapi-passwords`. CLI `--password=…` overrides. |

### `[Auth]` — login rate limiter

Drives the `/auth/login` per-IP throttle (`CRateLimiter` in `Auth.cpp`). Failures inside the sliding window count toward the threshold; tripping it locks the offending IP out for `LoginLockoutSeconds`. Successful logins reset the bucket immediately.

| Key | Default | Meaning |
| --- | --- | --- |
| `LoginFailureWindowSeconds` | `60` | Sliding window in seconds. Failures older than this fall off the count. |
| `LoginFailureThreshold` | `5` | Failures within the window before the IP is locked out. |
| `LoginLockoutSeconds` | `300` | Duration of the IP lockout once tripped. While locked, `/auth/login` returns `429 rate_limited` with a `Retry-After` header. |

### `[Streaming]` — SSE event bus

| Key | Default | Meaning |
| --- | --- | --- |
| `EventBusRingCapacity` | `16384` | Number of events the in-memory SSE bus retains for `Last-Event-ID` replay. Sized to absorb a cold-start tick on a busy node (5 K downloads + 5 K shared can publish ~10 K `*_added` events in a single tick). Worst-case memory ≈ capacity × ~1 KB JSON payload. Values below the bus's compile-time floor (16) are clamped up. Raise this on operator-heavy nodes where reconnecting clients are hitting `resync` events from natural traffic; lower it (e.g. `32`) only for the smoke-test gap-path scenario. |

CLI `--bind`, `--http-port`, `--host`, `--port`, `--password`, and `--config-dir` override the matching keys at runtime without rewriting the file.

## CORS

By default amuleapi serves no CORS headers (same-origin only). To allow
cross-origin browser clients, set in `amuleapi.conf`:

```ini
[Server]
AllowCORS=1
CorsOriginAllowlist=https://your-app.example.com,https://staging.example.com
```

Leave `CorsOriginAllowlist` empty to echo any caller's `Origin` header
(wildcard-equivalent that stays cookie-compatible).

> **CORS note.** The empty-allowlist form is *not* literally
> `Access-Control-Allow-Origin: *`. amuleapi echoes the caller's
> exact `Origin` value, which is the only shape browsers accept
> together with `Access-Control-Allow-Credentials: true` (RFC 6454
> + Fetch spec). A literal `*` would refuse cookie auth on cross-
> origin requests — which is what every browser session relies on.

## What ships

- `/api/v0/auth/login` / `logout` / `session` — JWT and session-cookie auth
- `/api/v0/version`, `/status`, `/preferences`
- `/api/v0/downloads`, `/shared`, `/servers`, `/kad`,
`/clients` (the per-peer view, with optional
`?filter=uploads|downloads|active` for the legacy "Uploads" page
subset), `/categories`, `/logs/{amule,serverinfo}`,
`/stats/{tree,graphs/{graph}}`, `/search`, `/search/results`
- POST / PATCH / DELETE mutations on each resource (admin role)
- ETag-on-GET conditional caching (304 Not Modified on `If-None-Match`)
- `/api/v0/events` — long-lived Server-Sent Events stream with
`Last-Event-ID` replay and typed `resync` events for cache invalidation
- Every runtime tunable lives in `amuleapi.conf`; see the §`amuleapi.conf` reference above for sections, keys, and defaults.

## Notes on a few responses

- **`POST /api/v0/downloads` partial success.** The endpoint accepts
a single `ed2k_link` or an array of `links`. When some links land
cleanly and others fail (already on queue, malformed magnet,
category out of range, or EC disconnect mid-batch) the response is
`207 Multi-Status` with `ok: false` and four parallel arrays —
`accepted_links`, `failed_links`, `disconnected_links`, plus
counters and a `first_error`. `207` is borrowed from WebDAV (RFC
4918 §13) for "the request was answered in pieces"; clients should
treat it as success-with-details, not as a 4xx. `503` is reserved
for "every link blocked by an EC disconnect" — nothing landed and
the caller can retry once `GET /status` reports `ec_connected:
true`.

## Security notes

- The admin role grants the holder full control of the daemon's
network surface — that includes `POST /api/v0/servers/update
{"servers_url": "..."}`, which makes amuled fetch the supplied URL
to refresh the server list. This is the same behaviour amuled has
exposed via the desktop GUI and amuleweb for years, but it widens
what an admin token *grants* — anyone who steals one can ask
amuled to perform an HTTP GET against arbitrary network-reachable
URLs (a classic SSRF surface) and bring the response back into
amuled's process. The `http://` / `https://` pre-check in the API
is hygienic input validation, not a security boundary; protect
the admin password and the JWT signing secret accordingly.
- The default `BindAddress=127.0.0.1` is load-bearing. The HTTP
server spawns one OS thread per Server-Sent Events subscriber, so
binding amuleapi to a non-loopback interface exposes the
thread-per-connection model to unauthenticated peers. If you need
remote access, put a reverse proxy in front and keep the bind on
loopback.
Loading
Loading