diff options
| author | user <user@clank> | 2026-09-22 21:54:13 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 21:54:13 +0200 |
| commit | 0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495 (patch) | |
| tree | 35d6e8713ed3ea638e5aaea04d9c726f8f2c2d49 | |
| parent | net: one socket per client - four-tuple demux, spawning accept, bounded close (diff) | |
| download | gbos-0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495.tar.gz gbos-0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495.tar.xz gbos-0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495.zip | |
usr: ircd - an IRC server running on the Game Boy
/# ircd &
host$ irssi -c 10.0.0.2 /join #gbos
other GB$ irc 10.0.0.2 gb2 /join #gbos
Implements the subset a real client needs to get in and talk: NICK, USER,
PING/PONG, JOIN, PART, PRIVMSG/NOTICE (to a channel or to a nick), NAMES and
QUIT, with the 001/004/375/372/376 numerics clients wait for on registration,
353/366 on join, and 421 for anything else. One channel per client keeps the
bookkeeping in fixed-size tables; this is a handheld with 8 KiB of task RAM.
MAXCL is 3, bounded by the kernel socket table (listener + client each take a
slot, the rest left for other programs). A fourth client is refused and the
daemon keeps serving - which is how the ~30s close() stall turned up.
Every event is logged to the LCD, so the Game Boy shows its own server
traffic: "+ alice", "alice joined", "<alice> hello".
Verified with real clients over the hub: two and three simultaneous hosts
registering, joining, channel relay (sender not echoed to itself), private
messages by nick, PING/PONG, unknown-command numerics, QUIT propagation, the
overflow refusal, and a second Game Boy running gbos's own irc(1) in the same
channel as a laptop client - messages crossing both ways between them.
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | README.md | 17 | ||||
| -rw-r--r-- | include/gbos.inc | 1 | ||||
| -rw-r--r-- | src/programs.asm | 7 | ||||
| -rw-r--r-- | usr/ircd.c | 403 |
5 files changed, 425 insertions, 5 deletions
@@ -28,7 +28,7 @@ $(BUILD)/%.o: src/%.asm $(INCS) | $(BUILD) PROGS := chello echo true false uname pid cat wc head args \ ls save rm sh ps kill spin blk mkdir count ptest necho \ wget chat netd ping nslookup dhcp irc uptime ansi \ - grep more cp nc httpd + grep more cp nc httpd ircd CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS)) $(BUILD)/programs.o: $(CBLOBS) @@ -26,10 +26,14 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* `nc` (netcat: `nc HOST PORT` interactively, or `nc -s HOST PORT < file` to pipe a request in and print the reply), `chat`, and a colorized **IRC client** (`irc HOST [NICK]`, `/join` and friends, SELECT for the keyboard). -- **It serves, too**: the kernel does passive opens (`listen`/`accept`), and - `httpd` puts the cart's filesystem on the web — `httpd &`, then point a - browser at the Game Boy: files are served from battery-backed RAM and - directories come back as clickable listings. +- **It serves, too**: the kernel does passive opens (`listen`/`accept`, one + socket per client), so the Game Boy runs daemons. `httpd` puts the cart's + filesystem on the web — `httpd &`, then point a browser at it: files come + from battery-backed RAM, directories as clickable listings. `ircd` is a + real IRC server — `ircd &`, then `irssi -c 10.0.0.2`: NICK/USER/JOIN/ + PRIVMSG/PING/NAMES/PART/QUIT, with the channel relayed between clients. + A laptop and a second Game Boy can sit in the same channel, hosted by a + Game Boy. - **A network to plug into**: `tools/gbhub` is a virtual switch + DHCP server + NAT gateway — spawn a fleet of Game Boys that ping *each other* and reach the real internet; `tools/gbjoin` attaches an interactive GB to it, and @@ -59,6 +63,11 @@ tools/gbtype 'ping 10.0.0.3' # or type into it from the host # serve the Game Boy's filesystem to your browser: tools/gbtype 'httpd &' # then open http://10.0.0.2/ curl http://10.0.0.2/readme # files straight out of cart RAM + +# or host an IRC network on it: +tools/gbtype 'ircd &' # then, from the host: +irssi -c 10.0.0.2 # /join #gbos +tools/gbtype -g 1 'irc 10.0.0.2 gb2' # a second GB joins the same channel ``` On the Game Boy: **SELECT** toggles the on-screen keyboard, d-pad + **A** diff --git a/include/gbos.inc b/include/gbos.inc index 7f4431b..eabdcec 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -306,5 +306,6 @@ DEF PROG_MORE EQU 34 DEF PROG_CP EQU 35 DEF PROG_NC EQU 36 DEF PROG_HTTPD EQU 37 +DEF PROG_IRCD EQU 38 ENDC diff --git a/src/programs.asm b/src/programs.asm index 31125a2..0277dcc 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -154,6 +154,9 @@ ProgNc: SECTION "prog_httpd", ROMX[$4000], BANK[39] ProgHttpd: INCBIN "build/usr/httpd.bin" +SECTION "prog_ircd", ROMX[$4000], BANK[40] +ProgIrcd: + INCBIN "build/usr/ircd.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and @@ -272,6 +275,8 @@ ProgramTable:: dw ProgNc db LOW(BANK(ProgHttpd)), HIGH(BANK(ProgHttpd)) dw ProgHttpd + db LOW(BANK(ProgIrcd)), HIGH(BANK(ProgIrcd)) + dw ProgIrcd ProgramTableEnd:: ; ----------------------------------------------------------------------------- @@ -355,4 +360,6 @@ NameTable:: db PROG_NC db "httpd", 0 db PROG_HTTPD + db "ircd", 0 + db PROG_IRCD db 0 diff --git a/usr/ircd.c b/usr/ircd.c new file mode 100644 index 0000000..b960dd6 --- /dev/null +++ b/usr/ircd.c @@ -0,0 +1,403 @@ +#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 seq(const char *a, const char *b) { + while (*a && *a == *b) { + a++; + b++; + } + return (unsigned char)(*a == *b); +} +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); +} +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 && seq(cchan[i], chan)) msend(i); +} + +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] && seq(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; + 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] && seq(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; + } + } +} |
