|
|
1.1 root 1: /*
2: * Copyright (c) 1985 Regents of the University of California.
3: * All rights reserved.
4: *
5: * Redistribution and use in source and binary forms are permitted
6: * provided that this notice is preserved and that due credit is given
7: * to the University of California at Berkeley. The name of the University
8: * may not be used to endorse or promote products derived from this
9: * software without specific prior written permission. This software
10: * is provided ``as is'' without express or implied warranty.
11: */
12:
13: #ifndef lint
14: static char sccsid[] = "@(#)list.c 5.10 (Berkeley) 2/17/88";
15: #endif /* not lint */
16:
17: /*
18: *******************************************************************************
19: *
20: * list.c --
21: *
22: * Routines to obtain info from name and finger servers.
23: *
24: * Adapted from 4.3BSD BIND ns_init.c and from /usr/src/ucb/finger.c
25: *
26: *******************************************************************************
27: */
28:
29: #include <sys/types.h>
30: #include <sys/socket.h>
31: #include <netinet/in.h>
32: #include <netdb.h>
33: #include <stdio.h>
34: #include <strings.h>
35: #include <ctype.h>
36: #include <arpa/nameser.h>
37: #include <resolv.h>
38: #include "res.h"
39:
40: /*
41: * Imported from res_debug.c
42: */
43: extern char *_res_resultcodes[];
44:
45: typedef union {
46: HEADER qb1;
47: char qb2[PACKETSZ];
48: } querybuf;
49:
50: extern u_long inet_addr();
51: extern HostInfo *defaultPtr;
52: extern HostInfo curHostInfo;
53: extern int curHostValid;
54:
55: /*
56: * During a listing to a file, hash marks are printed
57: * every HASH_SIZE records.
58: */
59:
60: #define HASH_SIZE 50
61:
62:
63: /*
64: *******************************************************************************
65: *
66: * ListHosts --
67: *
68: * Requests the name server to do a zone transfer so we
69: * find out what hosts it knows about.
70: *
71: * There are five types of output:
72: * - internet addresses (default)
73: * - cpu type and operating system (-h option)
74: * - canonical and alias names (-a option)
75: * - well-known service names (-s option)
76: * - ALL records (-d option)
77: *
78: * To see all three types of information in sorted order,
79: * do the following:
80: * ls domain.edu > file
81: * ls -a domain.edu >> file
82: * ls -h domain.edu >> file
83: * ls -s domain.edu >> file
84: * view file
85: *
86: * Results:
87: * SUCCESS the listing was successful.
88: * ERROR the server could not be contacted because
89: * a socket could not be obtained or an error
90: * occured while receiving, or the output file
91: * could not be opened.
92: *
93: *******************************************************************************
94: */
95:
96: int
97: ListHosts(string, putToFile)
98: char *string;
99: int putToFile;
100: {
101: querybuf buf;
102: struct sockaddr_in sin;
103: HEADER *headerPtr;
104: int queryType;
105: int msglen;
106: int amtToRead;
107: int numRead;
108: int i;
109: int numAnswers = 0;
110: int result;
111: int soacnt = 0;
112: u_short len;
113: char *cp, *nmp;
114: char name[NAME_LEN];
115: char dname[2][NAME_LEN];
116: char option[NAME_LEN];
117: char file[NAME_LEN];
118: char *namePtr;
119: enum {
120: NO_ERRORS,
121: ERR_READING_LEN,
122: ERR_READING_MSG,
123: ERR_PRINTING,
124: } error = NO_ERRORS;
125:
126:
127: /*
128: *
129: /*
130: * Parse the command line. It maybe of the form "ls domain",
131: * "ls -a domain" or "ls -h domain".
132: */
133: i = sscanf(string, " ls %s %s", option, name);
134: if (putToFile && i == 2 && name[0] == '>') {
135: i--;
136: }
137: if (i == 2) {
138: if (strcmp("-a", option) == 0) {
139: queryType = T_CNAME;
140: } else if (strcmp("-h", option) == 0) {
141: queryType = T_HINFO;
142: } else if (strcmp("-m", option) == 0) {
143: queryType = T_MX;
144: } else if (strcmp("-s", option) == 0) {
145: queryType = T_WKS;
146: } else if (strcmp("-d", option) == 0) {
147: queryType = T_ANY;
148: } else {
149: queryType = T_A;
150: }
151: namePtr = name;
152: } else if (i == 1) {
153: namePtr = option;
154: queryType = T_A;
155: } else {
156: fprintf(stderr, "ListHosts: invalid request %s\n",string);
157: return(ERROR);
158: }
159:
160:
161: /*
162: * Create a query packet for the requested domain name.
163: *
164: */
165: msglen = res_mkquery(QUERY, namePtr, C_IN, T_AXFR,
166: (char *)0, 0, (char *)0,
167: (char *) &buf, sizeof(buf));
168: if (msglen < 0) {
169: if (_res.options & RES_DEBUG) {
170: fprintf(stderr, "ListHosts: Res_mkquery failed\n");
171: }
172: return (ERROR);
173: }
174:
175: bzero((char *)&sin, sizeof(sin));
176: sin.sin_family = AF_INET;
177: sin.sin_port = htons(NAMESERVER_PORT);
178:
179: /*
180: * Check to see if we have the address of the server or the
181: * address of a server who knows about this domain.
182: *
183: * For now, just use the first address in the list.
184: */
185:
186: if (defaultPtr->addrList != NULL) {
187: sin.sin_addr = *(struct in_addr *) defaultPtr->addrList[0];
188: } else {
189: sin.sin_addr = *(struct in_addr *)defaultPtr->servers[0]->addrList[0];
190: }
191:
192: /*
193: * Set up a virtual circuit to the server.
194: */
195: if ((sockFD = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
196: perror("ListHosts");
197: return(ERROR);
198: }
199: if (connect(sockFD, &sin, sizeof(sin)) < 0) {
200: perror("ListHosts");
201: (void) close(sockFD);
202: sockFD = -1;
203: return(ERROR);
204: }
205:
206: /*
207: * Send length & message for zone transfer
208: */
209:
210: len = htons(msglen);
211:
212: if (write(sockFD, (char *)&len, sizeof(len)) != sizeof(len) ||
213: write(sockFD, (char *) &buf, msglen) != msglen) {
214: perror("ListHosts");
215: (void) close(sockFD);
216: sockFD = -1;
217: return(ERROR);
218: }
219:
220: fprintf(stdout,"[%s]\n",
221: (defaultPtr->addrList != NULL) ? defaultPtr->name :
222: defaultPtr->servers[0]->name);
223:
224: if (!putToFile) {
225: filePtr = stdout;
226: } else {
227: filePtr = OpenFile(string, file);
228: if (filePtr == NULL) {
229: fprintf(stderr, "*** Can't open %s for writing\n", file);
230: (void) close(sockFD);
231: sockFD = -1;
232: return(ERROR);
233: }
234: fprintf(filePtr, "> %s\n", string);
235: fprintf(filePtr,"[%s]\n",
236: (defaultPtr->addrList != NULL) ? defaultPtr->name :
237: defaultPtr->servers[0]->name);
238: }
239:
240: fprintf(filePtr, "%-30s", "Host or domain name");
241: switch(queryType) {
242: case T_ANY:
243: fprintf(filePtr, " %-30s\n", "Resource record info");
244: break;
245: case T_A:
246: fprintf(filePtr, " %-30s\n", "Internet address");
247: break;
248: case T_HINFO:
249: fprintf(filePtr, " %-10s %s\n", "CPU", "OS");
250: break;
251: case T_CNAME:
252: fprintf(filePtr, " %-30s\n", "Alias");
253: break;
254: case T_MX:
255: fprintf(filePtr, " %3s %s\n", "Metric", "Host");
256: break;
257: case T_WKS:
258: fprintf(filePtr, " %-4s %s\n", "Protocol", "Services");
259: }
260:
261:
262: while (1) {
263:
264: /*
265: * Read the length of the response.
266: */
267:
268: cp = (char *) &buf;
269: amtToRead = sizeof(u_short);
270: while(amtToRead > 0 && (numRead = read(sockFD, cp, amtToRead)) > 0){
271: cp += numRead;
272: amtToRead -= numRead;
273: }
274: if (numRead <= 0) {
275: error = ERR_READING_LEN;
276: break;
277: }
278:
279: if ((len = htons(*(u_short *)&buf)) == 0) {
280: break; /* nothing left to read */
281: }
282:
283: /*
284: * Read the response.
285: */
286:
287: amtToRead = len;
288: cp = (char *) &buf;
289: while(amtToRead > 0 && (numRead = read(sockFD, cp, amtToRead)) > 0){
290: cp += numRead;
291: amtToRead -= numRead;
292: }
293: if (numRead <= 0) {
294: error = ERR_READING_MSG;
295: break;
296: }
297:
298: result = PrintListInfo(filePtr, (char *) &buf, cp, queryType);
299: if (result != SUCCESS) {
300: error = ERR_PRINTING;
301: break;
302: }
303:
304: numAnswers++;
305: if (putToFile && ((numAnswers % HASH_SIZE) == 0)) {
306: fprintf(stdout, "#");
307: fflush(stdout);
308: }
309: cp = buf.qb2 + sizeof(HEADER);
310: if (ntohs(buf.qb1.qdcount) > 0)
311: cp += dn_skipname(cp, buf.qb2 + len) + QFIXEDSZ;
312: nmp = cp;
313: cp += dn_skipname(cp, (u_char *)&buf + len);
314: if ((_getshort(cp) == T_SOA)) {
315: dn_expand(buf.qb2, buf.qb2 + len, nmp, dname[soacnt],
316: sizeof(dname[0]));
317: if (soacnt) {
318: if (strcmp(dname[0], dname[1]) == 0)
319: break;
320: } else
321: soacnt++;
322: }
323: }
324:
325: if (putToFile) {
326: fprintf(stdout, "%sReceived %d record%s.\n",
327: (numAnswers >= HASH_SIZE) ? "\n" : "",
328: numAnswers,
329: (numAnswers > 1) ? "s" : "");
330: }
331:
332: (void) close(sockFD);
333: sockFD = -1;
334: if (putToFile) {
335: fclose(filePtr);
336: filePtr = NULL;
337: }
338:
339: switch (error) {
340: case NO_ERRORS:
341: return (SUCCESS);
342:
343: case ERR_READING_LEN:
344: return(ERROR);
345:
346: case ERR_PRINTING:
347: fprintf(stderr,"*** Error during listing of %s: %s\n",
348: namePtr, DecodeError(result));
349: return(result);
350:
351: case ERR_READING_MSG:
352: headerPtr = (HEADER *) &buf;
353: fprintf(stderr,"ListHosts: error receiving zone transfer:\n");
354: fprintf(stderr,
355: " result: %s, answers = %d, authority = %d, additional = %d\n",
356: _res_resultcodes[headerPtr->rcode],
357: ntohs(headerPtr->ancount), ntohs(headerPtr->nscount),
358: ntohs(headerPtr->arcount));
359: return(ERROR);
360: default:
361: return(ERROR);
362: }
363: }
364:
365:
366: /*
367: *******************************************************************************
368: *
369: * PrintListInfo --
370: *
371: * Used by the ListInfo routine to print the answer
372: * received from the name server. Only the desired
373: * information is printed.
374: *
375: * Results:
376: * SUCCESS the answer was printed without a problem.
377: * NO_INFO the answer packet did not contain an answer.
378: * ERROR the answer was malformed.
379: * Misc. errors returned in the packet header.
380: *
381: *******************************************************************************
382: */
383:
384: #define NAME_FORMAT " %-30s"
385: #define STRIP_DOMAIN(string) if((dot = index(string, '.')) != NULL) *dot = '\0'
386:
387:
388: PrintListInfo(file, msg, eom, queryType)
389: FILE *file;
390: char *msg, *eom;
391: int queryType;
392: {
393: register char *cp;
394: HEADER *headerPtr;
395: int type, class, dlen, nameLen;
396: u_long ttl;
397: int n;
398: struct in_addr inaddr;
399: char name[NAME_LEN];
400: char name2[NAME_LEN];
401: char *dot;
402:
403: /*
404: * Read the header fields.
405: */
406: headerPtr = (HEADER *)msg;
407: cp = msg + sizeof(HEADER);
408: if (headerPtr->rcode != NOERROR) {
409: return(headerPtr->rcode);
410: }
411:
412: /*
413: * We are looking for info from answer resource records.
414: * If there aren't any, return with an error. We assume
415: * there aren't any question records.
416: */
417:
418: if (ntohs(headerPtr->ancount) == 0) {
419: return(NO_INFO);
420: } else {
421: if (ntohs(headerPtr->qdcount) > 0) {
422: nameLen = dn_skipname(cp, eom);
423: if (nameLen < 0)
424: return (ERROR);
425: cp += nameLen + QFIXEDSZ;
426: }
427: if ((nameLen = dn_expand(msg, eom, cp, name, sizeof(name))) < 0) {
428: return (ERROR);
429: }
430: cp += nameLen;
431: type = _getshort(cp);
432: cp += sizeof(u_short);
433: class = _getshort(cp);
434: cp += sizeof(u_short);
435: ttl = _getlong(cp);
436: cp += sizeof(u_long);
437: dlen = _getshort(cp);
438: cp += sizeof(u_short);
439: if (name[0] == 0)
440: strcpy(name, "(root)");
441:
442: /*
443: * QueryType is used to specify the type of desired information.
444: * T_A - internet address
445: * T_CNAME - aliases
446: * T_HINFO - cpu, OS type
447: * T_MX - mail routing
448: * T_WKS - well known service
449: * T_ANY - any
450: *
451: */
452: switch (type) {
453:
454: case T_A:
455: if (queryType != T_A && queryType != T_ANY)
456: break;
457:
458: if ((_res.options & RES_DEBUG) == 0)
459: STRIP_DOMAIN(name);
460: fprintf(file, NAME_FORMAT, name);
461: if (queryType == T_ANY)
462: fprintf(file," %-5s", p_type(type));
463: if (class == C_IN) {
464: bcopy(cp, (char *)&inaddr, sizeof(inaddr));
465: if (dlen == 4) {
466: fprintf(file," %s", inet_ntoa(inaddr));
467: } else if (dlen == 7) {
468: fprintf(file," %s", inet_ntoa(inaddr));
469: fprintf(file," (%d, %d)", cp[4],(cp[5] << 8) + cp[6]);
470: } else
471: fprintf(file, " (dlen = %d?)", dlen);
472: if (_res.options & RES_DEBUG)
473: fprintf(file,"\t\t\t%lu", ttl);
474: fprintf(file,"\n");
475: } else
476: goto other;
477: break;
478:
479: case T_CNAME:
480: if (queryType != T_CNAME && queryType != T_ANY)
481: break;
482:
483: if ((_res.options & RES_DEBUG) == 0)
484: STRIP_DOMAIN(name);
485: fprintf(file, NAME_FORMAT, name);
486: if (queryType == T_ANY)
487: fprintf(file," %-5s", p_type(type));
488: if ((nameLen = dn_expand(msg, eom, cp, name2, sizeof(name2))) < 0) {
489: fprintf(file, " ***\n");
490: return (ERROR);
491: }
492: /*
493: * a bug -- cnames need not be in same domain!
494: * STRIP_DOMAIN(name2);
495: */
496:
497: fprintf(file, NAME_FORMAT, name2);
498: if (_res.options & RES_DEBUG)
499: fprintf(file,"\t%lu", ttl);
500: fprintf(file,"\n");
501: break;
502:
503: case T_HINFO:
504: if (queryType != T_HINFO && queryType != T_ANY)
505: break;
506:
507: if ((_res.options & RES_DEBUG) == 0)
508: STRIP_DOMAIN(name);
509: fprintf(file, NAME_FORMAT, name);
510: if (queryType == T_ANY)
511: fprintf(file," %-5s", p_type(type));
512: if (n = *cp++) {
513: (void)sprintf(name,"%.*s", n, cp);
514: fprintf(file," %-10s", name);
515: cp += n;
516: } else {
517: fprintf(file," %-10s", " ");
518: }
519: if (n = *cp++) {
520: fprintf(file," %.*s", n, cp);
521: cp += n;
522: }
523: if (_res.options & RES_DEBUG)
524: fprintf(file,"\t\t%lu", ttl);
525: fprintf(file,"\n");
526: break;
527:
528: case T_MX:
529: if (queryType != T_MX && queryType != T_ANY)
530: break;
531:
532: if ((_res.options & RES_DEBUG) == 0)
533: STRIP_DOMAIN(name);
534: fprintf(file, NAME_FORMAT, name);
535: if (queryType == T_ANY)
536: fprintf(file," %-5s", p_type(type));
537:
538: {
539: short pref;
540:
541: pref = _getshort(cp);
542: cp += sizeof(u_short);
543: fprintf(file," %-3d ",pref);
544: }
545: if ((nameLen = dn_expand(msg, eom, cp, name2, sizeof(name2))) < 0) {
546: fprintf(file, " ***\n");
547: return (ERROR);
548: }
549: fprintf(file, " %s", name2);
550: if (_res.options & RES_DEBUG)
551: fprintf(file,"\t%lu", ttl);
552: fprintf(file,"\n");
553:
554: break;
555:
556:
557: case T_NS:
558: case T_PTR:
559: if (queryType != T_A && queryType != T_ANY)
560: break;
561: /*
562: * Found a name server or pointer record.
563: */
564: if ((_res.options & RES_DEBUG) == 0)
565: STRIP_DOMAIN(name);
566: fprintf(file, NAME_FORMAT, name);
567: if (queryType == T_ANY)
568: fprintf(file," %-5s", p_type(type));
569: fprintf(file," %s = ", type == T_PTR ? "host" : "server");
570: cp = Print_cdname2(cp, msg, eom, file);
571: if (_res.options & RES_DEBUG)
572: fprintf(file,"\t%lu", ttl);
573: fprintf(file,"\n");
574: break;
575:
576: case T_WKS:
577: if (queryType != T_WKS && queryType != T_ANY)
578: break;
579:
580: if ((_res.options & RES_DEBUG) == 0)
581: STRIP_DOMAIN(name);
582: fprintf(file, NAME_FORMAT, name);
583: if (queryType == T_ANY)
584: fprintf(file," %-5s", p_type(type));
585: if (class == C_IN) {
586: cp += 4; dlen -= 4;
587: {
588: struct protoent *pp;
589: struct servent *ss;
590: u_short port;
591:
592: setprotoent(1);
593: setservent(1);
594: n = *cp & 0377;
595: pp = getprotobynumber(n);
596: if(pp == 0)
597: fprintf(file," %-3d ", n);
598: else
599: fprintf(file," %-3s ", pp->p_name);
600: cp++; dlen--;
601:
602: port = 0;
603: while(dlen-- > 0) {
604: n = *cp++;
605: do {
606: if(n & 0200) {
607: ss = getservbyport((int)htons(port), pp->p_name);
608: if(ss == 0)
609: fprintf(file," %d", port);
610: else
611: fprintf(file," %s", ss->s_name);
612: }
613: n <<= 1;
614: } while(++port & 07);
615: }
616: }
617: } else
618: goto other;
619: if (_res.options & RES_DEBUG)
620: fprintf(file,"\t%lu", ttl);
621: fprintf(file,"\n");
622: endprotoent();
623: endservent();
624: break;
625:
626: case T_SOA:
627: case T_AXFR:
628: if (queryType != T_ANY)
629: break;
630: fprintf(file, NAME_FORMAT, name);
631: if (queryType == T_ANY)
632: fprintf(file," %-5s", p_type(type));
633: if ((nameLen = dn_expand(msg, eom, cp, name2, sizeof(name2))) < 0) {
634: fprintf(file, " ***\n");
635: return (ERROR);
636: }
637: cp += nameLen;
638: fprintf(file, " %s", name2);
639: if ((nameLen = dn_expand(msg, eom, cp, name2, sizeof(name2))) < 0) {
640: fprintf(file, " ***\n");
641: return (ERROR);
642: }
643: cp += nameLen;
644: fprintf(file, " %s. (", name2);
645: for (n = 0; n < 5; n++) {
646: u_long u;
647:
648: u = _getlong(cp);
649: cp += sizeof(u_long);
650: fprintf(file,"%s%d", n? " " : "", u);
651: }
652: fprintf(file, ")", name2);
653: if (_res.options & RES_DEBUG)
654: fprintf(file,"\t%lu", ttl);
655: fprintf(file,"\n");
656: break;
657:
658: default:
659: /*
660: * Unwanted answer type -- ignore it.
661: */
662: if (queryType != T_ANY)
663: break;
664: if ((_res.options & RES_DEBUG) == 0)
665: STRIP_DOMAIN(name);
666: fprintf(file, NAME_FORMAT, name);
667: other:
668: fprintf(file," type = %-5s", p_type(type));
669: fprintf(file,", class = %-5s", p_class(class));
670: if (_res.options & RES_DEBUG)
671: fprintf(file,"\t%lu\n", ttl);
672: break;
673: }
674: }
675: return(SUCCESS);
676: }
677:
678:
679: /*
680: *******************************************************************************
681: *
682: * ViewList --
683: *
684: * A hack to view the output of the ls command in sorted
685: * order using more.
686: *
687: *******************************************************************************
688: */
689:
690: ViewList(string)
691: char *string;
692: {
693: char file[NAME_LEN];
694: char command[NAME_LEN];
695:
696: sscanf(string, " view %s", file);
697: (void)sprintf(command, "grep \"^ \" %s | sort | more", file);
698: system(command);
699: }
700:
701: /*
702: *******************************************************************************
703: *
704: * Finger --
705: *
706: * Connects with the finger server for the current host
707: * to request info on the specified person (long form)
708: * who is on the system (short form).
709: *
710: * Results:
711: * SUCCESS the finger server was contacted.
712: * ERROR the server could not be contacted because
713: * a socket could not be obtained or connected
714: * to or the service could not be found.
715: *
716: *******************************************************************************
717: */
718:
719: Finger(string, putToFile)
720: char *string;
721: int putToFile;
722: {
723: struct servent *sp;
724: struct sockaddr_in sin;
725: register FILE *f;
726: register int c;
727: register int lastc;
728: char name[NAME_LEN];
729: char file[NAME_LEN];
730:
731: /*
732: * We need a valid current host info to get an inet address.
733: */
734: if (!curHostValid) {
735: fprintf(stderr, "Finger: no current host defined.\n");
736: return (ERROR);
737: }
738:
739: if (sscanf(string, " finger %s", name) == 1) {
740: if (putToFile && (name[0] == '>')) {
741: name[0] = '\0';
742: }
743: } else {
744: name[0] = '\0';
745: }
746:
747: sp = getservbyname("finger", "tcp");
748: if (sp == 0) {
749: fprintf(stderr, "Finger: unknown service\n");
750: return (ERROR);
751: }
752:
753: bzero((char *)&sin, sizeof(sin));
754: sin.sin_family = curHostInfo.addrType;
755: sin.sin_port = sp->s_port;
756: bcopy(curHostInfo.addrList[0], (char *)&sin.sin_addr,
757: curHostInfo.addrLen);
758:
759: /*
760: * Set up a virtual circuit to the host.
761: */
762:
763: sockFD = socket(curHostInfo.addrType, SOCK_STREAM, 0);
764: if (sockFD < 0) {
765: fflush(stdout);
766: perror("Finger");
767: return (ERROR);
768: }
769:
770: if (connect(sockFD, (char *)&sin, sizeof (sin)) < 0) {
771: fflush(stdout);
772: perror("Finger");
773: close(sockFD);
774: sockFD = -1;
775: return (ERROR);
776: }
777:
778: if (!putToFile) {
779: filePtr = stdout;
780: } else {
781: filePtr = OpenFile(string, file);
782: if (filePtr == NULL) {
783: fprintf(stderr, "*** Can't open %s for writing\n", file);
784: close(sockFD);
785: sockFD = -1;
786: return(ERROR);
787: }
788: fprintf(filePtr,"> %s\n", string);
789: }
790: fprintf(filePtr, "[%s]\n", curHostInfo.name);
791:
792: if (name[0] != '\0') {
793: write(sockFD, "/W ", 3);
794: }
795: write(sockFD, name, strlen(name));
796: write(sockFD, "\r\n", 2);
797: f = fdopen(sockFD, "r");
798: while ((c = getc(f)) != EOF) {
799: switch(c) {
800: case 0210:
801: case 0211:
802: case 0212:
803: case 0214:
804: c -= 0200;
805: break;
806: case 0215:
807: c = '\n';
808: break;
809: }
810: putc(lastc = c, filePtr);
811: }
812: if (lastc != '\n') {
813: putc('\n', filePtr);
814: }
815: putc('\n', filePtr);
816:
817: close(sockFD);
818: sockFD = -1;
819:
820: if (putToFile) {
821: fclose(filePtr);
822: filePtr = NULL;
823: }
824: return (SUCCESS);
825: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.