docs: document IRC message protocol, signing, and canonicalization
- Add IRC command/numeric mapping tables (C2S, S2C, S2S) - Document structured message bodies (array/object, never raw strings) - Document RFC 8785 JCS canonicalization for deterministic hashing - Document Ed25519 signing/verification flow with TOFU key distribution - Document PUBKEY message type for public key announcement - Update message examples to use IRC command format - Update curl examples to use command-based messages - Note web client as convenience UI; primary interface is IRC-style clients - Add schema/ to project structure
This commit is contained in:
parent
909da3cc99
commit
02acf1c919
248
README.md
248
README.md
@ -20,7 +20,7 @@ This project builds a chat server that:
|
||||
- Exposes a minimal, clean HTTP+JSON API — easy to build clients against
|
||||
- Supports multiple concurrent connections per user session
|
||||
- Provides IRC-like semantics: channels, nicks, topics, modes
|
||||
- Uses structured JSON messages with arbitrary extensibility
|
||||
- Uses structured JSON messages with IRC command names and numeric reply codes
|
||||
|
||||
## Architecture
|
||||
|
||||
@ -38,6 +38,151 @@ request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP.
|
||||
The entire read/write loop for a client is two endpoints. Everything else is
|
||||
channel management and history.
|
||||
|
||||
### Message Protocol
|
||||
|
||||
All messages use **IRC command names and numeric reply codes** from RFC 1459/2812.
|
||||
The `command` field identifies the message type.
|
||||
|
||||
#### Message Envelope
|
||||
|
||||
Every message is a JSON object with these fields:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-----------|-----------------|----------|-------------|
|
||||
| `command` | string | ✓ | IRC command name or 3-digit numeric code |
|
||||
| `from` | string | | Sender nick or server name |
|
||||
| `to` | string | | Destination: `#channel` or nick |
|
||||
| `params` | array\<string\> | | Additional IRC-style parameters |
|
||||
| `body` | array \| object | | Structured body (never a raw string) |
|
||||
| `meta` | object | | Extensible metadata (signatures, etc.) |
|
||||
| `id` | string (uuid) | | Server-assigned message ID |
|
||||
| `ts` | string | | Server-assigned ISO 8601 timestamp |
|
||||
|
||||
**Important:** Message bodies MUST be objects or arrays, never raw strings.
|
||||
This enables:
|
||||
- Multiline messages (array of lines)
|
||||
- Deterministic canonicalization for hashing/signing (RFC 8785 JCS)
|
||||
- Structured data where needed (e.g. PUBKEY)
|
||||
|
||||
#### IRC Command Mapping
|
||||
|
||||
**Client-to-Server (C2S):**
|
||||
|
||||
| Command | Description |
|
||||
|----------|-------------|
|
||||
| PRIVMSG | Send message to channel or user |
|
||||
| NOTICE | Send notice (no auto-reply expected) |
|
||||
| JOIN | Join a channel |
|
||||
| PART | Leave a channel |
|
||||
| QUIT | Disconnect from server |
|
||||
| NICK | Change nickname |
|
||||
| MODE | Set/query channel or user modes |
|
||||
| TOPIC | Set/query channel topic |
|
||||
| KICK | Kick a user from a channel |
|
||||
| PING | Client keepalive |
|
||||
| PUBKEY | Announce public signing key |
|
||||
|
||||
**Server-to-Client (S2C):**
|
||||
|
||||
All C2S commands may be echoed back as S2C (relayed to other users), plus:
|
||||
|
||||
| Command | Description |
|
||||
|----------|-------------|
|
||||
| PONG | Server keepalive response |
|
||||
| PUBKEY | Relayed public key from another user |
|
||||
| ERROR | Server error message |
|
||||
|
||||
**Numeric Reply Codes (S2C):**
|
||||
|
||||
| Code | Name | Description |
|
||||
|------|-------------------|-------------|
|
||||
| 001 | RPL_WELCOME | Welcome after registration |
|
||||
| 002 | RPL_YOURHOST | Server host information |
|
||||
| 322 | RPL_LIST | Channel list entry |
|
||||
| 353 | RPL_NAMREPLY | Names list for a channel |
|
||||
| 366 | RPL_ENDOFNAMES | End of names list |
|
||||
| 372 | RPL_MOTD | Message of the day line |
|
||||
| 375 | RPL_MOTDSTART | Start of MOTD |
|
||||
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
||||
| 401 | ERR_NOSUCHNICK | No such nick or channel |
|
||||
| 403 | ERR_NOSUCHCHANNEL | No such channel |
|
||||
| 433 | ERR_NICKNAMEINUSE | Nickname already in use |
|
||||
|
||||
**Server-to-Server (S2S):**
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| RELAY | Relay message to linked server |
|
||||
| LINK | Establish server link |
|
||||
| UNLINK | Tear down server link |
|
||||
| SYNC | Synchronize state between servers |
|
||||
| PING | Server-to-server keepalive |
|
||||
| PONG | Server-to-server keepalive response |
|
||||
|
||||
#### Message Examples
|
||||
|
||||
```json
|
||||
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["hello world"], "meta": {"sig": "base64...", "alg": "ed25519"}}
|
||||
|
||||
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["line one", "line two"]}
|
||||
|
||||
{"command": "001", "to": "alice", "body": ["Welcome to the network, alice"]}
|
||||
|
||||
{"command": "353", "to": "alice", "params": ["=", "#general"], "body": ["alice", "bob", "@charlie"]}
|
||||
|
||||
{"command": "JOIN", "from": "bob", "to": "#general", "body": []}
|
||||
|
||||
{"command": "ERROR", "body": ["Closing link: connection timeout"]}
|
||||
```
|
||||
|
||||
#### JSON Schemas
|
||||
|
||||
Full JSON Schema (draft 2020-12) definitions for all message types are in
|
||||
[`schema/`](schema/). See [`schema/README.md`](schema/README.md) for the
|
||||
complete index.
|
||||
|
||||
### Canonicalization and Signing
|
||||
|
||||
Messages support optional cryptographic signatures for integrity verification.
|
||||
|
||||
#### Canonicalization (RFC 8785 JCS)
|
||||
|
||||
To produce a deterministic byte representation of a message for signing:
|
||||
|
||||
1. Remove `meta.sig` from the message (the signature itself is not signed)
|
||||
2. Serialize using [RFC 8785 JSON Canonicalization Scheme (JCS)](https://www.rfc-editor.org/rfc/rfc8785):
|
||||
- Object keys sorted lexicographically
|
||||
- No whitespace
|
||||
- Numbers in shortest form
|
||||
- UTF-8 encoding
|
||||
3. The resulting byte string is the signing input
|
||||
|
||||
This is why `body` must be an object or array — raw strings would be ambiguous
|
||||
under canonicalization.
|
||||
|
||||
#### Signing Flow
|
||||
|
||||
1. Client generates an Ed25519 keypair
|
||||
2. Client announces public key: `{"command": "PUBKEY", "body": {"alg": "ed25519", "key": "base64..."}}`
|
||||
3. Server relays PUBKEY to channel members / stores for the session
|
||||
4. When sending a message, client:
|
||||
a. Constructs the message without `meta.sig`
|
||||
b. Canonicalizes per JCS
|
||||
c. Signs with private key
|
||||
d. Adds `meta.sig` (base64) and `meta.alg`
|
||||
5. Recipients verify by repeating steps a–c and checking the signature
|
||||
against the sender's announced public key
|
||||
|
||||
#### PUBKEY Message
|
||||
|
||||
```json
|
||||
{"command": "PUBKEY", "from": "alice", "body": {"alg": "ed25519", "key": "base64-encoded-pubkey"}}
|
||||
```
|
||||
|
||||
Servers SHOULD relay PUBKEY messages to all channel members. Clients SHOULD
|
||||
cache public keys and use them to verify `meta.sig` on incoming messages.
|
||||
Key distribution is trust-on-first-use (TOFU) by default.
|
||||
|
||||
### Core Concepts
|
||||
|
||||
#### Users
|
||||
@ -63,36 +208,6 @@ channel management and history.
|
||||
- Channel history is stored server-side (configurable depth)
|
||||
- No eternal logging by default — history rotates
|
||||
|
||||
#### Messages
|
||||
|
||||
Every message is a structured JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"ts": "2026-02-09T20:00:00.000Z",
|
||||
"from": "nick",
|
||||
"to": "#channel",
|
||||
"type": "message",
|
||||
"body": "Hello, world!",
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `id` — Server-assigned UUID, globally unique
|
||||
- `ts` — Server-assigned timestamp (ISO 8601)
|
||||
- `from` — Sender nick
|
||||
- `to` — Destination: channel name (`#foo`) or nick (for DMs)
|
||||
- `type` — Message type: `message`, `action`, `notice`, `join`, `part`, `quit`,
|
||||
`topic`, `mode`, `nick`, `system`
|
||||
- `body` — Message content (UTF-8 text)
|
||||
- `meta` — Arbitrary extensible metadata (JSON object). Can carry:
|
||||
- Cryptographic signatures
|
||||
- Rich content hints (URLs, embeds)
|
||||
- Client-specific extensions
|
||||
- Reactions, edits, threading references
|
||||
|
||||
### API Endpoints
|
||||
|
||||
All endpoints accept and return `application/json`. Authenticated endpoints
|
||||
@ -116,14 +231,14 @@ TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \
|
||||
-d '{"nick":"alice"}' | jq -r .token)
|
||||
|
||||
# Join a channel
|
||||
curl -s -X POST http://localhost:8080/api/v1/channels/join \
|
||||
curl -s -X POST http://localhost:8080/api/v1/messages \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"channel":"#general"}'
|
||||
-d '{"command":"JOIN","to":"#general"}'
|
||||
|
||||
# Send a message
|
||||
curl -s -X POST http://localhost:8080/api/v1/messages \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"to":"#general","content":"hello world"}'
|
||||
-d '{"command":"PRIVMSG","to":"#general","body":["hello world"]}'
|
||||
|
||||
# Poll for messages (long-poll)
|
||||
curl -s http://localhost:8080/api/v1/messages?after=0 \
|
||||
@ -149,8 +264,8 @@ GET /api/v1/state — User state: nick, id, and list of joined channe
|
||||
GET /api/v1/messages — Single message stream (long-poll supported)
|
||||
All message types: channel, DM, notices, events
|
||||
Query params: ?after=<message-id>&timeout=30
|
||||
POST /api/v1/messages — Send a message
|
||||
Body: { "to": "#channel" or "nick", "content": "..." }
|
||||
POST /api/v1/messages — Send a message (IRC command in body)
|
||||
Body: { "command": "PRIVMSG", "to": "#channel", "body": ["..."] }
|
||||
```
|
||||
|
||||
#### History
|
||||
@ -187,8 +302,9 @@ POST /api/v1/federation/relay — Relay messages between linked servers
|
||||
GET /api/v1/federation/status — Link status
|
||||
```
|
||||
|
||||
Federation uses the same HTTP+JSON transport. Messages are relayed between
|
||||
servers so users on different servers can share channels.
|
||||
Federation uses the same HTTP+JSON transport. S2S messages use the RELAY, LINK,
|
||||
UNLINK, SYNC, PING, and PONG commands. Messages are relayed between servers so
|
||||
users on different servers can share channels.
|
||||
|
||||
### Channel Modes
|
||||
|
||||
@ -239,44 +355,31 @@ Following [gohttpserver CONVENTIONS.md](https://git.eeqj.de/sneak/gohttpserver/s
|
||||
```
|
||||
chat/
|
||||
├── cmd/
|
||||
│ └── chat/
|
||||
│ └── chatd/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go
|
||||
│ ├── database/
|
||||
│ │ └── database.go
|
||||
│ ├── globals/
|
||||
│ │ └── globals.go
|
||||
│ ├── handlers/
|
||||
│ │ ├── handlers.go
|
||||
│ │ ├── auth.go
|
||||
│ │ ├── channels.go
|
||||
│ │ ├── federation.go
|
||||
│ │ ├── healthcheck.go
|
||||
│ │ ├── messages.go
|
||||
│ │ └── users.go
|
||||
│ ├── healthcheck/
|
||||
│ │ └── healthcheck.go
|
||||
│ ├── logger/
|
||||
│ │ └── logger.go
|
||||
│ ├── middleware/
|
||||
│ │ └── middleware.go
|
||||
│ ├── models/
|
||||
│ │ ├── channel.go
|
||||
│ │ ├── message.go
|
||||
│ │ └── user.go
|
||||
│ ├── queue/
|
||||
│ │ └── queue.go
|
||||
│ └── server/
|
||||
│ ├── server.go
|
||||
│ ├── http.go
|
||||
│ └── routes.go
|
||||
├── schema/
|
||||
│ ├── message.schema.json
|
||||
│ ├── c2s/
|
||||
│ ├── s2c/
|
||||
│ ├── s2s/
|
||||
│ └── README.md
|
||||
├── web/
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── Makefile
|
||||
├── Dockerfile
|
||||
├── CONVENTIONS.md → (copy from gohttpserver)
|
||||
├── CONVENTIONS.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
@ -298,26 +401,27 @@ Per gohttpserver conventions:
|
||||
### Web Client
|
||||
|
||||
The server embeds a single-page web client (Preact) served at `/`. This is a
|
||||
**convenience/reference implementation** — not the primary interface. It
|
||||
demonstrates the API and provides a quick way to test the server in a browser.
|
||||
|
||||
The primary intended clients are IRC-style terminal applications and bots
|
||||
talking directly to the HTTP API.
|
||||
**convenience/reference implementation** — not the primary interface. The
|
||||
primary intended clients are IRC-style terminal applications, bots, and custom
|
||||
clients talking directly to the HTTP API.
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **API-first** — the HTTP API is the product. Clients are thin. If you can't
|
||||
build a working IRC-style TUI client in an afternoon, the API is too complex.
|
||||
2. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
|
||||
2. **IRC semantics over HTTP** — command names and numeric codes from RFC 1459/2812.
|
||||
Familiar to anyone who's built IRC clients or bots.
|
||||
3. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
|
||||
negotiation. HTTP is universal, proxy-friendly, and works everywhere.
|
||||
3. **Server holds state** — clients are stateless. Reconnect, switch devices,
|
||||
4. **Server holds state** — clients are stateless. Reconnect, switch devices,
|
||||
lose connectivity — your messages are waiting.
|
||||
4. **Structured messages** — JSON with extensible metadata. Enables signatures,
|
||||
rich content, client extensions without protocol changes.
|
||||
5. **Simple deployment** — single binary, SQLite default, zero mandatory
|
||||
5. **Structured messages** — JSON with extensible metadata. Bodies are always
|
||||
objects or arrays for deterministic canonicalization (JCS) and signing.
|
||||
6. **Simple deployment** — single binary, SQLite default, zero mandatory
|
||||
external dependencies.
|
||||
6. **No eternal logs** — history rotates. Chat should be ephemeral by default.
|
||||
7. **Federation optional** — single server works standalone. Linking is opt-in.
|
||||
7. **No eternal logs** — history rotates. Chat should be ephemeral by default.
|
||||
8. **Federation optional** — single server works standalone. Linking is opt-in.
|
||||
9. **Signable messages** — optional Ed25519 signatures with TOFU key distribution.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user