feat: add OPER command and oper-only WHOIS client info
- Add OPER command with NEOIRC_OPER_NAME/NEOIRC_OPER_PASSWORD config - Add is_oper column to sessions table - Add RPL_WHOISACTUALLY (338): show client IP/hostname to opers - Add RPL_WHOISOPERATOR (313): show oper status in WHOIS - Add GetOperCount for accurate LUSERS oper count - Fix README schema: add ip/is_oper to sessions, ip/hostname to clients - Add OPER command documentation and numeric references to README - Refactor executeWhois to stay under funlen limit - Add comprehensive tests for OPER auth, oper WHOIS, non-oper WHOIS Closes #81
This commit is contained in:
72
README.md
72
README.md
@@ -222,11 +222,16 @@ Each session has an IRC-style hostmask composed of three parts:
|
||||
Each **client connection** (created at session creation, registration, or login)
|
||||
also stores its own **ip** and **hostname**, allowing the server to track the
|
||||
network origin of each individual client independently from the session.
|
||||
Client-level IP and hostname are **not displayed to regular users**. They are
|
||||
only visible to **server operators** (o-line) via `RPL_WHOISACTUALLY` (338)
|
||||
when the oper performs a WHOIS on a user.
|
||||
|
||||
The hostmask appears in:
|
||||
|
||||
- **WHOIS** (`311 RPL_WHOISUSER`) — `params` contains
|
||||
`[nick, username, hostname, "*"]`
|
||||
- **WHOIS (oper-only)** (`338 RPL_WHOISACTUALLY`) — when the querier is a
|
||||
server operator, includes the target's current client IP and hostname
|
||||
- **WHO** (`352 RPL_WHOREPLY`) — `params` contains
|
||||
`[channel, username, hostname, server, nick, flags]`
|
||||
|
||||
@@ -909,7 +914,12 @@ for each channel followed by RPL_LISTEND (323).
|
||||
#### WHOIS — User Information
|
||||
|
||||
Query information about a user. Returns RPL_WHOISUSER (311),
|
||||
RPL_WHOISSERVER (312), RPL_WHOISCHANNELS (319), and RPL_ENDOFWHOIS (318).
|
||||
RPL_WHOISSERVER (312), RPL_WHOISOPERATOR (313, if target is oper),
|
||||
RPL_WHOISIDLE (317), RPL_WHOISCHANNELS (319), and RPL_ENDOFWHOIS (318).
|
||||
|
||||
If the querying user is a **server operator** (authenticated via `OPER`),
|
||||
the response additionally includes RPL_WHOISACTUALLY (338) with the
|
||||
target's current client IP address and hostname.
|
||||
|
||||
**C2S:**
|
||||
```json
|
||||
@@ -944,6 +954,35 @@ LUSERS replies are also sent automatically during connection registration.
|
||||
|
||||
**IRC reference:** RFC 1459 §4.3.2
|
||||
|
||||
#### OPER — Gain Server Operator Status
|
||||
|
||||
Authenticate as a server operator (o-line). On success, the session gains
|
||||
oper privileges, which currently means additional information is visible in
|
||||
WHOIS responses (e.g., target user's current client IP and hostname).
|
||||
|
||||
**C2S:**
|
||||
```json
|
||||
{"command": "OPER", "body": ["opername", "operpassword"]}
|
||||
```
|
||||
|
||||
**S2C (via message queue on success):**
|
||||
```json
|
||||
{"command": "381", "to": "alice", "body": ["You are now an IRC operator"]}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- `body[0]` is the operator name, `body[1]` is the operator password.
|
||||
- The server checks against the configured `NEOIRC_OPER_NAME` and
|
||||
`NEOIRC_OPER_PASSWORD` environment variables.
|
||||
- On success, the session's `is_oper` flag is set and `381 RPL_YOUREOPER`
|
||||
is returned.
|
||||
- On failure (wrong credentials or no o-line configured), `491 ERR_NOOPERHOST`
|
||||
is returned.
|
||||
- Oper status persists for the session lifetime. There is no de-oper command.
|
||||
|
||||
**IRC reference:** RFC 1459 §4.1.5
|
||||
|
||||
#### KICK — Kick User (Planned)
|
||||
|
||||
Remove a user from a channel.
|
||||
@@ -1004,9 +1043,11 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `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","bobident","host.example.com","*"],"body":["bob"]}` |
|
||||
| `312` | RPL_WHOISSERVER | In response to WHOIS | `{"command":"312","to":"alice","params":["bob","neoirc"],"body":["neoirc server"]}` |
|
||||
| `313` | RPL_WHOISOPERATOR | In WHOIS if target is oper | `{"command":"313","to":"alice","params":["bob"],"body":["is an IRC operator"]}` |
|
||||
| `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"]}` |
|
||||
| `319` | RPL_WHOISCHANNELS | In response to WHOIS | `{"command":"319","to":"alice","params":["bob"],"body":["#general #dev"]}` |
|
||||
| `338` | RPL_WHOISACTUALLY | In WHOIS when querier is oper | `{"command":"338","to":"alice","params":["bob","192.168.1.1"],"body":["is actually using host client.example.com"]}` |
|
||||
| `322` | RPL_LIST | In response to LIST | `{"command":"322","to":"alice","params":["#general","5"],"body":["General discussion"]}` |
|
||||
| `323` | RPL_LISTEND | End of LIST response | `{"command":"323","to":"alice","body":["End of /LIST"]}` |
|
||||
| `324` | RPL_CHANNELMODEIS | In response to channel MODE query | `{"command":"324","to":"alice","params":["#general","+n"]}` |
|
||||
@@ -1019,6 +1060,7 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `372` | RPL_MOTD | MOTD line | `{"command":"372","to":"alice","body":["Welcome to the server"]}` |
|
||||
| `375` | RPL_MOTDSTART | Start of MOTD | `{"command":"375","to":"alice","body":["- neoirc-server Message of the Day -"]}` |
|
||||
| `376` | RPL_ENDOFMOTD | End of MOTD | `{"command":"376","to":"alice","body":["End of /MOTD command"]}` |
|
||||
| `381` | RPL_YOUREOPER | Successful OPER auth | `{"command":"381","to":"alice","body":["You are now an IRC operator"]}` |
|
||||
| `401` | ERR_NOSUCHNICK | DM to nonexistent nick | `{"command":"401","to":"alice","params":["bob"],"body":["No such nick/channel"]}` |
|
||||
| `403` | ERR_NOSUCHCHANNEL | Action on nonexistent channel | `{"command":"403","to":"alice","params":["#nope"],"body":["No such channel"]}` |
|
||||
| `421` | ERR_UNKNOWNCOMMAND | Unrecognized command | `{"command":"421","to":"alice","params":["FOO"],"body":["Unknown command"]}` |
|
||||
@@ -1027,6 +1069,7 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `442` | ERR_NOTONCHANNEL | Action on unjoined channel | `{"command":"442","to":"alice","params":["#general"],"body":["You're not on that channel"]}` |
|
||||
| `461` | ERR_NEEDMOREPARAMS | Missing required fields | `{"command":"461","to":"alice","params":["JOIN"],"body":["Not enough parameters"]}` |
|
||||
| `482` | ERR_CHANOPRIVSNEEDED | Non-op tries op action | `{"command":"482","to":"alice","params":["#general"],"body":["You're not channel operator"]}` |
|
||||
| `491` | ERR_NOOPERHOST | Failed OPER auth | `{"command":"491","to":"alice","body":["No O-lines for your host"]}` |
|
||||
|
||||
**Note:** Numeric replies are now implemented. All IRC command responses
|
||||
(success and error) are delivered as numeric replies through the message queue.
|
||||
@@ -1432,6 +1475,7 @@ reference with all required and optional fields.
|
||||
| `WHOIS` | `to` or `body` | | 200 OK |
|
||||
| `WHO` | `to` | | 200 OK |
|
||||
| `LUSERS` | | | 200 OK |
|
||||
| `OPER` | `body` | | 200 OK |
|
||||
| `QUIT` | | `body` | 200 OK |
|
||||
| `PING` | | | 200 OK |
|
||||
|
||||
@@ -1460,6 +1504,7 @@ auth tokens (401), and server errors (500).
|
||||
| 433 | ERR_NICKNAMEINUSE | NICK target is taken |
|
||||
| 442 | ERR_NOTONCHANNEL | Not a member of the target channel |
|
||||
| 461 | ERR_NEEDMOREPARAMS | Missing required fields (to, body) |
|
||||
| 491 | ERR_NOOPERHOST | Failed OPER authentication |
|
||||
|
||||
**IRC numeric success replies (delivered via message queue):**
|
||||
|
||||
@@ -1477,9 +1522,11 @@ auth tokens (401), and server errors (500).
|
||||
| 255 | RPL_LUSERME | On connect or LUSERS command |
|
||||
| 311 | RPL_WHOISUSER | WHOIS user info |
|
||||
| 312 | RPL_WHOISSERVER | WHOIS server info |
|
||||
| 313 | RPL_WHOISOPERATOR | WHOIS target is oper |
|
||||
| 315 | RPL_ENDOFWHO | End of WHO list |
|
||||
| 318 | RPL_ENDOFWHOIS | End of WHOIS list |
|
||||
| 319 | RPL_WHOISCHANNELS | WHOIS channels list |
|
||||
| 338 | RPL_WHOISACTUALLY | WHOIS client IP (oper-only) |
|
||||
| 322 | RPL_LIST | Channel in LIST response |
|
||||
| 323 | RPL_LISTEND | End of LIST |
|
||||
| 324 | RPL_CHANNELMODEIS | Channel mode query response |
|
||||
@@ -1492,6 +1539,7 @@ auth tokens (401), and server errors (500).
|
||||
| 375 | RPL_MOTDSTART | Start of MOTD |
|
||||
| 372 | RPL_MOTD | MOTD line |
|
||||
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
||||
| 381 | RPL_YOUREOPER | Successful OPER authentication |
|
||||
|
||||
### GET /api/v1/history — Message History
|
||||
|
||||
@@ -2032,6 +2080,8 @@ The database schema is managed via embedded SQL migration files in
|
||||
| `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 |
|
||||
| `ip` | TEXT | Real IP address of the session creator |
|
||||
| `is_oper` | INTEGER | Server operator (o-line) status (0 = no, 1 = yes) |
|
||||
| `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) |
|
||||
@@ -2041,14 +2091,16 @@ The database schema is managed via embedded SQL migration files in
|
||||
Index on `(uuid)`.
|
||||
|
||||
#### `clients`
|
||||
| Column | Type | Description |
|
||||
|--------------|----------|-------------|
|
||||
| `id` | INTEGER | Primary key (auto-increment) |
|
||||
| `uuid` | TEXT | Unique client UUID |
|
||||
| `session_id` | INTEGER | FK → sessions.id (cascade delete) |
|
||||
| `token` | TEXT | Unique auth token (SHA-256 hash of 64 hex chars) |
|
||||
| `created_at` | DATETIME | Client creation time |
|
||||
| `last_seen` | DATETIME | Last API request time |
|
||||
| Column | Type | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `id` | INTEGER | Primary key (auto-increment) |
|
||||
| `uuid` | TEXT | Unique client UUID |
|
||||
| `session_id`| INTEGER | FK → sessions.id (cascade delete) |
|
||||
| `token` | TEXT | Unique auth token (SHA-256 hash of 64 hex chars) |
|
||||
| `ip` | TEXT | Real IP address of this client connection |
|
||||
| `hostname` | TEXT | Reverse DNS hostname of this client connection |
|
||||
| `created_at`| DATETIME | Client creation time |
|
||||
| `last_seen` | DATETIME | Last API request time |
|
||||
|
||||
Indexes on `(token)` and `(session_id)`.
|
||||
|
||||
@@ -2149,6 +2201,8 @@ directory is also loaded automatically via
|
||||
| `METRICS_USERNAME` | string | `""` | Basic auth username for `/metrics` endpoint. If empty, metrics endpoint is disabled. |
|
||||
| `METRICS_PASSWORD` | string | `""` | Basic auth password for `/metrics` endpoint |
|
||||
| `NEOIRC_HASHCASH_BITS` | int | `20` | Required hashcash proof-of-work difficulty (leading zero bits in SHA-256) for session creation. Set to `0` to disable. |
|
||||
| `NEOIRC_OPER_NAME` | string | `""` | Server operator (o-line) username. Both name and password must be set to enable OPER. |
|
||||
| `NEOIRC_OPER_PASSWORD` | string | `""` | Server operator (o-line) password. Both name and password must be set to enable OPER. |
|
||||
| `MAINTENANCE_MODE` | bool | `false` | Maintenance mode flag (reserved) |
|
||||
|
||||
### Example `.env` file
|
||||
|
||||
Reference in New Issue
Block a user