aboutsummaryrefslogtreecommitdiffstats
path: root/usr/ircd.c
blob: eccaf828bb3910a5246771feda319a1f042f15ac (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#include "sock.h"
/* ircd [PORT] - an IRC server running on the Game Boy. Default port 6667.
 *
 *   /# ircd &
 *   host$ irssi -c 10.0.0.2          (or any client)
 *   ... and gbos's own irc(1) can join the same channel from another GB.
 *
 * Implements the subset a real client needs to get in and talk: NICK, USER,
 * PING/PONG, JOIN, PART, PRIVMSG/NOTICE (to a channel or a nick), NAMES,
 * QUIT, plus the 001/004/375/372/376 numerics clients wait for on registration
 * and 353/366 on join. One channel per client keeps the bookkeeping to a
 * fixed-size table - this is a 1997 handheld with 8 KiB of task RAM.
 *
 * MAXCL is bounded by the kernel socket table (MAX_SOCKS), which the listener
 * and every client each take one of; the rest are left for other programs.
 *
 * Statics are fine here because main() copies the command line off $A000
 * before touching any of them - sock.h's _nr already lives there.
 */

#define PORT_DEF 6667
#define MAXCL    3   /* MAX_SOCKS(6) - listener - 2 spare */
#define INBUF    128 /* longest line we will reassemble */
#define NICKSZ   10
#define CHANSZ   12
#define OBUFSZ   200 /* one segment */
#define RBUFSZ   200

static unsigned char lsock;
static unsigned char csock[MAXCL];
static unsigned char cused[MAXCL];
static unsigned char creg[MAXCL];  /* sent the welcome burst      */
static unsigned char cgotu[MAXCL]; /* seen USER                   */
static char cnick[MAXCL][NICKSZ];
static char cchan[MAXCL][CHANSZ];
static char cin[MAXCL][INBUF];
static unsigned char cinlen[MAXCL];
static unsigned char rbuf[RBUFSZ];
static char msg[OBUFSZ];
static unsigned char mlen;
static char line[INBUF]; /* the line being dispatched */

/* ---- tiny string helpers -------------------------------------------------- */
static unsigned char up(char c) { return (unsigned char)((c >= 'a' && c <= 'z') ? c - 32 : c); }
/* case-insensitive compare of a command word */
static unsigned char cmdis(const char *w, const char *name) {
    while (*name) {
        if (up(*w) != (unsigned char)*name) return 0;
        w++;
        name++;
    }
    return (unsigned char)(*w == 0);
}
/* IRC names - nicknames AND channels - are case-insensitive, so "Alice" and
   "alice" are one person, "#GB" and "#gb" one channel. (RFC 1459 also folds
   []\\~ onto {}|^; plain ASCII is close enough for a Game Boy.) */
static unsigned char irceq(const char *a, const char *b) {
    while (*a && up(*a) == up(*b)) {
        a++;
        b++;
    }
    return (unsigned char)(*a == 0 && *b == 0);
}
static void scpy(char *d, const char *s, unsigned char max) {
    unsigned char i = 0;
    while (s[i] && i < max - 1) {
        d[i] = s[i];
        i++;
    }
    d[i] = 0;
}

/* ---- outgoing message builder --------------------------------------------- */
static void m(const char *s) {
    while (*s && mlen < OBUFSZ - 2) msg[mlen++] = *s++;
}
static void mstart(void) { mlen = 0; }
static void mend(void) {
    msg[mlen++] = '\r';
    msg[mlen++] = '\n';
}
static void msend(unsigned char ci) {
    if (cused[ci]) net_send(csock[ci], msg, mlen);
}
/* ":nick!u@gbos " - the prefix a client needs to attribute an event */
static void mprefix(unsigned char ci) {
    mstart();
    m(":");
    m(cnick[ci]);
    m("!u@gbos ");
}
/* ":gbos NNN nick " - a numeric reply */
static void mnumeric(unsigned char ci, const char *num) {
    mstart();
    m(":gbos ");
    m(num);
    m(" ");
    m(cnick[ci][0] ? cnick[ci] : "*");
    m(" ");
}
/* everyone in ci's channel (except `except`, use 0xFF for nobody) */
static void bcast(const char *chan, unsigned char except) {
    unsigned char i;
    if (!chan[0]) return;
    for (i = 0; i < MAXCL; i++)
        if (cused[i] && creg[i] && i != except && irceq(cchan[i], chan)) msend(i);
}

/* is `n` already in use by some OTHER client? */
static unsigned char nick_taken(const char *n, unsigned char except) {
    unsigned char i;
    for (i = 0; i < MAXCL; i++)
        if (cused[i] && i != except && cnick[i][0] && irceq(cnick[i], n)) return 1;
    return 0;
}

static void logline(const char *a, const char *b) {
    puts(a);
    if (b) puts(b);
    nl();
}

/* ---- client lifecycle ------------------------------------------------------ */
static void drop(unsigned char ci, const char *why) {
    if (!cused[ci]) return;
    if (creg[ci] && cchan[ci][0]) { /* tell the channel it lost them */
        mprefix(ci);
        m("QUIT :");
        m(why);
        mend();
        bcast(cchan[ci], ci);
    }
    logline("- ", cnick[ci][0] ? cnick[ci] : "(unregistered)");
    net_close(csock[ci]);
    cused[ci] = 0;
    creg[ci] = 0;
    cgotu[ci] = 0;
    cinlen[ci] = 0;
    cnick[ci][0] = 0;
    cchan[ci][0] = 0;
}

static void welcome(unsigned char ci) {
    creg[ci] = 1;
    mnumeric(ci, "001");
    m(":Welcome to gbos IRC, ");
    m(cnick[ci]);
    mend();
    msend(ci);
    mnumeric(ci, "004");
    m("gbos gbos-1.0 o o");
    mend();
    msend(ci);
    mnumeric(ci, "375");
    m(":- gbos Message of the Day -");
    mend();
    msend(ci);
    mnumeric(ci, "372");
    m(":- an IRC server on a Game Boy Color");
    mend();
    msend(ci);
    mnumeric(ci, "376");
    m(":End of /MOTD command.");
    mend();
    msend(ci);
    logline("+ ", cnick[ci]);
}

/* 353 (names) + 366 (end of names) for ci's channel */
static void names(unsigned char ci) {
    unsigned char i;
    mnumeric(ci, "353");
    m("= ");
    m(cchan[ci]);
    m(" :");
    for (i = 0; i < MAXCL; i++)
        if (cused[i] && creg[i] && irceq(cchan[i], cchan[ci])) {
            m(cnick[i]);
            m(" ");
        }
    mend();
    msend(ci);
    mnumeric(ci, "366");
    m(cchan[ci]);
    m(" :End of /NAMES list");
    mend();
    msend(ci);
}

/* ---- command dispatch ------------------------------------------------------ */
/* line[] holds one command, already stripped of CR/LF. Splits in place. */
static void dispatch(unsigned char ci) {
    char *cmd, *a1, *a2, *p;
    unsigned char i;

    p = line;
    if (*p == ':') { /* ignore any client-sent prefix */
        while (*p && *p != ' ') p++;
        while (*p == ' ') p++;
    }
    cmd = p;
    while (*p && *p != ' ') p++;
    if (*p) {
        *p++ = 0;
        while (*p == ' ') p++;
    }
    a1 = p; /* first argument (may be empty) */
    while (*p && *p != ' ') p++;
    if (*p) {
        *p++ = 0;
        while (*p == ' ') p++;
    }
    a2 = p; /* trailing text (may start ':') */
    if (*a2 == ':') a2++;

    if (cmdis(cmd, "NICK")) {
        if (!a1[0]) return;
        if (nick_taken(a1, ci)) { /* 433: keep the nick they have */
            mnumeric(ci, "433");
            m(a1);
            m(" :Nickname is already in use");
            mend();
            msend(ci);
            return;
        }
        if (irceq(cnick[ci], a1)) { /* same nick, only case: just take it */
            scpy(cnick[ci], a1, NICKSZ);
            return;
        }
        if (creg[ci]) { /* a rename: everyone must be told */
            mprefix(ci);
            m("NICK :");
            m(a1);
            mend();
            msend(ci);
            bcast(cchan[ci], ci);
            logline(cnick[ci], " renamed");
        }
        scpy(cnick[ci], a1, NICKSZ);
        if (cgotu[ci] && !creg[ci]) welcome(ci);
        return;
    }
    if (cmdis(cmd, "USER")) {
        cgotu[ci] = 1;
        if (cnick[ci][0] && !creg[ci]) welcome(ci);
        return;
    }
    if (cmdis(cmd, "PING")) {
        mstart();
        m(":gbos PONG gbos :");
        m(a1[0] ? a1 : "gbos");
        mend();
        msend(ci);
        return;
    }
    if (cmdis(cmd, "QUIT")) {
        drop(ci, a2[0] ? a2 : "Client quit");
        return;
    }
    if (!creg[ci]) return; /* everything below needs a nick */

    if (cmdis(cmd, "JOIN")) {
        if (!a1[0]) return;
        if (cchan[ci][0]) { /* one channel at a time */
            mprefix(ci);
            m("PART ");
            m(cchan[ci]);
            mend();
            bcast(cchan[ci], 0xFF);
        }
        scpy(cchan[ci], a1, CHANSZ);
        mprefix(ci);
        m("JOIN ");
        m(cchan[ci]);
        mend();
        msend(ci); /* the joiner sees it too */
        bcast(cchan[ci], ci);
        names(ci);
        logline(cnick[ci], " joined");
        return;
    }
    if (cmdis(cmd, "PART")) {
        if (!cchan[ci][0]) return;
        mprefix(ci);
        m("PART ");
        m(cchan[ci]);
        mend();
        msend(ci);
        bcast(cchan[ci], ci);
        cchan[ci][0] = 0;
        return;
    }
    if (cmdis(cmd, "NAMES")) {
        if (cchan[ci][0]) names(ci);
        return;
    }
    if (cmdis(cmd, "PRIVMSG") || cmdis(cmd, "NOTICE")) {
        if (!a1[0]) return;
        mprefix(ci);
        m(cmdis(cmd, "NOTICE") ? "NOTICE " : "PRIVMSG ");
        m(a1);
        m(" :");
        m(a2);
        mend();
        if (a1[0] == '#') {
            bcast(a1, ci); /* to a channel, not back to us */
            puts("<");
            puts(cnick[ci]);
            puts("> ");
            puts(a2);
            nl();
        } else {
            for (i = 0; i < MAXCL; i++) /* to one nick */
                if (cused[i] && creg[i] && irceq(cnick[i], a1)) msend(i);
        }
        return;
    }
    mnumeric(ci, "421");
    m(cmd);
    m(" :Unknown command");
    mend();
    msend(ci);
}

/* feed received bytes into ci's line buffer, dispatching each complete line */
static void feed(unsigned char ci, unsigned char n) {
    unsigned char i;
    char c;
    for (i = 0; i < n; i++) {
        c = (char)rbuf[i];
        if (c == '\n' || c == '\r') {
            if (cinlen[ci]) {
                scpy(line, cin[ci], INBUF);
                cinlen[ci] = 0;
                cin[ci][0] = 0;
                dispatch(ci);
                if (!cused[ci]) return; /* QUIT closed us mid-buffer */
            }
            continue;
        }
        if (cinlen[ci] < INBUF - 1) {
            cin[ci][cinlen[ci]++] = c;
            cin[ci][cinlen[ci]] = 0;
        }
    }
}

static unsigned int atou16(const char *s) {
    unsigned int n = 0;
    while (*s >= '0' && *s <= '9') {
        n = n * 10 + (unsigned char)(*s - '0');
        s++;
    }
    return n;
}

void main(void) {
    char args[16];
    unsigned int port;
    unsigned char i, s, n, idle;

    { /* copy argv before any static (incl. _nr) lands on $A000 */
        char *a = getargs();
        for (i = 0; a[i] && i < 15; i++) args[i] = a[i];
        args[i] = 0;
    }
    for (i = 0; i < MAXCL; i++) { /* gbos does not zero statics */
        cused[i] = 0;
        creg[i] = 0;
        cgotu[i] = 0;
        cinlen[i] = 0;
        cnick[i][0] = 0;
        cchan[i][0] = 0;
        cin[i][0] = 0;
    }
    mlen = 0;
    port = (args[0] >= '0' && args[0] <= '9') ? atou16(args) : PORT_DEF;

    lsock = net_socket(SOCK_TCP);
    if (lsock == 0xFF) {
        puts("ircd: no socket");
        nl();
        sexit(1);
    }
    if (net_listen(lsock, port) == 0xFF) {
        puts("ircd: listen failed (port in use?)");
        nl();
        net_close(lsock);
        sexit(1);
    }
    puts("ircd: listening on port ");
    putu(port);
    puts(" (max ");
    putu(MAXCL);
    puts(" clients)");
    nl();

    idle = 0;
    for (;;) {
        s = net_accept(lsock); /* one RX pump per call */
        if (s != 0xFE) {
            for (i = 0; i < MAXCL && cused[i]; i++);
            if (i == MAXCL) {
                net_close(s); /* full: let them try later */
                logline("! refused (full)", 0);
            } else {
                csock[i] = s;
                cused[i] = 1;
                creg[i] = 0;
                cgotu[i] = 0;
                cinlen[i] = 0;
                cnick[i][0] = 0;
                cchan[i][0] = 0;
                cin[i][0] = 0;
                logline("* client connected", 0);
            }
            idle = 0;
        }
        for (i = 0; i < MAXCL; i++) {
            if (!cused[i]) continue;
            n = net_recv_nb(csock[i], rbuf, RBUFSZ);
            if (n == 0) {
                drop(i, "Connection closed");
                continue;
            }
            if (n == 0xFE || n == 0xFF) continue;
            feed(i, n);
            idle = 0;
        }
        if (++idle >= 4) {
            msleep(20);
            idle = 4;
        }
    }
}