mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
Compare commits
No commits in common. "master" and "v0.15.10" have entirely different histories.
8 changed files with 2 additions and 439 deletions
|
|
@ -33,23 +33,8 @@ bin/ln-cli bob fwdinghistory
|
|||
docker compose logs -f rtl
|
||||
```
|
||||
|
||||
Testing the **BTCPay Server integration** (RTL in single-sign-on mode behind a proxy) —
|
||||
behind a compose profile, so a plain `up` does not start it:
|
||||
|
||||
```bash
|
||||
docker compose --profile sso up -d
|
||||
./scripts/verify-sso.sh # 11 assertions over the whole entry path; non-zero on failure
|
||||
open "$(bin/sso-url)" # the link BTCPay renders on its Services page
|
||||
```
|
||||
|
||||
Key facts when working with the fixture:
|
||||
|
||||
- **Run `scripts/verify-sso.sh` after touching authentication, CSRF or static serving.**
|
||||
BTCPay reaches RTL over a path the standalone login never exercises — a rotating cookie
|
||||
file, an unregistered `/rtl/api/authenticate/cookie` URL that falls through to the
|
||||
catch-all in `server/utils/app.ts`, and a reverse proxy. Note `GET /rtl/` is served by
|
||||
`express.static` and mints **no** `XSRF-TOKEN`; only the catch-all does, so a client
|
||||
entering there 403s on its first POST. That is long-standing, not a regression.
|
||||
- **`scripts/seed.sh` is deterministic but not idempotent.** Every amount is fixed, so a
|
||||
fresh run always produces identical state (screenshots differ only by your change) — so
|
||||
**do not introduce randomness**. It refuses to run twice against an already-seeded
|
||||
|
|
|
|||
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -50,10 +50,7 @@ frequently do, since the controllers were written in parallel.
|
|||
`ERESOLVE` conflict from `@fortawesome/angular-fontawesome`.
|
||||
- **`npm run server` only works on Windows** — it sets `NODE_ENV` with `set X=Y&&` syntax. On
|
||||
macOS/Linux use `npm run serverUbuntu`.
|
||||
- **`npm run lint` and `npm run test` must both be green before a PR.** Nothing will check
|
||||
this for you: no build or test CI runs on an open PR. `checks.yml` fires on
|
||||
`pull_request: closed` (i.e. on merge) and on tags/releases, and `rtlreviewbot` only on a
|
||||
requested review or a comment — so running both locally is the only gate before merge.
|
||||
- **`npm run lint` and `npm run test` must both be green before a PR.**
|
||||
- If lint reports hundreds of template "Parsing error" failures, look for a stale
|
||||
**`coverage/`** directory (git-ignored Karma output). The template linter walks its HTML
|
||||
report. Delete it and re-run.
|
||||
|
|
@ -94,17 +91,6 @@ Eclair, wired to RTL — for end-to-end testing across all three implementations
|
|||
`docker/README.md`, or the `rtl-docker-fixture` skill in `.claude/skills/`. It is dev-only;
|
||||
every credential in it is throwaway.
|
||||
|
||||
The fixture also carries a **BTCPay Server SSO harness** behind a compose profile
|
||||
(`docker compose --profile sso up -d`). BTCPay bundles RTL and reaches it over a path the
|
||||
standalone login never exercises: a rotating cookie file, an unregistered
|
||||
`/rtl/api/authenticate/cookie` URL that is not a route at all and falls through to the
|
||||
catch-all in `server/utils/app.ts`, and a reverse proxy serving it under `/rtl`. Run
|
||||
`docker/scripts/verify-sso.sh` (11 assertions, exits non-zero) after touching
|
||||
authentication, CSRF or static serving — none of that path is covered by logging into the
|
||||
fixture's own RTL. One trap it encodes: `GET /rtl/` is served by `express.static`, which
|
||||
sits above the catch-all and mints no `XSRF-TOKEN`, so a client entering there gets a 403
|
||||
on its first POST. That is long-standing behaviour, not a regression.
|
||||
|
||||
Backend regression tests live in `test/backend/` (plain `node:test`, run against the
|
||||
compiled `backend/`). `npm run test` compiles the backend, then runs them
|
||||
(`npm run testbackend`) before the frontend Karma/Jasmine specs, so they never test stale
|
||||
|
|
|
|||
|
|
@ -74,9 +74,6 @@ docker compose up -d # bitcoind, alice, bob, carol, cln, eclair, rtl
|
|||
./scripts/seed.sh # fund, connect, open channels, make payments
|
||||
```
|
||||
|
||||
To also bring up the BTCPay single-sign-on harness, add `--profile sso` — see
|
||||
[BTCPay SSO harness](#btcpay-sso-harness).
|
||||
|
||||
Then open <http://localhost:3000> — password `rtldev`. All five nodes (alice, bob,
|
||||
carol, cln, eclair) appear in the node switcher.
|
||||
|
||||
|
|
@ -121,7 +118,6 @@ bin/ln-cli bob listchannels
|
|||
bin/ln-cli bob fwdinghistory # forwarding history
|
||||
bin/e-cli getinfo # eclair-cli
|
||||
bin/e-cli channels
|
||||
bin/sso-url # BTCPay-style SSO link (needs --profile sso)
|
||||
docker compose exec cln lightning-cli --network=regtest listpeerchannels # Core Lightning
|
||||
```
|
||||
|
||||
|
|
@ -132,94 +128,6 @@ docker compose logs -f rtl
|
|||
docker compose logs alice
|
||||
```
|
||||
|
||||
## BTCPay SSO harness
|
||||
|
||||
BTCPay Server bundles RTL and runs it in single-sign-on mode, reached through a very
|
||||
different entry path than the standalone login: no password, a rotating cookie, and a
|
||||
reverse proxy in front. That path has broken before without the standalone flow
|
||||
noticing, so the fixture can reproduce it.
|
||||
|
||||
It is behind a compose profile, so a plain `docker compose up -d` does not start it:
|
||||
|
||||
```bash
|
||||
docker compose --profile sso up -d
|
||||
./scripts/verify-sso.sh # 11 assertions over the whole entry path
|
||||
open "$(bin/sso-url)" # or click through it yourself
|
||||
```
|
||||
|
||||
`bin/sso-url` prints the link BTCPay renders on its Services page. Following it lands
|
||||
you in RTL already authenticated, against the `alice` node.
|
||||
|
||||
### How the flow works
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser
|
||||
participant P as rtl-sso-proxy<br/>(stands in for traefik)
|
||||
participant R as rtl-sso<br/>(RTL_SSO=1)
|
||||
participant C as .cookie<br/>(shared volume)
|
||||
|
||||
R->>C: writes 64 random bytes at startup
|
||||
Note over B: bin/sso-url reads the cookie —<br/>BTCPay reads the same file
|
||||
B->>P: GET /rtl/api/authenticate/cookie?access-key=<cookie>
|
||||
P->>R: same URI, prefix passed through
|
||||
R-->>B: not a registered route → catch-all:<br/>mints XSRF-TOKEN, serves index.html
|
||||
B->>P: POST /rtl/api/authenticate<br/>{ PASSWORD, sha256(access-key) }
|
||||
P->>R:
|
||||
R->>C: matches → rotates the cookie
|
||||
R-->>B: JWT
|
||||
```
|
||||
|
||||
The three services are `rtl-sso-config-init` (stages `rtl/RTL-Config.sso.json`, same
|
||||
copy-into-a-volume dance as the standalone RTL), `rtl-sso` (RTL with `RTL_SSO=1`,
|
||||
`RTL_COOKIE_PATH` and `LOGOUT_REDIRECT_LINK` — the env block is lifted verbatim from
|
||||
BTCPay's own compose fragment), and `rtl-sso-proxy` (nginx standing in for BTCPay's
|
||||
traefik). `RTL_IMAGE` overrides both RTL containers at once, so a branch build gets
|
||||
tested through both entry paths.
|
||||
|
||||
It is a second RTL container rather than a flag on the first because RTL picks one
|
||||
authentication mode at startup — SSO and the password login cannot coexist in one
|
||||
instance. Both are up at the same time on different ports.
|
||||
|
||||
### Things this makes visible
|
||||
|
||||
**No prefix stripping anywhere.** RTL is built with `<base href="/rtl/">` and mounts
|
||||
every route under `baseHref '/rtl'`, so BTCPay's traefik — and the nginx here — pass
|
||||
`/rtl/…` through unmodified. The proxy deliberately 404s everything outside `/rtl`, so
|
||||
a request escaping the prefix shows up as a failure instead of being quietly served.
|
||||
|
||||
**The entry URL is not a real route.** `/rtl/api/authenticate/cookie` matches nothing in
|
||||
`server/routes/shared/authenticate.ts`; it falls through to the catch-all in
|
||||
`server/utils/app.ts`, which is what mints the `XSRF-TOKEN` cookie and serves the SPA.
|
||||
The access-key is the raw cookie file content — the frontend sha256s it before posting
|
||||
and the backend compares against `sha256(cookieValue)`.
|
||||
|
||||
**`GET /rtl/` mints no CSRF token.** That path is served by `express.static`, which
|
||||
sits *above* the catch-all, so a client entering there has no `XSRF-TOKEN` and its first
|
||||
POST gets a 403. Only the catch-all mints one. This is long-standing behaviour, not a
|
||||
regression — but it is why `verify-sso.sh` always seeds its cookie jar from the entry
|
||||
URL, and worth remembering before concluding that CSRF is broken.
|
||||
|
||||
**The cookie is effectively single-use.** Authenticating rotates it, so a stale
|
||||
`bin/sso-url` link fails. BTCPay re-reads the file on every page render, which is why
|
||||
this is invisible in normal use.
|
||||
|
||||
### What it does not cover
|
||||
|
||||
BTCPay itself is not here — no postgres, nbxplorer or btcpayserver container. So this
|
||||
does not exercise BTCPay *generating* the link, its Services page, or its own upgrades.
|
||||
For that, run BTCPay's own regtest stack and point it at a local image:
|
||||
|
||||
```bash
|
||||
# in a btcpayserver-docker checkout, after building an RTL image locally
|
||||
docker build -t shahanafarooqui/rtl:dev /path/to/RTL
|
||||
# then edit the rtl image tag in the generated docker-compose, or set it in
|
||||
# docker-compose-generator/docker-fragments/bitcoin-lnd.yml before generating
|
||||
```
|
||||
|
||||
That tests the real composition rather than this reconstruction of it; the harness here
|
||||
is the fast everyday check.
|
||||
|
||||
## Notes and gotchas
|
||||
|
||||
**RTL's config.** `rtl/RTL-Config.regtest.json` is the tracked template. RTL rewrites
|
||||
|
|
@ -272,7 +180,3 @@ one LND uses, eclair never sees new blocks and channels never confirm.
|
|||
## Not included
|
||||
|
||||
The Boltz swap service.
|
||||
|
||||
BTCPay Server itself (postgres + nbxplorer + btcpayserver). The `sso` profile
|
||||
reproduces the entry path BTCPay uses to reach RTL without running BTCPay — see
|
||||
[BTCPay SSO harness](#btcpay-sso-harness) for what that covers and what it does not.
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Print the BTCPay-style single-sign-on entry URL for the SSO harness.
|
||||
#
|
||||
# docker compose --profile sso up -d
|
||||
# bin/sso-url # print it
|
||||
# open "$(bin/sso-url)" # or follow it straight into RTL
|
||||
#
|
||||
# This is the link BTCPay renders on its Services page. BTCPay builds it from
|
||||
# BTCPAY_BTCEXTERNALRTL="server=/rtl/api/authenticate/cookie;cookiefile=..."
|
||||
# by reading the cookie file RTL wrote and appending it as ?access-key=. The
|
||||
# value is the raw file content: RTL's frontend sha256s it before posting
|
||||
# (src/app/app.component.ts) and the backend compares against
|
||||
# sha256(cookieValue), so no hashing happens here.
|
||||
#
|
||||
# Authenticating rotates the cookie (common.refreshCookie), so re-run this for
|
||||
# each login -- exactly as BTCPay re-reads the file on every page render.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
# shellcheck disable=SC1091
|
||||
[ -f .env ] && source .env
|
||||
|
||||
port="${RTL_SSO_PORT:-3001}"
|
||||
|
||||
if ! docker compose --profile sso ps --status running --services 2>/dev/null | grep -qx rtl-sso; then
|
||||
echo "rtl-sso is not running. Start it with: docker compose --profile sso up -d" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cookie="$(docker compose --profile sso exec -T rtl-sso cat /RTL/cookie/.cookie | tr -d '\r\n')"
|
||||
|
||||
if [ -z "$cookie" ]; then
|
||||
echo "The cookie file /RTL/cookie/.cookie is empty. Is RTL_SSO=1 set on rtl-sso?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "http://localhost:${port}/rtl/api/authenticate/cookie?access-key=${cookie}"
|
||||
|
|
@ -17,9 +17,6 @@ volumes:
|
|||
eclair_data:
|
||||
rtl_db:
|
||||
rtl_config:
|
||||
rtl_sso_db:
|
||||
rtl_sso_config:
|
||||
rtl_sso_cookie:
|
||||
|
||||
x-lnd: &lnd
|
||||
image: polarlightning/lnd:0.20.0-beta
|
||||
|
|
@ -288,7 +285,7 @@ services:
|
|||
container_name: ${COMPOSE_PROJECT_NAME}_rtl
|
||||
# Defaults to the published image; override with RTL_IMAGE (e.g. a locally built
|
||||
# branch image) to test unreleased changes: RTL_IMAGE=rtl:pr1625 docker compose up.
|
||||
image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.10}
|
||||
image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.8}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
rtl-config-init:
|
||||
|
|
@ -314,87 +311,3 @@ services:
|
|||
- carol_data:/lnd/carol:ro
|
||||
- cln_data:/cln:ro
|
||||
- rtl_db:/RTL/database
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BTCPay Server SSO harness -- profile "sso", so a plain 'up' does not start it:
|
||||
#
|
||||
# docker compose --profile sso up -d
|
||||
# bin/sso-url
|
||||
#
|
||||
# BTCPay bundles RTL as a service and runs it in single-sign-on mode. RTL
|
||||
# writes a random cookie to RTL_COOKIE_PATH; BTCPay reads that file and renders
|
||||
# a link to /rtl/api/authenticate/cookie?access-key=<cookie> on its Services
|
||||
# page. That path is not a registered route -- it falls through to RTL's
|
||||
# catch-all, which mints the XSRF-TOKEN cookie and serves index.html, and the
|
||||
# SPA then reads access-key from the query string and posts it as a password
|
||||
# login. Reproducing that entry path is the whole point of this harness: it is
|
||||
# where RTL's auth, CSRF and static-serving behaviour meet an external caller,
|
||||
# and it has broken before without the standalone flow noticing.
|
||||
#
|
||||
# BTCPay itself (postgres + nbxplorer + btcpayserver) is deliberately not here.
|
||||
# See README.md, "BTCPay SSO harness", for what that leaves untested and how to
|
||||
# run against a real BTCPay when it matters.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Same copy-into-a-volume dance as rtl-config-init above, and for the same
|
||||
# reason: RTL rewrites its config on startup.
|
||||
rtl-sso-config-init:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso_config_init
|
||||
profiles: ["sso"]
|
||||
image: busybox:1.36
|
||||
command: >
|
||||
sh -c "cp /template/RTL-Config.sso.json /config/RTL-Config.json &&
|
||||
chmod 644 /config/RTL-Config.json &&
|
||||
echo 'sso config staged'"
|
||||
volumes:
|
||||
- ./rtl/RTL-Config.sso.json:/template/RTL-Config.sso.json:ro
|
||||
- rtl_sso_config:/config
|
||||
|
||||
# A second RTL rather than a flag on the first: RTL picks one authentication
|
||||
# mode at startup, so SSO and the password login cannot coexist in one
|
||||
# instance. Only alice is wired up, matching BTCPay's one-node-per-RTL layout.
|
||||
#
|
||||
# The environment block is copied from BTCPay's own compose fragment
|
||||
# (docker-compose-generator/docker-fragments/bitcoin-lnd.yml in
|
||||
# btcpayserver-docker), so this exercises the env-driven SSO path BTCPay
|
||||
# actually uses rather than the config-file equivalent -- which is why
|
||||
# RTL-Config.sso.json leaves its SSO block zeroed and carries no multiPass.
|
||||
#
|
||||
# Only 'expose'd, never published: all access goes through the proxy, as it
|
||||
# does under BTCPay.
|
||||
rtl-sso:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso
|
||||
profiles: ["sso"]
|
||||
image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.10}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
rtl-sso-config-init:
|
||||
condition: service_completed_successfully
|
||||
alice:
|
||||
condition: service_started
|
||||
environment:
|
||||
RTL_CONFIG_PATH: /RTL/config
|
||||
RTL_SSO: 1
|
||||
RTL_COOKIE_PATH: /RTL/cookie/.cookie
|
||||
LOGOUT_REDIRECT_LINK: /server/services
|
||||
expose:
|
||||
- "3000"
|
||||
volumes:
|
||||
- rtl_sso_config:/RTL/config
|
||||
- rtl_sso_cookie:/RTL/cookie
|
||||
- alice_data:/lnd/alice:ro
|
||||
- rtl_sso_db:/RTL/database
|
||||
|
||||
# Stands in for BTCPay's traefik. Routes only /rtl and /rtl/*, mirroring the
|
||||
# label BTCPay puts on its RTL container; see nginx/rtl-sso.conf.
|
||||
rtl-sso-proxy:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}_rtl_sso_proxy
|
||||
profiles: ["sso"]
|
||||
image: nginx:1.27-alpine
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- rtl-sso
|
||||
ports:
|
||||
- "${RTL_SSO_PORT:-3001}:80"
|
||||
volumes:
|
||||
- ./nginx/rtl-sso.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Reverse proxy in front of RTL running in BTCPay Server's single-sign-on mode.
|
||||
#
|
||||
# Stands in for the traefik instance BTCPay puts in front of its bundled RTL.
|
||||
# The location regex mirrors the router rule BTCPay labels that container with:
|
||||
#
|
||||
# Host(`${BTCPAY_HOST}`) && (Path(`/rtl`) || PathPrefix(`/rtl/`))
|
||||
#
|
||||
# so only /rtl and /rtl/* are proxied and everything else 404s here. That
|
||||
# strictness is deliberate: RTL is built with <base href="/rtl/"> (angular.json)
|
||||
# and mounts every route under baseHref '/rtl' (server/utils/common.ts), so a
|
||||
# request that escapes the prefix is a bug the harness should surface rather
|
||||
# than quietly serve.
|
||||
#
|
||||
# The prefix is passed through unmodified -- there is no strip-prefix step to
|
||||
# get wrong, because RTL expects to see it. proxy_pass without a URI part
|
||||
# preserves the original request URI.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location ~ ^/rtl(/|$) {
|
||||
proxy_pass http://rtl-sso:3000;
|
||||
|
||||
# RTL runs with Express 'trust proxy' enabled, so these are what it sees
|
||||
# as the client address in its logs and rate limiting.
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# RTL's websocket lives at /rtl/api/ws and stays open for the life of the
|
||||
# session. Without the upgrade headers it fails the handshake and the UI
|
||||
# silently stops receiving live updates; without the long read timeout
|
||||
# nginx drops it after 60s.
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"port": "3000",
|
||||
"defaultNodeIndex": 1,
|
||||
"dbDirectoryPath": "/RTL/database",
|
||||
"SSO": {
|
||||
"rtlSSO": 0,
|
||||
"rtlCookiePath": "",
|
||||
"logoutRedirectLink": ""
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"index": 1,
|
||||
"lnNode": "alice",
|
||||
"lnImplementation": "LND",
|
||||
"authentication": {
|
||||
"macaroonPath": "/lnd/alice/data/chain/bitcoin/regtest"
|
||||
},
|
||||
"settings": {
|
||||
"userPersona": "OPERATOR",
|
||||
"themeMode": "DAY",
|
||||
"themeColor": "PURPLE",
|
||||
"logLevel": "ERROR",
|
||||
"lnServerUrl": "https://alice:8080",
|
||||
"fiatConversion": false,
|
||||
"unannouncedChannels": false,
|
||||
"blockExplorerUrl": "https://mempool.space"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Check the BTCPay SSO harness end to end.
|
||||
#
|
||||
# Walks the exact path a browser takes when an operator clicks the RTL link on
|
||||
# BTCPay Server's Services page, and asserts each step. Run it after any change
|
||||
# to authentication, CSRF or static serving -- this is the entry path BTCPay
|
||||
# uses, and none of it is covered by logging into the standalone fixture RTL.
|
||||
#
|
||||
# docker compose --profile sso up -d
|
||||
# ./scripts/verify-sso.sh
|
||||
#
|
||||
# Usage: ./scripts/verify-sso.sh (from the docker/ directory)
|
||||
#
|
||||
# Exits non-zero if any check fails, so it can gate a PR.
|
||||
|
||||
# Deliberately no -e: a failing assertion must record itself and let the rest of
|
||||
# the checks run, rather than aborting on the first one. That means anything
|
||||
# that would normally rely on -e needs its own guard.
|
||||
set -uo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
BASE="http://localhost:${RTL_SSO_PORT:-3001}"
|
||||
JAR="$(mktemp)"
|
||||
JAR2="$(mktemp)"
|
||||
trap 'rm -f "$JAR" "$JAR2"' EXIT
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
# Both return 0 explicitly: the checks below are written as `test && ok || bad`,
|
||||
# which would also run `bad` if `ok` itself ever returned non-zero.
|
||||
ok() { echo " PASS: $1"; pass=$((pass + 1)); return 0; }
|
||||
bad() { echo " FAIL: $1"; fail=$((fail + 1)); return 0; }
|
||||
|
||||
if ! docker compose --profile sso ps --status running --services 2>/dev/null | grep -qx rtl-sso; then
|
||||
echo "rtl-sso is not running. Start it with: docker compose --profile sso up -d" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cookie="$(docker compose --profile sso exec -T rtl-sso cat /RTL/cookie/.cookie | tr -d '\r\n')"
|
||||
echo "cookie: ${cookie:0:16}... (${#cookie} chars)"
|
||||
|
||||
echo
|
||||
echo "1. the proxy routes only /rtl, mirroring BTCPay's traefik rule"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/")
|
||||
[ "$code" = "404" ] && ok "GET / -> 404" || bad "GET / -> $code (want 404)"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/authenticate")
|
||||
[ "$code" = "404" ] && ok "GET /api/authenticate -> 404" || bad "GET /api/authenticate -> $code (want 404)"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/rtl/")
|
||||
[ "$code" = "200" ] && ok "GET /rtl/ -> 200" || bad "GET /rtl/ -> $code (want 200)"
|
||||
|
||||
echo
|
||||
echo "2. the entry URL falls through to the catch-all, which mints the CSRF token"
|
||||
body=$(curl -s -c "$JAR" "$BASE/rtl/api/authenticate/cookie?access-key=$cookie")
|
||||
echo "$body" | grep -q '<base href="/rtl/">' \
|
||||
&& ok "entry URL serves the SPA shell with base href /rtl/" \
|
||||
|| bad "entry URL did not serve index.html"
|
||||
xsrf=$(awk '/XSRF-TOKEN/ {print $7}' "$JAR")
|
||||
[ -n "$xsrf" ] && ok "XSRF-TOKEN cookie minted" || bad "no XSRF-TOKEN cookie"
|
||||
grep -q '_csrf' "$JAR" && ok "_csrf cookie set" || bad "no _csrf cookie"
|
||||
|
||||
echo
|
||||
echo "3. the SPA posts sha256(access-key) as a password login"
|
||||
hash=$(printf '%s' "$cookie" | shasum -a 256 | cut -d' ' -f1)
|
||||
resp=$(curl -s -b "$JAR" -c "$JAR" -X POST "$BASE/rtl/api/authenticate" \
|
||||
-H 'Content-Type: application/json' -H "X-XSRF-TOKEN: $xsrf" \
|
||||
-d "{\"authenticateWith\":\"PASSWORD\",\"authenticationValue\":\"$hash\"}")
|
||||
token=$(echo "$resp" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("token",""))' 2>/dev/null)
|
||||
[ -n "$token" ] && ok "authenticated, JWT issued" || bad "auth failed: $resp"
|
||||
|
||||
echo
|
||||
echo "4. the JWT reaches the node behind it"
|
||||
info=$(curl -s -b "$JAR" "$BASE/rtl/api/lnd/getinfo" -H "Authorization: Bearer $token")
|
||||
node_alias=$(echo "$info" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("alias",""))' 2>/dev/null)
|
||||
[ "$node_alias" = "alice" ] && ok "GET /rtl/api/lnd/getinfo -> alias '$node_alias'" || bad "getinfo returned: ${info:0:200}"
|
||||
|
||||
echo
|
||||
echo "5. the cookie rotates on login, so each BTCPay page render hands out a fresh one"
|
||||
after=$(docker compose --profile sso exec -T rtl-sso cat /RTL/cookie/.cookie | tr -d '\r\n')
|
||||
[ "$after" != "$cookie" ] && ok "cookie rotated after authentication" || bad "cookie did NOT rotate"
|
||||
|
||||
echo
|
||||
echo "6. a wrong access-key is refused"
|
||||
# The token has to come from the catch-all: GET /rtl/ is served by express.static,
|
||||
# which mints no XSRF-TOKEN, and the POST would then fail CSRF (403) before it
|
||||
# ever reached the access-key comparison this step is checking.
|
||||
curl -s -c "$JAR2" "$BASE/rtl/api/authenticate/cookie?access-key=x" > /dev/null
|
||||
x2=$(awk '/XSRF-TOKEN/ {print $7}' "$JAR2")
|
||||
badhash=$(printf '%s' "not-the-cookie-value-but-long-enough-to-pass-the-length-check" | shasum -a 256 | cut -d' ' -f1)
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -b "$JAR2" -X POST "$BASE/rtl/api/authenticate" \
|
||||
-H 'Content-Type: application/json' -H "X-XSRF-TOKEN: $x2" \
|
||||
-d "{\"authenticateWith\":\"PASSWORD\",\"authenticationValue\":\"$badhash\"}")
|
||||
[ "$code" = "406" ] && ok "wrong access-key -> 406" || bad "wrong access-key -> $code (want 406)"
|
||||
|
||||
echo
|
||||
echo "7. the standalone fixture RTL is unaffected"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${RTL_PORT:-3000}/rtl/")
|
||||
[ "$code" = "200" ] && ok "standalone RTL still serving on ${RTL_PORT:-3000}" || bad "standalone RTL -> $code"
|
||||
|
||||
echo
|
||||
echo "=== $pass passed, $fail failed ==="
|
||||
[ "$fail" -eq 0 ]
|
||||
Loading…
Add table
Add a link
Reference in a new issue