Annotation of libwww/Library/src/HTGopher.c, revision 2.16
1.1 timbl 1: /* GOPHER ACCESS HTGopher.c
2: ** =============
3: **
4: ** History:
5: ** 26 Sep 90 Adapted from other accesses (News, HTTP) TBL
6: ** 29 Nov 91 Downgraded to C, for portable implementation.
7: */
8:
1.2 timbl 9: /* Implements:
10: */
11: #include "HTGopher.h"
12:
1.3 timbl 13:
1.1 timbl 14: #define GOPHER_PORT 70 /* See protocol spec */
15: #define BIG 1024 /* Bug */
16: #define LINE_LENGTH 256 /* Bug */
17:
18: /* Gopher entity types:
19: */
20: #define GOPHER_TEXT '0'
21: #define GOPHER_MENU '1'
22: #define GOPHER_CSO '2'
23: #define GOPHER_ERROR '3'
24: #define GOPHER_MACBINHEX '4'
25: #define GOPHER_PCBINHEX '5'
26: #define GOPHER_UUENCODED '6'
27: #define GOPHER_INDEX '7'
28: #define GOPHER_TELNET '8'
2.7 secret 29: #define GOPHER_BINARY '9'
1.3 timbl 30: #define GOPHER_GIF 'g'
2.7 secret 31: #define GOPHER_HTML 'h' /* HTML */
32: #define GOPHER_SOUND 's'
33: #define GOPHER_WWW 'w' /* W3 address */
1.3 timbl 34: #define GOPHER_IMAGE 'I'
2.7 secret 35: #define GOPHER_TN3270 'T'
1.1 timbl 36: #define GOPHER_DUPLICATE '+'
37:
38: #include <ctype.h>
39: #include "HTUtils.h" /* Coding convention macros */
40: #include "tcp.h"
41:
42:
43: #include "HTParse.h"
44: #include "HTFormat.h"
45: #include "HTTCP.h"
2.16 ! luotonen 46: #include "HTFile.h" /* HTFileFormat() */
1.1 timbl 47:
1.2 timbl 48: /* Hypertext object building machinery
49: */
50: #include "HTML.h"
51:
52: #define PUTC(c) (*targetClass.put_character)(target, c)
53: #define PUTS(s) (*targetClass.put_string)(target, s)
54: #define START(e) (*targetClass.start_element)(target, e, 0, 0)
55: #define END(e) (*targetClass.end_element)(target, e)
56: #define FREE_TARGET (*targetClass.free)(target)
57: struct _HTStructured {
58: CONST HTStructuredClass * isa;
59: /* ... */
60: };
61:
62: PRIVATE HTStructured *target; /* the new hypertext */
63: PRIVATE HTStructuredClass targetClass; /* Its action routines */
64:
65:
2.8 timbl 66: #define GOPHER_PROGRESS(foo) HTAlert(foo)
1.1 timbl 67:
68:
2.12 timbl 69: #define NEXT_CHAR HTInputSocket_getCharacter(isoc)
1.1 timbl 70:
71:
2.8 timbl 72:
1.1 timbl 73: /* Module-wide variables
74: */
75: PRIVATE int s; /* Socket for GopherHost */
76:
77:
1.2 timbl 78:
1.1 timbl 79: /* Matrix of allowed characters in filenames
80: ** -----------------------------------------
81: */
82:
83: PRIVATE BOOL acceptable[256];
84: PRIVATE BOOL acceptable_inited = NO;
85:
86: PRIVATE void init_acceptable NOARGS
87: {
88: unsigned int i;
89: char * good =
90: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./-_$";
91: for(i=0; i<256; i++) acceptable[i] = NO;
92: for(;*good; good++) acceptable[(unsigned int)*good] = YES;
93: acceptable_inited = YES;
94: }
95:
96: PRIVATE CONST char hex[17] = "0123456789abcdef";
97:
98: /* Decdoe one hex character
99: */
100:
101: PRIVATE char from_hex ARGS1(char, c)
102: {
103: return (c>='0')&&(c<='9') ? c-'0'
104: : (c>='A')&&(c<='F') ? c-'A'+10
105: : (c>='a')&&(c<='f') ? c-'a'+10
106: : 0;
107: }
108:
109:
110:
111: /* Paste in an Anchor
112: ** ------------------
113: **
114: ** The title of the destination is set, as there is no way
115: ** of knowing what the title is when we arrive.
116: **
117: ** On entry,
118: ** HT is in append mode.
119: ** text points to the text to be put into the file, 0 terminated.
120: ** addr points to the hypertext refernce address 0 terminated.
121: */
122: PRIVATE void write_anchor ARGS2(CONST char *,text, CONST char *,addr)
123: {
1.2 timbl 124:
125:
126:
127: BOOL present[HTML_A_ATTRIBUTES];
128: CONST char * value[HTML_A_ATTRIBUTES];
1.1 timbl 129:
1.2 timbl 130: int i;
131:
132: for (i=0; i<HTML_A_ATTRIBUTES; i++) present[i]=0;
133: present[HTML_A_HREF] = YES;
134: value[HTML_A_HREF] = addr;
135: present[HTML_A_TITLE] = YES;
136: value[HTML_A_TITLE] = text;
137:
138: (*targetClass.start_element)(target, HTML_A, present, value);
1.1 timbl 139:
1.2 timbl 140: PUTS(text);
141: END(HTML_A);
1.1 timbl 142: }
143:
144:
145: /* Parse a Gopher Menu document
146: ** ============================
147: **
148: */
149:
2.11 timbl 150: PRIVATE void parse_menu ARGS3 (
151: int , s,
1.2 timbl 152: CONST char *, arg,
153: HTParentAnchor *, anAnchor)
1.1 timbl 154: {
155: char gtype;
156: char ch;
157: char line[BIG];
158: char address[BIG];
2.14 luotonen 159: char *name = "";
160: char *selector = ""; /* Gopher menu fields */
161: char *host = "";
1.1 timbl 162: char *port;
163: char *p = line;
1.2 timbl 164: CONST char *title;
2.12 timbl 165: HTInputSocket * isoc = HTInputSocket_new(s);
2.11 timbl 166:
1.1 timbl 167: #define TAB '\t'
168: #define HEX_ESCAPE '%'
169:
170:
1.2 timbl 171: title = HTAnchor_title(anAnchor);
172: if (title) {
173: START(HTML_H1);
174: PUTS(title);
175: END(HTML_H1);
176: } else
177: PUTS("Select one of:\n\n");
1.1 timbl 178:
1.2 timbl 179: START(HTML_MENU);
1.1 timbl 180: while ((ch=NEXT_CHAR) != (char)EOF) {
1.3 timbl 181: if (ch != LF) {
1.1 timbl 182: *p = ch; /* Put character in line */
183: if (p< &line[BIG-1]) p++;
184:
185: } else {
186: *p++ = 0; /* Terminate line */
187: p = line; /* Scan it to parse it */
188: port = 0; /* Flag "not parsed" */
189: if (TRACE) fprintf(stderr, "HTGopher: Menu item: %s\n", line);
190: gtype = *p++;
191:
192: /* Break on line with a dot by itself */
193: if ((gtype=='.') && ((*p=='\r') || (*p==0))) break;
194:
195: if (gtype && *p) {
196: name = p;
197: selector = strchr(name, TAB);
1.3 timbl 198: START(HTML_LI);
1.1 timbl 199: if (selector) {
200: *selector++ = 0; /* Terminate name */
201: host = strchr(selector, TAB);
202: if (host) {
203: *host++ = 0; /* Terminate selector */
204: port = strchr(host, TAB);
205: if (port) {
206: char *junk;
207: port[0] = ':'; /* delimit host a la W3 */
208: junk = strchr(port, TAB);
209: if (junk) *junk++ = 0; /* Chop port */
210: if ((port[1]=='0') && (!port[2]))
211: port[0] = 0; /* 0 means none */
212: } /* no port */
213: } /* host ok */
214: } /* selector ok */
215: } /* gtype and name ok */
216:
217: if (gtype == GOPHER_WWW) { /* Gopher pointer to W3 */
218: write_anchor(name, selector);
2.7 secret 219:
1.1 timbl 220: } else if (port) { /* Other types need port */
221: if (gtype == GOPHER_TELNET) {
222: if (*selector) sprintf(address, "telnet://%s@%s/",
2.7 secret 223: selector, host);
1.1 timbl 224: else sprintf(address, "telnet://%s/", host);
2.7 secret 225: }
226: else if (gtype == GOPHER_TN3270)
227: {
228: if (*selector)
229: sprintf(address, "tn3270://%s@%s/",
230: selector, host);
231: else
232: sprintf(address, "tn3270://%s/", host);
233: }
234: else { /* If parsed ok */
1.1 timbl 235: char *q;
236: char *p;
237: sprintf(address, "//%s/%c", host, gtype);
238: q = address+ strlen(address);
239: for(p=selector; *p; p++) { /* Encode selector string */
2.14 luotonen 240: if (acceptable[(int)*p]) *q++ = *p;
1.1 timbl 241: else {
242: *q++ = HEX_ESCAPE; /* Means hex coming */
243: *q++ = hex[(TOASCII(*p)) >> 4];
244: *q++ = hex[(TOASCII(*p)) & 15];
245: }
246: }
247: *q++ = 0; /* terminate address */
248: }
1.2 timbl 249: PUTS(" "); /* Prettier JW/TBL */
2.7 secret 250: /* Error response from Gopher doesn't deserve to
251: be a hyperlink. */
252: if (strcmp (address, "gopher://error.host:1/0"))
253: write_anchor(name, address);
254: else
255: PUTS(name);
256: PUTS("\n");
1.1 timbl 257: } else { /* parse error */
258: if (TRACE) fprintf(stderr,
259: "HTGopher: Bad menu item.\n");
1.2 timbl 260: PUTS(line);
261:
1.1 timbl 262: } /* parse error */
263:
264: p = line; /* Start again at beginning of line */
265:
266: } /* if end of line */
267:
268: } /* Loop over characters */
269:
1.2 timbl 270: END(HTML_MENU);
271: FREE_TARGET;
272:
2.11 timbl 273: HTInputSocket_free(isoc);
1.1 timbl 274: return;
275: }
2.11 timbl 276:
277:
2.7 secret 278: /* Parse a Gopher CSO document
279: ** ============================
280: **
281: ** Accepts an open socket to a CSO server waiting to send us
282: ** data and puts it on the screen in a reasonable manner.
283: **
284: ** Perhaps this data can be automatically linked to some
285: ** other source as well???
286: **
287: ** Taken from hacking by Lou Montulli@ukanaix.cc.ukans.edu
288: ** on XMosaic-1.1, and put on libwww 2.11 by Arthur Secret,
289: ** secret@dxcern.cern.ch .
290: */
291:
2.11 timbl 292: PRIVATE void parse_cso ARGS3 (
293: int, s,
294: CONST char *, arg,
295: HTParentAnchor *, anAnchor)
2.7 secret 296: {
297: char ch;
298: char line[BIG];
299: char *p = line;
300: char *second_colon, last_char='\0';
301: CONST char *title;
2.11 timbl 302: HTInputSocket * isoc = HTInputSocket_new(s);
2.7 secret 303:
304: title = HTAnchor_title(anAnchor);
305: START(HTML_H1);
306: PUTS("CSO Search Results");
307: END(HTML_H1);
308: START(HTML_PRE);
309:
310: /* start grabbing chars from the network */
311: while ((ch=NEXT_CHAR) != (char)EOF)
312: {
313: if (ch != '\n')
314: {
315: *p = ch; /* Put character in line */
316: if (p< &line[BIG-1]) p++;
317: }
318: else
319: {
320: *p++ = 0; /* Terminate line */
321: p = line; /* Scan it to parse it */
322:
323: /* OK we now have a line in 'p' lets parse it and
324: print it */
325:
326: /* Break on line that begins with a 2. It's the end of
327: * data.
328: */
329: if (*p == '2')
330: break;
331:
332: /* lines beginning with 5 are errors,
333: * print them and quit
334: */
335: if (*p == '5') {
336: START(HTML_H2);
337: PUTS(p+4);
338: END(HTML_H2);
339: break;
340: }
341:
342: if(*p == '-') {
343: /* data lines look like -200:#:
344: * where # is the search result number and can be
345: * multiple digits (infinate?)
346: * find the second colon and check the digit to the
347: * left of it to see if they are diferent
348: * if they are then a different person is starting.
349: * make this line an <h2>
350: */
351:
352: /* find the second_colon */
353: second_colon = strchr( strchr(p,':')+1, ':');
354:
355: if(second_colon != NULL) { /* error check */
356:
357: if (*(second_colon-1) != last_char)
358: /* print seperator */
359: {
360: END(HTML_PRE);
361: START(HTML_H2);
362: }
363:
364:
365: /* right now the record appears with the alias
366: * (first line)
367: * as the header and the rest as <pre> text
368: * It might look better with the name as the
369: * header and the rest as a <ul> with <li> tags
370: * I'm not sure whether the name field comes in any
371: * special order or if its even required in a
372: * record,
373: * so for now the first line is the header no
374: * matter
375: * what it is (it's almost always the alias)
376: * A <dl> with the first line as the <DT> and
377: * the rest as some form of <DD> might good also?
378: */
379:
380: /* print data */
381: PUTS(second_colon+1);
382: PUTS("\n");
383:
384: if (*(second_colon-1) != last_char)
385: /* end seperator */
386: {
387: END(HTML_H2);
388: START(HTML_PRE);
389: }
390:
391: /* save the char before the second colon
392: * for comparison on the next pass
393: */
394: last_char = *(second_colon-1) ;
395:
396: } /* end if second_colon */
397: } /* end if *p == '-' */
398: } /* if end of line */
399:
400: } /* Loop over characters */
401:
402: /* end the text block */
403: PUTS("\n");
404: END(HTML_PRE);
405: PUTS("\n");
406: FREE_TARGET;
2.11 timbl 407: HTInputSocket_free(isoc);
2.7 secret 408:
409: return; /* all done */
410: } /* end of procedure */
1.1 timbl 411:
412: /* Display a Gopher Index document
2.7 secret 413: ** -------------------------------
414: */
1.1 timbl 415:
416: PRIVATE void display_index ARGS2 (
2.7 secret 417: CONST char *, arg,
418: HTParentAnchor *,anAnchor)
1.1 timbl 419: {
1.2 timbl 420:
421: START(HTML_H1);
422: PUTS(arg);
2.7 secret 423: PUTS(" index");
1.2 timbl 424: END(HTML_H1);
2.7 secret 425: START(HTML_ISINDEX);
426: PUTS("\nThis is a searchable Gopher index.");
427: PUTS(" Please enter keywords to search for.\n");
428:
429: if (!HTAnchor_title(anAnchor))
430: HTAnchor_setTitle(anAnchor, arg);
1.2 timbl 431:
2.7 secret 432: FREE_TARGET;
433: return;
434: }
435:
436:
437: /* Display a CSO index document
438: ** -------------------------------
439: */
440:
441: PRIVATE void display_cso ARGS2 (
442: CONST char *, arg,
443: HTParentAnchor *,anAnchor)
444: {
445: START(HTML_H1);
446: PUTS(arg);
447: PUTS(" index");
448: END(HTML_H1);
449: START(HTML_ISINDEX);
450: PUTS("\nThis is a searchable index of a CSO database.\n");
451: PUTS(" Please enter keywords to search for. The keywords that you enter");
452: PUTS(" will allow you to search on a person's name in the database.\n");
453:
1.1 timbl 454: if (!HTAnchor_title(anAnchor))
1.2 timbl 455: HTAnchor_setTitle(anAnchor, arg);
1.1 timbl 456:
1.2 timbl 457: FREE_TARGET;
1.1 timbl 458: return;
459: }
460:
461:
462: /* De-escape a selector into a command
463: ** -----------------------------------
464: **
465: ** The % hex escapes are converted. Otheriwse, the string is copied.
466: */
467: PRIVATE void de_escape ARGS2(char *, command, CONST char *, selector)
468: {
469: CONST char * p = selector;
470: char * q = command;
471: if (command == NULL) outofmem(__FILE__, "HTLoadGopher");
472: while (*p) { /* Decode hex */
473: if (*p == HEX_ESCAPE) {
474: char c;
475: unsigned int b;
476: p++;
477: c = *p++;
478: b = from_hex(c);
479: c = *p++;
480: if (!c) break; /* Odd number of chars! */
481: *q++ = FROMASCII((b<<4) + from_hex(c));
482: } else {
483: *q++ = *p++; /* Record */
484: }
485: }
486: *q++ = 0; /* Terminate command */
487:
488: }
489:
490:
491: /* Load by name HTLoadGopher
492: ** ============
493: **
494: ** Bug: No decoding of strange data types as yet.
495: **
496: */
2.13 timbl 497: PUBLIC int HTLoadGopher ARGS1(HTRequest *, request)
1.1 timbl 498: {
2.13 timbl 499: CONST char * arg = HTAnchor_physical(request->anchor);
1.1 timbl 500: char *command; /* The whole command */
501: int status; /* tcp return */
502: char gtype; /* Gopher Node type */
503: char * selector; /* Selector string */
504: struct sockaddr_in soc_address; /* Binary network address */
505: struct sockaddr_in* sin = &soc_address;
506:
507: if (!acceptable_inited) init_acceptable();
508:
509: if (!arg) return -3; /* Bad if no name sepcified */
510: if (!*arg) return -2; /* Bad if name had zero length */
511:
512: if (TRACE) fprintf(stderr, "HTGopher: Looking for %s\n", arg);
513:
514:
515: /* Set up defaults:
516: */
517: sin->sin_family = AF_INET; /* Family, host order */
518: sin->sin_port = htons(GOPHER_PORT); /* Default: new port, */
519:
520: /* Get node name and optional port number:
521: */
522: {
523: char *p1 = HTParse(arg, "", PARSE_HOST);
524: int status = HTParseInet(sin, p1);
525: free(p1);
526: if (status) return status; /* Bad */
527: }
528:
529: /* Get entity type, and selector string.
530: */
531: {
532: char * p1 = HTParse(arg, "", PARSE_PATH|PARSE_PUNCTUATION);
533: gtype = '1'; /* Default = menu */
534: selector = p1;
535: if ((*selector++=='/') && (*selector)) { /* Skip first slash */
536: gtype = *selector++; /* Pick up gtype */
537: }
538: if (gtype == GOPHER_INDEX) {
539: char * query;
2.10 timbl 540: HTAnchor_setIndex(request->anchor); /* Search is allowed */
1.1 timbl 541: query = strchr(selector, '?'); /* Look for search string */
542: if (!query || !query[1]) { /* No search required */
2.11 timbl 543: target = HTML_new(request, NULL, WWW_HTML,
544: request->output_format, request->output_stream);
1.2 timbl 545: targetClass = *target->isa;
2.10 timbl 546: display_index(arg, request->anchor); /* Display "cover page" */
2.15 luotonen 547: free(p1); /* Leak fixed Henrik 27 Feb 94 */
2.6 timbl 548: return HT_LOADED; /* Local function only */
1.1 timbl 549: }
550: *query++ = 0; /* Skip '?' */
551: command = malloc(strlen(selector)+ 1 + strlen(query)+ 2 + 1);
552: if (command == NULL) outofmem(__FILE__, "HTLoadGopher");
553:
554: de_escape(command, selector); /* Bug fix TBL 921208 */
555:
556: strcat(command, "\t");
557:
558: { /* Remove plus signs 921006 */
559: char *p;
560: for (p=query; *p; p++) {
561: if (*p == '+') *p = ' ';
562: }
563: }
564: strcat(command, query);
2.7 secret 565: } else if (gtype == GOPHER_CSO) {
566: char * query;
2.10 timbl 567: HTAnchor_setIndex(request->anchor); /* Search is allowed */
2.7 secret 568: query = strchr(selector, '?'); /* Look for search string */
569: if (!query || !query[1]) { /* No search required */
2.11 timbl 570: target = HTML_new(request, NULL, WWW_HTML,
571: request->output_format, request->output_stream);
2.7 secret 572: targetClass = *target->isa;
2.10 timbl 573: display_cso(arg, request->anchor); /* Display "cover page" */
2.15 luotonen 574: free(p1); /* Leak fixed Henrik 27 Feb 94 */
2.7 secret 575: return HT_LOADED; /* Local function only */
576: }
577: *query++ = 0; /* Skip '?' */
578: command = malloc(strlen("query")+ 1 + strlen(query)+ 2 + 1);
579: if (command == NULL) outofmem(__FILE__, "HTLoadGopher");
580:
581: de_escape(command, selector); /* Bug fix TBL 921208 */
582:
583: strcpy(command, "query ");
584:
585: { /* Remove plus signs 921006 */
586: char *p;
587: for (p=query; *p; p++) {
588: if (*p == '+') *p = ' ';
589: }
590: }
591: strcat(command, query);
592:
1.1 timbl 593:
594: } else { /* Not index */
595: command = command = malloc(strlen(selector)+2+1);
596: de_escape(command, selector);
597: }
598: free(p1);
599: }
600:
1.3 timbl 601: {
602: char * p = command + strlen(command);
603: *p++ = CR; /* Macros to be correct on Mac */
604: *p++ = LF;
605: *p++ = 0;
606: /* strcat(command, "\r\n"); */ /* CR LF, as in rfc 977 */
607: }
1.1 timbl 608:
609: /* Set up a socket to the server for the data:
610: */
611: s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
612: status = connect(s, (struct sockaddr*)&soc_address, sizeof(soc_address));
613: if (status<0){
614: if (TRACE) fprintf(stderr, "HTTPAccess: Unable to connect to remote host for `%s'.\n",
615: arg);
616: free(command);
617: return HTInetStatus("connect");
618: }
619:
620:
621: if (TRACE) fprintf(stderr, "HTGopher: Connected, writing command `%s' to socket %d\n", command, s);
622:
623: #ifdef NOT_ASCII
624: {
625: char * p;
626: for(p = command; *p; p++) {
627: *p = TOASCII(*p);
628: }
629: }
630: #endif
631:
632: status = NETWRITE(s, command, (int)strlen(command));
633: free(command);
634: if (status<0){
635: if (TRACE) fprintf(stderr, "HTGopher: Unable to send command.\n");
636: return HTInetStatus("send");
637: }
638:
639: /* Now read the data from the socket:
640: */
641: switch (gtype) {
642:
643: case GOPHER_HTML :
2.11 timbl 644: HTParseSocket(WWW_HTML, s, request);
1.2 timbl 645: break;
1.1 timbl 646:
1.3 timbl 647: case GOPHER_GIF:
648: case GOPHER_IMAGE:
2.11 timbl 649: HTParseSocket(HTAtom_for("image/gif"), s, request);
1.3 timbl 650: break;
1.1 timbl 651: case GOPHER_MENU :
652: case GOPHER_INDEX :
2.11 timbl 653: target = HTML_new(request, NULL, WWW_HTML,
654: request->output_format, request->output_stream);
1.2 timbl 655: targetClass = *target->isa;
2.11 timbl 656: parse_menu(s,arg, request->anchor);
1.2 timbl 657: break;
2.7 secret 658:
659: case GOPHER_CSO:
2.11 timbl 660: target = HTML_new(request, NULL, WWW_HTML,
661: request->output_format, request->output_stream);
2.7 secret 662: targetClass = *target->isa;
2.11 timbl 663: parse_cso(s, arg, request->anchor);
2.7 secret 664: break;
665:
666: case GOPHER_MACBINHEX:
667: case GOPHER_PCBINHEX:
668: case GOPHER_UUENCODED:
669: case GOPHER_BINARY:
2.16 ! luotonen 670: { /* Do our own filetyping -- maybe we get lucky */
! 671: HTFormat format = HTFileFormat(arg,
! 672: &request->content_encoding,
! 673: &request->content_language);
! 674: if (format) {
! 675: CTRACE(stderr,
! 676: "Gopher...... figured out content-type myself: %s\n",
! 677: HTAtom_name(format));
! 678: HTParseSocket(format, s, request);
! 679: }
! 680: else {
! 681: CTRACE(stderr,"Gopher...... using www/unknown\n");
! 682: /* Specifying WWW_UNKNOWN forces dump to local disk. */
! 683: HTParseSocket (WWW_UNKNOWN, s, request);
! 684: }
! 685: }
2.7 secret 686: break;
687:
1.1 timbl 688: case GOPHER_TEXT :
689: default: /* @@ parse as plain text */
2.11 timbl 690: HTParseSocket(WWW_PLAINTEXT, s, request);
2.7 secret 691: break;
692:
693: case GOPHER_SOUND :
2.11 timbl 694: HTParseSocket(WWW_AUDIO, s, request);
1.2 timbl 695: break;
696:
1.1 timbl 697: } /* switch(gtype) */
1.2 timbl 698:
699: NETCLOSE(s);
700: return HT_LOADED;
1.1 timbl 701: }
1.2 timbl 702:
2.10 timbl 703: GLOBALDEF PUBLIC HTProtocol HTGopher = { "gopher", HTLoadGopher, NULL, NULL };
1.1 timbl 704:
Webmaster