#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; } } }