You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Web UI exposes an amuleapi admin password field, but changing it does
not do what a user expects: the new password never reaches the running daemon,
and in a standalone deployment it is silently discarded forever. The guest password has no API surface at all — it cannot be set, changed or
disabled from the Web UI.
The root cause is that amuleapi's credentials live in two stores with a
load-time-only precedence rule between them. amuleapi has never shipped in a
release — this is its first one — so there is no installed base to stay
compatible with and no migration to write: the proposal is simply to delete
the duplication rather than synchronise it. amuleapi-passwords becomes the
single source of truth, the amule.conf keys and EC tags for these two secrets
are removed, and amuleapi changes its own credentials in-process — no restart,
no sync, no precedence. That deletes more code than it adds.
Goal: a user with an admin session can, from the Web UI, change the admin
password, set/change the guest password and disable the guest role,
with the change persisted and effective immediately, in every
deployment mode.
amuleweb is out of scope. It has the same architecture and the same
defects (credentials read once at startup, from amule.conf in WebInterface.cpp:420-430 or from its own remote.conf in WebInterface.cpp:455-461, compared per request in WebServer.cpp:2012-2030,
with no reload path). It is deprecated and slated for removal, so it is
referenced below only as an example of the pattern we are moving away from.
No amuleweb changes are requested.
Part 1 — Current implementation (as of 765d332)
1.1 Two sources of truth, both load-time only
Mode
How amuleapi is started
Credential source
A — aMule-launched
amule/amuled spawns it (src/amule.cpp:1126) with --amule-config-file=<amule.conf>
amule.conf keys /AmuleApi/Password, /AmuleApi/GuestPassword (MD5 hex), applied in memory only — src/webapi/App.cpp:305-317
B — standalone
systemd unit, container, manual run — no --amule-config-file
In mode A amule.confwins and amuleapi-passwords is deliberately left
untouched (src/webapi/App.cpp:305-310). In mode B amule.conf is never read.
Both paths run once, at startup: there is no reload — SIGHUP is mapped to a
soft shutdown (src/webapi/App.cpp:374-379).
1.2 What each entry point does today
Entry point
Persisted where
Reaches the running daemon?
Web UI → PATCH /preferencesremote_controls.amuleapi_password
amule.conf:/AmuleApi/Password — via EC → thePrefs::SetAmuleApiPass() (ECSpecialMuleTags.cpp:687) → glob_prefs->Save() (ExternalConn.cpp:2830)
Never. Mode A: only after amuleapi restarts. Mode B: never at all — that key is never read, and amuleapi-passwords is never written
Web UI → remote_controls.amuleapi_guest_password
—
The key does not exist. Silently ignored (Api.cpp:6225-6330 only handles webserver_guest_password)
amule prefs dialog (local)
amule.conf
Only after aMule restarts and respawns the child. Prompted (PrefsUnifiedDlg.cpp:1128-1140)
amulegui prefs dialog (remote)
pushed over EC → the daemon saves amule.conf
Only after the daemon restarts. The prompt says "aMule must be restarted", which in amulegui reads as "restart amulegui" — restarting the GUI changes nothing
amulegui, field emptied to disable a role
not propagated
The hash tags are only emitted when non-empty (ECSpecialMuleTags.cpp:244-253) and Apply() only writes when the tag is present (ECSpecialMuleTags.cpp:687-692). So the guest role cannot be disabled from a remote amulegui. Locally in amule emptying does work (Cfg_Str_Encrypted::TransferFromWindow, Preferences.cpp:430-444) — a third, different behaviour
amuleapi --set-admin-pass= / --set-guest-pass=
amuleapi-passwords (atomic, 0600)
Needs a restart; and in mode A the value is then overridden by amule.conf
Three entry points, three behaviours, none of which applies the change to the
live daemon. That is the "shouldn't it behave the same?" question answered: it
should, and the way to get there is to have exactly one owner for these secrets.
1.3 Additional problems found
The Web UI field is hidden from exactly the users who need it. amuleapi_password is gated by gatedBy: "amuleapi_enabled"
(preferences.js:217). In a standalone deployment /AmuleApi/Enabled is false in amule.conf (aMule didn't launch it), so GET /preferences
returns "amuleapi_enabled": false and the field is hidden entirely —
verified against a live standalone instance.
No way to express "disable the guest role." The amuleapi guest role is
implicit ("enabled" == "digest non-empty"), and PrefTakePassword
(Api.cpp:5602), WritePasswordsFile (AmuleApiConfig.cpp:511) and the EC
tags all treat empty as "leave unchanged".
Password changes don't invalidate sessions. JWTs stay valid for their full
24 h; revocation is per-jti via /auth/logout only (Auth.h:53-68).
Whoever holds a stolen session keeps it across a rotation.
No current-password confirmation anywhere — an admin session (or an
XSS) is enough to lock the owner out.
Empty string becomes a valid password.PrefTakePassword hashes whatever
it is given, including "" → MD5("") = d41d8cd98f00b204e9800998ecf8486e,
a valid-looking digest. PATCH {"remote_controls":{"amuleapi_password":""}}
therefore makes POST /auth/login {"password":""} succeed. The Web UI skips
empty password inputs client-side (preferences.js:354-356, "leave empty to
keep current"), so it is API-only — but reachable, and webserver_password, webserver_guest_password and proxy_password share the helper.
The daemon never reports its own live state.GET /version returns only
names and versions — nothing about the effective bind address, port,
deployment mode, or whether an admin/guest password exists. The Web UI has no
way to tell truth from amule.conf fiction (Part 2).
Part 2 — Does amuleapi configuration belong in the Web UI?
The credentials: yes — they are amuleapi's own, and the Web UI is its
primary interface.
The rest (amuleapi_enabled, amuleapi_port, amuleapi_bind): not as it
stands. Those keys describe the very process serving the page, and in a
standalone deployment they are fiction — they describe a process aMule would
spawn, not the one answering the request (which took its bind/port from amuleapi.conf or the CLI). A user who edits amuleapi_bind in the Web UI
reasonably expects the API to move; nothing happens, and in standalone mode
nothing ever will. That is worse than not offering the control.
Proposed split in the Web UI:
"This server" — a new section backed by a read-only endpoint (3.3):
effective bind address, port, deployment mode (amule-launched | standalone), config dir, and the booleans admin_password_set / guest_password_set. The credential form (3.2) lives here, not gated by amuleapi_enabled.
Preferences → Remote controls — keeps amuleapi_enabled / _port / _bind as what they really are: the parameters aMule uses when it launches
amuleapi. Relabelled accordingly, and hidden (or explicitly marked inert)
in standalone mode. amuleapi_password is removed from this tab.
Part 3 — Proposed solution
3.1 amuleapi owns its credentials: delete the second store
amuleapi-passwords becomes the single source of truth in both modes.
Remove — not synchronise — the amule.conf half:
Delete the tag codes 0x1509 / 0x150B outright from ECCodes.h and ECCodes.abstract (including their GetTagName() cases) and leave the
numbering gap. Nothing has ever shipped that sends them, so there is no client
to stay compatible with and no reason to renumber EC_TAG_AMULEAPI_BIND just to
close a hole.
No migration is needed either — these keys have never existed in a released amule.conf. A user upgrading from 3.0.x has no /AmuleApi/* section at all,
and a developer running an unreleased build sets the password once through the
first-run flow below.
What this deletes along with the duplication: the precedence rule, the
mode A/B asymmetry, the need for a "clear this credential" encoding in EC, the
need for live reload of amule.conf, and the whole class of
"persisted-but-inert" outcomes in 1.2.
3.2 A dedicated credential endpoint
admin-only, separate from the bulk preferences PATCH — rotation needs
re-authentication, session handling and per-field errors that don't fit a batch
update:
omitted key → unchanged; null → clear that digest (disable the role);
empty string → 400 bad_request (never "hash of the empty string");
clearing the admin digest is refused while the bind address is
non-loopback, mirroring the startup guard at App.cpp:451;
current_password mismatch → 401, counted by the same login rate limiter /auth/login uses (m_rateLimiter), so this is not a brute-force bypass.
The handler does exactly two things — that is the point of 3.1:
Persist via CAmuleApiConfig::WritePasswordsFile(), which is already an
atomic write-temp/fsync/rename at mode 0600 (AmuleApiConfig.cpp:146). It
needs a "keep vs clear" distinction (e.g. std::optional<std::string> per
role) — same for Set{Admin,Guest}PasswordMd5().
Apply in memory, so the change is effective immediately and the HTTP
response still gets delivered (a self-respawn could not guarantee either).
First-run claim. Loopback + no password is an explicitly supported state
(App.cpp:449-451), and in it the whole authenticated surface is unusable
(login returns 503 login_disabled). So a desktop user whose amuleapi was
launched by aMule needs a way in without a terminal. Allow unauthenticated POST /api/v0/auth/passwordsonly when no admin digest is configured and
the peer is loopback; it sets the initial admin password (and optionally the
guest one) and returns a session. Any other combination → 403. Recovery from
a forgotten password stays amuleapi --set-admin-pass=…, where filesystem
access is the proof of ownership. This replaces what the aMule prefs dialog
used to provide, and it works identically in both modes.
3.3 Report the live state
New read-only endpoint (or an additive section on GET /version):
Digests are never exposed. setup_required drives the first-run claim screen; mode lets the Web UI hide the autostart fields that are inert in standalone
mode (Part 2). Decide whether it is public or admin-gated — setup_required
has to be readable before any session exists, so either the endpoint is public
with a minimal field set pre-setup, or setup_required is surfaced on the
already-public /version.
3.4 Invalidate other sessions on a password change
A rotation that leaves old JWTs valid is not a rotation. Cheapest correct
mechanism: a per-role min_iat epoch in CJwt — reject any verified token
whose iat predates the epoch for its role. Bump the epoch for the role whose
password changed, then re-issue the caller's own token and return it via Set-Cookie (same shape as /auth/login) so the initiator is not logged out.
Trade-off for the implementer: an in-memory epoch means revoked sessions become
valid again after an amuleapi restart (the JWT secret is persistent).
Persisting it as one key in amuleapi.conf ([Auth]/MinIssuedAt) closes that
hole for one config line.
3.5 Web UI (src/webapi/static)
New "This server" section: live values from 3.3 plus the credential form
— current password, new admin password + confirm, guest password +
confirm, and a "Disable guest access" toggle that sends "guest_password": null. Guest inputs hidden while the toggle is on. Not
gated by amuleapi_enabled.
First-run screen when setup_required is true: ask for the admin
password (+ confirm) and call the claim endpoint, instead of showing a login
form that can only return 503.
Remove amuleapi_password from Preferences → Remote controls; relabel the
remaining amuleapi fields as aMule's autostart parameters and hide them in
standalone mode.
New i18n keys in en.json / es.json; success state mentions that sessions
on other devices were signed out.
3.6 Docs
docs/api/REFERENCE.md — the new endpoints; update Role model, which
currently says the two passwords are configured via --set-admin-pass / --set-guest-pass only.
docs/QUICKSTART-AMULEAPI.md — rewrite Auto-starting from aMule: aMule no
longer carries amuleapi credentials; the first-run flow is loopback + claim
(or the CLI); passwords are changed at runtime from the Web UI with no
restart.
docs/CHANGELOG.md — no compatibility note needed; this is part of the
initial amuleapi feature, which has not shipped yet.
Open decisions for the implementer
amuleapi_port / amuleapi_bind duplication. The same single-owner
argument applies: in mode A aMule passes --bind / --http-port, which
override amuleapi.conf, so the live values can disagree with both stores.
Cleanest would be to let amuleapi.conf own the HTTP binding and reduce
aMule's role to "launch it or not". The counter-argument is the desktop
escape hatch: a user who cannot reach the Web UI (wrong port) can currently
fix it from the GUI. Suggested for now: leave the mechanism alone, make the
UI honest (Part 2), and revisit separately.
Public vs gated /server/info — see 3.3.
min_iat persistence — see 3.4.
Acceptance criteria
amuleapi-passwords is the only credential store; /AmuleApi/Password
and /AmuleApi/GuestPassword no longer exist in amule.conf, in thePrefs, in the EC prefs packet or in the preferences dialog.
PATCH /api/v0/auth/passwords exists, is admin-only, requires current_password, and is covered by the login rate limiter.
Changing the admin password takes effect immediately — old password
rejected, new one accepted, no restart — in both modes, and survives a
restart of amuleapi and of the core.
The guest password can be set, changed and disabled from the Web UI;
once disabled, login with the old guest password returns 401.
Clearing the admin digest is refused when the bind address is not
loopback.
First run on loopback with no admin password: the Web UI offers a setup
screen and the claim endpoint sets the password. The claim endpoint
returns 403 once an admin digest exists, and 403 from a non-loopback
peer.
Empty plaintext is rejected with 400 on every remaining password field
(webserver, webserver_guest, proxy) — no more MD5("") credentials.
After an admin password change, previously issued admin tokens are
rejected while the caller's own session keeps working.
A read-only endpoint reports the live bind/port/mode and admin_password_set / guest_password_set; the Web UI no longer infers
amuleapi's state from amule.conf.
Passwords are never returned by any GET; plaintext is never logged nor
written to disk. amuleapi-passwords keeps mode 0600 and its atomic
write.
Docs and changelog updated.
Files involved
File
Why
src/webapi/Api.cpp
new credential + server-info handlers and routes; remove amuleapi_password; PrefTakePassword empty-string guard
src/webapi/AmuleApiConfig.{h,cpp}
"clear digest" support; drop the in-memory-override contract
src/webapi/App.cpp
remove the amule.conf credential handoff; expose mode + effective bind/port to the dispatcher
Standalone amuleapi (mode B), admin password admin. Step 1 was run against a
live instance; steps 2-4 follow from the code paths cited above.
T=$(curl -s -X POST http://127.0.0.1:4713/api/v0/auth/login \ -H 'Accept: application/jwt' -d '{"password":"admin"}' \| sed -n 's/.*"token":"\([^"]*\)".*/\1/p')# 1. amuleapi_enabled is false in a standalone deployment → the Web UI hides# the admin password field entirely.
curl -s http://127.0.0.1:4713/api/v0/preferences -H "Authorization: Bearer $T" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["remote_controls"])'# {... 'amuleapi_enabled': False, 'amuleapi_port': 4713, 'amuleapi_bind': '127.0.0.1'}# 2. Change the password through the documented path.
curl -s -X PATCH http://127.0.0.1:4713/api/v0/preferences \
-H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"remote_controls":{"amuleapi_password":"newSecret123"}}'# 3. The new password does not work — and never will, in this mode:
curl -s -X POST http://127.0.0.1:4713/api/v0/auth/login -d '{"password":"newSecret123"}'# 401 invalid_credentials (before AND after restarting amuleapi)# The old password still works. `amuleapi-passwords` is unchanged; the hash# went into amule.conf:/AmuleApi/Password, which a standalone amuleapi# never reads.# 4. There is no amuleapi guest password key at all — silently ignored:
curl -s -X PATCH http://127.0.0.1:4713/api/v0/preferences \
-H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"remote_controls":{"amuleapi_guest_password":"guestpw"}}'
From amulegui connected to a remote amuled: open Preferences → Remote
Controls, clear the guest password field and apply. The daemon keeps
accepting the old guest password — the cleared value is never sent (1.2, last
row).
The Web UI exposes an amuleapi admin password field, but changing it does
not do what a user expects: the new password never reaches the running daemon,
and in a standalone deployment it is silently discarded forever. The
guest password has no API surface at all — it cannot be set, changed or
disabled from the Web UI.
The root cause is that amuleapi's credentials live in two stores with a
load-time-only precedence rule between them. amuleapi has never shipped in a
release — this is its first one — so there is no installed base to stay
compatible with and no migration to write: the proposal is simply to delete
the duplication rather than synchronise it.
amuleapi-passwordsbecomes thesingle source of truth, the
amule.confkeys and EC tags for these two secretsare removed, and amuleapi changes its own credentials in-process — no restart,
no sync, no precedence. That deletes more code than it adds.
Goal: a user with an
adminsession can, from the Web UI, change the adminpassword, set/change the guest password and disable the guest role,
with the change persisted and effective immediately, in every
deployment mode.
Part 1 — Current implementation (as of
765d332)1.1 Two sources of truth, both load-time only
amule/amuledspawns it (src/amule.cpp:1126) with--amule-config-file=<amule.conf>amule.confkeys/AmuleApi/Password,/AmuleApi/GuestPassword(MD5 hex), applied in memory only —src/webapi/App.cpp:305-317--amule-config-file${config_dir}/amuleapi-passwords(admin=<md5>/guest=<md5>, mode 0600) —src/webapi/AmuleApiConfig.cpp:407In mode A
amule.confwins andamuleapi-passwordsis deliberately leftuntouched (
src/webapi/App.cpp:305-310). In mode Bamule.confis never read.Both paths run once, at startup: there is no reload —
SIGHUPis mapped to asoft shutdown (
src/webapi/App.cpp:374-379).1.2 What each entry point does today
PATCH /preferencesremote_controls.amuleapi_passwordamule.conf:/AmuleApi/Password— via EC →thePrefs::SetAmuleApiPass()(ECSpecialMuleTags.cpp:687) →glob_prefs->Save()(ExternalConn.cpp:2830)amuleapi-passwordsis never writtenremote_controls.amuleapi_guest_passwordApi.cpp:6225-6330only handleswebserver_guest_password)amuleprefs dialog (local)amule.confPrefsUnifiedDlg.cpp:1128-1140)amuleguiprefs dialog (remote)amule.confamuleguireads as "restart amulegui" — restarting the GUI changes nothingamulegui, field emptied to disable a roleECSpecialMuleTags.cpp:244-253) andApply()only writes when the tag is present (ECSpecialMuleTags.cpp:687-692). So the guest role cannot be disabled from a remote amulegui. Locally inamuleemptying does work (Cfg_Str_Encrypted::TransferFromWindow,Preferences.cpp:430-444) — a third, different behaviouramuleapi --set-admin-pass=/--set-guest-pass=amuleapi-passwords(atomic, 0600)amule.confThree entry points, three behaviours, none of which applies the change to the
live daemon. That is the "shouldn't it behave the same?" question answered: it
should, and the way to get there is to have exactly one owner for these secrets.
1.3 Additional problems found
amuleapi_passwordis gated bygatedBy: "amuleapi_enabled"(
preferences.js:217). In a standalone deployment/AmuleApi/Enabledisfalseinamule.conf(aMule didn't launch it), soGET /preferencesreturns
"amuleapi_enabled": falseand the field is hidden entirely —verified against a live standalone instance.
implicit ("enabled" == "digest non-empty"), and
PrefTakePassword(
Api.cpp:5602),WritePasswordsFile(AmuleApiConfig.cpp:511) and the ECtags all treat empty as "leave unchanged".
24 h; revocation is per-
jtivia/auth/logoutonly (Auth.h:53-68).Whoever holds a stolen session keeps it across a rotation.
adminsession (or anXSS) is enough to lock the owner out.
PrefTakePasswordhashes whateverit is given, including
""→MD5("")=d41d8cd98f00b204e9800998ecf8486e,a valid-looking digest.
PATCH {"remote_controls":{"amuleapi_password":""}}therefore makes
POST /auth/login {"password":""}succeed. The Web UI skipsempty password inputs client-side (
preferences.js:354-356, "leave empty tokeep current"), so it is API-only — but reachable, and
webserver_password,webserver_guest_passwordandproxy_passwordshare the helper.GET /versionreturns onlynames and versions — nothing about the effective bind address, port,
deployment mode, or whether an admin/guest password exists. The Web UI has no
way to tell truth from
amule.conffiction (Part 2).Part 2 — Does amuleapi configuration belong in the Web UI?
The credentials: yes — they are amuleapi's own, and the Web UI is its
primary interface.
The rest (
amuleapi_enabled,amuleapi_port,amuleapi_bind): not as itstands. Those keys describe the very process serving the page, and in a
standalone deployment they are fiction — they describe a process aMule would
spawn, not the one answering the request (which took its bind/port from
amuleapi.confor the CLI). A user who editsamuleapi_bindin the Web UIreasonably expects the API to move; nothing happens, and in standalone mode
nothing ever will. That is worse than not offering the control.
Proposed split in the Web UI:
effective bind address, port, deployment mode (
amule-launched|standalone), config dir, and the booleansadmin_password_set/guest_password_set. The credential form (3.2) lives here, not gated byamuleapi_enabled.Preferences → Remote controls— keepsamuleapi_enabled/_port/_bindas what they really are: the parameters aMule uses when it launchesamuleapi. Relabelled accordingly, and hidden (or explicitly marked inert)
in standalone mode.
amuleapi_passwordis removed from this tab.Part 3 — Proposed solution
3.1 amuleapi owns its credentials: delete the second store
amuleapi-passwordsbecomes the single source of truth in both modes.Remove — not synchronise — the
amule.confhalf:/AmuleApi/Password,/AmuleApi/GuestPasswordconfig itemsPreferences.cpp:1392-1394thePrefs::{Get,Set}AmuleApiPass/…GuestPass+ their staticsPreferences.h:554-557,Preferences.h:1052-1053,Preferences.cpp:189-190EC_TAG_AMULEAPI_PASSWD/EC_TAG_AMULEAPI_GUEST_PASSWDECSpecialMuleTags.cpp:244-253,ECSpecialMuleTags.cpp:687-692muuli_wdr.cpp:2102,2107,muuli_wdr.h:340,PrefsUnifiedDlg.cpp:1136-1137--amule-config-filecredential handoffApp.cpp:305-317remote_controls.amuleapi_password(API + UI + i18n key)Api.cpp:6281-6286,preferences.js:217,i18n/*.jsonAmuleApiConfig.h:116-126,docs/QUICKSTART-AMULEAPI.md:140-158Delete the tag codes
0x1509/0x150Boutright fromECCodes.handECCodes.abstract(including theirGetTagName()cases) and leave thenumbering gap. Nothing has ever shipped that sends them, so there is no client
to stay compatible with and no reason to renumber
EC_TAG_AMULEAPI_BINDjust toclose a hole.
No migration is needed either — these keys have never existed in a released
amule.conf. A user upgrading from 3.0.x has no/AmuleApi/*section at all,and a developer running an unreleased build sets the password once through the
first-run flow below.
What this deletes along with the duplication: the precedence rule, the
mode A/B asymmetry, the need for a "clear this credential" encoding in EC, the
need for live reload of
amule.conf, and the whole class of"persisted-but-inert" outcomes in 1.2.
3.2 A dedicated credential endpoint
admin-only, separate from the bulk preferences PATCH — rotation needsre-authentication, session handling and per-field errors that don't fit a batch
update:
null→ clear that digest (disable the role);400 bad_request(never "hash of the empty string");non-loopback, mirroring the startup guard at
App.cpp:451;current_passwordmismatch →401, counted by the same login rate limiter/auth/loginuses (m_rateLimiter), so this is not a brute-force bypass.The handler does exactly two things — that is the point of 3.1:
CAmuleApiConfig::WritePasswordsFile(), which is already anatomic write-temp/fsync/rename at mode 0600 (
AmuleApiConfig.cpp:146). Itneeds a "keep vs clear" distinction (e.g.
std::optional<std::string>perrole) — same for
Set{Admin,Guest}PasswordMd5().response still gets delivered (a self-respawn could not guarantee either).
Response:
{ "admin_password_set": true, "guest_password_set": false }First-run claim. Loopback + no password is an explicitly supported state
(
App.cpp:449-451), and in it the whole authenticated surface is unusable(login returns
503 login_disabled). So a desktop user whose amuleapi waslaunched by aMule needs a way in without a terminal. Allow unauthenticated
POST /api/v0/auth/passwordsonly when no admin digest is configured andthe peer is loopback; it sets the initial admin password (and optionally the
guest one) and returns a session. Any other combination →
403. Recovery froma forgotten password stays
amuleapi --set-admin-pass=…, where filesystemaccess is the proof of ownership. This replaces what the aMule prefs dialog
used to provide, and it works identically in both modes.
3.3 Report the live state
New read-only endpoint (or an additive section on
GET /version):{ "mode": "standalone" | "amule-launched", "bind_address": "127.0.0.1", "port": 4713, "config_dir": "/home/u/.aMule", "admin_password_set": true, "guest_password_set": false, "setup_required": false }Digests are never exposed.
setup_requireddrives the first-run claim screen;modelets the Web UI hide the autostart fields that are inert in standalonemode (Part 2). Decide whether it is public or
admin-gated —setup_requiredhas to be readable before any session exists, so either the endpoint is public
with a minimal field set pre-setup, or
setup_requiredis surfaced on thealready-public
/version.3.4 Invalidate other sessions on a password change
A rotation that leaves old JWTs valid is not a rotation. Cheapest correct
mechanism: a per-role
min_iatepoch inCJwt— reject any verified tokenwhose
iatpredates the epoch for its role. Bump the epoch for the role whosepassword changed, then re-issue the caller's own token and return it via
Set-Cookie(same shape as/auth/login) so the initiator is not logged out.Trade-off for the implementer: an in-memory epoch means revoked sessions become
valid again after an amuleapi restart (the JWT secret is persistent).
Persisting it as one key in
amuleapi.conf([Auth]/MinIssuedAt) closes thathole for one config line.
3.5 Web UI (
src/webapi/static)— current password, new admin password + confirm, guest password +
confirm, and a "Disable guest access" toggle that sends
"guest_password": null. Guest inputs hidden while the toggle is on. Notgated by
amuleapi_enabled.setup_requiredis true: ask for the adminpassword (+ confirm) and call the claim endpoint, instead of showing a login
form that can only return
503.amuleapi_passwordfromPreferences → Remote controls; relabel theremaining amuleapi fields as aMule's autostart parameters and hide them in
standalone mode.
en.json/es.json; success state mentions that sessionson other devices were signed out.
3.6 Docs
docs/api/REFERENCE.md— the new endpoints; update Role model, whichcurrently says the two passwords are configured via
--set-admin-pass/--set-guest-passonly.docs/QUICKSTART-AMULEAPI.md— rewrite Auto-starting from aMule: aMule nolonger carries amuleapi credentials; the first-run flow is loopback + claim
(or the CLI); passwords are changed at runtime from the Web UI with no
restart.
docs/CHANGELOG.md— no compatibility note needed; this is part of theinitial amuleapi feature, which has not shipped yet.
Open decisions for the implementer
amuleapi_port/amuleapi_bindduplication. The same single-ownerargument applies: in mode A aMule passes
--bind/--http-port, whichoverride
amuleapi.conf, so the live values can disagree with both stores.Cleanest would be to let
amuleapi.confown the HTTP binding and reduceaMule's role to "launch it or not". The counter-argument is the desktop
escape hatch: a user who cannot reach the Web UI (wrong port) can currently
fix it from the GUI. Suggested for now: leave the mechanism alone, make the
UI honest (Part 2), and revisit separately.
/server/info— see 3.3.min_iatpersistence — see 3.4.Acceptance criteria
amuleapi-passwordsis the only credential store;/AmuleApi/Passwordand
/AmuleApi/GuestPasswordno longer exist inamule.conf, inthePrefs, in the EC prefs packet or in the preferences dialog.PATCH /api/v0/auth/passwordsexists, isadmin-only, requirescurrent_password, and is covered by the login rate limiter.rejected, new one accepted, no restart — in both modes, and survives a
restart of amuleapi and of the core.
once disabled, login with the old guest password returns
401.loopback.
screen and the claim endpoint sets the password. The claim endpoint
returns
403once an admin digest exists, and403from a non-loopbackpeer.
400on every remaining password field(
webserver,webserver_guest,proxy) — no moreMD5("")credentials.admintokens arerejected while the caller's own session keeps working.
admin_password_set/guest_password_set; the Web UI no longer infersamuleapi's state from
amule.conf.GET; plaintext is never logged norwritten to disk.
amuleapi-passwordskeeps mode 0600 and its atomicwrite.
Files involved
src/webapi/Api.cppamuleapi_password;PrefTakePasswordempty-string guardsrc/webapi/AmuleApiConfig.{h,cpp}src/webapi/App.cppamule.confcredential handoff; expose mode + effective bind/port to the dispatchersrc/webapi/Auth.{h,cpp}min_iatepoch; unauthenticated loopback claim pathsrc/Preferences.{h,cpp},src/ECSpecialMuleTags.cpp,src/muuli_wdr.{h,cpp},src/PrefsUnifiedDlg.cppsrc/webapi/static/js/views/preferences.js,src/webapi/static/i18n/*.jsondocs/api/REFERENCE.md,docs/QUICKSTART-AMULEAPI.mdReproduction
Standalone amuleapi (mode B), admin password
admin. Step 1 was run against alive instance; steps 2-4 follow from the code paths cited above.
From
amuleguiconnected to a remoteamuled: open Preferences → RemoteControls, clear the guest password field and apply. The daemon keeps
accepting the old guest password — the cleared value is never sent (1.2, last
row).