mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
mc-webui has no login, which is fine on a LAN and not fine once the address is handed to testers: they assume the node is theirs and rename it, and the console is right there for anyone curious enough to type into it. MC_DEMO=true refuses everything that reconfigures the radio, destroys data, reaches other people's repeaters, rebuilds the containers, publishes to an external broker, or hands out a secret. Reading and sending messages keep working, so the instance is still worth visiting. The guard is default-deny: an api_bp.before_request hook refuses every non-GET unless it is on a short allowlist, so an endpoint added later is locked without touching this module. That mattered — six mutating endpoints already had no frontend caller and would never have made a hand-written blacklist. A small read denylist covers the GETs that leak (repeater passwords, backup download, capture download, /api/logs), which the write guard never sees. Two paths bypass the HTTP layer and are closed separately: the console is a Socket.IO event, not a route, so it is checked against a read-only command set; the log viewer is refused at the connect handshake. Access is kept two ways. MC_DEMO_TRUSTED_NETS unlocks by CIDR, and MC_DEMO_UNLOCK_CODE unlocks by a code typed once in Settings and held in an HMAC cookie for 30 days — stateless, and deliberately not Flask's session, whose SECRET_KEY is a hardcoded public constant. The trap the network list walks into: published through a tunnel, every visitor arrives from the tunnel's address, and if the tunnel runs on the LAN then trusting the LAN unlocks the instance for the whole internet. An address arriving with a proxy header is therefore not matched unless MC_TRUST_PROXY is on, and startup logs report the resulting shape. Frontend is advisory only: demo-lock.js loads from _head_i18n.html so it reaches all eight entry points including the six iframes, greys out [data-demo-lock] rather than hiding it (the instance exists to show the app off), re-applies via MutationObserver for JS-rendered buttons, and turns any 403 demo_locked into a toast. Verified on the local container: the rename, reboot, updater, vacuum, cleanup and the leaky GETs all 403; sending, read markers and ordinary reads pass; `set name` is refused over the console socket while `infos` still answers; unlock/lock round-trips. 86 new unit tests, en/pl at 100%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
25 lines
1.2 KiB
HTML
25 lines
1.2 KiB
HTML
{# Translation catalog + runtime.
|
|
|
|
Both must block: window.t has to exist before any inline handler or page script runs,
|
|
so never add defer/async. Include this in every <head>, immediately after the theme
|
|
pre-apply IIFE, giving a uniform ordering: theme -> i18n -> everything else.
|
|
|
|
The catalog URL is content-hashed and served immutable, so all 8 entry points
|
|
(including the 6 iframes) share a single cached fetch. #}
|
|
<script src="{{ i18n_catalog_url }}"></script>
|
|
<script src="{{ url_for('static', filename='js/i18n-runtime.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/datetime-utils.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/clipboard-utils.js') }}"></script>
|
|
|
|
{# Demo mode. Injected here rather than through MC_CONFIG because only index.html
|
|
and dm.html define that object, and the console — the page most in need of
|
|
locking — is not one of them. Must be set before demo-lock.js runs. #}
|
|
<script>
|
|
window.MC_DEMO = {
|
|
enabled: {{ 'true' if demo_mode else 'false' }},
|
|
unlocked: {{ 'true' if demo_unlocked else 'false' }},
|
|
hasUnlockCode: {{ 'true' if demo_has_unlock_code else 'false' }}
|
|
};
|
|
</script>
|
|
<script src="{{ url_for('static', filename='js/demo-lock.js') }}"></script>
|