~ $ cat docs/protocol.md
The server protocol
chasen talks to chasen-server over HTTPS. The cloud answers the same protocol and passes each request on to the server of the app, so a client does not know which one it talks to.
The whole contract is in one Go file: protocol/protocol.go. The three programs import it. This document describes version 1 of it.
Why it is plain HTTP
What crosses the wire is a command and its output. HTTP does that natively: a request body in, a stream of text out. So there is no schema compiler, no generated code, and you can call the API with curl. It also passes the proxy of the server and Cloudflare with no extra setting.
A command
POST /v1/<command>?arg=<first>&arg=<second>
Authorization: Bearer <token>
- The command is one word from the list below.
- The arguments are
argquery parameters, in order. The first one is the app, for every command exceptlistandload. - The request body is the input of the command. Most commands have none.
- The response is
text/plain. It is the output of the command, sent line by line as the command runs. - The last line is a zero byte, then
chasen-exit, then the exit code of the command:\x00chasen-exit 0. A response that ends without this line means the connection broke.
curl -N -X POST "https://api.example.com/v1/status?arg=shop" \
-H "Authorization: Bearer $CHASEN_TOKEN"
Status codes
| Code | Meaning |
|---|---|
200 |
The command started. Its result is the exit code in the last line, not the status code |
401 |
The token is not valid. The client starts a new login |
404, 409, 502, 503 |
The cloud only: no such server, a server choice is needed, or the server does not answer. The body is one line of text |
A command that fails still answers 200: the server already sent the output when it knows the result.
The commands
| Command | Arguments | Body | What it does |
|---|---|---|---|
list |
The apps of the server | ||
load |
The load, the memory, and the disk of the server | ||
run |
app, then the words of the command | Runs the command in the container of the app. The exit code is the one of the command | |
deploy |
<app> <version> |
The settings. For a website: then its files | Pull the image of the settings (or wrap the files of a website), back up, start, and swap |
check |
<app> <version> |
Like deploy |
Run the image next to the live app and test it against the standard. It keeps nothing |
enable |
<addon> [domain] |
Run an addon from its image | |
restart |
<app> |
The settings, or nothing | Start the app again from the image it has, with these settings |
status |
<app> |
The version, the state, the URLs, and the backups | |
logs |
<app> |
Follow the logs. It stops when the client goes away | |
history |
<app> [id] |
The activity feed, or the output of one entry | |
domains |
<app> [add|rm <domain>] |
List or change the domains | |
backup |
<app> |
Make a snapshot now | |
backups |
<app> |
List the backups | |
restore |
<app> [backup|live] |
Restore a backup | |
remove |
<app> |
Stop the app. The data stays | |
logout |
Make the API forget the token of the request |
Every command except logs runs to its end on the server, also when the client goes away. So a lost connection never leaves a deploy half done.
The body of a deploy
deploy, check, and restart take the settings of the app in the request body: one line of JSON.
{"image": "ghcr.io/you/shop:3f9a2c1d5e8b7a6094c3f2e1d0b9a8c7d6e5f4a3", "registry": {"username": "you", "password": "..."}, "env": {"LOG_LEVEL": "info", "STRIPE_KEY": "..."}, "port": 3000, "health": "/up", "health_timeout": 30, "volumes": ["/app/storage"]}
- Only
envis always there. A zero or missing value means “use the default of the standard”. - The server uses
registryfor one pull and does not keep it. - A website has no
image. Its files follow the line of the settings, as atar.gzarchive. The server puts them in a Caddy image; it is the only image a server makes. restartwith an empty body keeps the settings that the server has.
POST /v1/deploy?arg=shop&arg=3f9a2c1
The second argument is the version that the app shows (APP_VERSION).
The login
The login is the OAuth 2.0 device flow (RFC 8628), in oauth.
| Request | What it does |
|---|---|
POST /oauth/device_authorization |
Start a login. The answer has a device code, a user code, and the page to open |
GET /oauth/device?user_code=... |
The page. The person types the token of the server (or the key of a cloud account) |
POST /oauth/token |
The client asks until the login is approved, and gets a token of its own |
In CI there is no login: the token of the server (or the key of the account) is the bearer token.
What only the cloud answers
| Request | What it does |
|---|---|
GET /v1/placement?app=<app> |
Which servers the account has, and what a new one costs. JSON |
Header Chasen-Server: <id> |
Send the command to this server of the account |
Header Chasen-Server: new, new:cx33, new:@ash, new:cx33@ash |
Create a server for the app: the cheapest one, or of this type, or in this location |
A plain server answers 404 to /v1/placement, and the client then asks no question.
Limits
- A command is one word from the list. The server refuses every other word, and the client sends the word as one path segment.
- The login page accepts 20 wrong keys or codes in a minute, for all visitors together, and 1000 logins can wait at one time. After that it answers
429. - The API reads the headers of a request for 10 seconds at most. It has no limit on the time of a response: a deploy streams for minutes.
- The client sends a token over plain
http://only to this computer or to a private network address.
Compatibility
- The path says
/v1. A change that breaks a client gets a new number. - A new command is a new word in the list. An old server answers it with an error line and exit code 1.
- A new field in the settings is ignored by an old server. So a new client with an old server can lose a setting without an error: keep the server as new as the client.
- Version 0.2 moved the settings into the body of
deploy,check, andrestart, and removed theenvcommand. A 0.1 client does not work with a 0.2 server. GET /upanswers200when the API runs. It needs no token.