Sign in to the admin API, create a port and a proxy, and purge the cache from CI

Driving r3v3rs3 from a Script

The WebUI has no button that the WebUI itself does not use. Everything it does goes through the admin API under /api, so a script can do the same. This guide signs in, creates a port and a proxy, reads the status, purges the cache from a deploy job and gives the job an account that cannot do anything else.

The examples use curl against http://localhost:46492, the default address of the admin panel.

Step 1: Sign In

POST /api/login returns a session cookie:

$ curl -s -c cookies.txt \
    -H 'Content-Type: application/json' \
    -d '{"username":"admin","method":"password","password":"passw0rd"}' \
    http://localhost:46492/api/login
"success"

The response carries the cookie:

set-cookie: token=QWZ1uBQz5A9gAnmK2b18EZ7tD7CpkJ9I; HttpOnly; SameSite=Strict

The server automatically adds the Secure attribute on HTTPS connections and trusted reverse proxies, and leaves it off on local loopback HTTP connections.

For an account with TOTP, the first response is "totp_required". Send the code in a second request with the cookie of the first one:

$ curl -s -b cookies.txt -c cookies.txt \
    -H 'Content-Type: application/json' \
    -d '{"username":"admin","method":"totp","token":"123456"}' \
    http://localhost:46492/api/login
"success"

Every other endpoint needs that cookie:

$ curl -s -b cookies.txt http://localhost:46492/api/ports
[{"id":"dpv-ylj","name":"web","listen":"/ip4/127.0.0.1/tcp/8473/http"}]
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:46492/api/ports
401

A wrong password answers 400:

{"message":"invalid login credentials","error":{"message":"invalid_login_credentials"}}

Repeated failed sign-in attempts from an IP address trigger a temporary brute-force ban across all usernames (answering 429 with ip_banned). Administrators can inspect and lift active bans via GET /api/banned-ips and DELETE /api/banned-ips/{ip}, or from the Banned IP Addresses card in the Settings panel.

GET /api/session tells you who you are, and POST /api/logout ends the session. After a logout the cookie is worthless:

$ curl -s -b cookies.txt http://localhost:46492/api/session
{"username":"admin","role":"admin","cert_expiry_warning":"14days"}

Sessions live in memory. A restart of r3v3rs3 ends every session, so a long-running script must handle a 401 by signing in again.

Step 2: Read the API Document

r3v3rs3 generates the OpenAPI document from the server code, so it always describes the running version:

Both need the session cookie. Open them in the browser where you signed in to the WebUI; the API link in the WebUI footer opens the Swagger UI.

$ curl -s -b cookies.txt http://localhost:46492/api/openapi.json | jq '.info.version, (.paths | length)'
"2.6.7"
201

Use the document to generate a client, or to see the exact body of an endpoint instead of guessing it.

Step 3: Create a Port

A port takes a name and a multiaddr:

$ curl -s -b cookies.txt -X POST http://localhost:46492/api/ports \
    -H 'Content-Type: application/json' \
    -d '{"name":"api-demo","listen":"/ip4/127.0.0.1/tcp/8475/http"}'
null

null is the success body. Read the id back from the list:

$ curl -s -b cookies.txt http://localhost:46492/api/ports | jq -r '.[] | select(.name=="api-demo") | .id'
smc-gzh

r3v3rs3 generates the id. Never write one by hand: a PUT with an id that does not exist answers 404 id_not_found.

PUT /api/ports/{id} replaces a port, DELETE /api/ports/{id} removes it, and GET /api/ports/{id}/status reports whether the socket listens.

Step 4: Create a Proxy

A proxy names the ports it runs on, its protocol and its routes:

$ curl -s -b cookies.txt -X POST http://localhost:46492/api/proxies \
    -H 'Content-Type: application/json' \
    -d '{
      "name": "api-demo-app",
      "ports": ["smc-gzh"],
      "protocol": "http",
      "vhosts": ["demo.example.com"],
      "routes": [{ "path": "/", "servers": [{ "url": "http://127.0.0.1:9000/" }] }]
    }'
null

The proxy answers at once, without a restart:

$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: demo.example.com' http://127.0.0.1:8475/
200

protocol selects the proxy type: http, tcp or udp. A TCP or UDP proxy carries upstream_servers instead of routes.

GET /api/proxies/{id}/status reports the upstream servers:

$ curl -s -b cookies.txt http://localhost:46492/api/proxies/jzr-pgf/status
{"state":"active","upstreams":[{"addr":"http://127.0.0.1:9000/","weight":1,"healthy":true,"failures":0}]}

Step 5: Purge the Cache After a Deploy

A deploy that changes static files leaves stale bodies in the HTTP cache. One call clears the cache of one proxy:

$ curl -s -b cookies.txt -X DELETE http://localhost:46492/api/proxies/jzr-pgf/cache
null

A whole deploy job needs three lines:

#!/bin/sh
set -eu
curl -sf -c "$PWD/j.txt" -H 'Content-Type: application/json' \
  -d "{\"username\":\"$R3_USER\",\"method\":\"password\",\"password\":\"$R3_PASS\"}" \
  "$R3_URL/api/login" > /dev/null
curl -sf -b "$PWD/j.txt" -X DELETE "$R3_URL/api/proxies/$R3_PROXY/cache" > /dev/null
curl -sf -b "$PWD/j.txt" -X POST "$R3_URL/api/logout" > /dev/null

Keep the password in the secret store of your CI, never in the repository. -f makes curl fail the job on a 4xx or 5xx status.

Step 6: Give the Job Its Own Account

Do not give a deploy job the admin password. Create an account with the role the job needs:

$ curl -s -b cookies.txt -X POST http://localhost:46492/api/accounts \
    -H 'Content-Type: application/json' \
    -d '{"username":"ci","password":"a-long-password","role":"editor"}'

The three roles:

RoleWhat it can do
adminEverything, including accounts, settings and the audit log.
editorChanges the ports, the proxies, the certificates, the ACME entries, the access lists and the apps. With a proxy list, it adds proxies and changes only the proxies of its list.
viewerReads only.

A viewer reads the proxy list but cannot change anything. With the cookie of a viewer account in viewer.txt:

$ curl -s -b viewer.txt -X DELETE http://localhost:46492/api/proxies/jzr-pgf/cache
{"message":"the role of the account does not allow this action","error":{"message":"forbidden"}}

That request answers 403. A cache purge needs at least editor. An editor cannot read the audit log or the accounts; both answer 403. See Accounts for the per-account proxy list and TOTP.

Step 7: Read the Audit Log

Every change through the WebUI or the API is recorded. Only an admin account can read it:

$ curl -s -b cookies.txt 'http://localhost:46492/api/audit?limit=5' | jq -c '.[]'
{"time":1789647517635,"username":"admin","client":"127.0.0.1","action":"purge_proxy_cache","resource_id":"jzr-pgf"}
{"time":1789647512463,"username":"admin","client":"127.0.0.1","action":"add_proxy","resource_id":"jzr-pgf","summary":"api-demo-app"}
{"time":1789647508125,"username":"admin","client":"127.0.0.1","action":"add_port","resource_id":"smc-gzh","summary":"api-demo /ip4/127.0.0.1/tcp/8475/http"}
{"time":1789647489459,"username":"admin","client":"127.0.0.1","action":"login"}

time is in milliseconds since the Unix epoch. The summary never holds a password, a token or a key. See Audit Log for the query parameters.

Step 8: Watch the Changes

GET /api/events is a server-sent event stream that the WebUI uses to refresh itself. Each change sends one line:

$ curl -N -b cookies.txt http://localhost:46492/api/events
data: {"event":"proxies_updated","entries":[...]}

data: {"event":"proxy_status_updated","id":"pdb-khh","status":{"state":"active"}}

data: {"event":"port_table_updated","entries":[...]}

data: {"event":"port_status_updated","id":"dpv-ylj","status":{"state":{"socket":"listening","tls":null}}}

Use it for a dashboard or an alert instead of asking the status endpoints in a loop.

Reference

Next Steps