feat: add username/hostname support with IRC hostmask format
- Add username and hostname columns to sessions table (001_initial.sql) - Accept optional username field in session creation and registration endpoints; defaults to nick if not provided - Resolve hostname via reverse DNS of connecting client IP at session creation time (supports X-Forwarded-For and X-Real-IP headers) - Display real username and hostname in WHOIS (311 RPL_WHOISUSER) and WHO (352 RPL_WHOREPLY) responses instead of nick/servername - Add FormatHostmask helper for nick!user@host format - Add SessionHostInfo type and GetSessionHostInfo query - Include username/hostname in MemberInfo and ChannelMembers results - Extract validateHashcash and resolveUsername helpers to stay under funlen limits - Add comprehensive unit tests for all new DB functions, hostmask formatting, and integration tests for WHOIS/WHO responses - Update README with hostmask documentation, new API fields, and updated schema reference
This commit is contained in:
62
README.md
62
README.md
@@ -207,6 +207,26 @@ removal. Identity verification at the message layer via cryptographic
|
||||
signatures (see [Security Model](#security-model)) remains independent
|
||||
of account registration.
|
||||
|
||||
### Hostmask (nick!user@host)
|
||||
|
||||
Each session has an IRC-style hostmask composed of three parts:
|
||||
|
||||
- **nick** — the user's current nick (changes with `NICK` command)
|
||||
- **username** — an ident-like identifier set at session creation (optional
|
||||
`username` field in the session/register request; defaults to the nick)
|
||||
- **hostname** — automatically resolved via reverse DNS of the connecting
|
||||
client's IP address at session creation time
|
||||
|
||||
The hostmask appears in:
|
||||
|
||||
- **WHOIS** (`311 RPL_WHOISUSER`) — `params` contains
|
||||
`[nick, username, hostname, "*"]`
|
||||
- **WHO** (`352 RPL_WHOREPLY`) — `params` contains
|
||||
`[channel, username, hostname, server, nick, flags]`
|
||||
|
||||
The hostmask format (`nick!user@host`) is stored for future use in ban matching
|
||||
(`+b` mode) and other access control features.
|
||||
|
||||
### Nick Semantics
|
||||
|
||||
- Nicks are **unique per server at any point in time** — two sessions cannot
|
||||
@@ -976,7 +996,7 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `252` | RPL_LUSEROP | On connect or LUSERS command | `{"command":"252","to":"alice","params":["0"],"body":["operator(s) online"]}` |
|
||||
| `254` | RPL_LUSERCHANNELS | On connect or LUSERS command | `{"command":"254","to":"alice","params":["3"],"body":["channels formed"]}` |
|
||||
| `255` | RPL_LUSERME | On connect or LUSERS command | `{"command":"255","to":"alice","body":["I have 5 clients and 1 servers"]}` |
|
||||
| `311` | RPL_WHOISUSER | In response to WHOIS | `{"command":"311","to":"alice","params":["bob","bob","neoirc","*"],"body":["bob"]}` |
|
||||
| `311` | RPL_WHOISUSER | In response to WHOIS | `{"command":"311","to":"alice","params":["bob","bobident","host.example.com","*"],"body":["bob"]}` |
|
||||
| `312` | RPL_WHOISSERVER | In response to WHOIS | `{"command":"312","to":"alice","params":["bob","neoirc"],"body":["neoirc server"]}` |
|
||||
| `315` | RPL_ENDOFWHO | End of WHO response | `{"command":"315","to":"alice","params":["#general"],"body":["End of /WHO list"]}` |
|
||||
| `318` | RPL_ENDOFWHOIS | End of WHOIS response | `{"command":"318","to":"alice","params":["bob"],"body":["End of /WHOIS list"]}` |
|
||||
@@ -987,7 +1007,7 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `329` | RPL_CREATIONTIME | After channel MODE query | `{"command":"329","to":"alice","params":["#general","1709251200"]}` |
|
||||
| `331` | RPL_NOTOPIC | Channel has no topic (on JOIN) | `{"command":"331","to":"alice","params":["#general"],"body":["No topic is set"]}` |
|
||||
| `332` | RPL_TOPIC | On JOIN or TOPIC query | `{"command":"332","to":"alice","params":["#general"],"body":["Welcome!"]}` |
|
||||
| `352` | RPL_WHOREPLY | In response to WHO | `{"command":"352","to":"alice","params":["#general","bob","neoirc","neoirc","bob","H"],"body":["0 bob"]}` |
|
||||
| `352` | RPL_WHOREPLY | In response to WHO | `{"command":"352","to":"alice","params":["#general","bobident","host.example.com","neoirc","bob","H"],"body":["0 bob"]}` |
|
||||
| `353` | RPL_NAMREPLY | On JOIN or NAMES query | `{"command":"353","to":"alice","params":["=","#general"],"body":["@op1 alice bob +voiced1"]}` |
|
||||
| `366` | RPL_ENDOFNAMES | End of NAMES response | `{"command":"366","to":"alice","params":["#general"],"body":["End of /NAMES list"]}` |
|
||||
| `372` | RPL_MOTD | MOTD line | `{"command":"372","to":"alice","body":["Welcome to the server"]}` |
|
||||
@@ -1107,14 +1127,20 @@ difficulty is advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{"nick": "alice", "pow_token": "1:20:260310:neoirc::3a2f1"}
|
||||
{"nick": "alice", "username": "alice", "pow_token": "1:20:260310:neoirc::3a2f1"}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|------------|--------|-------------|-------------|
|
||||
| `nick` | string | Yes | 1–32 characters, must be unique on the server |
|
||||
| `username` | string | No | 1–32 characters, IRC ident-style. Defaults to nick if omitted. |
|
||||
| `pow_token` | string | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) |
|
||||
|
||||
The `username` field sets the user portion of the IRC hostmask
|
||||
(`nick!user@host`). The hostname is automatically resolved via reverse DNS of
|
||||
the connecting client's IP address at session creation time. Together these form
|
||||
the hostmask used in WHOIS, WHO, and future ban matching (`+b`).
|
||||
|
||||
**Response:** `201 Created`
|
||||
```json
|
||||
{
|
||||
@@ -1135,6 +1161,7 @@ difficulty is advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
||||
| Status | Error | When |
|
||||
|--------|-------|------|
|
||||
| 400 | `nick must be 1-32 characters` | Empty or too-long nick |
|
||||
| 400 | `invalid username format` | Username doesn't match allowed format |
|
||||
| 402 | `hashcash proof-of-work required` | Missing `pow_token` field in request body when hashcash is enabled |
|
||||
| 402 | `invalid hashcash stamp: ...` | Stamp fails validation (wrong bits, expired, reused, etc.) |
|
||||
| 409 | `nick already taken` | Another active session holds this nick |
|
||||
@@ -1156,14 +1183,18 @@ remains active.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{"nick": "alice", "password": "mypassword"}
|
||||
{"nick": "alice", "username": "alice", "password": "mypassword"}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|------------|--------|----------|-------------|
|
||||
| `nick` | string | Yes | 1–32 characters, must be unique on the server |
|
||||
| `username` | string | No | 1–32 characters, IRC ident-style. Defaults to nick if omitted. |
|
||||
| `password` | string | Yes | Minimum 8 characters |
|
||||
|
||||
The `username` and hostname (auto-resolved via reverse DNS) form the IRC
|
||||
hostmask (`nick!user@host`) shown in WHOIS and WHO responses.
|
||||
|
||||
**Response:** `201 Created`
|
||||
```json
|
||||
{
|
||||
@@ -1184,6 +1215,7 @@ remains active.
|
||||
| Status | Error | When |
|
||||
|--------|-------|------|
|
||||
| 400 | `invalid nick format` | Nick doesn't match allowed format |
|
||||
| 400 | `invalid username format` | Username doesn't match allowed format |
|
||||
| 400 | `password must be at least 8 characters` | Password too short |
|
||||
| 409 | `nick already taken` | Another active session holds this nick |
|
||||
|
||||
@@ -1987,16 +2019,18 @@ The database schema is managed via embedded SQL migration files in
|
||||
**Current tables:**
|
||||
|
||||
#### `sessions`
|
||||
| Column | Type | Description |
|
||||
|-----------------|----------|-------------|
|
||||
| `id` | INTEGER | Primary key (auto-increment) |
|
||||
| `uuid` | TEXT | Unique session UUID |
|
||||
| `nick` | TEXT | Unique nick |
|
||||
| `password_hash` | TEXT | bcrypt hash (empty string for anonymous sessions) |
|
||||
| `signing_key` | TEXT | Public signing key (empty string if unset) |
|
||||
| `away_message` | TEXT | Away message (empty string if not away) |
|
||||
| `created_at` | DATETIME | Session creation time |
|
||||
| `last_seen` | DATETIME | Last API request time |
|
||||
| Column | Type | Description |
|
||||
|----------------|----------|-------------|
|
||||
| `id` | INTEGER | Primary key (auto-increment) |
|
||||
| `uuid` | TEXT | Unique session UUID |
|
||||
| `nick` | TEXT | Unique nick |
|
||||
| `username` | TEXT | IRC ident/username portion of the hostmask (defaults to nick) |
|
||||
| `hostname` | TEXT | Reverse DNS hostname of the connecting client IP |
|
||||
| `password_hash`| TEXT | bcrypt hash (empty string for anonymous sessions) |
|
||||
| `signing_key` | TEXT | Public signing key (empty string if unset) |
|
||||
| `away_message` | TEXT | Away message (empty string if not away) |
|
||||
| `created_at` | DATETIME | Session creation time |
|
||||
| `last_seen` | DATETIME | Last API request time |
|
||||
|
||||
Index on `(uuid)`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user