JSON API¶
dzo serve provides a JSON API under /api/v1 for scripts, bots, the
dzo player and dzo vehicle commands, and the web interface. The API is
the only interface of an installation that clients need: it contains nothing
specific to the host, so a client can use several installations by keeping one
base URL and one token for each.
Running it¶
dzo instance apply (dzo units sync) writes dzo-serve.service and enables it as soon as an
instance runs the dzo-admin mod, which needs the endpoint; serve.enabled: true or false in
config.yaml forces it on or off. systemctl --user reload dzo-serve re-reads the site (the same as
SIGHUP).
dzo serve listens on serve.listen (default 127.0.0.1:8080) for the
API and on serve.mod_listen (default 127.0.0.1:2400) for the
dzo-admin mods. Both refuse non-local addresses unless
serve.allow_insecure_http is set: put a reverse proxy with HTTPS in front
of the API. Send SIGHUP after changing the site repository; new instances
appear and the live state of the others is kept.
Tokens¶
Every request needs Authorization: Bearer <token>. Create tokens on the host:
dzo token create deploy-bot --role operator --instance deerisle --ttl 720h
dzo token list
dzo token revoke <id>
A token has a role (Web interface), optionally a list of instances it may touch,
and an expiry of at most one year. The secret is shown once; only a hash is
stored. A token created with --delegate (for the web interface) may name the
person it acts for in the X-Dzo-Actor header, and the origin in
X-Dzo-Source; both go into the audit log. For other tokens these headers are
ignored.
A token created with --web is for dzo web and the only kind that acts for logged-in people
(Web interface). On its own it can do nothing: every request must also carry the login session of a
person in X-Dzo-Session, and then acts as that person. Without the header the answer is 403 this
token needs a user session, for an unknown or expired session 401 session expired, for a
disabled person or one with no role on any existing instance 403, and while serve.web_url is
not set (user logins are off) 503 logins are not configured. The audit actor is <token
name>:<Steam ID>; X-Dzo-Actor is ignored. A token is either --delegate or --web, and a web
token has the role viewer, moderator, operator or admin.
Roles per instance¶
A person has grants: a role on an instance, or on * (every instance, also later ones). Their
effective role on an instance is the higher of the grant on the instance and the grant on *,
capped by the web token: it is the lower of that and the token’s role, and nothing at all on
instances the token’s list does not include. An instance without a role does not exist for them
(404, and it is left out of lists). Being moderator on one instance never unlocks anything on
another. A token without a person is checked against its own role on the instances it may touch, as it
always was.
Routes without an instance in the path check the permission like this:
Route |
Rule |
|---|---|
|
|
|
|
|
|
|
|
|
as for creating, on the old and the new instances; only the creator or whoever holds
|
|
|
|
|
|
|
Login¶
Only dzo web logs people in, with its web token and no session. Logging in is always two steps: a
first factor (password or Steam) and a second factor (TOTP code, passkey or recovery code). A passkey is
never a first factor. The routes need serve.web_url (503 without) and answer 403 to any
other kind of token.
Request |
Answer |
|---|---|
|
|
|
|
|
the query Steam sent the browser back with. Serve has Steam check it ( |
|
|
|
|
Passkeys are a second factor, checked with the WebAuthn library (relying party ID: the host of
serve.web_url; the only accepted origin is serve.web_url itself; the person’s passkeys are listed in
allowCredentials; user verification is preferred). The options and the answers are the JSON the
browser’s navigator.credentials takes and gives:
Request |
Answer |
|---|---|
|
the WebAuthn request options ( |
|
|
A person who signs in and has no second factor yet gets "enrol": true and may only enrol one, with one
of the following; each finish ends the login with a session and shows the 10 recovery codes once
({"session","expires","recovery_codes":[…]}). Any other pending login is refused with 403.
Request |
Answer |
|---|---|
|
|
|
a first code of the new secret proves the app; it is stored and that code is used up |
|
the WebAuthn creation options |
|
the passkey is stored under |
The browser’s address is sent in X-Dzo-Client (from a web token only; other tokens’ header is ignored)
and, behind a proxy, the proxy’s address in X-Dzo-Proxy; without X-Dzo-Client it is the connection’s
address. Failed first and second factors are counted per account and per address: five are free, then the
wait doubles from one second to 15 minutes (a success clears the account, 1 hour without a failure
clears all). While waiting a request gets 429 with Retry-After, with the same message whether or
not the username exists. The counters live in the memory of dzo serve.
Every step writes one line to the service log, login: <step> <ok|failed|refused> steamid=<id|unknown>
client=<addr> proxy=<addr|->, never a secret, and a finished login or logout writes an audit entry
(auth.login, auth.logout) with the client address. Every audit entry of a web action has the
client address in its client field.
Account¶
What a logged-in person (a web token with a session) does with their own login. No role is needed beyond
being logged in; every change is audited (account.*) without secrets.
Request |
Answer |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Users¶
Managing people needs users.manage, which only an admin on * holds (through a token for every
instance). Every change goes to the audit log (users.put, users.delete, users.reset) with the
Steam ID and the grants.
Request |
Answer |
|---|---|
|
the people: |
|
creates the person or replaces their username, name, disabled flag and grants, and answers the person.
|
|
|
|
|
Nobody can change their own access through the API: a PUT for oneself that changes the grants (up or
down) or the disabled flag, and a DELETE or reset for oneself, answer 409 you cannot change your
own access; another admin or dzo user on the serve host can. Changing only the display name is fine. The
rule also holds if users.manage is ever given to a lower role.
GET / of a request that acts for a person has a user part: {"steam_id","name","username"(when set),"roles":{"<instance>":"<role>"},
"installation":["audit.view",…]}. roles is the effective role on each existing instance the person has
one on (the web interface shows controls from it; the API checks every action regardless), installation
the installation-wide permissions they hold. role stays the highest role anywhere.
Streams (/stream) of a person are re-checked every 60 seconds: when the session ended, the person was
disabled or lost the right to view the instance, the stream closes.
Reading¶
All paths start with /api/v1. Responses are JSON.
Request |
Answer |
|---|---|
|
installation name, dzo version, API version, your role |
|
instances you may see, each with the mod’s connection state and counts |
|
one instance’s state, with |
|
online players: Steam ID, name, position, health, ping, vehicle |
|
persistent vehicles: id, kind ( |
|
active events (effect areas, markers from mods) |
|
marker layers you may see |
|
markers you may see |
|
search the item classes the server reported: public (scope 2) items, weapons, magazines and clothing, without the classes that crash a vanilla server or raise script errors when they are created (found with |
|
search the vehicle classes the server reported: public cars, boats and helicopters of the game and the loaded mods. |
|
server-sent events |
|
the game events (see Event feed), oldest first: those with an |
|
audit entries, newest first ( |
Players are identified by their Steam ID only.
Acting¶
Request and JSON body |
Needs |
Does |
|---|---|---|
|
|
message to everyone |
|
|
direct message. |
|
|
kick an online player through BattlEye; the reason is shown to them (1 to 64 bytes). |
|
|
the address the player is connected from ( |
|
|
heal the player, repair everything they carry and wear, or kill them (no body). Each fails for a player who is not online, and heal and fix gear for a dead one |
|
|
teleport; without |
|
|
spawn an item ( |
|
|
spawn an item on the ground at a position, as |
|
|
spawn a vehicle on the ground at a position, with the parts and fuel of the game’s debug spawn |
|
|
|
|
|
stand the vehicle upright on its wheels |
|
|
put the vehicle on the ground at |
|
|
raise a territory flag to the top of its pole and fill its timer (the 40 days a freshly raised flag has); |
|
|
delete a vehicle ( |
Actions are carried out by the game server, so the call waits for its answer:
200with{"id", "ok": true}: done.422with{"ok": false, "message"}: the game server refused (for example the player is not online).503: the instance’s dzo-admin is not connected.504: it did not answer in time.400: the request is invalid, or the class is denied on this instance.429: rate limit (120 actions per minute per token, 30 per minute per target).?wait=<seconds>changes how long to wait (default 10, at most 25).?wait=0returns202with a commandidat once; pollGET /instances/<i>/commands/<id>(202while pending).
Every action, including refused and rate-limited ones, is written to the audit
log (${paths.db}/audit.jsonl) with the actor, the source, the target and the
result.
Event feed¶
dzo serve keeps the newest 1000 game events of every instance and serves them on
GET /instances/<i>/feed and the feed events of the stream. They are also written to
${paths.db}/feed/<instance>.jsonl (mode 0600, one JSON object per line), so the feed is not empty
after dzo serve restarts. The file holds chat and positions, which is personal data: it keeps at most the
newest 1000 events per instance and is safe to delete, the feed then starts empty.
Every event has an id (dzo’s, increasing per instance), a time and a kind. The other fields are
present when they apply:
Field |
Meaning |
|---|---|
|
the actor: the killer, the player who built, joined, chatted. |
|
the victim of a |
|
where it happened (the victim’s position for a kill) |
|
a |
|
a |
|
the part, item or killer class; the chat channel; the chat text, kick reason or the details of the event |
The kinds:
Kind |
Happened |
|---|---|
|
a player or an AI was killed by someone, or died without a killer |
|
a player joined or left |
|
in-game chat and BattlEye kicks, from BattlEye RCon (needs RCon, as bans do) |
|
base building and raiding: a part built, taken apart or destroyed, a combination lock cut |
|
a territory flag moved |
|
a stash buried or dug up |
|
an action of an operator (kick, ban, teleport, broadcast, …), from the audit log |
|
the mod’s buffer overflowed; |
Bans and evidence¶
These routes need the ban database of dzo serve (503 without it) and are described in
Bans and evidence. A restricted token sees and changes only the bans that apply to its instances.
Request and JSON body |
Needs |
Does |
|---|---|---|
|
|
bans, newest first ( |
|
|
create a ban; |
|
|
one ban |
|
|
change a ban; only its creator or an |
|
|
end a ban now; it stays in the list and the history |
|
|
every change with the actor and the old and new values |
|
|
evidence filed against a player |
|
|
file a log ( |
|
|
one item, and its image (always served as an image type, with |
|
|
the GUIDs and IPs the player was seen with, per instance, and how many other Steam IDs share each IP |
|
|
the player database: |
|
|
one player: names, per-instance totals, the newest 50 sessions, BattlEye’s answer per game ( |
|
|
ask BattlEye and Steam about the player now and answer the fresh record (audited as |
Errors¶
Errors are {"error": "<text>"} with a matching status: 401 for a missing
or invalid token, 403 for a role without the permission, 404 for an
unknown instance or one the token may not use.
Markers and roles¶
A marker is shown to roles at least as high as its visibility. Markers
without one are visible to admin only.