|
|
1.1 root 1: /*
2: * Copyright (c) 1995 Danny Gasparovski.
3: *
4: * Please read the file COPYRIGHT for the
5: * terms and conditions of the copyright.
6: */
7:
8: #define WANT_SYS_IOCTL_H
9: #include <stdlib.h>
10: #include <slirp.h>
11: #include "ip_icmp.h"
12: #include "main.h"
13: #ifdef __sun__
14: #include <sys/filio.h>
15: #endif
16:
17: #ifdef _WIN32
18: #define IS_EAGAIN(e) ((e) == WSAEINTR || (e) == EAGAIN)
19: #else
20: #define IS_EAGAIN(e) ((e) == EAGAIN)
21: #endif
22:
23: void
24: so_init()
25: {
26: /* Nothing yet */
27: }
28:
29:
30: struct socket *
31: solookup(head, laddr, lport, faddr, fport)
32: struct socket *head;
33: struct in_addr laddr;
34: u_int lport;
35: struct in_addr faddr;
36: u_int fport;
37: {
38: struct socket *so;
39:
40: for (so = head->so_next; so != head; so = so->so_next) {
41: if (so->so_lport == lport &&
42: so->so_laddr.s_addr == laddr.s_addr &&
43: so->so_faddr.s_addr == faddr.s_addr &&
44: so->so_fport == fport)
45: break;
46: }
47:
48: if (so == head)
49: return (struct socket *)NULL;
50: return so;
51:
52: }
53:
54: /*
55: * Create a new socket, initialise the fields
56: * It is the responsibility of the caller to
57: * insque() it into the correct linked-list
58: */
59: struct socket *
60: socreate()
61: {
62: struct socket *so;
63:
64: so = (struct socket *)malloc(sizeof(struct socket));
65: if(so) {
66: memset(so, 0, sizeof(struct socket));
67: so->so_state = SS_NOFDREF;
68: so->s = -1;
69: }
70: return(so);
71: }
72:
73: /*
74: * remque and free a socket, clobber cache
75: */
76: void
77: sofree(so)
78: struct socket *so;
79: {
80: if (so->so_emu==EMU_RSH && so->extra) {
81: sofree(so->extra);
82: so->extra=NULL;
83: }
84: if (so == tcp_last_so)
85: tcp_last_so = &tcb;
86: else if (so == udp_last_so)
87: udp_last_so = &udb;
88:
89: m_free(so->so_m);
90:
91: if(so->so_next && so->so_prev)
92: remque(so); /* crashes if so is not in a queue */
93:
94: free(so);
95: }
96:
97: /*
98: * Read from so's socket into sb_snd, updating all relevant sbuf fields
99: * NOTE: This will only be called if it is select()ed for reading, so
100: * a read() of 0 (or less) means it's disconnected
101: */
102: int
103: soread(so)
104: struct socket *so;
105: {
106: int n, nn;
107: u_int lss, total;
108: struct sbuf *sb = &so->so_snd;
109: u_int len = sb->sb_datalen - sb->sb_cc;
110: struct iovec iov[2];
111: u_int mss = so->so_tcpcb->t_maxseg;
112:
113: DEBUG_CALL("soread");
114: DEBUG_ARG("so = %lx", (long )so);
115:
116: /*
117: * No need to check if there's enough room to read.
118: * soread wouldn't have been called if there weren't
119: */
120:
121: len = sb->sb_datalen - sb->sb_cc;
122:
123: iov[0].iov_base = sb->sb_wptr;
124: if (sb->sb_wptr < sb->sb_rptr) {
125: iov[0].iov_len = sb->sb_rptr - sb->sb_wptr;
126: /* Should never succeed, but... */
127: if (iov[0].iov_len > len)
128: iov[0].iov_len = len;
129: if (iov[0].iov_len > mss)
130: iov[0].iov_len -= iov[0].iov_len%mss;
131: n = 1;
132: } else {
133: iov[0].iov_len = (sb->sb_data + sb->sb_datalen) - sb->sb_wptr;
134: /* Should never succeed, but... */
135: if (iov[0].iov_len > len) iov[0].iov_len = len;
136: len -= iov[0].iov_len;
137: if (len) {
138: iov[1].iov_base = sb->sb_data;
139: iov[1].iov_len = sb->sb_rptr - sb->sb_data;
140: if(iov[1].iov_len > len)
141: iov[1].iov_len = len;
142: total = iov[0].iov_len + iov[1].iov_len;
143: if (total > mss) {
144: lss = total%mss;
145: if (iov[1].iov_len > lss) {
146: iov[1].iov_len -= lss;
147: n = 2;
148: } else {
149: lss -= iov[1].iov_len;
150: iov[0].iov_len -= lss;
151: n = 1;
152: }
153: } else
154: n = 2;
155: } else {
156: if (iov[0].iov_len > mss)
157: iov[0].iov_len -= iov[0].iov_len%mss;
158: n = 1;
159: }
160: }
161:
162: #ifdef HAVE_READV
163: nn = readv(so->s, (struct iovec *)iov, n);
164: DEBUG_MISC((dfd, " ... read nn = %d bytes\n", nn));
165: #else
166: nn = recv(so->s, iov[0].iov_base, iov[0].iov_len,0);
167: #endif
168: if (nn <= 0) {
169: int error = WSAGetLastError();
170: if (nn < 0 && IS_EAGAIN(error))
171: return 0;
172: else {
173: DEBUG_MISC((dfd, " --- soread() disconnected, nn = %d, errno = %d-%s\n", nn, errno,strerror(errno)));
174: sofcantrcvmore(so);
175: tcp_sockclosed(sototcpcb(so));
176: return -1;
177: }
178: }
179:
180: #ifndef HAVE_READV
181: /*
182: * If there was no error, try and read the second time round
183: * We read again if n = 2 (ie, there's another part of the buffer)
184: * and we read as much as we could in the first read
185: * We don't test for <= 0 this time, because there legitimately
186: * might not be any more data (since the socket is non-blocking),
187: * a close will be detected on next iteration.
188: * A return of -1 wont (shouldn't) happen, since it didn't happen above
189: */
190: if (n == 2 && nn == iov[0].iov_len) {
191: int ret;
192: ret = recv(so->s, iov[1].iov_base, iov[1].iov_len,0);
193: if (ret > 0)
194: nn += ret;
195: }
196:
197: DEBUG_MISC((dfd, " ... read nn = %d bytes\n", nn));
198: #endif
199:
200: /* Update fields */
201: sb->sb_cc += nn;
202: sb->sb_wptr += nn;
203: if (sb->sb_wptr >= (sb->sb_data + sb->sb_datalen))
204: sb->sb_wptr -= sb->sb_datalen;
205: return nn;
206: }
207:
208: /*
209: * Get urgent data
210: *
211: * When the socket is created, we set it SO_OOBINLINE,
212: * so when OOB data arrives, we soread() it and everything
213: * in the send buffer is sent as urgent data
214: */
215: void
216: sorecvoob(so)
217: struct socket *so;
218: {
219: struct tcpcb *tp = sototcpcb(so);
220:
221: DEBUG_CALL("sorecvoob");
222: DEBUG_ARG("so = %lx", (long)so);
223:
224: /*
225: * We take a guess at how much urgent data has arrived.
226: * In most situations, when urgent data arrives, the next
227: * read() should get all the urgent data. This guess will
228: * be wrong however if more data arrives just after the
229: * urgent data, or the read() doesn't return all the
230: * urgent data.
231: */
232: soread(so);
233: tp->snd_up = tp->snd_una + so->so_snd.sb_cc;
234: tp->t_force = 1;
235: tcp_output(tp);
236: tp->t_force = 0;
237: }
238:
239: /*
240: * Send urgent data
241: * There's a lot duplicated code here, but...
242: */
243: int
244: sosendoob(so)
245: struct socket *so;
246: {
247: struct sbuf *sb = &so->so_rcv;
248: char buff[2048]; /* XXX Shouldn't be sending more oob data than this */
249:
250: int n, len;
251:
252: DEBUG_CALL("sosendoob");
253: DEBUG_ARG("so = %lx", (long)so);
254: DEBUG_ARG("sb->sb_cc = %d", sb->sb_cc);
255:
256: if (so->so_urgc > 2048)
257: so->so_urgc = 2048; /* XXXX */
258:
259: if (sb->sb_rptr < sb->sb_wptr) {
260: /* We can send it directly */
261: n = send(so->s, sb->sb_rptr, so->so_urgc, (MSG_OOB)); /* |MSG_DONTWAIT)); */
262: so->so_urgc -= n;
263:
264: DEBUG_MISC((dfd, " --- sent %d bytes urgent data, %d urgent bytes left\n", n, so->so_urgc));
265: } else {
266: /*
267: * Since there's no sendv or sendtov like writev,
268: * we must copy all data to a linear buffer then
269: * send it all
270: */
271: len = (sb->sb_data + sb->sb_datalen) - sb->sb_rptr;
272: if (len > so->so_urgc) len = so->so_urgc;
273: memcpy(buff, sb->sb_rptr, len);
274: so->so_urgc -= len;
275: if (so->so_urgc) {
276: n = sb->sb_wptr - sb->sb_data;
277: if (n > so->so_urgc) n = so->so_urgc;
278: memcpy((buff + len), sb->sb_data, n);
279: so->so_urgc -= n;
280: len += n;
281: }
282: n = send(so->s, buff, len, (MSG_OOB)); /* |MSG_DONTWAIT)); */
283: #ifdef DEBUG
284: if (n != len)
285: DEBUG_ERROR((dfd, "Didn't send all data urgently XXXXX\n"));
286: #endif
287: DEBUG_MISC((dfd, " ---2 sent %d bytes urgent data, %d urgent bytes left\n", n, so->so_urgc));
288: }
289:
290: sb->sb_cc -= n;
291: sb->sb_rptr += n;
292: if (sb->sb_rptr >= (sb->sb_data + sb->sb_datalen))
293: sb->sb_rptr -= sb->sb_datalen;
294:
295: return n;
296: }
297:
298: /*
299: * Write data from so_rcv to so's socket,
300: * updating all sbuf field as necessary
301: */
302: int
303: sowrite(so)
304: struct socket *so;
305: {
306: int n,nn;
307: struct sbuf *sb = &so->so_rcv;
308: u_int len = sb->sb_cc;
309: struct iovec iov[2];
310:
311: DEBUG_CALL("sowrite");
312: DEBUG_ARG("so = %lx", (long)so);
313:
314: if (so->so_urgc) {
315: sosendoob(so);
316: if (sb->sb_cc == 0)
317: return 0;
318: }
319:
320: /*
321: * No need to check if there's something to write,
322: * sowrite wouldn't have been called otherwise
323: */
324:
325: len = sb->sb_cc;
326:
327: iov[0].iov_base = sb->sb_rptr;
328: if (sb->sb_rptr < sb->sb_wptr) {
329: iov[0].iov_len = sb->sb_wptr - sb->sb_rptr;
330: /* Should never succeed, but... */
331: if (iov[0].iov_len > len) iov[0].iov_len = len;
332: n = 1;
333: } else {
334: iov[0].iov_len = (sb->sb_data + sb->sb_datalen) - sb->sb_rptr;
335: if (iov[0].iov_len > len) iov[0].iov_len = len;
336: len -= iov[0].iov_len;
337: if (len) {
338: iov[1].iov_base = sb->sb_data;
339: iov[1].iov_len = sb->sb_wptr - sb->sb_data;
340: if (iov[1].iov_len > len) iov[1].iov_len = len;
341: n = 2;
342: } else
343: n = 1;
344: }
345: /* Check if there's urgent data to send, and if so, send it */
346:
347: #ifdef HAVE_READV
348: nn = writev(so->s, (const struct iovec *)iov, n);
349:
350: DEBUG_MISC((dfd, " ... wrote nn = %d bytes\n", nn));
351: #else
352: nn = send(so->s, iov[0].iov_base, iov[0].iov_len,0);
353: #endif
354: /* This should never happen, but people tell me it does *shrug* */
355: if (nn < 0) {
356: int error = WSAGetLastError();
357: if (IS_EAGAIN(error))
358: return 0;
359: }
360:
361: if (nn <= 0) {
362: DEBUG_MISC((dfd, " --- sowrite disconnected, so->so_state = %x, errno = %d\n",
363: so->so_state, errno));
364: sofcantsendmore(so);
365: tcp_sockclosed(sototcpcb(so));
366: return -1;
367: }
368:
369: #ifndef HAVE_READV
370: if (n == 2 && nn == iov[0].iov_len) {
371: int ret;
372: ret = send(so->s, iov[1].iov_base, iov[1].iov_len,0);
373: if (ret > 0)
374: nn += ret;
375: }
376: DEBUG_MISC((dfd, " ... wrote nn = %d bytes\n", nn));
377: #endif
378:
379: /* Update sbuf */
380: sb->sb_cc -= nn;
381: sb->sb_rptr += nn;
382: if (sb->sb_rptr >= (sb->sb_data + sb->sb_datalen))
383: sb->sb_rptr -= sb->sb_datalen;
384:
385: /*
386: * If in DRAIN mode, and there's no more data, set
387: * it CANTSENDMORE
388: */
389: if ((so->so_state & SS_FWDRAIN) && sb->sb_cc == 0)
390: sofcantsendmore(so);
391:
392: return nn;
393: }
394:
395: /*
396: * recvfrom() a UDP socket
397: */
398: void
399: sorecvfrom(so)
400: struct socket *so;
401: {
402: struct sockaddr_in addr;
403: socklen_t addrlen = sizeof(struct sockaddr_in);
404:
405: DEBUG_CALL("sorecvfrom");
406: DEBUG_ARG("so = %lx", (long)so);
407:
408: if (so->so_type == IPPROTO_ICMP) { /* This is a "ping" reply */
409: char buff[256];
410: int len;
411:
412: len = recvfrom(so->s, buff, 256, 0,
413: (struct sockaddr *)&addr, &addrlen);
414: /* XXX Check if reply is "correct"? */
415:
416: if(len == -1 || len == 0) {
417: u_char code=ICMP_UNREACH_PORT;
418:
419: int error = WSAGetLastError();
420: if(error == WSAEHOSTUNREACH) code=ICMP_UNREACH_HOST;
421: else if(error == WSAENETUNREACH) code=ICMP_UNREACH_NET;
422:
423: DEBUG_MISC((dfd," udp icmp rx errno = %d-%s\n",
424: errno,strerror(errno)));
425: icmp_error(so->so_m, ICMP_UNREACH,code, 0,strerror(errno));
426: } else {
427: icmp_reflect(so->so_m);
428: so->so_m = 0; /* Don't m_free() it again! */
429: }
430: /* No need for this socket anymore, udp_detach it */
431: udp_detach(so);
432: } else { /* A "normal" UDP packet */
433: struct mbuf *m;
434: u_int len;
435: ioctlsockopt_t n;
436:
437: if (!(m = m_get())) return;
438: m->m_data += if_maxlinkhdr;
439:
440: /*
441: * XXX Shouldn't FIONREAD packets destined for port 53,
442: * but I don't know the max packet size for DNS lookups
443: */
444: len = M_FREEROOM(m);
445: /* if (so->so_fport != htons(53)) { */
446: ioctlsocket(so->s, FIONREAD, &n);
447:
448: if (n > len) {
449: n = (m->m_data - m->m_dat) + m->m_len + n + 1;
450: m_inc(m, n);
451: len = M_FREEROOM(m);
452: }
453: /* } */
454:
455: m->m_len = recvfrom(so->s, m->m_data, len, 0,
456: (struct sockaddr *)&addr, &addrlen);
457: DEBUG_MISC((dfd, " did recvfrom %zu, errno = %d-%s\n",
458: m->m_len, errno,strerror(errno)));
459: if((int)m->m_len<0) {
460: u_char code=ICMP_UNREACH_PORT;
461:
462: int error = WSAGetLastError();
463: if(error == WSAEHOSTUNREACH) code=ICMP_UNREACH_HOST;
464: else if(error == WSAENETUNREACH) code=ICMP_UNREACH_NET;
465:
466: DEBUG_MISC((dfd," rx error, tx icmp ICMP_UNREACH:%i\n", code));
467: icmp_error(so->so_m, ICMP_UNREACH,code, 0,strerror(errno));
468: m_free(m);
469: } else {
470: /*
471: * Hack: domain name lookup will be used the most for UDP,
472: * and since they'll only be used once there's no need
473: * for the 4 minute (or whatever) timeout... So we time them
474: * out much quicker (10 seconds for now...)
475: */
476: if (so->so_expire) {
477: if (so->so_fport == htons(53))
478: so->so_expire = curtime + SO_EXPIREFAST;
479: else
480: so->so_expire = curtime + SO_EXPIRE;
481: }
482:
483: /* if (m->m_len == len) {
484: * m_inc(m, MINCSIZE);
485: * m->m_len = 0;
486: * }
487: */
488:
489: /*
490: * If this packet was destined for CTL_ADDR,
491: * make it look like that's where it came from, done by udp_output
492: */
493: udp_output(so, m, &addr);
494: } /* rx error */
495: } /* if ping packet */
496: }
497:
498: /*
499: * sendto() a socket
500: */
501: int
502: sosendto(so, m)
503: struct socket *so;
504: struct mbuf *m;
505: {
506: int ret;
507: struct sockaddr_in addr;
508:
509: DEBUG_CALL("sosendto");
510: DEBUG_ARG("so = %lx", (long)so);
511: DEBUG_ARG("m = %lx", (long)m);
512:
513: addr.sin_family = AF_INET;
514: if ((so->so_faddr.s_addr & htonl(0xffffff00)) == special_addr.s_addr) {
515: /* It's an alias */
516: switch(ntohl(so->so_faddr.s_addr) & 0xff) {
517: case CTL_DNS:
518: addr.sin_addr = dns_addr;
519: break;
520: case CTL_ALIAS:
521: default:
522: addr.sin_addr = loopback_addr;
523: break;
524: }
525: } else
526: addr.sin_addr = so->so_faddr;
527: addr.sin_port = so->so_fport;
528:
529: char addrstr[INET_ADDRSTRLEN];
530: DEBUG_MISC((dfd, " sendto()ing, addr.sin_port=%d, addr.sin_addr.s_addr=%.16s\n",
531: ntohs(addr.sin_port), inet_ntop(AF_INET, &addr.sin_addr, addrstr, sizeof(addrstr))));
532:
533: /* Don't care what port we get */
534: ret = sendto(so->s, m->m_data, m->m_len, 0,
535: (struct sockaddr *)&addr, sizeof (struct sockaddr));
536: if (ret < 0)
537: return -1;
538:
539: /*
540: * Kill the socket if there's no reply in 4 minutes,
541: * but only if it's an expirable socket
542: */
543: if (so->so_expire)
544: so->so_expire = curtime + SO_EXPIRE;
545: so->so_state = SS_ISFCONNECTED; /* So that it gets select()ed */
546: return 0;
547: }
548:
549: /*
550: * XXX This should really be tcp_listen
551: */
552: struct socket *
553: solisten(port, laddr, lport, flags)
554: u_int port;
555: u_int32_t laddr;
556: u_int lport;
557: int flags;
558: {
559: struct sockaddr_in addr;
560: struct socket *so;
561: int s;
562: socklen_t addrlen = sizeof(addr);
563: int opt = 1;
564:
565: DEBUG_CALL("solisten");
566: DEBUG_ARG("port = %d", port);
567: DEBUG_ARG("laddr = %x", laddr);
568: DEBUG_ARG("lport = %d", lport);
569: DEBUG_ARG("flags = %x", flags);
570:
571: if ((so = socreate()) == NULL) {
572: /* free(so); Not sofree() ??? free(NULL) == NOP */
573: return NULL;
574: }
575:
576: /* Don't tcp_attach... we don't need so_snd nor so_rcv */
577: if ((so->so_tcpcb = tcp_newtcpcb(so)) == NULL) {
578: free(so);
579: return NULL;
580: }
581: insque(so,&tcb);
582:
583: /*
584: * SS_FACCEPTONCE sockets must time out.
585: */
586: if (flags & SS_FACCEPTONCE)
587: so->so_tcpcb->t_timer[TCPT_KEEP] = TCPTV_KEEP_INIT*2;
588:
589: so->so_state = (SS_FACCEPTCONN|flags);
590: so->so_lport = lport; /* Kept in network format */
591: so->so_laddr.s_addr = laddr; /* Ditto */
592:
593: memset(&addr, 0, sizeof(struct sockaddr_in));
594: addr.sin_family = AF_INET;
595: addr.sin_addr.s_addr = INADDR_ANY;
596: addr.sin_port = port;
597:
598: if (((s = socket(AF_INET,SOCK_STREAM,0)) < 0) ||
599: (setsockopt(s,SOL_SOCKET,SO_REUSEADDR,(char *)&opt,sizeof(int)) < 0) ||
600: (bind(s,(struct sockaddr *)&addr, sizeof(addr)) < 0) ||
601: (listen(s,1) < 0)) {
602: int error = WSAGetLastError(); /* Don't clobber the real reason we failed */
603:
604: close(s);
605: sofree(so);
606: /* Restore the real errno */
607: WSASetLastError(error);
608: return NULL;
609: }
610: setsockopt(s,SOL_SOCKET,SO_OOBINLINE,(char *)&opt,sizeof(int));
611:
612: getsockname(s,(struct sockaddr *)&addr,&addrlen);
613: so->so_fport = addr.sin_port;
614: if (addr.sin_addr.s_addr == 0 || addr.sin_addr.s_addr == loopback_addr.s_addr)
615: so->so_faddr = alias_addr;
616: else
617: so->so_faddr = addr.sin_addr;
618:
619: so->s = s;
620: return so;
621: }
622:
623: /*
624: * Data is available in so_rcv
625: * Just write() the data to the socket
626: * XXX not yet...
627: */
628: void
629: sorwakeup(so)
630: struct socket *so;
631: {
632: /* sowrite(so); */
633: /* FD_CLR(so->s,&writefds); */
634: }
635:
636: /*
637: * Data has been freed in so_snd
638: * We have room for a read() if we want to
639: * For now, don't read, it'll be done in the main loop
640: */
641: void
642: sowwakeup(so)
643: struct socket *so;
644: {
645: /* Nothing, yet */
646: }
647:
648: /*
649: * Various session state calls
650: * XXX Should be #define's
651: * The socket state stuff needs work, these often get call 2 or 3
652: * times each when only 1 was needed
653: */
654: void
655: soisfconnecting(so)
656: register struct socket *so;
657: {
658: so->so_state &= ~(SS_NOFDREF|SS_ISFCONNECTED|SS_FCANTRCVMORE|
659: SS_FCANTSENDMORE|SS_FWDRAIN);
660: so->so_state |= SS_ISFCONNECTING; /* Clobber other states */
661: }
662:
663: void
664: soisfconnected(so)
665: register struct socket *so;
666: {
667: so->so_state &= ~(SS_ISFCONNECTING|SS_FWDRAIN|SS_NOFDREF);
668: so->so_state |= SS_ISFCONNECTED; /* Clobber other states */
669: }
670:
671: void
672: sofcantrcvmore(so)
673: struct socket *so;
674: {
675: if ((so->so_state & SS_NOFDREF) == 0) {
676: shutdown(so->s,0);
677: if(global_writefds) {
678: FD_CLR(so->s,global_writefds);
679: }
680: }
681: so->so_state &= ~(SS_ISFCONNECTING);
682: if (so->so_state & SS_FCANTSENDMORE)
683: so->so_state = SS_NOFDREF; /* Don't select it */ /* XXX close() here as well? */
684: else
685: so->so_state |= SS_FCANTRCVMORE;
686: }
687:
688: void
689: sofcantsendmore(so)
690: struct socket *so;
691: {
692: if ((so->so_state & SS_NOFDREF) == 0) {
693: shutdown(so->s,1); /* send FIN to fhost */
694: if (global_readfds) {
695: FD_CLR(so->s,global_readfds);
696: }
697: if (global_xfds) {
698: FD_CLR(so->s,global_xfds);
699: }
700: }
701: so->so_state &= ~(SS_ISFCONNECTING);
702: if (so->so_state & SS_FCANTRCVMORE)
703: so->so_state = SS_NOFDREF; /* as above */
704: else
705: so->so_state |= SS_FCANTSENDMORE;
706: }
707:
708: void
709: soisfdisconnected(so)
710: struct socket *so;
711: {
712: /* so->so_state &= ~(SS_ISFCONNECTING|SS_ISFCONNECTED); */
713: /* close(so->s); */
714: /* so->so_state = SS_ISFDISCONNECTED; */
715: /*
716: * XXX Do nothing ... ?
717: */
718: }
719:
720: /*
721: * Set write drain mode
722: * Set CANTSENDMORE once all data has been write()n
723: */
724: void
725: sofwdrain(so)
726: struct socket *so;
727: {
728: if (so->so_rcv.sb_cc)
729: so->so_state |= SS_FWDRAIN;
730: else
731: sofcantsendmore(so);
732: }
733:
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.