|
|
1.1 root 1: #ifndef lint
2: static char *rcsid = "$Header: gaptelnet.c,v 2.0 85/11/21 07:23:04 jqj Exp $";
3: #endif
4:
5: /*
6: * XNS User telnet program.
7: */
8:
9: /* $Log: gaptelnet.c,v $
10: * Revision 2.0 85/11/21 07:23:04 jqj
11: * 4.3BSD standard release
12: *
13: * Revision 1.3 85/11/20 14:00:08 jqj
14: * added symbolic entries for Gap connection types
15: *
16: * Revision 1.2 85/05/22 09:46:37 jqj
17: * VAX 4.3beta baseline version
18: *
19: * Revision 1.2 85/05/22 09:46:37 jqj
20: * Beta-test GAP telnet
21: *
22: * based on tcp/telnet:
23: * static char *rcsid = "$Header: gaptelnet.c,v 2.0 85/11/21 07:23:04 jqj Exp $";
24: * static char sccsid[] = "@(#)telnet.c 4.24 (Berkeley) 7/20/83";
25: */
26:
27: #include <sys/types.h>
28: #include <sys/socket.h>
29: #include <sys/ioctl.h>
30:
31: #include <netns/ns.h>
32: #include <netns/idp.h>
33: #include <netns/sp.h> /* for spphdr */
34: #include <netns/spidp.h>
35:
36: #include <stdio.h>
37: #include <ctype.h>
38: #include <errno.h>
39: #include <signal.h>
40:
41: #include <xnscourier/Clearinghouse2.h>
42: #include "GAP3.h"
43: #include "gapcontrols.h"
44: #include <xnscourier/except.h>
45: #include <xnscourier/CH.h>
46:
47: #define strip(x) ((x)&0177)
48:
49: char ttyobuf[BUFSIZ], *tfrontp = ttyobuf, *tbackp = ttyobuf;
50: char netobuf[BUFSIZ], *nfrontp = netobuf, *nbackp = netobuf;
51:
52:
53: int connected;
54: CourierConnection *cconn;
55: int net;
56: FILE *logfile;
57: int debug = 0;
58: int crmod = 0;
59: char *prompt;
60: char escape = CTRL(]);
61: char on = 1;
62:
63: char line[200];
64: int margc;
65: char *margv[20];
66:
67: jmp_buf toplevel;
68: jmp_buf peerdied;
69:
70: extern int errno;
71:
72: int tn(), quit(), suspend(), bye(), help();
73: int setescape(), status(), toggle(), setoptions();
74: int setcrmod(), setdebug(), setlog();
75:
76: #define HELPINDENT (sizeof ("connect"))
77:
78: struct cmd {
79: char *name; /* command name */
80: char *help; /* help string */
81: int (*handler)(); /* routine which executes command */
82: };
83:
84: char openhelp[] = "connect to a site";
85: char closehelp[] = "close current connection";
86: char quithelp[] = "exit telnet";
87: char zhelp[] = "suspend telnet";
88: char debughelp[] = "toggle debugging";
89: char escapehelp[] = "set escape character";
90: char statushelp[] = "print status information";
91: char helphelp[] = "print help information";
92: char crmodhelp[] = "toggle mapping of received carriage returns";
93: char loghelp[] = "toggle logging of session";
94:
95: struct cmd cmdtab[] = {
96: { "open", openhelp, tn },
97: { "close", closehelp, bye },
98: { "quit", quithelp, quit },
99: { "z", zhelp, suspend },
100: { "escape", escapehelp, setescape },
101: { "status", statushelp, status },
102: /* { "crmod", crmodhelp, setcrmod }, */
103: { "debug", debughelp, setdebug },
104: { "log", loghelp, setlog },
105: { "?", helphelp, help },
106: 0
107: };
108:
109: struct sockaddr_ns sin;
110:
111: int intr(), deadpeer();
112: char *control();
113: struct cmd *getcmd();
114:
115: struct tchars otc;
116: struct ltchars oltc;
117: struct sgttyb ottyb;
118:
119: char *hostname;
120: char hnamebuf[45];
121:
122:
123: main(argc, argv)
124: int argc;
125: char *argv[];
126: {
127: ioctl(0, TIOCGETP, (char *)&ottyb);
128: ioctl(0, TIOCGETC, (char *)&otc);
129: ioctl(0, TIOCGLTC, (char *)&oltc);
130: setbuf(stdin, 0);
131: setbuf(stdout, 0);
132: prompt = argv[0];
133: if (argc > 1 && !strcmp(argv[1], "-d"))
134: debug = SO_DEBUG, argv++, argc--;
135: if (argc != 1) {
136: if (setjmp(toplevel) != 0)
137: exit(0);
138: tn(argc, argv);
139: }
140: setjmp(toplevel);
141: for (;;)
142: command(1);
143: }
144:
145: tn(argc, argv)
146: int argc;
147: char *argv[];
148: {
149: register int c;
150: register struct ns_addr *host;
151: extern struct ns_addr *getXNSaddr();
152: Clearinghouse2_ObjectName hostoname, hdefault;
153: LongCardinal servicetype;
154:
155: if (connected) {
156: printf("?Already connected to %s\n", hostname);
157: return;
158: }
159: if (argc < 2) {
160: strcpy(line, "Connect ");
161: printf("(to) ");
162: gets(&line[strlen(line)]);
163: makeargv();
164: argc = margc;
165: argv = margv;
166: }
167: if (argc > 3) {
168: printf("usage: %s host-name [service-type]\n", argv[0]);
169: return;
170: }
171: if (argc == 2) servicetype = TTYService_sa; /* default to 1 */
172: else if (strcmp(argv[2],"sa") == 0) servicetype = TTYService_sa;
173: else if (strncmp(argv[2],"re",2) == 0 ||
174: strcmp(argv[2],"exec") == 0) servicetype = TTYService_exec;
175: else if (strcmp(argv[2],"its") == 0) servicetype = TTYService_its;
176: else servicetype = atoi(argv[2]);
177: CH_NameDefault(&hdefault);
178: hostoname = CH_StringToName(argv[1], &hdefault);
179: if ((host = CH_LookupAddrDN(hostoname,0,hnamebuf,sizeof(hnamebuf)))) {
180: sin.sns_family = AF_NS;
181: host->x_port = htons(IDPPORT_COURIER);
182: bcopy(host, (caddr_t)&sin.sns_addr, sizeof(host));
183: /* hnamebuf is filled in by CH_LookupAddrDN */
184: hostname = hnamebuf;
185: } else if ((host = getXNSaddr(argv[1]))) {
186: sin.sns_family = AF_NS;
187: bcopy(host, (caddr_t)&sin.sns_addr, sizeof(host));
188: strcpy(hnamebuf, argv[1]);
189: hostname = hnamebuf;
190: } else {
191: printf("%s: unknown host\n", argv[1]);
192: return;
193: }
194: cconn = CourierOpen(host);
195: if(cconn == NULL) {
196: fprintf(stderr,"Courier connection failed\n");
197: return;
198: }
199: net = *(int*)cconn;
200: signal(SIGINT, intr);
201: signal(SIGPIPE, deadpeer);
202: printf("Trying...\n");
203: if (createsession(cconn,servicetype) < 0)
204: return;
205: connected++;
206: call(status, "status", 0);
207: sleep(1);
208: if (setjmp(peerdied) == 0)
209: telnet(net);
210: fprintf(stderr, "\nConnection closed by foreign host.\n");
211: exit(1);
212: }
213:
214: /*
215: * create a session
216: */
217: createsession(cconn, servicetype)
218: CourierConnection *cconn;
219: LongCardinal servicetype;
220: {
221: GAP3_SessionParameterObject pobj;
222: GAP3_TransportObject tobjs[2];
223: GAP3_CommParamObject *cp;
224: struct {
225: Cardinal length;
226: GAP3_TransportObject *sequence;
227: } tobjlist;
228: Authentication1_Credentials creds;
229: Authentication1_Verifier verifier;
230:
231: pobj.designator = oldTtyHost; /* 11 */
232: pobj.oldTtyHost_case.charLength = seven;
233: pobj.oldTtyHost_case.parity = none;
234: pobj.oldTtyHost_case.stopBits = oneStopBit;
235: pobj.oldTtyHost_case.frameTimeout = 20;
236: /*
237: tobjs[0].designator = rs232c;
238: cp = &tobjs[0].rs232c_case.commParams;
239: cp->accessDetail.designator = directConn;
240: cp->accessDetail.directConn_case.duplex = fullduplex;
241: cp->accessDetail.directConn_case.lineType = asynchronous;
242: cp->accessDetail.directConn_case.lineSpeed = bps300;
243: tobjs[0].rs232c_case.preemptOthers = preemptInactive;
244: tobjs[0].rs232c_case.preemptMe = preemptInactive;
245: tobjs[0].rs232c_case.phoneNumber = "";
246: tobjs[0].rs232c_case.line.designator = reserveNeeded;
247: tobjs[0].rs232c_case.line.reserveNeeded_case.lineNumber = 1;
248: */
249: tobjs[0].designator = service;
250: tobjs[0].service_case.id = servicetype; /* 1 == SA */
251:
252: tobjs[1].designator = teletype;
253: tobjlist.length = 2;
254: tobjlist.sequence = tobjs;
255: MakeSimpleCredsAndVerifier(0, 0, &creds, &verifier);
256: DURING
257: (void) GAP3_Create(cconn, NULL, pobj, tobjlist, 0, creds, verifier);
258: HANDLER {
259: char *msg;
260: switch (Exception.Code) {
261: case GAP3_mediumConnectFailed:
262: msg = "medium connect failed";
263: break;
264: case GAP3_illegalTransport:
265: msg = "illegal transport type";
266: break;
267: case GAP3_tooManyGateStreams:
268: case GAP3_serviceTooBusy:
269: msg = "insufficient resources";
270: break;
271: case GAP3_serviceNotFound:
272: msg = "service type not found";
273: break;
274: case GAP3_userNotAuthenticated:
275: case GAP3_userNotAuthorized:
276: msg = "authentication problem";
277: break;
278: case REJECT_ERROR:
279: switch (CourierErrArgs(rejectionDetails,designator)){
280: case noSuchProgramNumber:
281: msg = "server does not support GAP";
282: break;
283: case noSuchVersionNumber:
284: msg = "server does not support our GAP version";
285: break;
286: default:
287: msg = "connection rejected";
288: }
289: break;
290: case PROTOCOL_VIOLATION:
291: msg = "protocol violation by remote server";
292: break;
293: default:
294: msg = "some random error";
295: break;
296: }
297: fprintf(stderr,"Error creating connection, %s\n",
298: msg);
299: return(-1);
300: } END_HANDLER;
301: return(0);
302: }
303:
304: /*
305: * Print status about the connection.
306: */
307: /*VARARGS*/
308: status()
309: {
310: if (connected)
311: printf("Connected to %s.\n", hostname);
312: else
313: printf("No connection.\n");
314: printf("Escape character is '%s'.\n", control(escape));
315: fflush(stdout);
316: }
317:
318: makeargv()
319: {
320: register char *cp;
321: register char **argp = margv;
322:
323: margc = 0;
324: for (cp = line; *cp;) {
325: while (isspace(*cp))
326: cp++;
327: if (*cp == '\0')
328: break;
329: *argp++ = cp;
330: margc += 1;
331: while (*cp != '\0' && !isspace(*cp))
332: cp++;
333: if (*cp == '\0')
334: break;
335: *cp++ = '\0';
336: }
337: *argp++ = 0;
338: }
339:
340: /*VARARGS*/
341: suspend()
342: {
343: register int save;
344:
345: save = mode(0);
346: kill(0, SIGTSTP);
347: /* reget parameters in case they were changed */
348: ioctl(0, TIOCGETP, (char *)&ottyb);
349: ioctl(0, TIOCGETC, (char *)&otc);
350: ioctl(0, TIOCGLTC, (char *)&oltc);
351: (void) mode(save);
352: }
353:
354: /*VARARGS*/
355: bye()
356: {
357: register char *op;
358:
359: (void) mode(0);
360: if (connected) {
361: sendoobdata(GAPCTLcleanup);
362: setsockopt(net, NSPROTO_SPP, SO_HEADERS_ON_OUTPUT, &on,
363: sizeof(on));
364: sppclose(net);
365: printf("Connection closed.\n");
366: connected = 0;
367: }
368: }
369:
370: /*VARARGS*/
371: quit()
372: {
373: call(bye, "bye", 0);
374: exit(0);
375: }
376:
377: /*
378: * Toggle debugging
379: */
380: setdebug(argc, argv)
381: {
382: debug = ~debug;
383: }
384:
385: /*
386: * Toggle logging
387: */
388: setlog(argc, argv)
389: int argc;
390: char *argv[];
391: {
392: if (argc > 2)
393: printf("Syntax: %s [filename]\n",argv[0]);
394: else if (logfile != (FILE*) 0) {
395: /* currently logging */
396: fclose(logfile);
397: printf("Log file closed\n");
398: logfile = (FILE*) 0;
399: if (argc == 2 && (logfile = fopen(argv[1],"a")) != (FILE*)0)
400: printf("Logging to %s\n",argv[1]);
401: } else {
402: /* not currently logging */
403: if (argc == 1)
404: printf("Logging already disabled\n");
405: else if (argc == 2 &&
406: (logfile = fopen(argv[1],"a")) != (FILE*)0 )
407: printf("Logging to %s\n",argv[1]);
408: }
409: }
410:
411: /*
412: * Help command.
413: */
414: help(argc, argv)
415: int argc;
416: char *argv[];
417: {
418: register struct cmd *c;
419:
420: if (argc == 1) {
421: printf("Commands may be abbreviated. Commands are:\n\n");
422: for (c = cmdtab; c->name; c++)
423: printf("%-*s\t%s\n", HELPINDENT, c->name, c->help);
424: return;
425: }
426: while (--argc > 0) {
427: register char *arg;
428: arg = *++argv;
429: c = getcmd(arg);
430: if (c == (struct cmd *)-1)
431: printf("?Ambiguous help command %s\n", arg);
432: else if (c == (struct cmd *)0)
433: printf("?Invalid help command %s\n", arg);
434: else
435: printf("%s\n", c->help);
436: }
437: }
438:
439: /*
440: * Call routine with argc, argv set from args (terminated by 0).
441: * VARARGS2
442: */
443: call(routine, args)
444: int (*routine)();
445: int args;
446: {
447: register int *argp;
448: register int argc;
449:
450: for (argc = 0, argp = &args; *argp++ != 0; argc++)
451: ;
452: (*routine)(argc, &args);
453: }
454:
455: struct tchars notc = { -1, -1, -1, -1, -1, -1 };
456: struct ltchars noltc = { -1, -1, -1, -1, -1, -1 };
457:
458: mode(f)
459: register int f;
460: {
461: static int prevmode = 0;
462: struct tchars *tc;
463: struct ltchars *ltc;
464: struct sgttyb sb;
465: int onoff, old;
466:
467: if (prevmode == f)
468: return (f);
469: old = prevmode;
470: prevmode = f;
471: sb = ottyb;
472: switch (f) {
473:
474: case 0:
475: onoff = 0;
476: tc = &otc;
477: ltc = &oltc;
478: break;
479:
480: case 1:
481: case 2:
482: sb.sg_flags |= CBREAK;
483: if (f == 1)
484: sb.sg_flags &= ~(ECHO|CRMOD);
485: else
486: sb.sg_flags |= ECHO|CRMOD;
487: sb.sg_erase = sb.sg_kill = -1;
488: tc = ¬c;
489: ltc = &noltc;
490: onoff = 1;
491: break;
492:
493: default:
494: return;
495: }
496: ioctl(fileno(stdin), TIOCSLTC, (char *)ltc);
497: ioctl(fileno(stdin), TIOCSETC, (char *)tc);
498: ioctl(fileno(stdin), TIOCSETP, (char *)&sb);
499: ioctl(fileno(stdin), FIONBIO, &onoff);
500: ioctl(fileno(stdout), FIONBIO, &onoff);
501: return (old);
502: }
503:
504: struct {struct sphdr hdr;
505: char data[BUFSIZ];
506: } sibuf;
507: char *sbp;
508: char tibuf[BUFSIZ], *tbp;
509: int scc, tcc;
510:
511: /*
512: * Select from tty and network...
513: */
514: telnet(s)
515: int s;
516: {
517: register int c;
518: int tin = fileno(stdin), tout = fileno(stdout);
519: int on = 1;
520: int ibits, obits;
521:
522: (void) mode(1);
523: ioctl(s, FIONBIO, &on);
524: changeSPPopts(net, GAPCTLnone, 1); /* datastream "normal", eom */
525: for (;;) {
526: ibits = obits = 0;
527: if (nfrontp - nbackp)
528: obits |= (1 << s);
529: else
530: ibits |= (1 << tin);
531: if (tfrontp - tbackp)
532: obits |= (1 << tout);
533: else
534: ibits |= (1 << s);
535: if (scc < 0 && tcc < 0)
536: break;
537: select(16, &ibits, &obits, 0, 0);
538: if (ibits == 0 && obits == 0) {
539: sleep(5);
540: continue;
541: }
542:
543: /*
544: * Something to read from the network...
545: */
546: if (ibits & (1 << s)) {
547: scc = read(s, &sibuf, sizeof (sibuf))
548: - sizeof(struct sphdr);
549: #ifdef DEBUG
550: if (debug)
551: printf("reading %d bytes from net\n", scc);
552: #endif
553: if (scc < 0 && errno == EWOULDBLOCK)
554: scc = 0;
555: else if (scc < 0)
556: break; /* protocol violation? */
557: else if (sibuf.hdr.sp_cc & SP_OB) {
558: /* status or OOB control */
559: switch ((u_char) *sibuf.data) {
560: case GAPCTLareYouThere:
561: sendoobdata(GAPCTLiAmHere);
562: break;
563: case GAPCTLmediumDown:
564: (void) mode(0);
565: longjmp(peerdied, -1);
566: /*NOTREACHED*/
567: default:
568: /* ignore others */
569: break;
570: }
571: scc = 0;
572: }
573: else if (sibuf.hdr.sp_dt == GAPCTLnone) {
574: /* normal case */
575: sbp = sibuf.data;
576: }
577: else if(sibuf.hdr.sp_dt == GAPCTLcleanup){
578: sendoobdata(GAPCTLcleanup);
579: /* should get an END next */
580: scc = 0;
581: }
582: else if(sibuf.hdr.sp_dt == SPPSST_END) {
583: setsockopt(net, NSPROTO_SPP,
584: SO_HEADERS_ON_OUTPUT,
585: &on, sizeof(on));
586: sppclosereply(net);
587: (void) mode(0);
588: longjmp(peerdied, -1);
589: /*NOTREACHED*/
590: }
591: }
592:
593: /*
594: * Something to read from the tty...
595: */
596: if (ibits & (1 << tin)) {
597: tcc = read(tin, tibuf, sizeof (tibuf));
598: if (tcc < 0 && errno == EWOULDBLOCK)
599: tcc = 0;
600: else {
601: if (tcc <= 0)
602: break;
603: tbp = tibuf;
604: }
605: }
606:
607: while (tcc > 0) {
608: register int c;
609:
610: if ((&netobuf[BUFSIZ] - nfrontp) < 2)
611: break;
612: c = *tbp++ & 0377, tcc--;
613: if (strip(c) == escape) {
614: command(0);
615: tcc = 0;
616: break;
617: }
618: switch (c) {
619: case '\n':
620: /* *nfrontp++ = '\r'; */
621: *nfrontp++ = '\n';
622: break;
623: case '\r':
624: *nfrontp++ = '\r';
625: /* *nfrontp++ = '\n'; */
626: break;
627: default:
628: *nfrontp++ = c;
629: break;
630: }
631: }
632: if ((obits & (1 << s)) && (nfrontp - nbackp) > 0)
633: netflush(s);
634: while (scc > 0) {
635: register int c;
636: c = *sbp++&0377; scc--;
637: *tfrontp++ = c;
638: }
639: if ((obits & (1 << tout)) && (tfrontp - tbackp) > 0)
640: ttyflush(tout);
641: }
642: (void) mode(0);
643: }
644:
645: command(top)
646: int top;
647: {
648: register struct cmd *c;
649: int oldmode, wasopen;
650:
651: oldmode = mode(0);
652: if (!top)
653: putchar('\n');
654: else
655: signal(SIGINT, SIG_DFL);
656: for (;;) {
657: printf("%s> ", prompt);
658: if (gets(line) == 0) {
659: if (feof(stdin)) {
660: clearerr(stdin);
661: putchar('\n');
662: }
663: break;
664: }
665: if (line[0] == 0)
666: break;
667: makeargv();
668: c = getcmd(margv[0]);
669: if (c == (struct cmd *)-1) {
670: printf("?Ambiguous command\n");
671: continue;
672: }
673: if (c == 0) {
674: printf("?Invalid command\n");
675: continue;
676: }
677: (*c->handler)(margc, margv);
678: if (c->handler != help)
679: break;
680: }
681: if (!top) {
682: if (!connected)
683: longjmp(toplevel, 1);
684: (void) mode(oldmode);
685: }
686: }
687:
688: /*
689: * Set the escape character.
690: */
691: setescape(argc, argv)
692: int argc;
693: char *argv[];
694: {
695: register char *arg;
696: char buf[50];
697:
698: if (argc > 2)
699: arg = argv[1];
700: else {
701: printf("new escape character: ");
702: gets(buf);
703: arg = buf;
704: }
705: if (arg[0] != '\0')
706: escape = arg[0];
707: printf("Escape character is '%s'.\n", control(escape));
708: fflush(stdout);
709: }
710:
711: /*
712: * Construct a control character sequence
713: * for a special character.
714: */
715: char *
716: control(c)
717: register int c;
718: {
719: static char buf[3];
720:
721: if (c == 0177)
722: return ("^?");
723: if (c >= 040) {
724: buf[0] = c;
725: buf[1] = 0;
726: } else {
727: buf[0] = '^';
728: buf[1] = '@'+c;
729: buf[2] = 0;
730: }
731: return (buf);
732: }
733:
734: struct cmd *
735: getcmd(name)
736: register char *name;
737: {
738: register char *p, *q;
739: register struct cmd *c, *found;
740: register int nmatches, longest;
741:
742: longest = 0;
743: nmatches = 0;
744: found = 0;
745: for (c = cmdtab; p = c->name; c++) {
746: for (q = name; *q == *p++; q++)
747: if (*q == 0) /* exact match? */
748: return (c);
749: if (!*q) { /* the name was a prefix */
750: if (q - name > longest) {
751: longest = q - name;
752: nmatches = 1;
753: found = c;
754: } else if (q - name == longest)
755: nmatches++;
756: }
757: }
758: if (nmatches > 1)
759: return ((struct cmd *)-1);
760: return (found);
761: }
762:
763: deadpeer()
764: {
765: (void) mode(0);
766: longjmp(peerdied, -1);
767: }
768:
769: intr()
770: {
771: (void) mode(0);
772: longjmp(toplevel, -1);
773: }
774:
775: ttyflush(fd)
776: {
777: register int n;
778:
779: if ((n = tfrontp - tbackp) > 0) {
780: if (logfile != (FILE*)0)
781: fwrite(tbackp, 1, n, logfile);
782: n = write(fd, tbackp, n);
783: }
784: if (n < 0)
785: return;
786: tbackp += n;
787: if (tbackp == tfrontp)
788: tbackp = tfrontp = ttyobuf;
789: }
790:
791: netflush(fd)
792: {
793: int n;
794:
795: if ((n = nfrontp - nbackp) > 0)
796: n = write(fd, nbackp, n);
797: #ifdef DEBUG
798: if (debug)
799: printf("writing %d of %d bytes to net\n", n, nfrontp-nbackp);
800: #endif
801: if (n < 0) {
802: if (errno != ENOBUFS && errno != EWOULDBLOCK) {
803: (void) mode(0);
804: perror(hostname);
805: close(fd);
806: longjmp(peerdied, -1);
807: /*NOTREACHED*/
808: }
809: n = 0;
810: }
811: nbackp += n;
812: if (nbackp == nfrontp)
813: nbackp = nfrontp = netobuf;
814: }
815:
816: /*
817: * Send out of band data to other end of network
818: */
819: sendoobdata(value)
820: char value;
821: {
822: send(net, &value, 1, MSG_OOB);
823: }
824:
825: changeSPPopts(s, stream, eom)
826: int s; /* SPP socket */
827: u_char stream; /* datastream type */
828: char eom; /* Boolean EOM */
829: {
830: struct sphdr sphdr;
831: int off = 0;
832:
833: sphdr.sp_dt = stream;
834: sphdr.sp_cc = (eom ? SP_EM : 0);
835: setsockopt(s, NSPROTO_SPP, SO_HEADERS_ON_OUTPUT, &off, sizeof(off));
836: setsockopt(s, NSPROTO_SPP, SO_DEFAULT_HEADERS, &sphdr, sizeof(sphdr));
837: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.