Compare commits
1
Commits
next
..
1aa77e1bad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa77e1bad |
@@ -216,27 +216,25 @@ 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 request; defaults to the nick)
|
||||
- **hostname** — the reverse DNS name of the creating client's IP address,
|
||||
looked up at session creation time, or the IP address itself when the lookup
|
||||
gives no name
|
||||
- **hostname** — automatically resolved via reverse DNS of the connecting
|
||||
client's IP address at session creation time
|
||||
- **ip** — the real IP address of the session creator, extracted from
|
||||
`X-Forwarded-For`, `X-Real-IP`, or `RemoteAddr`
|
||||
|
||||
Each **client connection** (created at session creation or login) also stores
|
||||
its own **ip** and **hostname**, found the same way. The session's hostname is
|
||||
the hostname of the client that created it, and every user can see it: in WHOIS
|
||||
(311), WHO (352), USERHOST (302), NAMES (353) over the HTTP API, and
|
||||
`GET /api/v1/channels/{name}/members`. Only `RPL_WHOISACTUALLY` (338) is for
|
||||
**server operators** (o-line) alone: it gives the IP address and hostname of the
|
||||
target's newest client.
|
||||
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 IP address and hostname of the target's newest client
|
||||
(HTTP API only; the IRC listener's WHOIS does not send it)
|
||||
operator, includes the target's current client IP and hostname (HTTP API only;
|
||||
the IRC listener's WHOIS does not send it)
|
||||
- **WHO** (`352 RPL_WHOREPLY`) — `params` contains
|
||||
`[channel, username, hostname, server, nick, flags]`
|
||||
|
||||
@@ -440,8 +438,7 @@ The entire read/write loop for a client is two endpoints. Everything else
|
||||
```
|
||||
┌─ Client ──────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. POST /api/v1/session │
|
||||
│ {"nick":"alice","pow_token":"<hashcash stamp>"} │
|
||||
│ 1. POST /api/v1/session {"nick":"alice"} │
|
||||
│ → Set-Cookie: neoirc_auth=<random_hex>; HttpOnly; ... │
|
||||
│ → {"id":1, "nick":"alice"} │
|
||||
│ │
|
||||
@@ -474,8 +471,7 @@ The entire read/write loop for a client is two endpoints. Everything else
|
||||
```
|
||||
┌─ Client A ────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. POST /api/v1/session │
|
||||
│ {"nick":"alice","pow_token":"<hashcash stamp>"} │
|
||||
│ 1. POST /api/v1/session {"nick":"alice"} │
|
||||
│ → Set-Cookie: neoirc_auth=<cookie_a>; HttpOnly; ... │
|
||||
│ → {"id":1, "nick":"alice"} │
|
||||
│ │
|
||||
@@ -1083,8 +1079,8 @@ RPL_WHOISCHANNELS (319), and RPL_ENDOFWHOIS (318).
|
||||
|
||||
If the querying user is a **server operator** (authenticated via `OPER`), the
|
||||
response over the HTTP API additionally includes RPL_WHOISACTUALLY (338) with
|
||||
the IP address and hostname of the target's newest client. The IRC listener's
|
||||
WHOIS does not send it.
|
||||
the target's current client IP address and hostname. The IRC listener's WHOIS
|
||||
does not send it.
|
||||
|
||||
**C2S:**
|
||||
|
||||
@@ -1126,8 +1122,7 @@ LUSERS replies are also sent automatically during connection registration.
|
||||
|
||||
Authenticate as a server operator (o-line). On success, the session gains oper
|
||||
privileges: it can use `KILL` and `WALLOPS`, and over the HTTP API its WHOIS
|
||||
responses include more (the IP address and hostname of the target user's newest
|
||||
client).
|
||||
responses include more (the target user's current client IP and hostname).
|
||||
|
||||
**C2S:**
|
||||
|
||||
@@ -1351,10 +1346,10 @@ key ERR_BADCHANNELKEY (475), and one joining a full channel ERR_CHANNELISFULL
|
||||
`KICK #channel nick [:reason]`. The kicked user and all channel members receive
|
||||
the KICK message.
|
||||
|
||||
**NOTICE:** Over the HTTP API, a NOTICE gets no RPL_AWAY and no hashcash check
|
||||
on `+H` channels, but it gets the same error numerics as PRIVMSG (411, 412, 401,
|
||||
403, 404). The IRC listener handles NOTICE exactly as PRIVMSG, so a NOTICE to a
|
||||
user who is away gets RPL_AWAY there.
|
||||
**NOTICE:** Follows RFC 2812 over the HTTP API — NOTICE never triggers
|
||||
auto-replies (including RPL_AWAY), and skips hashcash validation on +H channels
|
||||
(servers and services use NOTICE). On the IRC listener, a NOTICE to a user who
|
||||
is away does get RPL_AWAY.
|
||||
|
||||
**ISUPPORT:** The server advertises `PREFIX=(ov)@+` in RPL_ISUPPORT (005), with
|
||||
`CHANMODES=b,k,Hl,imnst` over the HTTP API and `CHANMODES=,,H,imnst` on the IRC
|
||||
@@ -1453,11 +1448,9 @@ difficulty is advertised via `GET /api/v1/server` in the `hashcash_bits` field.
|
||||
| `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 the reverse DNS name of the connecting
|
||||
client's IP address, looked up at session creation time, or the IP address
|
||||
itself when the lookup gives no name; every user can see it (see
|
||||
[Hostmask](#hostmask-nickuserhost)). Together these form the hostmask used in
|
||||
WHOIS, WHO, NAMES, and ban matching (`+b`).
|
||||
(`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 ban matching (`+b`).
|
||||
|
||||
**Response:** `201 Created`
|
||||
|
||||
@@ -1736,12 +1729,12 @@ reference with all required and optional fields.
|
||||
All IRC commands return HTTP 200 OK, usually with `{"status": "error"}` when the
|
||||
reply is an error numeric (a WHOIS for an unknown nick still gets
|
||||
`{"status": "ok"}`), and otherwise a status of the command's own (see each
|
||||
command). **Numeric replies**, for success and for errors, are delivered through
|
||||
the message queue (see [Numeric Reply Codes](#numeric-reply-codes-s2c-only)); a
|
||||
command that has none, such as a successful `PRIVMSG`, is answered by the HTTP
|
||||
response alone, and `PING`'s `PONG` is the HTTP response body. HTTP error codes
|
||||
(4xx/5xx) are reserved for transport-level problems: malformed JSON (400),
|
||||
missing/invalid auth cookies (401), and server errors (500).
|
||||
command). IRC-level success and error responses are delivered as **numeric
|
||||
replies** through the message queue (see
|
||||
[Numeric Reply Codes](#numeric-reply-codes-s2c-only)); `PING` is the exception,
|
||||
its `PONG` is the HTTP response body. HTTP error codes (4xx/5xx) are reserved
|
||||
for transport-level problems: malformed JSON (400), missing/invalid auth cookies
|
||||
(401), and server errors (500).
|
||||
|
||||
**HTTP errors (transport-level only):**
|
||||
|
||||
@@ -2408,36 +2401,36 @@ applied; `001_initial.sql` creates the tables below.
|
||||
|
||||
#### `sessions`
|
||||
|
||||
| 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 name of the creating client's IP, or the IP itself when it has none |
|
||||
| `ip` | TEXT | Real IP address of the session creator |
|
||||
| `is_oper` | INTEGER | Server operator, user mode `+o` (0 = no, 1 = yes) |
|
||||
| `is_wallops` | INTEGER | User mode `+w`, receives `WALLOPS` (0 = no, 1 = yes) |
|
||||
| `password_hash` | TEXT | bcrypt hash (empty string for anonymous sessions) |
|
||||
| `signing_key` | TEXT | Public signing key; nothing sets or reads it yet |
|
||||
| `away_message` | TEXT | Away message (empty string if not away) |
|
||||
| `created_at` | DATETIME | Session creation time |
|
||||
| `last_seen` | DATETIME | Last login, authenticated HTTP API request or IRC listener command (or creation) |
|
||||
| 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 |
|
||||
| `ip` | TEXT | Real IP address of the session creator |
|
||||
| `is_oper` | INTEGER | Server operator, user mode `+o` (0 = no, 1 = yes) |
|
||||
| `is_wallops` | INTEGER | User mode `+w`, receives `WALLOPS` (0 = no, 1 = yes) |
|
||||
| `password_hash` | TEXT | bcrypt hash (empty string for anonymous sessions) |
|
||||
| `signing_key` | TEXT | Public signing key; nothing sets or reads it yet |
|
||||
| `away_message` | TEXT | Away message (empty string if not away) |
|
||||
| `created_at` | DATETIME | Session creation time |
|
||||
| `last_seen` | DATETIME | Last login or authenticated HTTP API request (or creation) |
|
||||
|
||||
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 | Auth cookie value (SHA-256 hash of the 64-hex-char cookie) |
|
||||
| `ip` | TEXT | Real IP address of this client connection |
|
||||
| `hostname` | TEXT | Reverse DNS name of this client's IP, or the IP itself when it has none |
|
||||
| `created_at` | DATETIME | Client creation time |
|
||||
| `last_seen` | DATETIME | Last authenticated HTTP API request or IRC listener command (or creation) |
|
||||
| Column | Type | Description |
|
||||
| ------------ | -------- | ---------------------------------------------------------- |
|
||||
| `id` | INTEGER | Primary key (auto-increment) |
|
||||
| `uuid` | TEXT | Unique client UUID |
|
||||
| `session_id` | INTEGER | FK → sessions.id (cascade delete) |
|
||||
| `token` | TEXT | Auth cookie value (SHA-256 hash of the 64-hex-char cookie) |
|
||||
| `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 authenticated HTTP API request (or creation) |
|
||||
|
||||
Indexes on `(token)` and `(session_id)`.
|
||||
|
||||
@@ -2555,9 +2548,10 @@ issues) and simpler than UUIDs (integer comparison vs. string comparison).
|
||||
session types in the cleanup path — `handleQuit` and `cleanupUser` both call
|
||||
`DeleteSession` unconditionally. Idle clients are automatically removed after
|
||||
`SESSION_IDLE_TIMEOUT` (default 30 days) without an authenticated HTTP API
|
||||
request or, on the IRC listener, a command — the server runs a background
|
||||
cleanup loop that, for a session left with no clients, parts the user from all
|
||||
channels, broadcasts QUIT, and releases the nick.
|
||||
request — the server runs a background cleanup loop that, for a session left
|
||||
with no clients, parts the user from all channels, broadcasts QUIT, and
|
||||
releases the nick. A client on the IRC listener never makes such a request, so
|
||||
it counts as idle from when it connected.
|
||||
- **Clients**: Individual client auth cookies are invalidated on
|
||||
`POST /api/v1/logout`. A session can have multiple clients; removing one
|
||||
doesn't affect others. However, when the last client is removed (via logout),
|
||||
@@ -2581,28 +2575,28 @@ and Viper also reads a YAML file `neoirc.yaml` from `/etc/neoirc/` or
|
||||
environment variable wins over the file. An environment variable set to the
|
||||
empty string counts as unset, so the file's value or the default applies.
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
| ---------------------- | ------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | int | `8080` | HTTP listen port |
|
||||
| `DBURL` | string | `file:///var/lib/neoirc/state.db?_journal_mode=WAL` | SQLite connection string. For file-based: `file:///path/to/db.db?_journal_mode=WAL`. For in-memory (testing): `file::memory:?cache=shared`. The SQLite driver ignores `_journal_mode`, so this does not turn on WAL mode. |
|
||||
| `DEBUG` | bool | `false` | Switch logging to debug level |
|
||||
| `MESSAGE_MAX_AGE` | string | `720h` | Maximum age of messages as a Go duration string (e.g. `720h`, `24h`). Messages older than this are pruned. Default is 30 days. |
|
||||
| `SESSION_IDLE_TIMEOUT` | string | `720h` | Idle timeout as a Go duration string (e.g. `720h`, `24h`). Clients with no authenticated HTTP API request or IRC listener command for this long are removed, and a session left with none is expired and its nick released (see [Data Lifecycle](#data-lifecycle)). Default is 30 days. |
|
||||
| `QUEUE_MAX_AGE` | string | `720h` | Maximum age of client output queue entries as a Go duration string (e.g. `720h`, `24h`). Entries older than this are pruned. Default is 30 days. |
|
||||
| `MAX_MESSAGE_SIZE` | int | `4096` | Maximum request body size in bytes for `POST /api/v1/session`, `POST /api/v1/login` and `POST /api/v1/messages`; a larger body gets `400` `invalid request body` |
|
||||
| `MOTD` | string | a built-in banner | Message of the day, sent to clients as MOTD numerics and shown via `GET /api/v1/server` |
|
||||
| `SERVER_NAME` | string | `""` | Server display name. If empty, the server calls itself `neoirc` in replies and in `GET /api/v1/server` returns it empty. |
|
||||
| `FEDERATION_KEY` | string | `""` | Shared key for server federation linking (planned) |
|
||||
| `SENTRY_DSN` | string | `""` | Sentry error tracking DSN (optional) |
|
||||
| `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. |
|
||||
| `LOGIN_RATE_LIMIT` | float | `1` | Allowed login attempts per second per IP address. |
|
||||
| `LOGIN_RATE_BURST` | int | `5` | Maximum burst of login attempts per IP before rate limiting kicks in. |
|
||||
| `IRC_LISTEN_ADDR` | string | `:6667` | TCP address for the traditional IRC protocol listener. Only an empty value in `neoirc.yaml` disables it; an empty environment variable does not. |
|
||||
| `MAINTENANCE_MODE` | bool | `false` | Maintenance mode flag (reserved) |
|
||||
| Variable | Type | Default | Description |
|
||||
| ---------------------- | ------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | int | `8080` | HTTP listen port |
|
||||
| `DBURL` | string | `file:///var/lib/neoirc/state.db?_journal_mode=WAL` | SQLite connection string. For file-based: `file:///path/to/db.db?_journal_mode=WAL`. For in-memory (testing): `file::memory:?cache=shared`. The SQLite driver ignores `_journal_mode`, so this does not turn on WAL mode. |
|
||||
| `DEBUG` | bool | `false` | Switch logging to debug level |
|
||||
| `MESSAGE_MAX_AGE` | string | `720h` | Maximum age of messages as a Go duration string (e.g. `720h`, `24h`). Messages older than this are pruned. Default is 30 days. |
|
||||
| `SESSION_IDLE_TIMEOUT` | string | `720h` | Idle timeout as a Go duration string (e.g. `720h`, `24h`). Clients with no authenticated HTTP API request for this long are removed, and a session left with none is expired and its nick released (see [Data Lifecycle](#data-lifecycle)). Default is 30 days. |
|
||||
| `QUEUE_MAX_AGE` | string | `720h` | Maximum age of client output queue entries as a Go duration string (e.g. `720h`, `24h`). Entries older than this are pruned. Default is 30 days. |
|
||||
| `MAX_MESSAGE_SIZE` | int | `4096` | Maximum request body size in bytes for `POST /api/v1/session`, `POST /api/v1/login` and `POST /api/v1/messages`; a larger body gets `400` `invalid request body` |
|
||||
| `MOTD` | string | a built-in banner | Message of the day, sent to clients as MOTD numerics and shown via `GET /api/v1/server` |
|
||||
| `SERVER_NAME` | string | `""` | Server display name. If empty, the server calls itself `neoirc` in replies and in `GET /api/v1/server` returns it empty. |
|
||||
| `FEDERATION_KEY` | string | `""` | Shared key for server federation linking (planned) |
|
||||
| `SENTRY_DSN` | string | `""` | Sentry error tracking DSN (optional) |
|
||||
| `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. |
|
||||
| `LOGIN_RATE_LIMIT` | float | `1` | Allowed login attempts per second per IP address. |
|
||||
| `LOGIN_RATE_BURST` | int | `5` | Maximum burst of login attempts per IP before rate limiting kicks in. |
|
||||
| `IRC_LISTEN_ADDR` | string | `:6667` | TCP address for the traditional IRC protocol listener. Only an empty value in `neoirc.yaml` disables it; an empty environment variable does not. |
|
||||
| `MAINTENANCE_MODE` | bool | `false` | Maintenance mode flag (reserved) |
|
||||
|
||||
### Example `.env` file
|
||||
|
||||
@@ -2677,22 +2671,8 @@ IRC_LISTEN_ADDR: ""
|
||||
|
||||
Messages sent by IRC clients appear in channels visible to HTTP/JSON API clients
|
||||
and vice versa. The IRC listener and HTTP API share the same database, broker,
|
||||
and session infrastructure, so a user connected via IRC and a user connected via
|
||||
the HTTP API can talk in the same channels, with these gaps today:
|
||||
|
||||
- An IRC listener user receives only the first line of a multi-line `body`.
|
||||
- A `+k`, `+l` or `+b` change made over the HTTP API reaches IRC listener
|
||||
members without its key, limit or mask.
|
||||
- The IRC listener's `JOIN` accepts any channel name, while over the HTTP API a
|
||||
name that is not `#` followed by 1–63 letters, digits, `_` or `-` gets
|
||||
ERR_NOSUCHCHANNEL (403), so HTTP API users cannot join such a channel.
|
||||
- The IRC listener does not pass on a `PRIVMSG`, `NOTICE`, `JOIN`, `PART`,
|
||||
`NICK` or `QUIT` whose sender's nick matches the user's own nick ignoring
|
||||
case, so an IRC listener user `alice` never sees these from `Alice`, although
|
||||
nicks are case-sensitive.
|
||||
|
||||
https://git.eeqj.de/sneak/neoirc/issues/127 tracks the first three and
|
||||
https://git.eeqj.de/sneak/neoirc/issues/128 the last.
|
||||
and session infrastructure. A user connected via IRC and a user connected via
|
||||
the HTTP API can communicate in the same channels seamlessly.
|
||||
|
||||
### Docker Usage
|
||||
|
||||
|
||||
+3
-24
@@ -204,42 +204,21 @@ func (database *Database) GetSessionByToken(
|
||||
)
|
||||
}
|
||||
|
||||
_ = database.UpdateLastSeen(ctx, sessionID, clientID)
|
||||
|
||||
return sessionID, clientID, nick, nil
|
||||
}
|
||||
|
||||
// UpdateLastSeen sets last_seen to now on a session and on
|
||||
// one of its clients, so that the idle cleanup keeps them.
|
||||
func (database *Database) UpdateLastSeen(
|
||||
ctx context.Context,
|
||||
sessionID, clientID int64,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := database.conn.ExecContext(
|
||||
_, _ = database.conn.ExecContext(
|
||||
ctx,
|
||||
"UPDATE sessions SET last_seen = ? WHERE id = ?",
|
||||
now, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"update session last_seen: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
_, err = database.conn.ExecContext(
|
||||
_, _ = database.conn.ExecContext(
|
||||
ctx,
|
||||
"UPDATE clients SET last_seen = ? WHERE id = ?",
|
||||
now, clientID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"update client last_seen: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
return sessionID, clientID, nick, nil
|
||||
}
|
||||
|
||||
// GetSessionByNick returns session id for a given nick.
|
||||
|
||||
@@ -368,17 +368,6 @@ func (c *Conn) handleMessage(
|
||||
return
|
||||
}
|
||||
|
||||
// Every command is activity, so the idle cleanup must not
|
||||
// remove this user.
|
||||
err := c.database.UpdateLastSeen(
|
||||
ctx, c.sessionID, c.clientID,
|
||||
)
|
||||
if err != nil {
|
||||
c.log.Error(
|
||||
"failed to update last_seen", "error", err,
|
||||
)
|
||||
}
|
||||
|
||||
handler, ok := c.commands[msg.Command]
|
||||
if !ok {
|
||||
c.sendNumeric(
|
||||
|
||||
@@ -2,7 +2,6 @@ package ircserver_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -70,11 +69,9 @@ func newTestEnvWithConfig(
|
||||
) *testEnv {
|
||||
t.Helper()
|
||||
|
||||
// A random name, so that no other test environment, not
|
||||
// even an earlier run of the same test, can share it.
|
||||
dsn := fmt.Sprintf(
|
||||
"file:%s?mode=memory&cache=shared&_journal_mode=WAL",
|
||||
rand.Text(),
|
||||
t.Name(),
|
||||
)
|
||||
|
||||
conn, err := sql.Open("sqlite", dsn)
|
||||
@@ -82,13 +79,6 @@ func newTestEnvWithConfig(
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
t.Logf("close db: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
conn.SetMaxOpenConns(1)
|
||||
|
||||
_, err = conn.ExecContext(
|
||||
@@ -133,9 +123,14 @@ func newTestEnvWithConfig(
|
||||
t.Fatalf("start irc server: %v", err)
|
||||
}
|
||||
|
||||
// Cleanups run last registered first, so the server stops
|
||||
// before its database is closed.
|
||||
t.Cleanup(srv.Stop)
|
||||
t.Cleanup(func() {
|
||||
srv.Stop()
|
||||
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
t.Logf("close db: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return &testEnv{
|
||||
database: database,
|
||||
@@ -347,20 +342,6 @@ func TestRegistration(t *testing.T) {
|
||||
assertContains(t, lines, " 001 ", "RPL_WELCOME")
|
||||
}
|
||||
|
||||
// TestEachTestEnvHasItsOwnDatabase checks that a test
|
||||
// environment starts on an empty database while another one
|
||||
// is still open.
|
||||
func TestEachTestEnvHasItsOwnDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first := newTestEnv(t)
|
||||
first.dial(t).register("samenick")
|
||||
|
||||
second := newTestEnv(t)
|
||||
lines := second.dial(t).register("samenick")
|
||||
assertContains(t, lines, " 001 ", "RPL_WELCOME")
|
||||
}
|
||||
|
||||
func TestWelcomeContainsNick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -388,39 +369,6 @@ func TestPingPong(t *testing.T) {
|
||||
assertContains(t, lines, "PONG", "PONG response")
|
||||
}
|
||||
|
||||
// TestIdleCleanupKeepsActiveUser checks that a user who
|
||||
// sends commands is kept by the idle cleanup, and a user who
|
||||
// sends nothing is not.
|
||||
func TestIdleCleanupKeepsActiveUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const idleTimeout = 500 * time.Millisecond
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
active := env.dial(t)
|
||||
active.register("active")
|
||||
|
||||
idle := env.dial(t)
|
||||
idle.register("idle")
|
||||
|
||||
time.Sleep(idleTimeout)
|
||||
active.sendAndExpect("PING :still here", "PONG")
|
||||
|
||||
// The idle cleanup in internal/handlers removes exactly
|
||||
// the users this returns.
|
||||
stale, err := env.database.GetStaleOrphanSessions(
|
||||
t.Context(), time.Now().Add(-idleTimeout),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("get stale sessions: %v", err)
|
||||
}
|
||||
|
||||
if len(stale) != 1 || stale[0].Nick != "idle" {
|
||||
t.Errorf("cleanup removes %v, want only idle", stale)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user