Annotation of dgamelaunch-openbsd/dgamelaunch.c, Revision 1.8
1.1 rubenllo 1: /* dgamelaunch.c
2: *
1.3 rubenllo 3: * (c)2021 Rubén Llorente <porting@use.startmail.com>
1.1 rubenllo 4: * (c)2001-4 M. Drew Streib <dtype@dtype.org>
5: * also parts (c) 2003-4 Joshua Kwan <joshk@triplehelix.org>,
6: * Brett Carrington <brettcar@segvio.org>,
7: * Jilles Tjoelker <jilles@stack.nl>
8: *
9: * This program is free software; you can redistribute it and/or modify
10: * it under the terms of the GNU General Public License as published by
11: * the Free Software Foundation; either version 2 of the License, or
12: * (at your option) any later version.
13: *
14: * This program is distributed in the hope that it will be useful,
15: * but WITHOUT ANY WARRANTY; without even the implied warranty of
16: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17: * GNU General Public License for more details.
18: *
19: * You should have received a copy of the GNU General Public License
20: * along with this program; if not, write to the Free Software
21: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22: */
23:
24: /*
25: * See this program in action at http://alt.org/nethack/
26: *
27: * This is a little wrapper for nethack (and soon other programs) that
28: * will allow them to be run from a telnetd session, chroot, shed privs,
29: * make a simple login, then play the game.
30: */
31:
32: #define _GNU_SOURCE
33:
34: #include "dgamelaunch.h"
35: #include "config.h"
36: #include "ttyplay.h"
37: #include "ttyrec.h"
38:
39: /* a request from the author: please leave some remnance of
40: * 'based on dgamelaunch version xxx' in any derivative works, or
41: * even keep the line the same altogether. I'm probably happy
42: * to make any changes you need. */
43:
44: /* ************************************************************* */
45:
46: /* program stuff */
47: #include <sys/types.h>
48: #include <sys/time.h>
49: #include <sys/wait.h>
50: #include <sys/ioctl.h> /* ttyrec */
51: #include <sys/stat.h>
52: #ifdef USE_RLIMIT
53: #include <sys/resource.h>
54: #endif
55:
56: #ifdef USE_SHMEM
57: #include <sys/ipc.h>
58: #include <sys/shm.h>
59: #endif
60:
61: #include <libgen.h>
62: #include <stdlib.h>
63: #include <curses.h>
64: #include <locale.h>
65:
66: #ifdef USE_SQLITE3
67: # include <sqlite3.h>
68: #endif
69:
70: #if defined(__FreeBSD__)
71: # include <libutil.h>
72: #elif defined(__NetBSD__)
73: # include <util.h>
1.2 rubenllo 74: #elif defined(__OpenBSD__)
75: # include <unistd.h>
1.1 rubenllo 76: #elif defined(__APPLE__)
77: # include <unistd.h>
78: #else
79: # include <crypt.h>
80: #endif
81:
82: #ifdef __linux__
83: # include <pty.h>
84: #endif
85:
86: #include <fcntl.h>
87: #include <pwd.h>
88: #include <grp.h>
89: #include <time.h>
90: #include <errno.h>
91: #include <dirent.h>
92: #include <string.h>
93: #include <signal.h>
94: #include <assert.h>
95: #include <ctype.h>
96: #include <unistd.h>
97: #include <termios.h>
98:
99: extern FILE* yyin;
100: extern int yyparse ();
101:
102: /* global variables */
103:
104: char * __progname;
105: int g_idle_alarm_enabled = 0;
106: int showplayers = 0;
107: int initplayer = 0;
108: void (*g_chain_winch)(int);
109:
110: #ifndef USE_SQLITE3
111: int f_num = 0;
112: struct dg_user **users = NULL;
113: #endif
114: struct dg_user *me = NULL;
115: struct dg_banner banner;
116:
117: static struct dg_watchcols default_watchcols[] = {
118: {SORTMODE_NONE, SORTMODE_NONE, 1, "", "%s)"},
119: {SORTMODE_USERNAME, SORTMODE_USERNAME, 4, "Username", "%-15s"},
120: {SORTMODE_GAMENUM, SORTMODE_GAMENUM, 21, "Game", "%-5s"},
121: {SORTMODE_WINDOWSIZE, SORTMODE_WINDOWSIZE, 28, " Size", "%s"},
122: {SORTMODE_STARTTIME, SORTMODE_STARTTIME, 37, "Start date & time", "%s"},
123: {SORTMODE_IDLETIME, SORTMODE_IDLETIME, 58, "Idle time", "%-10s"},
124: #ifdef USE_SHMEM
125: {SORTMODE_WATCHERS, SORTMODE_WATCHERS, 70, "Watchers", "%s"},
126: #endif
127: };
128:
129: int color_remap[16] = {
130: COLOR_PAIR(9) | A_NORMAL,
131: COLOR_PAIR(COLOR_BLUE) | A_NORMAL,
132: COLOR_PAIR(COLOR_GREEN) | A_NORMAL,
133: COLOR_PAIR(COLOR_CYAN) | A_NORMAL,
134: COLOR_PAIR(COLOR_RED) | A_NORMAL,
135: COLOR_PAIR(COLOR_MAGENTA) | A_NORMAL,
136: COLOR_PAIR(COLOR_YELLOW) | A_NORMAL,
137: COLOR_PAIR(COLOR_BLACK) | A_NORMAL,
138: COLOR_PAIR(10) | A_BOLD,
139: COLOR_PAIR(COLOR_BLUE) | A_BOLD,
140: COLOR_PAIR(COLOR_GREEN) | A_BOLD,
141: COLOR_PAIR(COLOR_CYAN) | A_BOLD,
142: COLOR_PAIR(COLOR_RED) | A_BOLD,
143: COLOR_PAIR(COLOR_MAGENTA) | A_BOLD,
144: COLOR_PAIR(COLOR_YELLOW) | A_BOLD,
145: COLOR_PAIR(COLOR_WHITE) | A_BOLD,
146: };
147:
148: static struct dg_watchcols *default_watchcols_list[DGL_MAXWATCHCOLS + 1];
149:
150: struct dg_user *
151: cpy_me(struct dg_user *me)
152: {
153: struct dg_user *tmp = malloc(sizeof(struct dg_user));
154:
155: if (tmp && me) {
156: #ifdef USE_SQLITE3
157: tmp->id = me->id;
158: #endif
159: if (me->username) tmp->username = strdup(me->username);
160: if (me->email) tmp->email = strdup(me->email);
161: if (me->env) tmp->env = strdup(me->env);
162: if (me->password) tmp->password = strdup(me->password);
163: tmp->flags = me->flags;
164: }
165: return tmp;
166: }
167:
168: #ifndef HAVE_SETENV
169: int
170: mysetenv (const char* name, const char* value, int overwrite)
171: {
172: int retval;
173: char *buf = NULL;
174:
175: if (getenv(name) == NULL || overwrite)
176: {
177: size_t len = strlen(name) + 1 + strlen(value) + 1; /* NAME=VALUE\0 */
178: buf = malloc(len);
179: snprintf(buf, len, "%s=%s", name, value);
180: retval = putenv(buf);
181: }
182: else
183: retval = -1;
184:
185: return retval;
186: }
187: #else /* use native setenv */
188: # define mysetenv setenv
189: #endif
190:
191: /* ************************************************************* */
192: /* for ttyrec */
193:
194: void
195: ttyrec_getpty ()
196: {
197: #ifdef HAVE_OPENPTY
198: if (openpty (&master, &slave, NULL, NULL, NULL) == -1) {
199: debug_write("cannot openpty");
200: graceful_exit (61);
201: }
202: #else
203: if ((master = open ("/dev/ptmx", O_RDWR)) < 0) {
204: debug_write("cannot open /dev/ptmx");
205: graceful_exit (62);
206: }
207: grantpt (master);
208: unlockpt (master);
209: if ((slave = open ((const char *) ptsname (master), O_RDWR)) < 0)
210: {
211: debug_write("cannot open master ptsname");
212: graceful_exit (65);
213: }
214: #endif
215: ioctl (slave, TIOCSWINSZ, (char *) &win);
216: tcsetattr(slave, TCSANOW, &tt);
217: }
218:
219: /* ************************************************************* */
220:
221: static int dgl_signal_blocked = 0;
222: static sigset_t dgl_signal_blockmask;
223: static sigset_t dgl_signal_oldmask;
224:
225: void
226: signals_block()
227: {
228: if (!dgl_signal_blocked) {
229: sigemptyset(&dgl_signal_blockmask);
230: sigaddset(&dgl_signal_blockmask, SIGHUP);
231: sigaddset(&dgl_signal_blockmask, SIGINT);
232: sigaddset(&dgl_signal_blockmask, SIGQUIT);
233: sigaddset(&dgl_signal_blockmask, SIGTERM);
234: sigprocmask(SIG_BLOCK, &dgl_signal_blockmask, &dgl_signal_oldmask);
235: dgl_signal_blocked = 1;
236: }
237: }
238:
239: void
240: signals_release()
241: {
242: if (dgl_signal_blocked) {
243: sigprocmask(SIG_SETMASK, &dgl_signal_oldmask, NULL);
244: dgl_signal_blocked = 0;
245: }
246: }
247:
248:
249: /* ************************************************************* */
250:
251: char *
252: get_mainmenu_name()
253: {
254: if (loggedin) {
255: if (me && (me->flags & DGLACCT_ADMIN)) return "mainmenu_admin";
256: return "mainmenu_user";
257: }
258: return "mainmenu_anon";
259: }
260:
261:
262: char*
263: gen_ttyrec_filename ()
264: {
265: time_t rawtime;
266: struct tm *ptm;
267: char *ttyrec_filename = calloc(100, sizeof(char));
268:
269: /* append time to filename */
270: time (&rawtime);
271: ptm = gmtime (&rawtime);
272: snprintf (ttyrec_filename, 100, "%04i-%02i-%02i.%02i:%02i:%02i.ttyrec",
273: ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
274: ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
275: return ttyrec_filename;
276: }
277:
278: /* ************************************************************* */
279:
280: char*
281: gen_inprogress_lock (int game, pid_t pid, char* ttyrec_filename)
282: {
283: char *lockfile = NULL, filebuf[80];
284: int fd;
285: size_t len, wrlen;
286: struct flock fl = { 0 };
287:
288: snprintf (filebuf, sizeof(filebuf), "%d\n%d\n%d\n",
289: pid, win.ws_row, win.ws_col);
290:
291: wrlen = strlen(filebuf);
292:
293: fl.l_type = F_WRLCK;
294: fl.l_whence = SEEK_SET;
295: fl.l_start = 0;
296: fl.l_len = 0;
297:
298: len = strlen(dgl_format_str(game, me, myconfig[game]->inprogressdir, NULL)) + strlen(me->username) + strlen(ttyrec_filename) + 13;
299: lockfile = calloc(len, sizeof(char));
300:
301: snprintf (lockfile, len, "%s%s:%s", dgl_format_str(game, me, myconfig[game]->inprogressdir, NULL),
302: me->username, ttyrec_filename);
303:
304: fd = open (lockfile, O_WRONLY | O_CREAT, 0644);
305: if (fcntl (fd, F_SETLKW, &fl) == -1) {
306: debug_write("cannot fnctl inprogress-lock");
307: graceful_exit (68);
308: }
309:
310: if (write (fd, filebuf, wrlen) != wrlen) {
311: debug_write("inprogress-lock write");
312: graceful_exit(70);
313: }
314:
315: return lockfile;
316: }
317:
318: /* ************************************************************* */
319:
320: #ifdef USE_SHMEM
321: int hup_shm_idx = -1;
322: char *hup_shm_ttyrec_fn = NULL;
323: #endif
324:
325: void
326: catch_sighup (int signum)
327: {
328: if (child)
329: {
330: sleep (10);
331: kill (child, SIGHUP);
332: sleep (5);
333: }
334: #ifdef USE_SHMEM
335: signals_block();
336: if (hup_shm_idx != -1) {
337: struct dg_shm *shm_dg_data = NULL;
338: struct dg_shm_game *shm_dg_game = NULL;
339: shm_init(&shm_dg_data, &shm_dg_game);
340:
341: shm_sem_wait(shm_dg_data);
342: if (shm_dg_game[hup_shm_idx].in_use &&
343: !strcmp(shm_dg_game[hup_shm_idx].ttyrec_fn, hup_shm_ttyrec_fn) &&
344: (shm_dg_game[hup_shm_idx].nwatchers > 0)) {
345: shm_dg_game[hup_shm_idx].nwatchers--;
346: }
347: shm_sem_post(shm_dg_data);
348: hup_shm_idx = -1;
349: free(hup_shm_ttyrec_fn);
350: }
351: signals_release();
352: #endif
353: debug_write("catchup sighup");
354: graceful_exit (7);
355: }
356:
357: /* ************************************************************* */
358:
359: int
360: dgl_getch(void)
361: {
362: const int c = getch();
363: idle_alarm_reset();
364: return c;
365: }
366:
367: /* ************************************************************* */
368:
369: static void
370: dgl_idle_kill(int signal)
371: {
372: kill(0, SIGHUP);
373: }
374:
375: void
376: idle_alarm_set_enabled(int enabled)
377: {
378: signal(SIGALRM, SIG_IGN);
379: g_idle_alarm_enabled = enabled;
380: idle_alarm_reset();
381: if (enabled)
382: signal(SIGALRM, dgl_idle_kill);
383: }
384:
385: void
386: idle_alarm_reset(void)
387: {
388: if (g_idle_alarm_enabled && globalconfig.menu_max_idle_time > 0)
389: alarm(globalconfig.menu_max_idle_time);
390: }
391:
392: /* ************************************************************* */
393:
394:
395: char *
396: bannerstrmangle(char *buf, char *bufnew, int buflen, char *fromstr, char *tostr)
397: {
398: char *loc;
399: char *b = buf;
400:
401: memset (bufnew, 0, buflen);
402:
403: if (strstr(b, fromstr)) {
404: int i = 0;
405: while ((loc = strstr (b, fromstr)) != NULL) {
406: for (; i < buflen; i++) {
407: if (loc != b)
408: bufnew[i] = *(b++);
409: else {
410: strlcat (bufnew, tostr, buflen);
411: b += strlen(fromstr);
412: i += strlen(tostr);
413: break;
414: }
415:
416: if (strlen (b) == 0)
417: break;
418: }
419: }
420:
421: if (*b)
422: strlcat(bufnew, b, buflen);
423: } else strncpy(bufnew, buf, buflen);
424: return bufnew;
425: }
426:
427: void
428: banner_var_add(char *name, char *value, int special)
429: {
430: struct dg_banner_var *tmp = (struct dg_banner_var *)malloc(sizeof(struct dg_banner_var));
431:
432: if (!tmp) return;
433:
434: tmp->name = strdup(name);
435: tmp->value = strdup(value);
436: tmp->special = special;
437: tmp->next = globalconfig.banner_var_list;
438: globalconfig.banner_var_list = tmp;
439: }
440:
441: void
442: banner_var_free()
443: {
444: struct dg_banner_var *tmp;
445: struct dg_banner_var *bv = globalconfig.banner_var_list;
446: while (bv) {
447: tmp = bv->next;
448: free(bv->name);
449: free(bv->value);
450: free(bv);
451: bv = tmp;
452: }
453: globalconfig.banner_var_list = NULL;
454: }
455:
456: char *
457: banner_var_resolve(struct dg_banner_var *bv)
458: {
459: static char tmpbuf[DGL_BANNER_LINELEN+1];
460: time_t tstamp;
461: struct tm *ptm;
462: if (!bv) return NULL;
463: if (!bv->special) return bv->value;
464: time(&tstamp);
465: ptm = gmtime(&tstamp);
466: strftime(tmpbuf, DGL_BANNER_LINELEN, bv->value, ptm);
467: return tmpbuf;
468: }
469:
470: char *
471: banner_var_value(char *name)
472: {
473: struct dg_banner_var *bv = globalconfig.banner_var_list;
474: while (bv) {
475: if (!strcmp(bv->name, name)) return banner_var_resolve(bv);
476: bv = bv->next;
477: }
478: return NULL;
479: }
480:
481: void
482: freebanner(struct dg_banner *ban)
483: {
484: unsigned int l;
485: if (!ban) return;
486: l = ban->len;
487:
488: while (l > 0) {
489: l--;
490: free(ban->lines[l]);
491: }
492: free(ban->lines);
493: ban->len = 0;
494: ban->lines = NULL;
495: }
496:
497: void
498: banner_addline(struct dg_banner *ban, char *line)
499: {
500: size_t len = strlen(line);
501: if (!ban) return;
502: ban->len++;
503: ban->lines = realloc (ban->lines, sizeof (char *) * ban->len);
504: if (len >= DGL_BANNER_LINELEN) {
505: len = DGL_BANNER_LINELEN;
506: ban->lines[ban->len - 1] = malloc(len);
507: strncpy(ban->lines[ban->len - 1], line, len);
508: ban->lines[ban->len - 1][len-1] = '\0';
509: } else
510: ban->lines[ban->len - 1] = strdup(line);
511: }
512:
513: void
514: loadbanner (char *fname, struct dg_banner *ban)
515: {
516: FILE *bannerfile;
517: char buf[DGL_BANNER_LINELEN+1];
518: if (ban->len > 23) return;
519:
520: memset (buf, 0, DGL_BANNER_LINELEN);
521:
522: bannerfile = fopen (fname, "r");
523:
524: if (!bannerfile)
525: {
526: if (ban->len == 0)
527: banner_addline(ban, "### dgamelaunch " PACKAGE_VERSION " - network console game launcher");
528: snprintf(buf, DGL_BANNER_LINELEN, "### NOTE: administrator has not installed a %s file", fname);
529: banner_addline(ban, buf);
530: return;
531: }
532:
533: while (fgets (buf, DGL_BANNER_LINELEN, bannerfile) != NULL)
534: {
535: char bufnew[DGL_BANNER_LINELEN+1];
536: int slen;
537:
538: memset (bufnew, 0, DGL_BANNER_LINELEN);
539:
540: slen = strlen(buf);
541: if ((slen > 0) && (buf[slen-1] == '\n')) buf[slen-1] = '\0';
542:
543: strncpy(bufnew, buf, DGL_BANNER_LINELEN);
544: if (strstr(bufnew, "$INCLUDE(")) {
545: char *fn = bufnew + 9;
546: char *fn_end = strchr(fn, ')');
547: if (fn_end) {
548: *fn_end = '\0';
549: if (strcmp(fname, fn)) {
550: loadbanner(fn, ban);
551: }
552: }
553: } else {
554: char tmpbufnew[DGL_BANNER_LINELEN+1];
555: struct dg_banner_var *bv = globalconfig.banner_var_list;
556: while (bv) {
557: strncpy(bufnew, bannerstrmangle(bufnew, tmpbufnew, DGL_BANNER_LINELEN, bv->name, banner_var_resolve(bv)), DGL_BANNER_LINELEN);
558: bv = bv->next;
559: }
560: strncpy(bufnew, bannerstrmangle(bufnew, tmpbufnew, DGL_BANNER_LINELEN, "$VERSION", PACKAGE_STRING), DGL_BANNER_LINELEN);
561: if (me && loggedin) {
562: strncpy(bufnew, bannerstrmangle(bufnew, tmpbufnew, DGL_BANNER_LINELEN, "$USERNAME", me->username), DGL_BANNER_LINELEN);
563: } else {
564: strncpy(bufnew, bannerstrmangle(bufnew, tmpbufnew, DGL_BANNER_LINELEN, "$USERNAME", "[Anonymous]"), DGL_BANNER_LINELEN);
565: }
566: banner_addline(ban, bufnew);
567: }
568:
569: memset (buf, 0, DGL_BANNER_LINELEN);
570:
571: if (ban->len >= 24)
572: break;
573: }
574:
575: fclose (bannerfile);
576: }
577:
578: void
579: drawbanner (struct dg_banner *ban)
580: {
581: unsigned int i;
582: char *tmpch, *tmpch2, *splch;
583: int attr = 0, oattr = 0;
584:
585: if (!ban) return;
586:
587: for (i = 0; i < ban->len; i++) {
588: char *tmpbuf = strdup(ban->lines[i]);
589: char *tmpbuf2 = tmpbuf;
590: int ok = 0;
591: int x = 1;
592: do {
593: ok = 0;
594: if ((tmpch = strstr(tmpbuf2, "$ATTR("))) {
595: if ((tmpch2 = strstr(tmpch, ")"))) {
596: int spl = 0;
597: char *nxttmpch;
598: ok = 1;
599: oattr = attr;
600: attr = A_NORMAL;
601: *tmpch = *tmpch2 = '\0';
602: tmpch += 6;
603: nxttmpch = tmpch;
604: do {
605: spl = 0;
606: splch = strchr(tmpch, ';');
607: if (splch && *splch) {
608: spl = 1;
609: nxttmpch = splch;
610: *nxttmpch = '\0';
611: nxttmpch++;
612: }
613: if (tmpch && *tmpch) {
614: switch (*tmpch) {
615: default: break;
616: case '0': case '1': case '2': case '3': case '4':
617: case '5': case '6': case '7': case '8': case '9':
618: {
619: int num = atoi(tmpch);
620: if (num >= 0 && num <= 15)
621: attr |= color_remap[num];
622: }
623: break;
624: case 'b': attr |= A_BOLD; break;
625: case 's': attr |= A_STANDOUT; break;
626: case 'u': attr |= A_UNDERLINE; break;
627: case 'r': attr |= A_REVERSE; break;
628: case 'd': attr |= A_DIM; break;
629: }
630: } else attr = A_NORMAL;
631: tmpch = nxttmpch;
632: } while (spl);
633:
634: mvaddstr(1 + i, x, tmpbuf2);
635: if (oattr) attroff(oattr);
636: if (attr) attron(attr);
637: x += strlen(tmpbuf2);
638: tmpch2++;
639: tmpbuf2 = tmpch2;
640: } else
641: mvaddstr (1 + i, x, tmpbuf2);
642: } else
643: mvaddstr (1 + i, x, tmpbuf2);
644: } while (ok);
645: free(tmpbuf);
646: }
647: }
648:
649: void
650: shm_sem_wait(struct dg_shm *shm_dg_data)
651: {
652: #ifdef USE_SHMEM
653: if (sem_wait(&(shm_dg_data->dg_sem)) == -1) {
654: debug_write("sem_wait");
655: graceful_exit(77);
656: }
657: #endif
658: }
659:
660: void
661: shm_sem_post(struct dg_shm *shm_dg_data)
662: {
663: #ifdef USE_SHMEM
664: if (sem_post(&(shm_dg_data->dg_sem)) == -1) {
665: debug_write("sem_post");
666: graceful_exit(78);
667: }
668: #endif
669: }
670:
671: void
672: shm_update(struct dg_shm *shm_dg_data, struct dg_game **games, int len)
673: {
674: #ifdef USE_SHMEM
675: int di, i;
676: struct dg_shm_game *shm_dg_game = (struct dg_shm_game *)(shm_dg_data + sizeof(struct dg_shm));
677:
678: signals_block();
679: shm_sem_wait(shm_dg_data);
680:
681: for (di = 0; di < shm_dg_data->max_n_games; di++)
682: if (shm_dg_game[di].in_use) {
683: int delgame = 1;
684: for (i = 0; i < len; i++) {
685: if (!strcmp(games[i]->ttyrec_fn, shm_dg_game[di].ttyrec_fn)) {
686: delgame = 0;
687: games[i]->is_in_shm = 1;
688: games[i]->shm_idx = di;
689: games[i]->nwatchers = shm_dg_game[di].nwatchers;
690: break;
691: }
692: }
693: if (delgame) {
694: shm_dg_game[di].in_use = 0;
695: if (shm_dg_data->cur_n_games > 0) shm_dg_data->cur_n_games--;
696: }
697: }
698:
699: if (shm_dg_data->cur_n_games < shm_dg_data->max_n_games) {
700: for (i = 0; i < len; i++)
701: if (!games[i]->is_in_shm) {
702: for (di = 0; di < shm_dg_data->max_n_games; di++)
703: if (!shm_dg_game[di].in_use) {
704: shm_dg_game[di].in_use = 1;
705: shm_dg_game[di].nwatchers = 0;
706: games[i]->nwatchers = 0;
707: games[i]->is_in_shm = 1;
708: games[i]->shm_idx = di;
709: shm_dg_data->cur_n_games++;
710: strncpy(shm_dg_game[di].ttyrec_fn, games[i]->ttyrec_fn, 150);
711: break;
712: }
713: }
714: }
715:
716: shm_sem_post(shm_dg_data);
717: signals_release();
718: #endif
719: }
720:
721: void
722: shm_mk_keys(key_t *shm_key, key_t *shm_sem_key)
723: {
724: #ifdef USE_SHMEM
725: if ((*shm_key = ftok(globalconfig.passwd, 'R')) == -1) {
726: debug_write("ftok shm_key");
727: graceful_exit(71);
728: }
729: if ((*shm_sem_key = ftok(globalconfig.passwd, 'S')) == -1) {
730: debug_write("ftok shm_sem_key");
731: graceful_exit(72);
732: }
733: #endif
734: }
735:
736: #ifdef USE_SHMEM
737: int
738: shm_free()
739: {
740: key_t shm, sem;
741: int shm_id;
742: int shm_size = sizeof(struct dg_shm) + shm_n_games * sizeof(struct dg_shm_game);
743: shm_mk_keys(&shm, &sem);
744: if ((shm_id = shmget(shm, shm_size, 0644)) != -1) {
745: shmctl(shm_id, IPC_RMID, NULL);
746: return 0;
747: }
748: return 1;
749: }
750: #endif
751:
752: void
753: shm_init(struct dg_shm **shm_dg_data, struct dg_shm_game **shm_dg_game)
754: {
755: #ifdef USE_SHMEM
756: key_t shm_key;
757: key_t shm_sem_key;
758: int shm_id;
759: int shm_size;
760: void *shm_data = NULL;
761: int shm_data_existed = 0;
762:
763: shm_mk_keys(&shm_key, &shm_sem_key);
764:
765: /* max. shm_n_games simultaneous games recorded in the shared memory */
766: shm_size = sizeof(struct dg_shm) + shm_n_games * sizeof(struct dg_shm_game);
767:
768: /* connect to (and possibly create) the segment */
769: if ((shm_id = shmget(shm_key, shm_size, 0644 | IPC_CREAT | IPC_EXCL)) == -1) {
770: /* creation failed, so it already exists. attach to it */
771: shm_data_existed = 1;
772: if ((shm_id = shmget(shm_key, shm_size, 0644)) == -1) {
773: debug_write("shmget");
774: graceful_exit(73);
775: }
776: }
777:
778: /* attach to the segment to get a pointer to it: */
779: shm_data = shmat(shm_id, (void *)0, 0);
780: if (shm_data == (char *)(-1)) {
781: debug_write("shmat");
782: graceful_exit(74);
783: }
784: if (!shm_data) {
785: debug_write("shm_data == null");
786: graceful_exit(75);
787: }
788:
789: (*shm_dg_data) = (struct dg_shm *)shm_data;
790: (*shm_dg_game) = (struct dg_shm_game *)((*shm_dg_data) + sizeof(struct dg_shm));
791:
792: if (!shm_data_existed && shm_data) {
793: memset(*shm_dg_game, 0, shm_n_games*sizeof(struct dg_shm_game));
794: (*shm_dg_data)->max_n_games = shm_n_games;
795: (*shm_dg_data)->cur_n_games = 0;
796: if (sem_init(&((*shm_dg_data)->dg_sem), 1,1) == -1) {
797: debug_write("sem_init");
798: graceful_exit(76);
799: }
800: }
801: #endif /* USE_SHMEM */
802: }
803:
804: #ifdef USE_SHMEM
805: void
806: shm_dump()
807: {
808: struct dg_shm *shm_dg_data = NULL;
809: struct dg_shm_game *shm_dg_game = NULL;
810: int di, unused = -1;
811: shm_init(&shm_dg_data, &shm_dg_game);
812: shm_sem_wait(shm_dg_data);
813: for (di = 0; di < shm_dg_data->max_n_games; di++) {
814: if (shm_dg_game[di].in_use) {
815: if (unused != -1) {
816: if (unused != di-1)
817: fprintf(stderr, "%i-%i:\tunused\n", unused, di-1);
818: else
819: fprintf(stderr, "%i:\tunused\n", unused);
820: unused = -1;
821: }
822: fprintf(stderr, "%i:\t\"%s\"\twatchers:%li\n", di, shm_dg_game[di].ttyrec_fn, shm_dg_game[di].nwatchers);
823: } else {
824: if (unused == -1) unused = di;
825: }
826: }
827: if (unused != -1) {
828: if (unused != di-1)
829: fprintf(stderr, "%i-%i:\tunused\n", unused, di-1);
830: else
831: fprintf(stderr, "%i:\tunused\n", unused);
832: unused = -1;
833: }
834: shm_sem_post(shm_dg_data);
835: shmdt(shm_dg_data);
836: }
837: #endif
838:
839: static
840: struct dg_watchcols **
841: globalconfig_watch_columns()
842: {
843: if (globalconfig.n_watch_columns)
844: return globalconfig.watch_columns;
845:
846: if (!*default_watchcols_list) {
847: int i;
848: for (i = 0; i < ARRAY_SIZE(default_watchcols); ++i)
849: default_watchcols_list[i] = &default_watchcols[i];
850: }
851: return default_watchcols_list;
852: }
853:
854: static
855: int
856: watchcol_find_index(struct dg_watchcols **watchcols,
857: int sortmode)
858: {
859: int i;
860: for (i = 0; watchcols[i]; ++i)
861: if (watchcols[i]->sortmode == sortmode)
862: return i;
863: return -1;
864: }
865:
866: static
867: void
868: sortmode_increment(struct dg_watchcols **watchcols,
869: dg_sortmode *sortmode,
870: int direction)
871: {
872: int watch_column_index = watchcol_find_index(watchcols, *sortmode);
873: int n_watchcols;
874: int wrap_count = 0;
875: const dg_sortmode old_sortmode = *sortmode;
876:
877: for (n_watchcols = 0; watchcols[n_watchcols]; ++n_watchcols)
878: ;
879:
880: if (watch_column_index == -1 || !n_watchcols)
881: return;
882:
883: do {
884: watch_column_index += direction;
885:
886: if (watch_column_index < 0) {
887: watch_column_index = n_watchcols - 1;
888: ++wrap_count;
889: } else if (watch_column_index >= n_watchcols) {
890: watch_column_index = 0;
891: ++wrap_count;
892: }
893:
894: *sortmode = watchcols[watch_column_index]->sortmode;
895: } while (wrap_count < 2 && !*sortmode);
896:
897: if (!*sortmode)
898: *sortmode = old_sortmode;
899: }
900:
901: char *
902: get_timediff(time_t ctime, long seconds)
903: {
904: static char data[32];
905: long secs, mins, hours;
906:
907: secs = (ctime - seconds);
908:
909: if (showplayers) {
910: snprintf(data, 10, "%ld", secs);
911: return data;
912: }
913:
914: hours = (secs / 3600);
915: secs -= (hours * 3600);
916: mins = (secs / 60) % 60;
917: secs -= (mins*60);
918: if (hours)
919: snprintf(data, 10, "%ldh %ldm", hours, mins);
920: else if (mins)
921: snprintf(data, 10, "%ldm %lds", mins, secs);
922: else if (secs > 4)
923: snprintf(data, 30, "%lds", secs);
924: else
925: snprintf(data, 10, " ");
926: return data;
927: }
928:
929: static
930: void
931: game_get_column_data(struct dg_game *game,
932: char selectorchar,
933: time_t ctime, struct dg_shm_game *shm_dg_game,
934: char *data, int bufsz, int *hilite,
935: dg_sortmode which_data)
936: {
937: *data = 0;
938:
939: switch (which_data) {
940: default: break;
941: case SORTMODE_NONE:
942: data[0] = selectorchar; data[1] = '\0';
943: break;
944:
945: case SORTMODE_USERNAME:
946: snprintf(data, bufsz, "%s", game->name);
947: break;
948:
949: case SORTMODE_GAMENUM:
950: snprintf(data, bufsz, "%s",
951: myconfig[game->gamenum]->shortname);
952: break;
953:
954: case SORTMODE_WINDOWSIZE:
955: snprintf(data, bufsz, "%3dx%3d", game->ws_col, game->ws_row);
956: if (showplayers)
957: snprintf(data, bufsz, "%dx%d", game->ws_col, game->ws_row);
958: else
959: snprintf(data, bufsz, "%3dx%3d", game->ws_col, game->ws_row);
960: if ((game->ws_col > COLS || game->ws_row > LINES))
961: *hilite = CLR_RED;
962: break;
963:
964: case SORTMODE_STARTTIME:
965: snprintf(data, bufsz, "%s %s", game->date,
966: game->time);
967: break;
968:
969: case SORTMODE_DURATION:
970: {
971: /* TODO: populate_games() should put st_ctime into game struct */
972: struct tm timetm;
973: char tmptimebuf[32];
974: snprintf(tmptimebuf, 30, "%s %s", game->date, game->time);
975: tmptimebuf[31] = '\0';
976: strptime(tmptimebuf, "%Y-%m-%d %H:%M:%S", &timetm);
977: snprintf(data, 10, "%s", get_timediff(ctime, mktime(&timetm)));
978: }
979: break;
980:
981: case SORTMODE_IDLETIME:
982: snprintf(data, 10, "%s", get_timediff(ctime, game->idle_time));
983: break;
984:
985: case SORTMODE_EXTRA_INFO:
986: if (game->extra_info)
987: strlcpy(data, game->extra_info, bufsz);
988: break;
989:
990: #ifdef USE_SHMEM
991: case SORTMODE_WATCHERS:
992: snprintf(data, bufsz, "%li",
993: (game->is_in_shm ?
994: shm_dg_game[game->shm_idx].nwatchers : -1));
995: break;
996: #endif
997: }
998: data[bufsz - 1] = '\0';
999: }
1000:
1001: void
1002: inprogressmenu (int gameid)
1003: {
1004: const char *selectorchars = "abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ";
1005: int i, menuchoice, len = 20, offset = 0;
1006: static dg_sortmode sortmode = NUM_SORTMODES;
1007: struct dg_game **games = NULL;
1008: char ttyrecname[130], gametype[10], idletime[10];
1009: sigset_t oldmask, toblock;
1010: int idx = -1;
1011: int shm_idx = -1;
1012: int max_height = -1;
1013: int selected = -1;
1014:
1015: int resizex = -1;
1016: int resizey = -1;
1017:
1018: char *selectedgame = NULL;
1019:
1020: int abs_max_height;
1021: int top_banner_hei = 5;
1022: int btm_banner_hei = 3;
1023: int btm;
1024:
1025: int title_attr = A_STANDOUT;
1026: int selected_attr = A_BOLD;
1027:
1028: int require_enter = 0; /* TODO: make configurable */
1029:
1030: time_t ctime;
1031:
1032: struct dg_shm *shm_dg_data = NULL;
1033: struct dg_shm_game *shm_dg_game = NULL;
1034:
1035: struct dg_watchcols **watchcols = globalconfig_watch_columns();
1036: struct dg_watchcols **curr_watchcol;
1037:
1038: if (sortmode == NUM_SORTMODES)
1039: sortmode = globalconfig.sortmode;
1040:
1041: abs_max_height = strlen(selectorchars);
1042:
1043: shm_init(&shm_dg_data, &shm_dg_game);
1044:
1045: games = populate_games (gameid, &len, NULL); /* FIXME: should be 'me' instead of 'NULL' */
1046: shm_update(shm_dg_data, games, len);
1047: games = sort_games (games, len, sortmode);
1048:
1049: while (1)
1050: {
1051: term_resize_check();
1052: max_height = dgl_local_LINES - (top_banner_hei + btm_banner_hei) - 1;
1053: if (max_height < 2) {
1054: free_populated_games(games, len);
1055: return;
1056: }
1057: if (max_height > abs_max_height) max_height = abs_max_height;
1058:
1059: if (len == 0)
1060: offset = 0;
1061:
1062: erase ();
1063: drawbanner (&banner);
1064:
1065: if (len > 0) {
1066: while (offset >= len) { offset -= max_height; }
1067: if (offset < 0) offset = 0;
1068: mvaddstr (3, 1, "The following games are in progress:");
1069:
1070: for (curr_watchcol = watchcols; *curr_watchcol; ++curr_watchcol) {
1071: struct dg_watchcols *wcol = *curr_watchcol;
1072: char *col = wcol->colname;
1073: int x = wcol->x;
1074: while (*col == ' ') { x++; col++; }
1075: if (sortmode == wcol->sortmode) attron(title_attr);
1076: mvprintw(top_banner_hei, x, col);
1077: if (sortmode == wcol->sortmode) attroff(title_attr);
1078: }
1079: }
1080:
1081: signals_block();
1082: shm_sem_wait(shm_dg_data);
1083:
1084: (void) time(&ctime);
1085:
1086: for (i = 0; i < max_height; i++)
1087: {
1088: if (i + offset >= len)
1089: break;
1090:
1091: if (i + offset == selected) attron(selected_attr);
1092:
1093: for (curr_watchcol = watchcols; *curr_watchcol; ++curr_watchcol) {
1094: struct dg_watchcols *col = *curr_watchcol;
1095: char tmpbuf[80];
1096: int hilite = 0;
1097: game_get_column_data(games[i + offset],
1098: selectorchars[i],
1099: ctime, shm_dg_game,
1100: tmpbuf, sizeof tmpbuf, &hilite,
1101: (dg_sortmode)col->dat);
1102: if (hilite) attron(hilite);
1103: mvprintw(top_banner_hei + 1 + i, col->x, col->fmt, tmpbuf);
1104: if (hilite) {
1105: attron(CLR_NORMAL);
1106: hilite = 0;
1107: }
1108: }
1109:
1110: if (i + offset == selected) attroff(selected_attr);
1111:
1112: }
1113:
1114: shm_sem_post(shm_dg_data);
1115: signals_release();
1116:
1117: btm = dgl_local_LINES-btm_banner_hei-top_banner_hei;
1118: if (len <= max_height)
1119: btm = i+1;
1120:
1121: if (len > 0) {
1122: mvprintw ((btm+top_banner_hei), 1, "(%d-%d of %d)", offset + 1, offset + i, len);
1123: mvaddstr ((btm+2+top_banner_hei), 1, "Watch which game? ('?' for help) => ");
1124: } else {
1125: mvprintw(top_banner_hei,4,"Sorry, no games available for viewing.");
1126: mvaddstr((btm+2+top_banner_hei), 1, "Press 'q' to return, or '?' for help => ");
1127: }
1128:
1129: refresh ();
1130:
1131: switch ((menuchoice = dgl_getch ()))
1132: {
1133: case KEY_DOWN:
1134: selected++;
1135: if (selected >= len) selected = 0;
1136: while (selected < offset) offset -= max_height;
1137: while (selected >= offset+max_height) offset += max_height;
1138: break;
1139: case KEY_UP:
1140: if (selected != -1) {
1141: if (selected == 0) selected = len;
1142: selected--;
1143: } else selected = len-1;
1144: while (selected < offset) offset -= max_height;
1145: while (selected >= offset+max_height) offset += max_height;
1146: break;
1147: case '*':
1148: if (len > 0) {
1149: int cnt = 20;
1150: (void) time(&ctime);
1151: do {
1152: idx = random() % len;
1153: } while ((--cnt > 0) ||
1154: !((games[idx]->ws_col <= COLS) &&
1155: (games[idx]->ws_row <= LINES) &&
1156: ((ctime - games[idx]->idle_time) < 15)));
1157: selected = idx;
1158: goto watchgame;
1159: }
1160: break;
1161: case '?':
1162: (void) runmenuloop(dgl_find_menu("watchmenu_help"));
1163: break;
1164: case '/':
1165: {
1166: int match = -1;
1167: int firstmatch = -1;
1168: int nmatches = 0;
1169: char findname[DGL_PLAYERNAMELEN+1];
1170: if (len <= 0) break;
1171: findname[0] = '\0';
1172: mvprintw ((btm+2+top_banner_hei), 1, "Watch which player? => "); /* stupid... */
1173: mvaddstr ((btm+2+top_banner_hei), 1, "Watch which player? => ");
1174: if ((mygetnstr(findname, DGL_PLAYERNAMELEN, 1) == OK) && (strlen(findname) > 1)) {
1175: int mlen = strlen(findname);
1176: for (i = 0; i < len; i++)
1177: if (!strncasecmp(games[i]->name, findname, mlen)) {
1178: if (firstmatch == -1) firstmatch = i;
1179: match = i;
1180: nmatches++;
1181: }
1182: if (nmatches > 1)
1183: match = firstmatch;
1184: if (match > -1) {
1185: idx = match;
1186: selected = idx;
1187: goto watchgame;
1188: }
1189: }
1190: }
1191: break;
1192: case KEY_NPAGE:
1193: case '>':
1194: if ((offset + max_height) < len) offset += max_height;
1195: break;
1196: case KEY_PPAGE:
1197: case '<':
1198: if ((offset - max_height) < 0)
1199: offset = 0;
1200: else
1201: offset -= max_height;
1202: break;
1203:
1204: case ERR:
1205: case 'q': case 'Q':
1206: case '\x1b':
1207: goto leavewatchgame;
1208: case KEY_RIGHT:
1209: case '.':
1210: sortmode_increment(watchcols, &sortmode, 1);
1211: break;
1212: case KEY_LEFT:
1213: case ',':
1214: sortmode_increment(watchcols, &sortmode, -1);
1215: break;
1216:
1217: case 12: case 18: /* ^L, ^R */
1218: if (globalconfig.utf8esc) (void) write(1, "\033%G", 3);
1219: clear ();
1220: break;
1221:
1222: case 13:
1223: case 10:
1224: case KEY_ENTER:
1225: if (selected >= 0 && selected < len) {
1226: idx = selected;
1227: goto watchgame;
1228: }
1229: break;
1230:
1231: default:
1232: if (strchr(selectorchars, menuchoice) && (len > 0)) {
1233: int sidx = strchr(selectorchars, menuchoice) - selectorchars;
1234:
1235: if ((sidx > max_height) || (sidx >= len)) {
1236: selected = -1;
1237: break;
1238: }
1239:
1240: idx = sidx + offset;
1241: if (require_enter) {
1242: if (selected == idx) selected = -1;
1243: else selected = idx;
1244: break;
1245: } else selected = idx;
1246: watchgame:
1247: if (!(idx >= 0 && idx < len && games[idx] && games[idx]->name))
1248: goto leavewatchgame;
1249: /* valid choice has been made */
1250: chosen_name = strdup (games[idx]->name);
1251: snprintf (ttyrecname, 130, "%s",
1252: games[idx]->ttyrec_fn);
1253:
1254: clear ();
1255: refresh ();
1256: endwin ();
1257: if (globalconfig.utf8esc) (void) write(1, "\033%G", 3);
1258: #ifdef USE_SHMEM
1259: signals_block();
1260: if (games[idx]->is_in_shm) {
1261: shm_idx = games[idx]->shm_idx;
1262: shm_sem_wait(shm_dg_data);
1263: if (shm_dg_game[shm_idx].in_use &&
1264: !strcmp(shm_dg_game[shm_idx].ttyrec_fn, games[idx]->ttyrec_fn)) {
1265: shm_dg_game[shm_idx].nwatchers++;
1266: games[idx]->nwatchers++;
1267: }
1268: hup_shm_idx = shm_idx;
1269: hup_shm_ttyrec_fn = strdup(games[idx]->ttyrec_fn);
1270: shm_sem_post(shm_dg_data);
1271: }
1272: signals_release();
1273: #endif
1274: resizey = games[idx]->ws_row;
1275: resizex = games[idx]->ws_col;
1276: if (loggedin)
1277: setproctitle("%s [watching %s]", me->username, chosen_name);
1278: else
1279: setproctitle("<Anonymous> [watching %s]", chosen_name);
1280: ttyplay_main (ttyrecname, 1, resizex, resizey);
1281: if (loggedin)
1282: setproctitle("%s", me->username);
1283: else
1284: setproctitle("<Anonymous>");
1285: #ifdef USE_SHMEM
1286: signals_block();
1287: if (games[idx]->is_in_shm) {
1288: hup_shm_idx = -1;
1289: free(hup_shm_ttyrec_fn);
1290: shm_sem_wait(shm_dg_data);
1291: if (shm_dg_game[shm_idx].in_use &&
1292: !strcmp(shm_dg_game[shm_idx].ttyrec_fn, games[idx]->ttyrec_fn) &&
1293: (shm_dg_game[shm_idx].nwatchers > 0)) {
1294: shm_dg_game[shm_idx].nwatchers--;
1295: games[idx]->nwatchers--;
1296: }
1297: shm_sem_post(shm_dg_data);
1298: }
1299: signals_release();
1300: #endif
1301: initcurses ();
1302: redrawwin(stdscr);
1303: }
1304: }
1305:
1306: if (selected >= 0 && selected < len)
1307: selectedgame = strdup(games[selected]->name);
1308: games = populate_games (gameid, &len, NULL); /* FIXME: should be 'me' instead of 'NULL' */
1309: shm_update(shm_dg_data, games, len);
1310: games = sort_games (games, len, sortmode);
1311: if (selectedgame) {
1312: selected = -1;
1313: for (i = 0; i < len; i++)
1314: if (!strcmp(games[i]->name, selectedgame)) {
1315: selected = i;
1316: break;
1317: }
1318: free(selectedgame);
1319: selectedgame = NULL;
1320: }
1321: }
1322: leavewatchgame:
1323: free_populated_games(games, len);
1324: #ifdef USE_SHMEM
1325: shmdt(shm_dg_data);
1326: #endif
1327: }
1328:
1329: void
1330: inprogressdisplay (int gameid)
1331: {
1332: const char *selectorchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ";
1333: int i, len = 20;
1334: static dg_sortmode sortmode = NUM_SORTMODES;
1335: struct dg_game **games = NULL;
1336: int shm_idx = -1;
1337:
1338: time_t ctime;
1339:
1340: struct dg_shm *shm_dg_data = NULL;
1341: struct dg_shm_game *shm_dg_game = NULL;
1342:
1343: struct dg_watchcols **watchcols = globalconfig_watch_columns();
1344: struct dg_watchcols **curr_watchcol;
1345:
1346: if (sortmode == NUM_SORTMODES)
1347: sortmode = globalconfig.sortmode;
1348:
1349: shm_init(&shm_dg_data, &shm_dg_game);
1350:
1351: games = populate_games (gameid, &len, NULL); /* FIXME: should be 'me' instead of 'NULL' */
1352: shm_update(shm_dg_data, games, len);
1353: games = sort_games (games, len, sortmode);
1354:
1355: signals_block();
1356: shm_sem_wait(shm_dg_data);
1357:
1358: (void) time(&ctime);
1359:
1360: for (i = 0; i < 100; i++) {
1361: if (i >= len)
1362: break;
1363:
1364: for (curr_watchcol = watchcols; *curr_watchcol; ++curr_watchcol) {
1365: struct dg_watchcols *col = *curr_watchcol;
1366: if ((dg_sortmode)col->dat == SORTMODE_NONE)
1367: continue;
1368: char tmpbuf[80];
1369: int hilite = 0;
1370: game_get_column_data(games[i],
1371: selectorchars[i],
1372: ctime, shm_dg_game,
1373: tmpbuf, sizeof tmpbuf, &hilite, (dg_sortmode)col->dat);
1374: fprintf(stdout, "%s#", tmpbuf); /* format in col->fmt */
1375: }
1376: fprintf(stdout, "\n");
1377: }
1378:
1379: shm_sem_post(shm_dg_data);
1380: signals_release();
1381:
1382: free_populated_games(games, len);
1383:
1384: #ifdef USE_SHMEM
1385: shmdt(shm_dg_data);
1386: #endif
1387: }
1388:
1389: /* ************************************************************* */
1390:
1391: /*
1392: * Check email address, returns 1 if valid, 0 otherwise.
1393: * Doesn't recognize addresses with parts in double-quotes.
1394: * Addresses with a colon in them are always rejected.
1395: */
1396: int
1397: check_email (char *s)
1398: {
1399: char *atomchars = "!#$%&'*+-/=?^_`{|}~" "0123456789"
1400: "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1401: int f;
1402:
1403: if (*s == '@')
1404: return 0;
1405:
1406: while (*s != '\0' && *s != '@')
1407: {
1408: if (strchr(atomchars, *s) == NULL)
1409: return 0;
1410: s++;
1411: if (*s == '.')
1412: s++;
1413: }
1414:
1415: if (*s == '\0')
1416: return 0;
1417: s++;
1418:
1419: f = 0;
1420: while (*s != '\0')
1421: {
1422: if (strchr(atomchars, *s) == NULL)
1423: return 0;
1424: s++;
1425: if (*s == '.')
1426: s++, f = 1;
1427: }
1428:
1429: return f;
1430: }
1431:
1432: void
1433: change_email ()
1434: {
1435: char buf[81];
1436:
1437: clear();
1438:
1439: if (me->flags & DGLACCT_EMAIL_LOCK) {
1440: drawbanner(&banner);
1441: mvprintw(5, 1, "Sorry, you cannot change the email.--More--");
1442: dgl_getch();
1443: return;
1444: }
1445:
1446: for (;;)
1447: {
1448: drawbanner(&banner);
1449:
1450: mvprintw(3, 1, "Your current email is: %s", me->email);
1451: mvaddstr(4, 1, "Please enter a new one (max 80 chars; blank line aborts)");
1452: mvaddstr(6, 1, "=> ");
1453:
1454: if (mygetnstr (buf, 80, 1) != OK)
1455: return;
1456:
1457: if (*buf == '\0')
1458: return;
1459: else if (!strcmp(me->email, buf))
1460: {
1461: clear();
1462: mvaddstr (8, 1, "That's the same one as before. Try again?");
1463: move(1,1);
1464: }
1465: else if (check_email (buf))
1466: {
1467: mvprintw (8, 1, "Changing email address to '%s'. Confirm (y/n): ", buf);
1468: if (dgl_getch() == 'y')
1469: {
1470: free(me->email);
1471: me->email = strdup(buf);
1472: writefile(0);
1473: return;
1474: }
1475: else
1476: {
1477: mvaddstr(9, 1, "No changes made. Press any key to continue...");
1478: dgl_getch();
1479: return;
1480: }
1481: }
1482: else
1483: {
1484: clear();
1485: mvaddstr (8, 1, "That doesn't look like an email address to me.");
1486: move(1,1);
1487: }
1488: }
1489: }
1490:
1491: int
1492: changepw (int dowrite)
1493: {
1494: char buf[DGL_PASSWDLEN+1];
1495: int error = 2;
1496:
1497: /* A precondition is that struct `me' exists because we can be not-yet-logged-in. */
1498: if (!me) {
1499: debug_write("no 'me' in changepw");
1500: graceful_exit (122); /* Die. */
1501: }
1502:
1503: if (me->flags & DGLACCT_PASSWD_LOCK) {
1504: clear();
1505: drawbanner(&banner);
1506: mvprintw(5, 1, "Sorry, you cannot change the password.--More--");
1507: dgl_getch();
1508: return 0;
1509: }
1510:
1511: while (error)
1512: {
1513: char repeatbuf[DGL_PASSWDLEN+1];
1514: clear ();
1515:
1516: drawbanner (&banner);
1517:
1518: mvprintw (5, 1,
1519: "Please enter a%s password. Remember that this is sent over the net",
1520: loggedin ? " new" : "");
1521: mvaddstr (6, 1,
1522: "in plaintext, so make it something new and expect it to be relatively");
1523: mvaddstr (7, 1, "insecure.");
1524: mvprintw (8, 1,
1525: "%i character max. No ':' characters. Blank line to abort.", DGL_PASSWDLEN);
1526: mvaddstr (10, 1, "=> ");
1527:
1528: if (error == 1)
1529: {
1530: mvaddstr (15, 1, "Sorry, the passwords don't match. Try again.");
1531: move (10, 4);
1532: }
1533:
1534: refresh ();
1535:
1536: if (mygetnstr (buf, DGL_PASSWDLEN, 0) != OK)
1537: return 0;
1538:
1539: if (*buf == '\0')
1540: return 0;
1541:
1542: if (strchr (buf, ':') != NULL) {
1543: debug_write("cannot have ':' in passwd");
1544: graceful_exit (112);
1545: }
1546:
1547: mvaddstr (12, 1, "And again:");
1548: mvaddstr (13, 1, "=> ");
1549:
1550: if (mygetnstr (repeatbuf, DGL_PASSWDLEN, 0) != OK)
1551: return 0;
1552:
1553: if (!strcmp (buf, repeatbuf))
1554: error = 0;
1555: else
1556: error = 1;
1557: }
1558:
1559: free(me->password);
1.2 rubenllo 1560: #if defined(__OpenBSD__)
1561: me->password = (char *) malloc((_PASSWORD_LEN+1)*sizeof(char));
1562: if ( crypt_newhash(buf, "blowfish,12", me->password, _PASSWORD_LEN) != 0)
1.4 rubenllo 1563: graceful_exit(300);
1.2 rubenllo 1564: #else
1.1 rubenllo 1565: me->password = strdup (crypt (buf, buf));
1.2 rubenllo 1566: #endif
1.1 rubenllo 1567:
1568: if (dowrite)
1569: writefile (0);
1570:
1571: return 1;
1572: }
1573:
1574: /* ************************************************************* */
1575:
1576: void
1577: wall_email(char *from, char *msg)
1578: {
1579: int len, i;
1580: struct dg_game **games = NULL;
1581: char spool_fn[1024+1];
1582: FILE *user_spool = NULL;
1583: struct flock fl = { 0 };
1584:
1585: fl.l_type = F_WRLCK;
1586: fl.l_whence = SEEK_SET;
1587: fl.l_start = 0;
1588: fl.l_len = 0;
1589:
1590: if (!from || !msg) return;
1591:
1592: if (strlen(from) < 1) {
1593: fprintf(stderr, "Error: wall: 'from' username is too short!\n");
1594: debug_write("wall: 'from' username too short");
1595: graceful_exit(121);
1596: }
1597:
1598: if (strlen(msg) >= DGL_MAILMSGLEN) {
1599: fprintf(stderr, "Error: wall: message too long!\n");
1600: debug_write("wall: message too long");
1601: graceful_exit(120);
1602: }
1603:
1604: games = populate_games(-1, &len, me);
1605:
1606: if (len == 0) {
1607: fprintf(stderr, "Error: wall: no one's logged in!\n");
1608: debug_write("wall: no people playing");
1609: graceful_exit(118);
1610: }
1611:
1612: for (i = 0; i < len; i++) {
1613: int game = games[i]->gamenum;
1614: int fnamelen;
1615: if (strlen(myconfig[game]->spool) < 1) continue;
1616:
1617: snprintf (spool_fn, 1024, "%s/%s", myconfig[game]->spool, games[i]->name);
1618:
1619: if ((user_spool = fopen (spool_fn, "a")) == NULL) continue;
1620:
1621: while (fcntl(fileno(user_spool), F_SETLK, &fl) == -1) {
1622: if (errno != EAGAIN) continue;
1623: sleep (1);
1624: }
1625: fprintf(user_spool, "%s:%s\n", from, msg);
1626: fclose(user_spool);
1627: }
1628: free_populated_games(games, len);
1629: }
1630:
1631: void
1632: domailuser (char *username)
1633: {
1634: unsigned int len, i;
1635: char *spool_fn, message[DGL_MAILMSGLEN+1];
1636: FILE *user_spool = NULL;
1637: time_t now;
1638: int mail_empty = 1;
1639: int game;
1640: struct flock fl = { 0 };
1641:
1642: fl.l_type = F_WRLCK;
1643: fl.l_whence = SEEK_SET;
1644: fl.l_start = 0;
1645: fl.l_len = 0;
1646:
1647: assert (loggedin);
1648:
1649: game = 0; /*TODO: find_curr_player_game(username) */
1650:
1651: if (strlen(myconfig[game]->spool) < 1) return;
1652:
1653: len = strlen(myconfig[game]->spool) + strlen (username) + 1;
1654: spool_fn = malloc (len + 1);
1655: time (&now);
1656: snprintf (spool_fn, len + 1, "%s/%s", myconfig[game]->spool, username);
1657:
1658: /* print the enter your message line */
1659: clear ();
1660: drawbanner (&banner);
1661: mvprintw (5, 1,
1662: "Enter your message here. It is to be one line only and %i characters or less.",
1663: DGL_MAILMSGLEN);
1664: mvaddstr (7, 1, "=> ");
1665:
1666: if (mygetnstr (message, DGL_MAILMSGLEN, 1) != OK)
1667: return;
1668:
1669: for (i = 0; i < strlen (message); i++)
1670: {
1671: if (message[i] != ' ' && message[i] != '\n' && message[i] != '\t')
1672: mail_empty = 0;
1673: }
1674:
1675: if (mail_empty)
1676: {
1677: mvaddstr (9, 1, "This scroll appears to be blank.");
1678: mvaddstr (10, 1, "(Aborting your message.)");
1679: mvaddstr (12, 1, "--More--");
1680: dgl_getch ();
1681: return;
1682: }
1683:
1684: if ((user_spool = fopen (spool_fn, "a")) == NULL)
1685: {
1686: mvaddstr (9, 1,
1687: "You fall into the water! You sink like a rock.");
1688: mvprintw (10, 1,
1689: "(Couldn't open %s'%c spool file. Aborting.)",
1690: username, (username[strlen (username) - 1] != 's') ? 's' : 0);
1691: mvaddstr (12, 1, "--More--");
1692: dgl_getch ();
1693: return;
1694: }
1695:
1696: mvaddstr (9, 1, "Sending your scroll...");
1697: refresh ();
1698:
1699: /* Getting a lock on the mailspool... */
1700: while (fcntl (fileno (user_spool), F_SETLK, &fl) == -1)
1701: {
1702: if (errno != EAGAIN)
1703: {
1704: mvaddstr (10, 1,
1705: "(Received a weird error from fcntl. Aborting.)");
1706: mvaddstr (12, 1, "--More--");
1707: dgl_getch ();
1708: return;
1709: }
1710: sleep (1);
1711: }
1712:
1713: fprintf (user_spool, "%s:%s\n", me->username, message);
1714:
1715: /*
1716: * Don't unlock the file ourselves, this way it will be done automatically
1717: * after all data has been written. (Using file locking with stdio is icky.)
1718: */
1719:
1720: fclose (user_spool);
1721:
1722: mvaddstr (9, 1, "Scroll delivered! ");
1723: move(9, 19); /* Pedantry! */
1724: refresh ();
1725: sleep (2);
1726:
1727: return;
1728: }
1729:
1730:
1731: /* ************************************************************* */
1732:
1733: void
1734: freefile ()
1735: {
1736: #ifndef USE_SQLITE3
1737: int i;
1738:
1739: /* free existing mem, clear existing entries */
1740: for (i = 0; i < f_num; i++)
1741: {
1742: if (users[i] != me)
1743: {
1744: free (users[i]->password);
1745: free (users[i]->username);
1746: free (users[i]->email);
1747: free (users[i]->env);
1748: free (users[i]);
1749: }
1750: }
1751:
1752: if (users)
1753: free (users);
1754:
1755: users = NULL;
1756: f_num = 0;
1757: #endif
1758: }
1759:
1760: /* ************************************************************* */
1761:
1762: void
1763: initcurses ()
1764: {
1765: printf("\033[2J");
1766: if (newterm(NULL, stdout, stdin) == NULL) {
1767: if (!globalconfig.defterm || (newterm(globalconfig.defterm, stdout, stdin) == NULL)) {
1768: debug_write("cannot create newterm");
1769: graceful_exit(60);
1770: }
1771: mysetenv("TERM", globalconfig.defterm, 1);
1772: }
1773: cbreak ();
1774: noecho ();
1775: nonl ();
1776: intrflush (stdscr, FALSE);
1777: keypad (stdscr, TRUE);
1778: #ifdef USE_NCURSES_COLOR
1779: start_color();
1780: use_default_colors();
1781:
1782: init_pair(COLOR_BLACK, COLOR_WHITE, COLOR_BLACK);
1783: init_pair(COLOR_RED, COLOR_RED, COLOR_BLACK);
1784: init_pair(COLOR_GREEN, COLOR_GREEN, COLOR_BLACK);
1785: init_pair(COLOR_YELLOW, COLOR_YELLOW, COLOR_BLACK);
1786: init_pair(COLOR_BLUE, COLOR_BLUE, COLOR_BLACK);
1787: init_pair(COLOR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK);
1788: init_pair(COLOR_CYAN, COLOR_CYAN, COLOR_BLACK);
1789: init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK);
1790: init_pair(9, 0, COLOR_BLACK);
1791: init_pair(10, COLOR_BLACK, COLOR_BLACK);
1792: init_pair(11, -1, -1);
1793:
1794: if (globalconfig.utf8esc) (void) write(1, "\033%G", 3);
1795: #endif
1796: clear();
1797: refresh();
1798: }
1799:
1800: /* ************************************************************* */
1801:
1802: void
1803: autologin (char* user, char *pass)
1804: {
1805: struct dg_user *tmp;
1806: tmp = userexist(user, 0);
1807: if (tmp) {
1808: me = cpy_me(tmp);
1809: if ((passwordgood(pass) || initplayer == 1) && !(me->flags & DGLACCT_LOGIN_LOCK)) {
1810: loggedin = 1;
1811: setproctitle ("%s", me->username);
1812: dgl_exec_cmdqueue(globalconfig.cmdqueue[DGLTIME_LOGIN], 0, me);
1813: }
1814: }
1815: }
1816:
1817: void
1818: loginprompt (int from_ttyplay)
1819: {
1820: char user_buf[DGL_PLAYERNAMELEN+1], pw_buf[DGL_PASSWDLEN+2];
1821: int error = 2;
1822:
1823: loggedin = 0;
1824:
1825: while (error)
1826: {
1827: clear ();
1828:
1829: drawbanner (&banner);
1830:
1831: if (from_ttyplay == 1)
1832: mvaddstr (4, 1, "This operation requires you to be logged in.");
1833:
1834: mvaddstr (5, 1,
1835: "Please enter your username. (blank entry aborts)");
1836: mvaddstr (7, 1, "=> ");
1837:
1838: if (error == 1)
1839: {
1840: mvaddstr (9, 1, "There was a problem with your last entry.");
1841: move (7, 4);
1842: }
1843:
1844: refresh ();
1845:
1846: if (mygetnstr (user_buf, DGL_PLAYERNAMELEN, 1) != OK)
1847: return;
1848:
1849: if (*user_buf == '\0')
1850: return;
1851:
1852: error = 1;
1853:
1854: {
1855: struct dg_user *tmpme;
1856: if ((tmpme = userexist(user_buf, 0))) {
1857: me = cpy_me(tmpme);
1858: error = 0;
1859: }
1860: }
1861: }
1862:
1863: clear ();
1864:
1865: drawbanner (&banner);
1866:
1867: mvaddstr (5, 1, "Please enter your password.");
1868: mvaddstr (7, 1, "=> ");
1869:
1870: refresh ();
1871:
1872: if (mygetnstr (pw_buf, DGL_PASSWDLEN, 0) != OK)
1873: return;
1874:
1875: if (passwordgood (pw_buf))
1876: {
1877: if (me->flags & DGLACCT_LOGIN_LOCK) {
1878: clear ();
1879: mvprintw(5, 1, "Sorry, that account has been banned.--More--");
1880: dgl_getch();
1881: return;
1882: }
1883:
1884: loggedin = 1;
1885: if (from_ttyplay)
1886: setproctitle("%s [watching %s]", me->username, chosen_name);
1887: else
1888: setproctitle("%s", me->username);
1889: dgl_exec_cmdqueue(globalconfig.cmdqueue[DGLTIME_LOGIN], 0, me);
1890: }
1891: else
1892: {
1893: me = NULL;
1894: if (from_ttyplay == 1)
1895: {
1896: mvaddstr(9, 1, "Login failed. Returning to game.");
1897: refresh();
1898: sleep(2);
1899: }
1900: }
1901: }
1902:
1903: /* ************************************************************* */
1904:
1905: void
1906: newuser ()
1907: {
1908: char buf[1024], dirname[100];
1909: int error = 2;
1910: unsigned int i;
1911:
1912: loggedin = 0;
1913:
1914: #ifndef USE_SQLITE3
1915: if (f_num >= globalconfig.max)
1916: {
1917: clear ();
1918:
1919: drawbanner (&banner);
1920:
1921: mvaddstr (5, 1, "Sorry, too many users have registered now.");
1922: mvaddstr (6, 1, "You might email the server administrator.");
1923: mvaddstr (7, 1, "Press return to return to the menu. ");
1924: dgl_getch ();
1925:
1926: return;
1927: }
1928: #endif
1929:
1930: if (me)
1931: free (me);
1932:
1933: me = calloc (1, sizeof (struct dg_user));
1934:
1935: while (error)
1936: {
1937: clear ();
1938:
1939: sprintf(buf, "%i character max.", globalconfig.max_newnick_len);
1940:
1941: drawbanner (&banner);
1942:
1943: mvaddstr (5, 1, "Welcome new user. Please enter a username.");
1944: mvaddstr (6, 1,
1945: "Only characters and numbers are allowed, with no spaces.");
1946: mvaddstr (7, 1, buf);
1947: mvaddstr (9, 1, "=> ");
1948:
1949: if (error == 1)
1950: {
1951: mvaddstr (11, 1, "There was a problem with your last entry.");
1952: move (9, 4);
1953: }
1954:
1955: refresh ();
1956:
1957: if (mygetnstr (buf, globalconfig.max_newnick_len, 1) != OK)
1958: buf[0] = 0;
1959:
1960: if (*buf == '\0') {
1961: free(me);
1962: me = NULL;
1963: return;
1964: }
1965:
1966: if (!userexist(buf, 1)) {
1967: error = 0;
1968: } else
1969: error = 1;
1970:
1971: for (i = 0; i < strlen (buf); i++)
1972: {
1973: if (!isalnum((int)buf[i]))
1974: error = 1;
1975: }
1976:
1977: if (strlen (buf) < 2)
1978: error = 1;
1979:
1980: if (strlen (buf) == 0)
1981: {
1982: free(me);
1983: me = NULL;
1984: return;
1985: }
1986: }
1987:
1988: me->username = strdup (buf);
1989:
1990: /* password step */
1991:
1992: clear ();
1993:
1994: if (!changepw (0)) /* Calling changepw instead to prompt twice. */
1995: {
1996: free(me->username);
1997: free(me);
1998: me = NULL;
1999: return;
2000: }
2001:
2002: /* email step */
2003:
2004: error = 2;
2005: while (error != 0)
2006: {
2007: clear ();
2008:
2009: drawbanner (&banner);
2010:
2011: mvaddstr (5, 1, "Please enter your email address.");
2012: mvaddstr (6, 1, "This is sent _nowhere_ but will be used if you ask"
2013: " the sysadmin for lost");
2014: mvaddstr (7, 1, "password help. Please use a correct one. It only"
2015: " benefits you.");
2016: mvaddstr (8, 1, "80 character max. No ':' characters. Blank line"
2017: " aborts.");
2018: mvaddstr (10, 1, "=> ");
2019:
2020: if (error == 1)
2021: {
2022: mvaddstr (12, 1, "There was a problem with your last entry.");
2023: move (10, 4);
2024: }
2025:
2026: refresh ();
2027: if (mygetnstr (buf, 80, 1) != OK)
2028: buf[0] = 0;
2029:
2030: if (check_email (buf))
2031: error = 0;
2032: else
2033: error = 1;
2034:
2035: if (*buf == '\0')
2036: {
2037: free (me->username);
2038: free (me->password);
2039: free (me);
2040: me = NULL;
2041: return;
2042: }
2043: }
2044:
2045: me->email = strdup (buf);
2046: me->env = calloc (1, 1);
2047: me->flags = 0;
2048:
2049: loggedin = 1;
2050:
2051: setproctitle ("%s", me->username);
2052:
2053: dgl_exec_cmdqueue(globalconfig.cmdqueue[DGLTIME_REGISTER], 0, me);
2054:
2055: writefile (1);
2056: }
2057:
2058: /* ************************************************************* */
2059:
2060: int
2061: passwordgood (char *cpw)
2062: {
1.2 rubenllo 2063: #if defined(__OpenBSD__)
2064: if ( crypt_checkpass(cpw, me->password) == 0)
2065: return 1;
2066: return 0;
2067: #else
1.1 rubenllo 2068: char *crypted;
2069: assert (me != NULL);
2070:
2071: crypted = crypt (cpw, cpw);
2072: if (crypted == NULL)
2073: return 0;
2074: if (!strncmp (crypted, me->password, DGL_PASSWDLEN))
2075: return 1;
2076: if (!strncmp (cpw, me->password, DGL_PASSWDLEN))
2077: return 1;
2078:
2079: return 0;
1.2 rubenllo 2080: #endif
1.1 rubenllo 2081: }
2082:
2083: /* ************************************************************* */
2084:
2085: int
2086: readfile (int nolock)
2087: {
2088: #ifndef USE_SQLITE3
2089: FILE *fp = NULL, *fpl = NULL;
2090: char buf[1200];
2091: struct flock fl = { 0 };
2092:
2093: fl.l_type = F_RDLCK;
2094: fl.l_whence = SEEK_SET;
2095: fl.l_start = 0;
2096: fl.l_len = 0;
2097:
2098: memset (buf, 0, 1024);
2099:
2100: /* read new stuff */
2101:
2102: if (!nolock)
2103: {
2104: fpl = fopen (globalconfig.lockfile, "r");
2105: if (!fpl) {
2106: debug_write("cannot fopen lockfile");
2107: graceful_exit (106);
2108: }
2109: if (fcntl (fileno (fpl), F_SETLKW, &fl) == -1) {
2110: debug_write("cannot fcntl lockfile");
2111: graceful_exit (95);
2112: }
2113: }
2114:
2115: fp = fopen (globalconfig.passwd, "r");
2116: if (!fp) {
2117: debug_write("cannot fopen passwd file");
2118: graceful_exit (106);
2119: }
2120:
2121: /* once per name in the file */
2122: while (fgets (buf, 1200, fp))
2123: {
2124: char *b = buf, *n = buf;
2125:
2126: users = realloc (users, sizeof (struct dg_user *) * (f_num + 1));
2127: users[f_num] = malloc (sizeof (struct dg_user));
2128: users[f_num]->username = (char *) calloc (DGL_PLAYERNAMELEN+2, sizeof (char));
2129: users[f_num]->email = (char *) calloc (82, sizeof (char));
2130: users[f_num]->password = (char *) calloc (DGL_PASSWDLEN+2, sizeof (char));
2131: users[f_num]->env = (char *) calloc (1026, sizeof (char));
2132:
2133: /* name field, must be valid */
2134: while (*b != ':')
2135: {
2136: if (!isalnum((int)*b))
2137: return 1;
2138: users[f_num]->username[(b - n)] = *b;
2139: b++;
2140: if ((b - n) >= DGL_PLAYERNAMELEN) {
2141: debug_write("name field too long");
2142: graceful_exit (100);
2143: }
2144: }
2145:
2146: /* advance to next field */
2147: n = b + 1;
2148: b = n;
2149:
2150: /* email field */
2151: while (*b != ':')
2152: {
2153: users[f_num]->email[(b - n)] = *b;
2154: b++;
2155: if ((b - n) > 80) {
2156: debug_write("email field too long");
2157: graceful_exit (101);
2158: }
2159: }
2160:
2161: /* advance to next field */
2162: n = b + 1;
2163: b = n;
2164:
2165: /* pw field */
1.7 rubenllo 2166: /* OpenBSD's implementation stores the password in hashed form.
2167: * crypt_checkpass takes care of validating the hashing so we
2168: * don't have to ensure the password field has a valid length */
2169:
1.8 ! rubenllo 2170: #if !defined(__OpenBSD__)
1.1 rubenllo 2171: while (*b != ':')
2172: {
2173: users[f_num]->password[(b - n)] = *b;
2174: b++;
2175: if ((b - n) >= DGL_PASSWDLEN) {
2176: debug_write("passwd field too long");
2177: graceful_exit (102);
2178: }
2179: }
1.7 rubenllo 2180: #endif
1.1 rubenllo 2181:
2182: /* advance to next field */
2183: n = b + 1;
2184: b = n;
2185:
2186: /* env field */
2187: while ((*b != '\n') && (*b != 0) && (*b != EOF))
2188: {
2189: users[f_num]->env[(b - n)] = *b;
2190: b++;
2191: if ((b - n) >= 1024) {
2192: debug_write("env field too long");
2193: graceful_exit (103);
2194: }
2195: }
2196:
2197: f_num++;
2198: /* prevent a buffer overrun here */
2199: if (f_num > globalconfig.max)
2200: {
2201: fprintf(stderr,"ERROR: number of users in database exceeds maximum. Exiting.\n");
2202: debug_write("too many users in database");
2203: graceful_exit (109);
2204: }
2205: }
2206:
2207: if (!nolock)
2208: fclose (fpl);
2209: fclose (fp);
2210: #endif
2211: return 0;
2212: }
2213:
2214: /* ************************************************************* */
2215:
2216: #ifndef USE_SQLITE3
2217: struct dg_user *userexist_tmp_me = NULL;
2218:
2219: struct dg_user *
2220: userexist (char *cname, int isnew)
2221: {
2222: int i;
2223:
2224: if (userexist_tmp_me) {
2225: free(userexist_tmp_me->username);
2226: free(userexist_tmp_me->email);
2227: free(userexist_tmp_me->env);
2228: free(userexist_tmp_me->password);
2229: free(userexist_tmp_me);
2230: userexist_tmp_me = NULL;
2231: }
2232:
2233: for (i = 0; i < f_num; i++)
2234: {
2235: if (!strncasecmp (cname, users[i]->username, (isnew ? globalconfig.max_newnick_len : DGL_PLAYERNAMELEN))) {
2236: userexist_tmp_me = cpy_me(users[i]);
2237: return userexist_tmp_me;
2238: }
2239: }
2240:
2241: return NULL;
2242: }
2243: #else
2244:
2245: struct dg_user *userexist_tmp_me = NULL;
2246:
2247: static int
2248: userexist_callback(void *NotUsed, int argc, char **argv, char **colname)
2249: {
2250: int i;
2251: NotUsed = NULL;
2252:
2253: userexist_tmp_me = malloc(sizeof(struct dg_user));
2254:
2255: for (i = 0; i < argc; i++) {
2256: if (!strcmp(colname[i], "username"))
2257: userexist_tmp_me->username = strdup(argv[i]);
2258: else if (!strcmp(colname[i], "email"))
2259: userexist_tmp_me->email = strdup(argv[i]);
2260: else if (!strcmp(colname[i], "env"))
2261: userexist_tmp_me->env = strdup(argv[i]);
2262: else if (!strcmp(colname[i], "password"))
2263: userexist_tmp_me->password = strdup(argv[i]);
2264: else if (!strcmp(colname[i], "flags"))
2265: userexist_tmp_me->flags = atoi(argv[i]);
2266: else if (!strcmp(colname[i], "id"))
2267: userexist_tmp_me->id = atoi(argv[i]);
2268: }
2269: return 0;
2270: }
2271:
2272: struct dg_user *
2273: userexist (char *cname, int isnew)
2274: {
2275: sqlite3 *db;
2276: char *errmsg = NULL;
2277: int ret, retry = 10;
2278:
2279: char *qbuf;
2280:
2281: char tmpbuf[DGL_PLAYERNAMELEN+2];
2282:
2283: memset(tmpbuf, 0, DGL_PLAYERNAMELEN+2);
2284: strncpy(tmpbuf, cname, (isnew ? globalconfig.max_newnick_len : DGL_PLAYERNAMELEN));
2285:
2286: /* Check that the nick doesn't interfere with already registered nicks */
2287: if (isnew && (strlen(cname) >= globalconfig.max_newnick_len))
2288: strcat(tmpbuf, "%");
2289:
2290: qbuf = sqlite3_mprintf("select * from dglusers where username = '%q' collate nocase limit 1", tmpbuf);
2291:
2292: ret = sqlite3_open(globalconfig.passwd, &db);
2293: if (ret) {
2294: sqlite3_close(db);
2295: debug_write("sqlite3_open failed");
2296: graceful_exit(96);
2297: }
2298:
2299: if (userexist_tmp_me) {
2300: free(userexist_tmp_me->username);
2301: free(userexist_tmp_me->email);
2302: free(userexist_tmp_me->env);
2303: free(userexist_tmp_me->password);
2304: free(userexist_tmp_me);
2305: userexist_tmp_me = NULL;
2306: }
2307:
2308: sqlite3_busy_timeout(db, 10000);
2309: ret = sqlite3_exec(db, qbuf, userexist_callback, 0, &errmsg);
2310:
2311: sqlite3_free(qbuf);
2312:
2313: if (ret != SQLITE_OK) {
2314: sqlite3_close(db);
2315: debug_write("sqlite3_exec failed");
2316: graceful_exit(108);
2317: }
2318: sqlite3_close(db);
2319:
2320: return userexist_tmp_me;
2321: }
2322: #endif
2323:
2324: /* ************************************************************* */
2325:
2326: void
2327: write_canned_rcfile (int game, char *target)
2328: {
2329: FILE *canned, *newfile;
2330: char buf[1024], *rfn;
2331: size_t bytes, len;
2332:
2333: len = strlen(myconfig[game]->rcfile) + 2;
2334: rfn = malloc(len);
2335: snprintf (rfn, len, "/%s", myconfig[game]->rcfile);
2336:
2337: if (!(newfile = fopen (target, "w")))
2338: {
2339: bail:
2340: mvaddstr (13, 1,
2341: "You don't know how to write that! You write \"%s was here\" and the scroll disappears.");
2342: mvaddstr (14, 1,
2343: "(Sorry, but I couldn't open one of the config files. This is a bug.)");
2344: return;
2345: }
2346:
2347: if (!(canned = fopen (rfn, "r")))
2348: goto bail;
2349:
2350: free(rfn);
2351:
2352: while ((bytes = fread (buf, 1, 1024, canned)) > 0)
2353: {
2354: if (fwrite (buf, 1, bytes, newfile) != bytes)
2355: {
2356: if (ferror (newfile))
2357: {
2358: mvaddstr (13, 1, "Your hand slips while engraving.");
2359: mvaddstr (14, 1,
2360: "(Encountered a problem writing the new file. This is a bug.)");
2361: fclose (canned);
2362: fclose (newfile);
2363: return;
2364: }
2365: }
2366: }
2367:
2368: fclose (canned);
2369: fclose (newfile);
2370: chmod (target, default_fmode);
2371: }
2372:
2373:
2374:
2375: /* ************************************************************* */
2376:
2377: #ifndef USE_SQLITE3
2378: void
2379: writefile (int requirenew)
2380: {
2381: FILE *fp, *fpl;
2382: int i = 0;
2383: int my_done = 0;
2384: struct flock fl = { 0 };
2385:
2386: fl.l_type = F_WRLCK;
2387: fl.l_whence = SEEK_SET;
2388: fl.l_start = 0;
2389: fl.l_len = 0;
2390:
2391: signals_block();
2392:
2393: fpl = fopen (globalconfig.lockfile, "r+");
2394: if (!fpl)
2395: {
2396: signals_release();
2397: debug_write("writefile locking failed");
2398: graceful_exit (115);
2399: }
2400: if (fcntl (fileno (fpl), F_SETLK, &fl))
2401: {
2402: signals_release();
2403: debug_write("writefile fcntl failed");
2404: graceful_exit (107);
2405: }
2406:
2407: fl.l_type = F_UNLCK;
2408:
2409: freefile ();
2410: readfile (1);
2411:
2412: fp = fopen (globalconfig.passwd, "w");
2413: if (!fp)
2414: {
2415: signals_release();
2416: debug_write("passwd file fopen failed");
2417: graceful_exit (99);
2418: }
2419:
2420: for (i = 0; i < f_num; i++)
2421: {
2422: if (loggedin && !strncmp (me->username, users[i]->username, DGL_PLAYERNAMELEN))
2423: {
2424: if (requirenew)
2425: {
2426: /* this is if someone managed to register at the same time
2427: * as someone else. just die. */
2428: fclose(fp);
2429: fclose(fpl);
2430: signals_release();
2431: debug_write("two users registering at the same time");
2432: graceful_exit (111);
2433: }
2434: fprintf (fp, "%s:%s:%s:%s\n", me->username, me->email, me->password,
2435: me->env);
2436: my_done = 1;
2437: }
2438: else
2439: {
2440: fprintf (fp, "%s:%s:%s:%s\n", users[i]->username, users[i]->email,
2441: users[i]->password, users[i]->env);
2442: }
2443: }
2444: if (loggedin && !my_done)
2445: { /* new entry */
2446: if (f_num < globalconfig.max)
2447: fprintf (fp, "%s:%s:%s:%s\n", me->username, me->email, me->password,
2448: me->env);
2449: else /* Oops, someone else registered the last available slot first */
2450: {
2451: fclose(fp);
2452: fclose(fpl);
2453: signals_release();
2454: debug_write("too many users in passwd db already");
2455: graceful_exit (116);
2456: }
2457: }
2458:
2459: fclose (fp);
2460: fclose (fpl);
2461:
2462: signals_release();
2463: }
2464: #else
2465: void
2466: writefile (int requirenew)
2467: {
2468: sqlite3 *db;
2469: char *errmsg = NULL;
2470: int ret, retry = 10;
2471:
2472: char *qbuf;
2473:
2474: if (requirenew) {
2475: qbuf = sqlite3_mprintf("insert into dglusers (username, email, env, password, flags) values ('%q', '%q', '%q', '%q', %li)", me->username, me->email, me->env, me->password, me->flags);
2476: } else {
2477: qbuf = sqlite3_mprintf("update dglusers set username='%q', email='%q', env='%q', password='%q', flags=%li where id=%i", me->username, me->email, me->env, me->password, me->flags, me->id);
2478: }
2479:
2480: ret = sqlite3_open(globalconfig.passwd, &db);
2481: if (ret) {
2482: sqlite3_close(db);
2483: debug_write("writefile sqlite3_open failed");
2484: graceful_exit(97);
2485: }
2486:
2487: sqlite3_busy_timeout(db, 10000);
2488: ret = sqlite3_exec(db, qbuf, NULL, NULL, &errmsg);
2489:
2490: sqlite3_free(qbuf);
2491:
2492: if (ret != SQLITE_OK) {
2493: sqlite3_close(db);
2494: debug_write("writefile sqlite3_exec failed");
2495: graceful_exit(98);
2496: }
2497: sqlite3_close(db);
2498: }
2499: #endif
2500:
2501: /* ************************************************************* */
2502:
2503: int
2504: purge_stale_locks (int game)
2505: {
2506: DIR *pdir;
2507: struct dirent *dent;
2508: char* dir;
2509: size_t len;
2510: short firsttime = 1;
2511:
2512: dir = strdup(dgl_format_str(game, me, myconfig[game]->inprogressdir, NULL));
2513:
2514: if (!(pdir = opendir (dir))) {
2515: debug_write("purge_stale_locks dir open failed");
2516: graceful_exit (200);
2517: }
2518:
2519: free(dir);
2520:
2521: while ((dent = readdir (pdir)) != NULL)
2522: {
2523: FILE *ipfile;
2524: char *colon, *fn;
2525: char buf[16];
2526: pid_t pid;
2527: size_t len;
2528: int seconds = 0;
2529:
2530: if (!strcmp (dent->d_name, ".") || !strcmp (dent->d_name, ".."))
2531: continue;
2532:
2533: colon = strchr (dent->d_name, ':');
2534: /* should never happen */
2535: if (!colon) {
2536: debug_write("purge_stale_locks !colon");
2537: graceful_exit (201);
2538: }
2539: if (colon - dent->d_name != strlen(me->username))
2540: continue;
2541: if (strncmp (dent->d_name, me->username, colon - dent->d_name))
2542: continue;
2543:
2544: len = strlen (dent->d_name) + strlen(dgl_format_str(game, me, myconfig[game]->inprogressdir, NULL)) + 1;
2545: fn = malloc (len);
2546:
2547: snprintf (fn, len, "%s%s", dgl_format_str(game, me, myconfig[game]->inprogressdir, NULL), dent->d_name);
2548:
2549: if (!(ipfile = fopen (fn, "r"))) {
2550: debug_write("purge_stale_locks fopen inprogressdir fail");
2551: graceful_exit (202);
2552: }
2553:
2554: if (fgets (buf, 16, ipfile) == NULL) {
2555: debug_write("purge_stale_locks fgets ipfile fail");
2556: graceful_exit (203);
2557: }
2558:
2559: fclose (ipfile);
2560:
2561: if (firsttime)
2562: {
2563: clear ();
2564: drawbanner (&banner);
2565:
2566: #define HUP_WAIT 10 /* seconds before HUPPING */
2567: mvprintw (3, 1,
2568: "There are some stale %s processes, will recover in %d seconds.",
2569: myconfig[game]->game_name, HUP_WAIT);
2570: mvaddstr (4, 1,
2571: "Press a key NOW if you don't want this to happen!");
2572:
2573: move (3, 51 + strlen(myconfig[game]->game_name)); /* pedantry */
2574: halfdelay(10);
2575:
2576: for (seconds = HUP_WAIT - 1; seconds >= 0; seconds--)
2577: {
2578: if (dgl_getch() != ERR)
2579: {
2580: nocbreak(); /* leave half-delay */
2581: cbreak();
2582: return 0;
2583: }
2584: mvprintw (3, 50 + strlen(myconfig[game]->game_name), "%d%s", seconds, (seconds > 9) ? "" : " ");
2585: }
2586:
2587: nocbreak();
2588: cbreak();
2589:
2590: firsttime = 0;
2591: }
2592:
2593: clear ();
2594: refresh ();
2595:
2596: pid = atoi (buf);
2597:
2598: kill (pid, SIGHUP);
2599:
2600: errno = 0;
2601:
2602: /* Wait for it to stop running */
2603: seconds = 0;
2604: while (kill (pid, 0) == 0)
2605: {
2606: seconds++;
2607: sleep (1);
2608: if (seconds == 10)
2609: {
2610: mvprintw (3, 1,
2611: "Couldn't terminate one of your stale %s processes gracefully.", myconfig[game]->game_name);
2612: mvaddstr (4, 1, "Force its termination? [yn] ");
2613: if (tolower (dgl_getch ()) == 'y')
2614: {
2615: kill (pid, SIGTERM);
2616: break;
2617: }
2618: else
2619: {
2620: endwin ();
2621: fprintf (stderr, "Sorry, no %s for you now, please "
2622: "contact the admin.\n", myconfig[game]->game_name);
2623: debug_write("could not terminate stale processes");
2624: graceful_exit (1);
2625: }
2626: }
2627: }
2628:
2629: /* Don't remove the lock file until the process is dead. */
2630: unlink (fn);
2631: free (fn);
2632: }
2633:
2634: closedir (pdir);
2635: return 1;
2636: }
2637:
2638:
2639: int
2640: runmenuloop(struct dg_menu *menu)
2641: {
2642: struct dg_banner ban;
2643: struct dg_menuoption *tmpopt;
2644: int userchoice = 0;
2645: int doclear = 1;
2646:
2647: if (!menu) return 1;
2648:
2649: ban.lines = NULL;
2650: ban.len = 0;
2651:
2652: loadbanner(menu->banner_fn, &ban);
2653: while (1) {
2654: term_resize_check();
2655: if (doclear) {
2656: doclear = 0;
2657: if (globalconfig.utf8esc) (void) write(1, "\033%G", 3);
2658: clear();
2659: }
2660: drawbanner(&ban);
2661: if (menu->cursor_x >= 0 && menu->cursor_y >= 0)
2662: mvprintw(menu->cursor_y, menu->cursor_x, "");
2663: refresh();
2664: userchoice = dgl_getch();
2665: if (userchoice == ERR) {
2666: freebanner(&ban);
2667: return 1;
2668: }
2669: tmpopt = menu->options;
2670: while (tmpopt) {
2671: if (strchr(tmpopt->keys, userchoice)) {
2672: dgl_exec_cmdqueue(tmpopt->cmdqueue, selected_game, me);
2673: doclear = 1;
2674: break;
2675: } else {
2676: tmpopt = tmpopt->next;
2677: }
2678: }
2679:
2680: if (return_from_submenu) {
2681: freebanner(&ban);
2682: return_from_submenu = 0;
2683: return 0;
2684: }
2685:
2686: if (check_retard(0)) {
2687: freebanner(&ban);
2688: debug_write("retard");
2689: graceful_exit(119);
2690: }
2691: }
2692: }
2693:
2694: int
2695: main (int argc, char** argv)
2696: {
2697: /* for chroot and program execution */
2698: char atrcfilename[81], *p, *auth = NULL;
2699: unsigned int len;
2700: int c, i;
2701: int userchoice;
2702: char *tmp;
2703: char *wall_email_str = NULL;
2704: #ifdef USE_RLIMIT
2705: struct rlimit lim;
2706: #endif
2707:
2708: #ifndef HAVE_SETPROCTITLE
2709: /* save argc, argv */
2710: char** saved_argv;
2711: int saved_argc;
2712:
2713: saved_argc = argc;
2714:
2715: saved_argv = malloc(sizeof(char**) * (argc + 1));
2716: for (i = 0; i < argc; i++)
2717: saved_argv[i] = strdup(argv[i]);
2718: saved_argv[i] = '\0';
2719:
2720: compat_init_setproctitle(argc, argv);
2721: argv = saved_argv;
2722: #endif
2723:
2724: p = getenv("DGLAUTH");
2725:
2726: /* Linux telnetd allows importing the USER envvar via telnet,
2727: * while FreeBSD does not. FreeBSD, on the other hand, does allow
2728: * the LOGNAME envvar. Check USER first, then LOGNAME.
2729: */
2730: if (p == NULL) {
2731: p = getenv("USER");
2732: }
2733:
2734: if (p == NULL) {
2735: p = getenv("LOGNAME");
2736: }
2737:
2738: if (p && *p != '\0')
2739: auth = strdup(p);
2740: /* else auth is still NULL */
2741:
2742: /* just to be sure */
2743: unsetenv("DGLAUTH"); unsetenv("USER"); unsetenv("LOGNAME");
2744:
2745: __progname = basename(strdup(argv[0]));
2746:
2747: while ((c = getopt(argc, argv, "csqh:pi:aeW:SD")) != -1)
2748: {
2749: /* Stop processing arguments at -c, so that user-provided
2750: * commands (via ssh for example) to the dgamelaunch login
2751: * shell are ignored.
2752: */
2753: if (c == 'c') break;
2754: switch (c)
2755: {
2756: case 's':
2757: showplayers = 1; break;
2758:
2759: case 'q':
2760: silent = 1; break;
2761:
2762: case 'i':
2763: if (optarg && *optarg != '\0') {
2764: if (p && *p != '\0')
2765: *p = '\0';
2766:
2767: p = strdup(optarg);
2768: initplayer = 1;
2769:
2770: if (auth && *auth != '\0')
2771: *auth = '\0';
2772: }
2773: break;
2774:
2775: case 'W':
2776: wall_email_str = strdup(optarg);
2777: break;
2778:
2779: case 'S': /* Free the shared memory block */
2780: #ifdef USE_SHMEM
2781: if (shm_free()) {
2782: if (!silent) fprintf(stderr, "nonexistent shmem block.\n");
2783: } else {
2784: if (!silent) fprintf(stderr, "shmem block freed.\n");
2785: }
2786: #else
2787: if (!silent) fprintf(stderr, "warning: dgamelaunch was compiled without shmem.\n");
2788: #endif
2789: graceful_exit(0);
2790: break;
2791:
2792: case 'D': /* dump the shared memory block data */
2793: #ifdef USE_SHMEM
2794: shm_dump();
2795: #else
2796: if (!silent) fprintf(stderr, "warning: dgamelaunch was compiled without shmem.\n");
2797: #endif
2798: graceful_exit(0);
2799: break;
2800:
2801: default:
2802: break; /*ignore */
2803: }
2804: }
2805:
2806: while (optind < argc)
2807: {
2808: size_t len = strlen(argv[optind]);
2809: memset(argv[optind++], 0, len);
2810: }
2811: setproctitle("<Anonymous>");
2812:
2813: srand(time(0));
2814:
2815: create_config();
2816:
2817: /* signal handlers */
2818: signal (SIGHUP, catch_sighup);
2819: signal (SIGINT, catch_sighup);
2820: signal (SIGQUIT, catch_sighup);
2821: signal (SIGTERM, catch_sighup);
2822:
2823: (void) tcgetattr (0, &tt);
2824:
2825: if (!globalconfig.flowctrl) {
2826: tt.c_iflag &= ~(IXON | IXOFF | IXANY); /* Disable XON/XOFF */
2827: (void) tcsetattr(0, TCSANOW, &tt);
2828: }
2829:
2830: if (-1 == ioctl (0, TIOCGWINSZ, (char *) &win) || win.ws_row < 4 ||
2831: win.ws_col < 4) /* Rudimentary validity check */
2832: {
2833: win.ws_row = 24;
2834: win.ws_col = 80;
2835: win.ws_xpixel = win.ws_col * 8;
2836: win.ws_ypixel = win.ws_row * 8;
2837: }
2838:
2839: /* get master tty just before chroot (lives in /dev) */
2840: ttyrec_getpty ();
2841:
2842: #ifdef USE_RLIMIT
2843: #ifdef USE_RLIMIT_CORE
2844: /* enable and set core dump size */
2845: if (!getrlimit(RLIMIT_CORE, &lim)) {
2846: lim.rlim_cur = USE_RLIMIT_CORE;
2847: setrlimit(RLIMIT_CORE, &lim);
2848: }
2849: #endif
2850: #ifdef USE_RLIMIT_AS
2851: /* set maximum memory usage */
2852: if (!getrlimit(RLIMIT_AS, &lim)) {
2853: lim.rlim_cur = USE_RLIMIT_AS;
2854: setrlimit(RLIMIT_AS, &lim);
2855: }
2856: #endif
2857: #endif
2858:
2859: if (geteuid () != globalconfig.shed_uid)
2860: {
2861: /* chroot */
2862: if (chroot (globalconfig.chroot))
2863: {
2864: perror ("cannot change root directory");
2865: graceful_exit (2);
2866: }
2867:
2868: if (chdir ("/"))
2869: {
2870: perror ("cannot chdir to root directory");
2871: graceful_exit (3);
2872: }
2873:
1.5 rubenllo 2874:
2875:
1.1 rubenllo 2876: /* shed privs. this is done immediately after chroot. */
2877: if (setgroups (1, &globalconfig.shed_gid) == -1)
2878: {
2879: perror ("setgroups");
2880: graceful_exit (4);
2881: }
2882:
2883: if (setgid (globalconfig.shed_gid) == -1)
2884: {
2885: perror ("setgid");
2886: graceful_exit (5);
2887: }
2888:
2889: if (setuid (globalconfig.shed_uid) == -1)
2890: {
2891: perror ("setuid");
2892: graceful_exit (6);
2893: }
1.5 rubenllo 2894:
1.6 rubenllo 2895: #if defined(__OpenBSD__)
1.5 rubenllo 2896: if ( pledge("stdio rpath wpath cpath fattr flock "
2897: "tty proc exec ps", NULL )==-1 )
2898: {
2899: perror("pledge");
2900: graceful_exit (301);
2901: }
2902: #endif
2903:
1.1 rubenllo 2904: }
2905:
2906: if (globalconfig.locale) {
2907: setlocale(LC_CTYPE, globalconfig.locale);
2908: }
2909:
2910: if (showplayers) {
2911: inprogressdisplay(-1);
2912: graceful_exit (0);
2913: }
2914:
2915: if (wall_email_str) {
2916: char *emailfrom = wall_email_str;
2917: char *emailmsg = strchr(wall_email_str, ':');
2918: if (!emailmsg) {
2919: debug_write("wall: no mail msg");
2920: graceful_exit(117);
2921: }
2922: *emailmsg = '\0';
2923: emailmsg++;
2924: if (emailmsg)
2925: wall_email(emailfrom, emailmsg);
2926: graceful_exit(0);
2927: }
2928:
2929: banner.len = 0;
2930: banner.lines = NULL;
2931: loadbanner(globalconfig.banner, &banner);
2932:
2933: dgl_exec_cmdqueue(globalconfig.cmdqueue[DGLTIME_DGLSTART], 0, NULL);
2934:
2935: if (initplayer) {
2936: char *user, *pass;
2937:
2938: user = strdup(p);
2939: pass = strdup(p);
2940:
2941: autologin(user, pass);
2942:
2943: if (loggedin) {
2944: dgl_exec_cmdqueue(globalconfig.cmdqueue[DGLTIME_REGISTER], 0, me);
2945: fprintf(stdout, "Setup of %s succeeded.\n", me->username);
2946: graceful_exit(0);
2947: }
2948: else {
2949: fprintf(stdout, "Setup of %s failed.\n", p);
2950: graceful_exit(10);
2951: }
2952: }
2953:
2954: /* simple login routine, uses ncurses */
2955: if (readfile (0)) {
2956: debug_write("log in fail");
2957: graceful_exit (110);
2958: }
2959:
2960: if (auth)
2961: {
2962: char *user, *pass, *p;
2963:
2964: p = strchr(auth, ':');
2965:
2966: if (p)
2967: {
2968: pass = p + 1;
2969:
2970: if (*pass != '\0')
2971: {
2972: *p = '\0';
2973: user = auth;
2974: autologin(user, pass);
2975: }
2976: }
2977: }
2978:
2979: initcurses ();
2980:
2981: g_chain_winch = signal(SIGWINCH, sigwinch_func);
2982:
2983: term_resize_check();
2984:
2985: idle_alarm_set_enabled(1);
2986:
2987: while (1) {
2988: if (runmenuloop(dgl_find_menu(get_mainmenu_name())))
2989: break;
2990: }
2991:
2992: idle_alarm_set_enabled(0);
2993:
2994: /* NOW we can safely kill this */
2995: freefile ();
2996:
2997: if (me)
2998: free (me);
2999:
3000: freebanner(&banner);
3001: banner_var_free();
3002: graceful_exit (20);
3003:
3004: return 1;
3005: }
CVSweb