4 * $Id: gopher.cc,v 1.148 1999/01/20 19:27:11 wessels Exp $
6 * DEBUG: section 10 Gopher
7 * AUTHOR: Harvest Derived
9 * SQUID Internet Object Cache http://squid.nlanr.net/Squid/
10 * ----------------------------------------------------------
12 * Squid is the result of efforts by numerous individuals from the
13 * Internet community. Development is led by Duane Wessels of the
14 * National Laboratory for Applied Network Research and funded by the
15 * National Science Foundation. Squid is Copyrighted (C) 1998 by
16 * Duane Wessels and the University of California San Diego. Please
17 * see the COPYRIGHT file for full details. Squid incorporates
18 * software developed and/or copyrighted by other sources. Please see
19 * the CREDITS file for full details.
21 * This program is free software; you can redistribute it and/or modify
22 * it under the terms of the GNU General Public License as published by
23 * the Free Software Foundation; either version 2 of the License, or
24 * (at your option) any later version.
26 * This program is distributed in the hope that it will be useful,
27 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 * GNU General Public License for more details.
31 * You should have received a copy of the GNU General Public License
32 * along with this program; if not, write to the Free Software
33 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
39 /* gopher type code from rfc. Anawat. */
40 #define GOPHER_FILE '0'
41 #define GOPHER_DIRECTORY '1'
42 #define GOPHER_CSO '2'
43 #define GOPHER_ERROR '3'
44 #define GOPHER_MACBINHEX '4'
45 #define GOPHER_DOSBIN '5'
46 #define GOPHER_UUENCODED '6'
47 #define GOPHER_INDEX '7'
48 #define GOPHER_TELNET '8'
49 #define GOPHER_BIN '9'
50 #define GOPHER_REDUNT '+'
51 #define GOPHER_3270 'T'
52 #define GOPHER_GIF 'g'
53 #define GOPHER_IMAGE 'I'
55 #define GOPHER_HTML 'h' /* HTML */
56 #define GOPHER_INFO 'i'
57 #define GOPHER_WWW 'w' /* W3 address */
58 #define GOPHER_SOUND 's'
60 #define GOPHER_PLUS_IMAGE ':'
61 #define GOPHER_PLUS_MOVIE ';'
62 #define GOPHER_PLUS_SOUND '<'
64 #define GOPHER_PORT 70
67 #define TEMP_BUF_SIZE 4096
68 #define MAX_CSO_RESULT 1024
70 typedef struct gopher_ds
{
72 char host
[SQUIDHOSTNAMELEN
+ 1];
81 int HTML_header_added
;
84 char request
[MAX_URL
];
88 char *buf
; /* pts to a 4k page */
93 static PF gopherStateFree
;
94 static void gopher_mime_content(MemBuf
* mb
, const char *name
, const char *def
);
95 static void gopherMimeCreate(GopherStateData
*);
96 static int gopher_url_parser(const char *url
,
101 static void gopherEndHTML(GopherStateData
*);
102 static void gopherToHTML(GopherStateData
*, char *inbuf
, int len
);
103 static PF gopherTimeout
;
104 static PF gopherReadReply
;
105 static CWCB gopherSendComplete
;
106 static PF gopherSendRequest
;
107 static GopherStateData
*CreateGopherStateData(void);
109 static char def_gopher_bin
[] = "www/unknown";
110 static char def_gopher_text
[] = "text/plain";
113 gopherStateFree(int fdnotused
, void *data
)
115 GopherStateData
*gopherState
= data
;
116 if (gopherState
== NULL
)
118 if (gopherState
->entry
) {
119 storeUnlockObject(gopherState
->entry
);
121 memFree(gopherState
->buf
, MEM_4K_BUF
);
122 gopherState
->buf
= NULL
;
123 cbdataFree(gopherState
);
127 /* figure out content type from file extension */
129 gopher_mime_content(MemBuf
* mb
, const char *name
, const char *def_ctype
)
131 char *ctype
= mimeGetContentType(name
);
132 char *cenc
= mimeGetContentEncoding(name
);
134 memBufPrintf(mb
, "Content-Encoding: %s\r\n", cenc
);
135 memBufPrintf(mb
, "Content-Type: %s\r\n",
136 ctype
? ctype
: def_ctype
);
141 /* create MIME Header for Gopher Data */
143 gopherMimeCreate(GopherStateData
* gopherState
)
150 "HTTP/1.0 200 OK Gatewaying\r\n"
151 "Server: Squid/%s\r\n"
153 "MIME-version: 1.0\r\n",
154 version_string
, mkrfc1123(squid_curtime
));
156 switch (gopherState
->type_id
) {
158 case GOPHER_DIRECTORY
:
163 memBufPrintf(&mb
, "Content-Type: text/html\r\n");
167 case GOPHER_PLUS_IMAGE
:
168 memBufPrintf(&mb
, "Content-Type: image/gif\r\n");
171 case GOPHER_PLUS_SOUND
:
172 memBufPrintf(&mb
, "Content-Type: audio/basic\r\n");
174 case GOPHER_PLUS_MOVIE
:
175 memBufPrintf(&mb
, "Content-Type: video/mpeg\r\n");
177 case GOPHER_MACBINHEX
:
179 case GOPHER_UUENCODED
:
181 /* Rightnow We have no idea what it is. */
182 gopher_mime_content(&mb
, gopherState
->request
, def_gopher_bin
);
186 gopher_mime_content(&mb
, gopherState
->request
, def_gopher_text
);
189 memBufPrintf(&mb
, "\r\n");
190 EBIT_CLR(gopherState
->entry
->flags
, ENTRY_FWD_HDR_WAIT
);
191 storeAppend(gopherState
->entry
, mb
.buf
, mb
.size
);
195 /* Parse a gopher url into components. By Anawat. */
197 gopher_url_parser(const char *url
, char *host
, int *port
, char *type_id
, char *request
)
199 LOCAL_ARRAY(char, proto
, MAX_URL
);
200 LOCAL_ARRAY(char, hostbuf
, MAX_URL
);
203 proto
[0] = hostbuf
[0] = '\0';
204 host
[0] = request
[0] = '\0';
210 "%[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]://%[^/]/%c%s",
212 "%[a-zA-Z]://%[^/]/%c%s",
214 proto
, hostbuf
, type_id
, request
);
215 if ((t
< 2) || strcasecmp(proto
, "gopher")) {
218 (*type_id
) = GOPHER_DIRECTORY
;
223 /* convert %xx to char */
224 url_convert_hex(request
, 0);
228 if (sscanf(hostbuf
, "%[^:]:%d", host
, port
) < 2)
229 (*port
) = GOPHER_PORT
;
235 gopherCachable(const char *url
)
237 GopherStateData
*gopherState
= NULL
;
239 /* use as temp data structure to parse gopher URL */
240 gopherState
= CreateGopherStateData();
241 /* parse to see type */
242 gopher_url_parser(url
,
245 &gopherState
->type_id
,
246 gopherState
->request
);
247 switch (gopherState
->type_id
) {
257 gopherStateFree(-1, gopherState
);
262 gopherEndHTML(GopherStateData
* gopherState
)
264 if (!gopherState
->data_in
)
265 storeAppendPrintf(gopherState
->entry
,
266 "<HTML><HEAD><TITLE>Server Return Nothing.</TITLE>\n"
267 "</HEAD><BODY><HR><H1>Server Return Nothing.</H1></BODY></HTML>\n");
271 /* Convert Gopher to HTML */
272 /* Borrow part of code from libwww2 came with Mosaic distribution */
274 gopherToHTML(GopherStateData
* gopherState
, char *inbuf
, int len
)
279 LOCAL_ARRAY(char, line
, TEMP_BUF_SIZE
);
280 LOCAL_ARRAY(char, tmpbuf
, TEMP_BUF_SIZE
);
281 LOCAL_ARRAY(char, outbuf
, TEMP_BUF_SIZE
<< 4);
283 char *selector
= NULL
;
286 char *escaped_selector
= NULL
;
287 char *icon_url
= NULL
;
289 StoreEntry
*entry
= NULL
;
291 memset(outbuf
, '\0', TEMP_BUF_SIZE
<< 4);
292 memset(tmpbuf
, '\0', TEMP_BUF_SIZE
);
293 memset(line
, '\0', TEMP_BUF_SIZE
);
295 entry
= gopherState
->entry
;
297 if (gopherState
->conversion
== HTML_INDEX_PAGE
) {
298 storeAppendPrintf(entry
,
299 "<HTML><HEAD><TITLE>Gopher Index %s</TITLE></HEAD>\n"
300 "<BODY><H1>%s<BR>Gopher Search</H1>\n"
301 "<p>This is a searchable Gopher index. Use the search\n"
302 "function of your browser to enter search terms.\n"
303 "<ISINDEX></BODY></HTML>\n",
304 storeUrl(entry
), storeUrl(entry
));
305 /* now let start sending stuff to client */
306 storeBufferFlush(entry
);
307 gopherState
->data_in
= 1;
311 if (gopherState
->conversion
== HTML_CSO_PAGE
) {
312 storeAppendPrintf(entry
,
313 "<HTML><HEAD><TITLE>CSO Search of %s</TITLE></HEAD>\n"
314 "<BODY><H1>%s<BR>CSO Search</H1>\n"
315 "<P>A CSO database usually contains a phonebook or\n"
316 "directory. Use the search function of your browser to enter\n"
317 "search terms.</P><ISINDEX></BODY></HTML>\n",
318 storeUrl(entry
), storeUrl(entry
));
319 /* now let start sending stuff to client */
320 storeBufferFlush(entry
);
321 gopherState
->data_in
= 1;
327 if (!gopherState
->HTML_header_added
) {
328 if (gopherState
->conversion
== HTML_CSO_RESULT
)
329 strcat(outbuf
, "<HTML><HEAD><TITLE>CSO Searchs Result</TITLE></HEAD>\n"
330 "<BODY><H1>CSO Searchs Result</H1>\n<PRE>\n");
332 strcat(outbuf
, "<HTML><HEAD><TITLE>Gopher Menu</TITLE></HEAD>\n"
333 "<BODY><H1>Gopher Menu</H1>\n<PRE>\n");
334 gopherState
->HTML_header_added
= 1;
336 while ((pos
!= NULL
) && (pos
< inbuf
+ len
)) {
338 if (gopherState
->len
!= 0) {
339 /* there is something left from last tx. */
340 xstrncpy(line
, gopherState
->buf
, gopherState
->len
);
341 lpos
= (char *) memccpy(line
+ gopherState
->len
, inbuf
, '\n', len
);
345 /* there is no complete line in inbuf */
346 /* copy it to temp buffer */
347 if (gopherState
->len
+ len
> TEMP_BUF_SIZE
) {
348 debug(10, 1) ("GopherHTML: Buffer overflow. Lost some data on URL: %s\n",
350 len
= TEMP_BUF_SIZE
- gopherState
->len
;
352 xmemcpy(gopherState
->buf
+ gopherState
->len
, inbuf
, len
);
353 gopherState
->len
+= len
;
358 pos
= (char *) memchr(pos
, '\n', len
);
362 /* we're done with the remain from last tx. */
363 gopherState
->len
= 0;
364 *(gopherState
->buf
) = '\0';
367 lpos
= (char *) memccpy(line
, pos
, '\n', len
- (pos
- inbuf
));
371 /* there is no complete line in inbuf */
372 /* copy it to temp buffer */
373 if ((len
- (pos
- inbuf
)) > TEMP_BUF_SIZE
) {
374 debug(10, 1) ("GopherHTML: Buffer overflow. Lost some data on URL: %s\n",
378 if (len
> (pos
- inbuf
)) {
379 xmemcpy(gopherState
->buf
, pos
, len
- (pos
- inbuf
));
380 gopherState
->len
= len
- (pos
- inbuf
);
386 pos
= (char *) memchr(pos
, '\n', len
);
392 /* at this point. We should have one line in buffer to process */
396 memset(line
, '\0', TEMP_BUF_SIZE
);
399 switch (gopherState
->conversion
) {
401 case HTML_INDEX_RESULT
:
406 selector
= strchr(tline
, TAB
);
409 host
= strchr(selector
, TAB
);
412 port
= strchr(host
, TAB
);
416 junk
= strchr(host
, TAB
);
418 *junk
++ = 0; /* Chop port */
420 junk
= strchr(host
, '\r');
422 *junk
++ = 0; /* Chop port */
424 junk
= strchr(host
, '\n');
426 *junk
++ = 0; /* Chop port */
429 if ((port
[1] == '0') && (!port
[2]))
430 port
[0] = 0; /* 0 means none */
432 /* escape a selector here */
433 escaped_selector
= xstrdup(rfc1738_escape(selector
));
436 case GOPHER_DIRECTORY
:
437 icon_url
= mimeGetIconURL("internal-menu");
440 icon_url
= mimeGetIconURL("internal-text");
444 icon_url
= mimeGetIconURL("internal-index");
448 case GOPHER_PLUS_IMAGE
:
449 icon_url
= mimeGetIconURL("internal-image");
452 case GOPHER_PLUS_SOUND
:
453 icon_url
= mimeGetIconURL("internal-sound");
455 case GOPHER_PLUS_MOVIE
:
456 icon_url
= mimeGetIconURL("internal-movie");
460 icon_url
= mimeGetIconURL("internal-telnet");
463 case GOPHER_MACBINHEX
:
465 case GOPHER_UUENCODED
:
466 icon_url
= mimeGetIconURL("internal-binary");
469 icon_url
= mimeGetIconURL("internal-unknown");
474 memset(tmpbuf
, '\0', TEMP_BUF_SIZE
);
475 if ((gtype
== GOPHER_TELNET
) || (gtype
== GOPHER_3270
)) {
476 if (strlen(escaped_selector
) != 0)
477 snprintf(tmpbuf
, TEMP_BUF_SIZE
, "<IMG BORDER=0 SRC=\"%s\"> <A HREF=\"telnet://%s@%s/\">%s</A>\n",
478 icon_url
, escaped_selector
, host
, name
);
480 snprintf(tmpbuf
, TEMP_BUF_SIZE
, "<IMG BORDER=0 SRC=\"%s\"> <A HREF=\"telnet://%s/\">%s</A>\n",
481 icon_url
, host
, name
);
484 snprintf(tmpbuf
, TEMP_BUF_SIZE
, "<IMG BORDER=0 SRC=\"%s\"> <A HREF=\"gopher://%s/%c%s\">%s</A>\n",
485 icon_url
, host
, gtype
, escaped_selector
, name
);
487 safe_free(escaped_selector
);
488 strcat(outbuf
, tmpbuf
);
489 gopherState
->data_in
= 1;
491 memset(line
, '\0', TEMP_BUF_SIZE
);
495 memset(line
, '\0', TEMP_BUF_SIZE
);
499 } /* HTML_DIR, HTML_INDEX_RESULT */
502 case HTML_CSO_RESULT
:{
506 LOCAL_ARRAY(char, result
, MAX_CSO_RESULT
);
510 if (tline
[0] == '-') {
511 t
= sscanf(tline
, "-%d:%d:%[^\n]", &code
, &recno
, result
);
518 if (gopherState
->cso_recno
!= recno
) {
519 snprintf(tmpbuf
, TEMP_BUF_SIZE
, "</PRE><HR><H2>Record# %d<br><i>%s</i></H2>\n<PRE>", recno
, result
);
520 gopherState
->cso_recno
= recno
;
522 snprintf(tmpbuf
, TEMP_BUF_SIZE
, "%s\n", result
);
524 strcat(outbuf
, tmpbuf
);
525 gopherState
->data_in
= 1;
528 /* handle some error codes */
529 t
= sscanf(tline
, "%d:%[^\n]", &code
, result
);
538 /* Do nothing here */
542 case 102: /* Number of matches */
543 case 501: /* No Match */
544 case 502: /* Too Many Matches */
546 /* Print the message the server returns */
547 snprintf(tmpbuf
, TEMP_BUF_SIZE
, "</PRE><HR><H2>%s</H2>\n<PRE>", result
);
548 strcat(outbuf
, tmpbuf
);
549 gopherState
->data_in
= 1;
557 } /* HTML_CSO_RESULT */
559 break; /* do nothing */
565 if ((int) strlen(outbuf
) > 0) {
566 storeAppend(entry
, outbuf
, strlen(outbuf
));
567 /* now let start sending stuff to client */
568 storeBufferFlush(entry
);
574 gopherTimeout(int fd
, void *data
)
576 GopherStateData
*gopherState
= data
;
577 StoreEntry
*entry
= gopherState
->entry
;
578 debug(10, 4) ("gopherTimeout: FD %d: '%s'\n", fd
, storeUrl(entry
));
579 if (entry
->store_status
== STORE_PENDING
) {
580 if (entry
->mem_obj
->inmem_hi
== 0) {
581 fwdFail(gopherState
->fwdState
,
582 errorCon(ERR_READ_TIMEOUT
, HTTP_GATEWAY_TIMEOUT
));
588 /* This will be called when data is ready to be read from fd. Read until
589 * error or connection closed. */
591 gopherReadReply(int fd
, void *data
)
593 GopherStateData
*gopherState
= data
;
594 StoreEntry
*entry
= gopherState
->entry
;
601 delay_id delay_id
= delayMostBytesAllowed(entry
->mem_obj
);
603 if (EBIT_TEST(entry
->flags
, ENTRY_ABORTED
)) {
608 buf
= memAllocate(MEM_4K_BUF
);
609 read_sz
= 4096 - 1; /* leave room for termination */
611 read_sz
= delayBytesWanted(delay_id
, 1, read_sz
);
613 /* leave one space for \0 in gopherToHTML */
614 Counter
.syscalls
.sock
.reads
++;
615 len
= read(fd
, buf
, read_sz
);
617 fd_bytes(fd
, len
, FD_READ
);
619 delayBytesIn(delay_id
, len
);
621 kb_incr(&Counter
.server
.all
.kbytes_in
, len
);
622 kb_incr(&Counter
.server
.other
.kbytes_in
, len
);
624 debug(10, 5) ("gopherReadReply: FD %d read len=%d\n", fd
, len
);
626 commSetTimeout(fd
, Config
.Timeout
.read
, NULL
, NULL
);
627 IOStats
.Gopher
.reads
++;
628 for (clen
= len
- 1, bin
= 0; clen
; bin
++)
630 IOStats
.Gopher
.read_hist
[bin
]++;
633 debug(50, 1) ("gopherReadReply: error reading: %s\n", xstrerror());
634 if (ignoreErrno(errno
)) {
635 commSetSelect(fd
, COMM_SELECT_READ
, gopherReadReply
, data
, 0);
636 } else if (entry
->mem_obj
->inmem_hi
== 0) {
638 err
= errorCon(ERR_READ_ERROR
, HTTP_INTERNAL_SERVER_ERROR
);
640 err
->url
= xstrdup(storeUrl(entry
));
641 errorAppendEntry(entry
, err
);
646 } else if (len
== 0 && entry
->mem_obj
->inmem_hi
== 0) {
648 err
= errorCon(ERR_ZERO_SIZE_OBJECT
, HTTP_SERVICE_UNAVAILABLE
);
650 err
->url
= xstrdup(gopherState
->request
);
651 errorAppendEntry(entry
, err
);
653 } else if (len
== 0) {
654 /* Connection closed; retrieval done. */
655 /* flush the rest of data in temp buf if there is one. */
656 if (gopherState
->conversion
!= NORMAL
)
658 storeTimestampsSet(entry
);
659 storeBufferFlush(entry
);
660 fwdComplete(gopherState
->fwdState
);
663 if (gopherState
->conversion
!= NORMAL
) {
664 gopherToHTML(data
, buf
, len
);
666 storeAppend(entry
, buf
, len
);
673 memFree(buf
, MEM_4K_BUF
);
677 /* This will be called when request write is complete. Schedule read of
680 gopherSendComplete(int fd
, char *buf
, size_t size
, int errflag
, void *data
)
682 GopherStateData
*gopherState
= (GopherStateData
*) data
;
683 StoreEntry
*entry
= gopherState
->entry
;
684 debug(10, 5) ("gopherSendComplete: FD %d size: %d errflag: %d\n",
687 fd_bytes(fd
, size
, FD_WRITE
);
688 kb_incr(&Counter
.server
.all
.kbytes_out
, size
);
689 kb_incr(&Counter
.server
.other
.kbytes_out
, size
);
693 err
= errorCon(ERR_CONNECT_FAIL
, HTTP_SERVICE_UNAVAILABLE
);
695 err
->host
= xstrdup(gopherState
->host
);
696 err
->port
= gopherState
->port
;
697 err
->url
= xstrdup(storeUrl(entry
));
698 errorAppendEntry(entry
, err
);
701 memFree(buf
, MEM_4K_BUF
); /* Allocated by gopherSendRequest. */
705 * OK. We successfully reach remote site. Start MIME typing
706 * stuff. Do it anyway even though request is not HTML type.
708 gopherMimeCreate(gopherState
);
709 switch (gopherState
->type_id
) {
710 case GOPHER_DIRECTORY
:
711 /* we got to convert it first */
713 gopherState
->conversion
= HTML_DIR
;
714 gopherState
->HTML_header_added
= 0;
717 /* we got to convert it first */
719 gopherState
->conversion
= HTML_INDEX_RESULT
;
720 gopherState
->HTML_header_added
= 0;
723 /* we got to convert it first */
725 gopherState
->conversion
= HTML_CSO_RESULT
;
726 gopherState
->cso_recno
= 0;
727 gopherState
->HTML_header_added
= 0;
730 gopherState
->conversion
= NORMAL
;
732 /* Schedule read reply. */
733 commSetSelect(fd
, COMM_SELECT_READ
, gopherReadReply
, gopherState
, 0);
734 commSetDefer(fd
, fwdCheckDeferRead
, entry
);
736 memFree(buf
, MEM_4K_BUF
); /* Allocated by gopherSendRequest. */
739 /* This will be called when connect completes. Write request. */
741 gopherSendRequest(int fd
, void *data
)
743 GopherStateData
*gopherState
= data
;
744 LOCAL_ARRAY(char, query
, MAX_URL
);
745 char *buf
= memAllocate(MEM_4K_BUF
);
747 if (gopherState
->type_id
== GOPHER_CSO
) {
748 sscanf(gopherState
->request
, "?%s", query
);
749 snprintf(buf
, 4096, "query %s\r\nquit\r\n", query
);
750 } else if (gopherState
->type_id
== GOPHER_INDEX
) {
751 if ((t
= strchr(gopherState
->request
, '?')))
753 snprintf(buf
, 4096, "%s\r\n", gopherState
->request
);
755 snprintf(buf
, 4096, "%s\r\n", gopherState
->request
);
757 debug(10, 5) ("gopherSendRequest: FD %d\n", fd
);
764 if (EBIT_TEST(gopherState
->entry
->flags
, ENTRY_CACHABLE
))
765 storeSetPublicKey(gopherState
->entry
); /* Make it public */
769 gopherStart(FwdState
* fwdState
)
771 int fd
= fwdState
->server_fd
;
772 StoreEntry
*entry
= fwdState
->entry
;
773 GopherStateData
*gopherState
= CreateGopherStateData();
774 storeLockObject(entry
);
775 gopherState
->entry
= entry
;
776 debug(10, 3) ("gopherStart: %s\n", storeUrl(entry
));
777 Counter
.server
.all
.requests
++;
778 Counter
.server
.other
.requests
++;
780 if (gopher_url_parser(storeUrl(entry
), gopherState
->host
, &gopherState
->port
,
781 &gopherState
->type_id
, gopherState
->request
)) {
783 err
= errorCon(ERR_INVALID_URL
, HTTP_BAD_REQUEST
);
784 err
->url
= xstrdup(storeUrl(entry
));
785 errorAppendEntry(entry
, err
);
786 gopherStateFree(-1, gopherState
);
789 comm_add_close_handler(fd
, gopherStateFree
, gopherState
);
790 if (((gopherState
->type_id
== GOPHER_INDEX
) || (gopherState
->type_id
== GOPHER_CSO
))
791 && (strchr(gopherState
->request
, '?') == NULL
)) {
792 /* Index URL without query word */
793 /* We have to generate search page back to client. No need for connection */
794 gopherMimeCreate(gopherState
);
795 if (gopherState
->type_id
== GOPHER_INDEX
) {
796 gopherState
->conversion
= HTML_INDEX_PAGE
;
798 if (gopherState
->type_id
== GOPHER_CSO
) {
799 gopherState
->conversion
= HTML_CSO_PAGE
;
801 gopherState
->conversion
= HTML_INDEX_PAGE
;
804 gopherToHTML(gopherState
, (char *) NULL
, 0);
805 fwdComplete(gopherState
->fwdState
);
809 gopherState
->fd
= fd
;
810 gopherState
->fwdState
= fwdState
;
811 commSetSelect(fd
, COMM_SELECT_WRITE
, gopherSendRequest
, gopherState
, 0);
812 commSetTimeout(fd
, Config
.Timeout
.read
, gopherTimeout
, gopherState
);
815 static GopherStateData
*
816 CreateGopherStateData(void)
818 GopherStateData
*gd
= xcalloc(1, sizeof(GopherStateData
));
819 cbdataAdd(gd
, cbdataXfree
, 0);
820 gd
->buf
= memAllocate(MEM_4K_BUF
);