xref: /haiku/src/kits/network/libnetservices/GopherRequest.cpp (revision cbe0a0c436162d78cc3f92a305b64918c839d079)
1 /*
2  * Copyright 2013-2014 Haiku Inc. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  * 		François Revol, revol@free.fr
7  */
8 
9 
10 #include <assert.h>
11 #include <ctype.h>
12 #include <stdlib.h>
13 #include <stdio.h>
14 
15 #include <Directory.h>
16 #include <DynamicBuffer.h>
17 #include <File.h>
18 #include <GopherRequest.h>
19 #include <NodeInfo.h>
20 #include <Path.h>
21 #include <Socket.h>
22 #include <StackOrHeapArray.h>
23 #include <String.h>
24 #include <StringList.h>
25 
26 
27 #ifndef LIBNETAPI_DEPRECATED
28 using namespace BPrivate::Network;
29 #endif
30 
31 /*
32  * TODO: fix '+' in selectors, cf. gopher://gophernicus.org/1/doc/gopher/
33  * TODO: add proper favicon
34  * TODO: add proper dir and document icons
35  * TODO: correctly eat the extraneous .\r\n at end of text files
36  * TODO: move parsing stuff to a translator?
37  *
38  * docs:
39  * gopher://gopher.floodgap.com/1/gopher/tech
40  * gopher://gopher.floodgap.com/0/overbite/dbrowse?pluginm%201
41  *
42  * tests:
43  * gopher://sdf.org/1/sdf/historical	images
44  * gopher://gopher.r-36.net/1/	large photos
45  * gopher://sdf.org/1/sdf/classes	binaries
46  * gopher://sdf.org/1/users/	long page
47  * gopher://jgw.mdns.org/1/	search items
48  * gopher://jgw.mdns.org/1/MISC/	's' item (sound)
49  * gopher://gopher.floodgap.com/1/gopher	broken link
50  * gopher://sdf.org/1/maps/m	missing lines
51  * gopher://sdf.org/1/foo	gophernicus reports errors incorrectly
52  * gopher://gopher.floodgap.com/1/foo	correct error report
53  */
54 
55 /** Type of Gopher items */
56 typedef enum {
57 	GOPHER_TYPE_NONE	= 0,	/**< none set */
58 	GOPHER_TYPE_ENDOFPAGE	= '.',	/**< a dot alone on a line */
59 	/* these come from http://tools.ietf.org/html/rfc1436 */
60 	GOPHER_TYPE_TEXTPLAIN	= '0',	/**< text/plain */
61 	GOPHER_TYPE_DIRECTORY	= '1',	/**< gopher directory */
62 	GOPHER_TYPE_CSO_SEARCH	= '2',	/**< CSO search */
63 	GOPHER_TYPE_ERROR	= '3',	/**< error message */
64 	GOPHER_TYPE_BINHEX	= '4',	/**< binhex encoded text */
65 	GOPHER_TYPE_BINARCHIVE	= '5',	/**< binary archive file */
66 	GOPHER_TYPE_UUENCODED	= '6',	/**< uuencoded text */
67 	GOPHER_TYPE_QUERY	= '7',	/**< gopher search query */
68 	GOPHER_TYPE_TELNET	= '8',	/**< telnet link */
69 	GOPHER_TYPE_BINARY	= '9',	/**< generic binary */
70 	GOPHER_TYPE_DUPSERV	= '+',	/**< duplicated server */
71 	GOPHER_TYPE_GIF		= 'g',	/**< GIF image */
72 	GOPHER_TYPE_IMAGE	= 'I',	/**< image (depends, usually jpeg) */
73 	GOPHER_TYPE_TN3270	= 'T',	/**< tn3270 session */
74 	/* not standardized but widely used,
75 	 * cf. http://en.wikipedia.org/wiki/Gopher_%28protocol%29#Gopher_item_types
76 	 */
77 	GOPHER_TYPE_HTML	= 'h',	/**< HTML file or URL */
78 	GOPHER_TYPE_INFO	= 'i',	/**< information text */
79 	GOPHER_TYPE_AUDIO	= 's',	/**< audio (wav?) */
80 	/* not standardized, some servers use them */
81 	GOPHER_TYPE_DOC		= 'd',	/**< gophernicus uses it for PS and PDF */
82 	GOPHER_TYPE_PNG		= 'p',	/**< PNG image */
83 		/* cf. gopher://namcub.accelera-labs.com/1/pics */
84 	GOPHER_TYPE_MIME	= 'M',	/**< multipart/mixed MIME data */
85 		/* cf. http://www.pms.ifi.lmu.de/mitarbeiter/ohlbach/multimedia/IT/IBMtutorial/3376c61.html */
86 	/* cf. http://nofixedpoint.motd.org/2011/02/22/an-introduction-to-the-gopher-protocol/ */
87 	GOPHER_TYPE_PDF		= 'P',	/**< PDF file */
88 	GOPHER_TYPE_BITMAP	= ':',	/**< Bitmap image (Gopher+) */
89 	GOPHER_TYPE_MOVIE	= ';',	/**< Movie (Gopher+) */
90 	GOPHER_TYPE_SOUND	= '<',	/**< Sound (Gopher+) */
91 	GOPHER_TYPE_CALENDAR	= 'c',	/**< Calendar */
92 	GOPHER_TYPE_EVENT	= 'e',	/**< Event */
93 	GOPHER_TYPE_MBOX	= 'm',	/**< mbox file */
94 } gopher_item_type;
95 
96 /** Types of fields in a line */
97 typedef enum {
98 	FIELD_NAME,
99 	FIELD_SELECTOR,
100 	FIELD_HOST,
101 	FIELD_PORT,
102 	FIELD_GPFLAG,
103 	FIELD_EOL,
104 	FIELD_COUNT = FIELD_EOL
105 } gopher_field;
106 
107 /** Map of gopher types to MIME types */
108 static struct {
109 	gopher_item_type type;
110 	const char *mime;
111 } gopher_type_map[] = {
112 	/* these come from http://tools.ietf.org/html/rfc1436 */
113 	{ GOPHER_TYPE_TEXTPLAIN, "text/plain" },
114 	{ GOPHER_TYPE_DIRECTORY, "text/html;charset=UTF-8" },
115 	{ GOPHER_TYPE_QUERY, "text/html;charset=UTF-8" },
116 	{ GOPHER_TYPE_GIF, "image/gif" },
117 	{ GOPHER_TYPE_HTML, "text/html" },
118 	/* those are not standardized */
119 	{ GOPHER_TYPE_PDF, "application/pdf" },
120 	{ GOPHER_TYPE_PNG, "image/png"},
121 	{ GOPHER_TYPE_NONE, NULL }
122 };
123 
124 static const char *kStyleSheet = "\n"
125 "/*\n"
126 " * gopher listing style\n"
127 " */\n"
128 "\n"
129 "body#gopher {\n"
130 "	/* margin: 10px;*/\n"
131 "	background-color: Window;\n"
132 "	color: WindowText;\n"
133 "	font-size: 100%;\n"
134 "	padding-bottom: 2em; }\n"
135 "\n"
136 "body#gopher div.uplink {\n"
137 "	padding: 0;\n"
138 "	margin: 0;\n"
139 "	position: fixed;\n"
140 "	top: 5px;\n"
141 "	right: 5px; }\n"
142 "\n"
143 "body#gopher h1 {\n"
144 "	padding: 5mm;\n"
145 "	margin: 0;\n"
146 "	border-bottom: 2px solid #777; }\n"
147 "\n"
148 "body#gopher span {\n"
149 "	margin-left: 1em;\n"
150 "	padding-left: 2em;\n"
151 "	font-family: 'Noto Sans Mono', Courier, monospace;\n"
152 "	word-wrap: break-word;\n"
153 "	white-space: pre-wrap; }\n"
154 "\n"
155 "body#gopher span.error {\n"
156 "	color: #f00; }\n"
157 "\n"
158 "body#gopher span.unknown {\n"
159 "	color: #800; }\n"
160 "\n"
161 "body#gopher span.dir {\n"
162 "	background-image: url('resource:icons/directory.png');\n"
163 "	background-repeat: no-repeat;\n"
164 "	background-position: bottom left; }\n"
165 "\n"
166 "body#gopher span.text {\n"
167 "	background-image: url('resource:icons/content.png');\n"
168 "	background-repeat: no-repeat;\n"
169 "	background-position: bottom left; }\n"
170 "\n"
171 "body#gopher span.query {\n"
172 "	background-image: url('resource:icons/search.png');\n"
173 "	background-repeat: no-repeat;\n"
174 "	background-position: bottom left; }\n"
175 "\n"
176 "body#gopher span.img img {\n"
177 "	display: block;\n"
178 "	margin-left:auto;\n"
179 "	margin-right:auto; }\n";
180 
181 static const int32 kGopherBufferSize = 4096;
182 
183 static const bool kInlineImages = true;
184 
185 
186 #ifdef LIBNETAPI_DEPRECATED
187 BGopherRequest::BGopherRequest(const BUrl& url, BUrlProtocolListener* listener,
188 	BUrlContext* context)
189 	:
190 	BNetworkRequest(url, listener, context, "BUrlProtocol.Gopher", "gopher"),
191 	fItemType(GOPHER_TYPE_NONE),
192 	fPosition(0),
193 	fResult()
194 {
195 	fSocket = new(std::nothrow) BSocket();
196 
197 	fUrl.UrlDecode();
198 	// the first part of the path is actually the document type
199 
200 	fPath = Url().Path();
201 	if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") {
202 		// default entry
203 		fItemType = GOPHER_TYPE_DIRECTORY;
204 		fPath = "";
205 	} else if (fPath.Length() > 1 && fPath[0] == '/') {
206 		fItemType = fPath[1];
207 		fPath.Remove(0, 2);
208 	}
209 }
210 
211 #else
212 
213 BGopherRequest::BGopherRequest(const BUrl& url, BDataIO* output,
214 	BUrlProtocolListener* listener, BUrlContext* context)
215 	:
216 	BNetworkRequest(url, output, listener, context, "BUrlProtocol.Gopher",
217 		"gopher"),
218 	fItemType(GOPHER_TYPE_NONE),
219 	fPosition(0),
220 	fResult()
221 {
222 	fSocket = new(std::nothrow) BSocket();
223 
224 	fUrl.UrlDecode();
225 	// the first part of the path is actually the document type
226 
227 	fPath = Url().Path();
228 	if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") {
229 		// default entry
230 		fItemType = GOPHER_TYPE_DIRECTORY;
231 		fPath = "";
232 	} else if (fPath.Length() > 1 && fPath[0] == '/') {
233 		fItemType = fPath[1];
234 		fPath.Remove(0, 2);
235 	}
236 }
237 #endif // LIBNETAPI_DEPRECATED
238 
239 
240 BGopherRequest::~BGopherRequest()
241 {
242 	Stop();
243 
244 	delete fSocket;
245 }
246 
247 
248 status_t
249 BGopherRequest::Stop()
250 {
251 	if (fSocket != NULL) {
252 		fSocket->Disconnect();
253 			// Unlock any pending connect, read or write operation.
254 	}
255 	return BNetworkRequest::Stop();
256 }
257 
258 
259 const BUrlResult&
260 BGopherRequest::Result() const
261 {
262 	return fResult;
263 }
264 
265 
266 #ifdef LIBNETAPI_DEPRECATED
267 status_t
268 BGopherRequest::_ProtocolLoop()
269 {
270 	if (fSocket == NULL)
271 		return B_NO_MEMORY;
272 
273 	if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) {
274 		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR,
275 			"Unable to resolve hostname (%s), aborting.",
276 				fUrl.Host().String());
277 		return B_SERVER_NOT_FOUND;
278 	}
279 
280 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.",
281 		fUrl.Authority().String(), fRemoteAddr.Port());
282 	status_t connectError = fSocket->Connect(fRemoteAddr);
283 
284 	if (connectError != B_OK) {
285 		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s",
286 			strerror(connectError));
287 		return connectError;
288 	}
289 
290 	//! ProtocolHook:ConnectionOpened
291 	if (fListener != NULL)
292 		fListener->ConnectionOpened(this);
293 
294 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
295 		"Connection opened, sending request.");
296 
297 	_SendRequest();
298 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent.");
299 
300 	// Receive loop
301 	bool receiveEnd = false;
302 	status_t readError = B_OK;
303 	ssize_t bytesRead = 0;
304 	//ssize_t bytesReceived = 0;
305 	//ssize_t bytesTotal = 0;
306 	bool dataValidated = false;
307 	BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize);
308 
309 	while (!fQuit && !receiveEnd) {
310 		bytesRead = fSocket->Read(chunk, kGopherBufferSize);
311 
312 		if (bytesRead < 0) {
313 			readError = bytesRead;
314 			break;
315 		} else if (bytesRead == 0)
316 			receiveEnd = true;
317 
318 		fInputBuffer.AppendData(chunk, bytesRead);
319 
320 		if (!dataValidated) {
321 			size_t i;
322 			// on error (file doesn't exist, ...) the server sends
323 			// a faked directory entry with an error message
324 			if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') {
325 				int tabs = 0;
326 				bool crlf = false;
327 
328 				// make sure the buffer only contains printable characters
329 				// and has at least 3 tabs before a CRLF
330 				for (i = 0; i < fInputBuffer.Size(); i++) {
331 					char c = fInputBuffer.Data()[i];
332 					if (c == '\t') {
333 						if (!crlf)
334 							tabs++;
335 					} else if (c == '\r' || c == '\n') {
336 						if (tabs < 3)
337 							break;
338 						crlf = true;
339 					} else if (!isprint(fInputBuffer.Data()[i])) {
340 						crlf = false;
341 						break;
342 					}
343 				}
344 				if (crlf && tabs > 2 && tabs < 5) {
345 					// TODO:
346 					//if enough data
347 					// else continue
348 					fItemType = GOPHER_TYPE_DIRECTORY;
349 					readError = B_RESOURCE_NOT_FOUND;
350 					// continue parsing the error text anyway
351 				}
352 			}
353 			// special case for buggy(?) Gophernicus/1.5
354 			static const char *buggy = "Error: File or directory not found!";
355 			if (fInputBuffer.Size() > strlen(buggy)
356 				&& !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) {
357 				fItemType = GOPHER_TYPE_DIRECTORY;
358 				readError = B_RESOURCE_NOT_FOUND;
359 				// continue parsing the error text anyway
360 				// but it won't look good
361 			}
362 
363 			// now we probably have correct data
364 			dataValidated = true;
365 
366 			//! ProtocolHook:ResponseStarted
367 			if (fListener != NULL)
368 				fListener->ResponseStarted(this);
369 
370 			// now we can assign MIME type if we know it
371 			const char *mime = "application/octet-stream";
372 			for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) {
373 				if (gopher_type_map[i].type == fItemType) {
374 					mime = gopher_type_map[i].mime;
375 					break;
376 				}
377 			}
378 			fResult.SetContentType(mime);
379 
380 			// we don't really have headers but well...
381 			//! ProtocolHook:HeadersReceived
382 			if (fListener != NULL)
383 				fListener->HeadersReceived(this, fResult);
384 		}
385 
386 		if (_NeedsParsing())
387 			_ParseInput(receiveEnd);
388 		else if (fInputBuffer.Size()) {
389 			// send input directly
390 			if (fListener != NULL) {
391 				fListener->DataReceived(this, (const char *)fInputBuffer.Data(),
392 					fPosition, fInputBuffer.Size());
393 			}
394 
395 			fPosition += fInputBuffer.Size();
396 
397 			if (fListener != NULL)
398 				fListener->DownloadProgress(this, fPosition, 0);
399 
400 			// XXX: this is plain stupid, we already copied the data
401 			// and just want to drop it...
402 			char *inputTempBuffer = new(std::nothrow) char[bytesRead];
403 			if (inputTempBuffer == NULL) {
404 				readError = B_NO_MEMORY;
405 				break;
406 			}
407 			fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size());
408 			delete[] inputTempBuffer;
409 		}
410 	}
411 
412 	if (fPosition > 0)
413 		fResult.SetLength(fPosition);
414 
415 	fSocket->Disconnect();
416 
417 	if (readError != B_OK)
418 		return readError;
419 
420 	return fQuit ? B_INTERRUPTED : B_OK;
421 }
422 
423 # else
424 
425 status_t
426 BGopherRequest::_ProtocolLoop()
427 {
428 	if (fSocket == NULL)
429 		return B_NO_MEMORY;
430 
431 	if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) {
432 		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR,
433 			"Unable to resolve hostname (%s), aborting.",
434 				fUrl.Host().String());
435 		return B_SERVER_NOT_FOUND;
436 	}
437 
438 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.",
439 		fUrl.Authority().String(), fRemoteAddr.Port());
440 	status_t connectError = fSocket->Connect(fRemoteAddr);
441 
442 	if (connectError != B_OK) {
443 		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s",
444 			strerror(connectError));
445 		return connectError;
446 	}
447 
448 	//! ProtocolHook:ConnectionOpened
449 	if (fListener != NULL)
450 		fListener->ConnectionOpened(this);
451 
452 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
453 		"Connection opened, sending request.");
454 
455 	_SendRequest();
456 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent.");
457 
458 	// Receive loop
459 	bool receiveEnd = false;
460 	status_t readError = B_OK;
461 	ssize_t bytesRead = 0;
462 	//ssize_t bytesReceived = 0;
463 	//ssize_t bytesTotal = 0;
464 	bool dataValidated = false;
465 	BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize);
466 
467 	while (!fQuit && !receiveEnd) {
468 		bytesRead = fSocket->Read(chunk, kGopherBufferSize);
469 
470 		if (bytesRead < 0) {
471 			readError = bytesRead;
472 			break;
473 		} else if (bytesRead == 0)
474 			receiveEnd = true;
475 
476 		fInputBuffer.AppendData(chunk, bytesRead);
477 
478 		if (!dataValidated) {
479 			size_t i;
480 			// on error (file doesn't exist, ...) the server sends
481 			// a faked directory entry with an error message
482 			if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') {
483 				int tabs = 0;
484 				bool crlf = false;
485 
486 				// make sure the buffer only contains printable characters
487 				// and has at least 3 tabs before a CRLF
488 				for (i = 0; i < fInputBuffer.Size(); i++) {
489 					char c = fInputBuffer.Data()[i];
490 					if (c == '\t') {
491 						if (!crlf)
492 							tabs++;
493 					} else if (c == '\r' || c == '\n') {
494 						if (tabs < 3)
495 							break;
496 						crlf = true;
497 					} else if (!isprint(fInputBuffer.Data()[i])) {
498 						crlf = false;
499 						break;
500 					}
501 				}
502 				if (crlf && tabs > 2 && tabs < 5) {
503 					// TODO:
504 					//if enough data
505 					// else continue
506 					fItemType = GOPHER_TYPE_DIRECTORY;
507 					readError = B_RESOURCE_NOT_FOUND;
508 					// continue parsing the error text anyway
509 				}
510 			}
511 			// special case for buggy(?) Gophernicus/1.5
512 			static const char *buggy = "Error: File or directory not found!";
513 			if (fInputBuffer.Size() > strlen(buggy)
514 				&& !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) {
515 				fItemType = GOPHER_TYPE_DIRECTORY;
516 				readError = B_RESOURCE_NOT_FOUND;
517 				// continue parsing the error text anyway
518 				// but it won't look good
519 			}
520 
521 			// now we probably have correct data
522 			dataValidated = true;
523 
524 			//! ProtocolHook:ResponseStarted
525 			if (fListener != NULL)
526 				fListener->ResponseStarted(this);
527 
528 			// now we can assign MIME type if we know it
529 			const char *mime = "application/octet-stream";
530 			for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) {
531 				if (gopher_type_map[i].type == fItemType) {
532 					mime = gopher_type_map[i].mime;
533 					break;
534 				}
535 			}
536 			fResult.SetContentType(mime);
537 
538 			// we don't really have headers but well...
539 			//! ProtocolHook:HeadersReceived
540 			if (fListener != NULL)
541 				fListener->HeadersReceived(this);
542 		}
543 
544 		if (_NeedsParsing())
545 			readError = _ParseInput(receiveEnd);
546 		else if (fInputBuffer.Size()) {
547 			// send input directly
548 			if (fOutput != NULL) {
549 				size_t written = 0;
550 				readError = fOutput->WriteExactly(
551 					(const char*)fInputBuffer.Data(), fInputBuffer.Size(),
552 					&written);
553 				if (fListener != NULL && written > 0)
554 					fListener->BytesWritten(this, written);
555 				if (readError != B_OK)
556 					break;
557 			}
558 
559 			fPosition += fInputBuffer.Size();
560 
561 			if (fListener != NULL)
562 				fListener->DownloadProgress(this, fPosition, 0);
563 
564 			// XXX: this is plain stupid, we already copied the data
565 			// and just want to drop it...
566 			char *inputTempBuffer = new(std::nothrow) char[bytesRead];
567 			if (inputTempBuffer == NULL) {
568 				readError = B_NO_MEMORY;
569 				break;
570 			}
571 			fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size());
572 			delete[] inputTempBuffer;
573 		}
574 	}
575 
576 	if (fPosition > 0)
577 		fResult.SetLength(fPosition);
578 
579 	fSocket->Disconnect();
580 
581 	if (readError != B_OK)
582 		return readError;
583 
584 	return fQuit ? B_INTERRUPTED : B_OK;
585 }
586 #endif // LIBNETAPI_DEPRECATED
587 
588 
589 void
590 BGopherRequest::_SendRequest()
591 {
592 	BString request;
593 
594 	request << fPath;
595 
596 	if (Url().HasRequest())
597 		request << '\t' << Url().Request();
598 
599 	request << "\r\n";
600 
601 	fSocket->Write(request.String(), request.Length());
602 }
603 
604 
605 bool
606 BGopherRequest::_NeedsParsing()
607 {
608 	if (fItemType == GOPHER_TYPE_DIRECTORY
609 		|| fItemType == GOPHER_TYPE_QUERY)
610 		return true;
611 	return false;
612 }
613 
614 
615 bool
616 BGopherRequest::_NeedsLastDotStrip()
617 {
618 	if (fItemType == GOPHER_TYPE_DIRECTORY
619 		|| fItemType == GOPHER_TYPE_QUERY
620 		|| fItemType == GOPHER_TYPE_TEXTPLAIN)
621 		return true;
622 	return false;
623 }
624 
625 
626 #ifdef LIBNETAPI_DEPRECATED
627 void
628 BGopherRequest::_ParseInput(bool last)
629 {
630 	BString line;
631 
632 	while (_GetLine(line) == B_OK) {
633 		char type = GOPHER_TYPE_NONE;
634 		BStringList fields;
635 
636 		line.MoveInto(&type, 0, 1);
637 
638 		line.Split("\t", false, fields);
639 
640 		if (type != GOPHER_TYPE_ENDOFPAGE
641 			&& fields.CountStrings() < FIELD_GPFLAG)
642 			_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
643 				"Unterminated gopher item (type '%c')", type);
644 
645 		BString pageTitle;
646 		BString item;
647 		BString title = fields.StringAt(FIELD_NAME);
648 		BString link("gopher://");
649 		BString user;
650 		if (fields.CountStrings() > 3) {
651 			link << fields.StringAt(FIELD_HOST);
652 			if (fields.StringAt(FIELD_PORT).Length())
653 				link << ":" << fields.StringAt(FIELD_PORT);
654 			link << "/" << type;
655 			//if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/')
656 			//	link << "/";
657 			link << fields.StringAt(FIELD_SELECTOR);
658 		}
659 		_HTMLEscapeString(title);
660 		_HTMLEscapeString(link);
661 
662 		switch (type) {
663 			case GOPHER_TYPE_ENDOFPAGE:
664 				/* end of the page */
665 				break;
666 			case GOPHER_TYPE_TEXTPLAIN:
667 				item << "<a href=\"" << link << "\">"
668 						"<span class=\"text\">" << title << "</span></a>"
669 						"<br/>\n";
670 				break;
671 			case GOPHER_TYPE_BINARY:
672 			case GOPHER_TYPE_BINHEX:
673 			case GOPHER_TYPE_BINARCHIVE:
674 			case GOPHER_TYPE_UUENCODED:
675 				item << "<a href=\"" << link << "\">"
676 						"<span class=\"binary\">" << title << "</span></a>"
677 						"<br/>\n";
678 				break;
679 			case GOPHER_TYPE_DIRECTORY:
680 				/*
681 				 * directory link
682 				 */
683 				item << "<a href=\"" << link << "\">"
684 						"<span class=\"dir\">" << title << "</span></a>"
685 						"<br/>\n";
686 				break;
687 			case GOPHER_TYPE_ERROR:
688 				item << "<span class=\"error\">" << title << "</span>"
689 						"<br/>\n";
690 				if (fPosition == 0 && pageTitle.Length() == 0)
691 					pageTitle << "Error: " << title;
692 				break;
693 			case GOPHER_TYPE_QUERY:
694 				/* TODO: handle search better.
695 				 * For now we use an unnamed input field and accept sending ?=foo
696 				 * as it seems at least Veronica-2 ignores the = but it's unclean.
697 				 */
698 				item << "<form method=\"get\" action=\"" << link << "\" "
699 							"onsubmit=\"window.location = this.action + '?' + "
700 								"this.elements['q'].value; return false;\">"
701 						"<span class=\"query\">"
702 						"<label>" << title << " "
703 						"<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />"
704 						"</label>"
705 						"</span></form>"
706 						"<br/>\n";
707 				break;
708 			case GOPHER_TYPE_TELNET:
709 				/* telnet: links
710 				 * cf. gopher://78.80.30.202/1/ps3
711 				 * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202
712 				 */
713 				link = "telnet://";
714 				user = fields.StringAt(FIELD_SELECTOR);
715 				if (user.FindLast('/') > -1) {
716 					user.Remove(0, user.FindLast('/'));
717 					link << user << "@";
718 				}
719 				link << fields.StringAt(FIELD_HOST);
720 				if (fields.StringAt(FIELD_PORT) != "23")
721 					link << ":" << fields.StringAt(FIELD_PORT);
722 
723 				item << "<a href=\"" << link << "\">"
724 						"<span class=\"telnet\">" << title << "</span></a>"
725 						"<br/>\n";
726 				break;
727 			case GOPHER_TYPE_TN3270:
728 				/* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */
729 				link = "tn3270://";
730 				user = fields.StringAt(FIELD_SELECTOR);
731 				if (user.FindLast('/') > -1) {
732 					user.Remove(0, user.FindLast('/'));
733 					link << user << "@";
734 				}
735 				link << fields.StringAt(FIELD_HOST);
736 				if (fields.StringAt(FIELD_PORT) != "23")
737 					link << ":" << fields.StringAt(FIELD_PORT);
738 
739 				item << "<a href=\"" << link << "\">"
740 						"<span class=\"telnet\">" << title << "</span></a>"
741 						"<br/>\n";
742 				break;
743 			case GOPHER_TYPE_CSO_SEARCH:
744 				/* CSO search.
745 				 * At least Lynx supports a cso:// URI scheme:
746 				 * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html
747 				 */
748 				link = "cso://";
749 				user = fields.StringAt(FIELD_SELECTOR);
750 				if (user.FindLast('/') > -1) {
751 					user.Remove(0, user.FindLast('/'));
752 					link << user << "@";
753 				}
754 				link << fields.StringAt(FIELD_HOST);
755 				if (fields.StringAt(FIELD_PORT) != "105")
756 					link << ":" << fields.StringAt(FIELD_PORT);
757 
758 				item << "<a href=\"" << link << "\">"
759 						"<span class=\"cso\">" << title << "</span></a>"
760 						"<br/>\n";
761 				break;
762 			case GOPHER_TYPE_GIF:
763 			case GOPHER_TYPE_IMAGE:
764 			case GOPHER_TYPE_PNG:
765 			case GOPHER_TYPE_BITMAP:
766 				/* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */
767 				if (kInlineImages) {
768 					item << "<a href=\"" << link << "\">"
769 							"<span class=\"img\">" << title << " "
770 							"<img src=\"" << link << "\" "
771 								"alt=\"" << title << "\"/>"
772 							"</span></a>"
773 							"<br/>\n";
774 					break;
775 				}
776 				/* fallback to default, link them */
777 				item << "<a href=\"" << link << "\">"
778 						"<span class=\"img\">" << title << "</span></a>"
779 						"<br/>\n";
780 				break;
781 			case GOPHER_TYPE_HTML:
782 				/* cf. gopher://pineapple.vg/1 */
783 				if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) {
784 					link = fields.StringAt(FIELD_SELECTOR);
785 					link.Remove(0, 4);
786 				}
787 				/* cf. gopher://sdf.org/1/sdf/classes/ */
788 
789 				item << "<a href=\"" << link << "\">"
790 						"<span class=\"html\">" << title << "</span></a>"
791 						"<br/>\n";
792 				break;
793 			case GOPHER_TYPE_INFO:
794 				// TITLE resource, cf.
795 				// gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt
796 				if (fPosition == 0 && pageTitle.Length() == 0
797 					&& fields.StringAt(FIELD_SELECTOR) == "TITLE") {
798 						pageTitle = title;
799 						break;
800 				}
801 				item << "<span class=\"info\">" << title << "</span>"
802 						"<br/>\n";
803 				break;
804 			case GOPHER_TYPE_AUDIO:
805 			case GOPHER_TYPE_SOUND:
806 				item << "<a href=\"" << link << "\">"
807 						"<span class=\"audio\">" << title << "</span></a>"
808 						"<audio src=\"" << link << "\" "
809 							//TODO:Fix crash in WebPositive with these
810 							//"controls=\"controls\" "
811 							//"width=\"300\" height=\"50\" "
812 							"alt=\"" << title << "\"/>"
813 						"<span>[player]</span></audio>"
814 						"<br/>\n";
815 				break;
816 			case GOPHER_TYPE_PDF:
817 			case GOPHER_TYPE_DOC:
818 				/* generic case for known-to-work items */
819 				item << "<a href=\"" << link << "\">"
820 						"<span class=\"document\">" << title << "</span></a>"
821 						"<br/>\n";
822 				break;
823 			case GOPHER_TYPE_MOVIE:
824 				item << "<a href=\"" << link << "\">"
825 						"<span class=\"video\">" << title << "</span></a>"
826 						"<video src=\"" << link << "\" "
827 							//TODO:Fix crash in WebPositive with these
828 							//"controls=\"controls\" "
829 							//"width=\"300\" height=\"300\" "
830 							"alt=\"" << title << "\"/>"
831 						"<span>[player]</span></audio>"
832 						"<br/>\n";
833 				break;
834 			default:
835 				_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
836 					"Unknown gopher item (type 0x%02x '%c')", type, type);
837 				item << "<a href=\"" << link << "\">"
838 						"<span class=\"unknown\">" << title << "</span></a>"
839 						"<br/>\n";
840 				break;
841 		}
842 
843 		if (fPosition == 0) {
844 			if (pageTitle.Length() == 0)
845 				pageTitle << "Index of " << Url();
846 
847 			const char *uplink = ".";
848 			if (fPath.EndsWith("/"))
849 				uplink = "..";
850 
851 			// emit header
852 			BString header;
853 			header <<
854 				"<html>\n"
855 				"<head>\n"
856 				"<meta http-equiv=\"Content-Type\""
857 					" content=\"text/html; charset=UTF-8\" />\n"
858 				//FIXME: fix links
859 				//"<link rel=\"icon\" type=\"image/png\""
860 				//	" href=\"resource:icons/directory.png\">\n"
861 				"<style type=\"text/css\">\n" << kStyleSheet << "</style>\n"
862 				"<title>" << pageTitle << "</title>\n"
863 				"</head>\n"
864 				"<body id=\"gopher\">\n"
865 				"<div class=\"uplink dontprint\">\n"
866 				"<a href=" << uplink << ">[up]</a>\n"
867 				"<a href=\"/\">[top]</a>\n"
868 				"</div>\n"
869 				"<h1>" << pageTitle << "</h1>\n";
870 
871 			if (fListener != NULL) {
872 				fListener->DataReceived(this, header.String(), fPosition,
873 					header.Length());
874 			}
875 
876 			fPosition += header.Length();
877 
878 			if (fListener != NULL)
879 				fListener->DownloadProgress(this, fPosition, 0);
880 		}
881 
882 		if (item.Length()) {
883 			if (fListener != NULL) {
884 				fListener->DataReceived(this, item.String(), fPosition,
885 					item.Length());
886 			}
887 
888 			fPosition += item.Length();
889 
890 			if (fListener != NULL)
891 				fListener->DownloadProgress(this, fPosition, 0);
892 		}
893 	}
894 
895 	if (last) {
896 		// emit footer
897 		BString footer =
898 			"</div>\n"
899 			"</body>\n"
900 			"</html>\n";
901 
902 		if (fListener != NULL) {
903 			fListener->DataReceived(this, footer.String(), fPosition,
904 				footer.Length());
905 		}
906 
907 		fPosition += footer.Length();
908 
909 		if (fListener != NULL)
910 			fListener->DownloadProgress(this, fPosition, 0);
911 	}
912 }
913 
914 #else
915 
916 status_t
917 BGopherRequest::_ParseInput(bool last)
918 {
919 	BString line;
920 
921 	while (_GetLine(line) == B_OK) {
922 		char type = GOPHER_TYPE_NONE;
923 		BStringList fields;
924 
925 		line.MoveInto(&type, 0, 1);
926 
927 		line.Split("\t", false, fields);
928 
929 		if (type != GOPHER_TYPE_ENDOFPAGE
930 			&& fields.CountStrings() < FIELD_GPFLAG)
931 			_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
932 				"Unterminated gopher item (type '%c')", type);
933 
934 		BString pageTitle;
935 		BString item;
936 		BString title = fields.StringAt(FIELD_NAME);
937 		BString link("gopher://");
938 		BString user;
939 		if (fields.CountStrings() > 3) {
940 			link << fields.StringAt(FIELD_HOST);
941 			if (fields.StringAt(FIELD_PORT).Length())
942 				link << ":" << fields.StringAt(FIELD_PORT);
943 			link << "/" << type;
944 			//if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/')
945 			//	link << "/";
946 			link << fields.StringAt(FIELD_SELECTOR);
947 		}
948 		_HTMLEscapeString(title);
949 		_HTMLEscapeString(link);
950 
951 		switch (type) {
952 			case GOPHER_TYPE_ENDOFPAGE:
953 				/* end of the page */
954 				break;
955 			case GOPHER_TYPE_TEXTPLAIN:
956 				item << "<a href=\"" << link << "\">"
957 						"<span class=\"text\">" << title << "</span></a>"
958 						"<br/>\n";
959 				break;
960 			case GOPHER_TYPE_BINARY:
961 			case GOPHER_TYPE_BINHEX:
962 			case GOPHER_TYPE_BINARCHIVE:
963 			case GOPHER_TYPE_UUENCODED:
964 				item << "<a href=\"" << link << "\">"
965 						"<span class=\"binary\">" << title << "</span></a>"
966 						"<br/>\n";
967 				break;
968 			case GOPHER_TYPE_DIRECTORY:
969 				/*
970 				 * directory link
971 				 */
972 				item << "<a href=\"" << link << "\">"
973 						"<span class=\"dir\">" << title << "</span></a>"
974 						"<br/>\n";
975 				break;
976 			case GOPHER_TYPE_ERROR:
977 				item << "<span class=\"error\">" << title << "</span>"
978 						"<br/>\n";
979 				if (fPosition == 0 && pageTitle.Length() == 0)
980 					pageTitle << "Error: " << title;
981 				break;
982 			case GOPHER_TYPE_QUERY:
983 				/* TODO: handle search better.
984 				 * For now we use an unnamed input field and accept sending ?=foo
985 				 * as it seems at least Veronica-2 ignores the = but it's unclean.
986 				 */
987 				item << "<form method=\"get\" action=\"" << link << "\" "
988 							"onsubmit=\"window.location = this.action + '?' + "
989 								"this.elements['q'].value; return false;\">"
990 						"<span class=\"query\">"
991 						"<label>" << title << " "
992 						"<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />"
993 						"</label>"
994 						"</span></form>"
995 						"<br/>\n";
996 				break;
997 			case GOPHER_TYPE_TELNET:
998 				/* telnet: links
999 				 * cf. gopher://78.80.30.202/1/ps3
1000 				 * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202
1001 				 */
1002 				link = "telnet://";
1003 				user = fields.StringAt(FIELD_SELECTOR);
1004 				if (user.FindLast('/') > -1) {
1005 					user.Remove(0, user.FindLast('/'));
1006 					link << user << "@";
1007 				}
1008 				link << fields.StringAt(FIELD_HOST);
1009 				if (fields.StringAt(FIELD_PORT) != "23")
1010 					link << ":" << fields.StringAt(FIELD_PORT);
1011 
1012 				item << "<a href=\"" << link << "\">"
1013 						"<span class=\"telnet\">" << title << "</span></a>"
1014 						"<br/>\n";
1015 				break;
1016 			case GOPHER_TYPE_TN3270:
1017 				/* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */
1018 				link = "tn3270://";
1019 				user = fields.StringAt(FIELD_SELECTOR);
1020 				if (user.FindLast('/') > -1) {
1021 					user.Remove(0, user.FindLast('/'));
1022 					link << user << "@";
1023 				}
1024 				link << fields.StringAt(FIELD_HOST);
1025 				if (fields.StringAt(FIELD_PORT) != "23")
1026 					link << ":" << fields.StringAt(FIELD_PORT);
1027 
1028 				item << "<a href=\"" << link << "\">"
1029 						"<span class=\"telnet\">" << title << "</span></a>"
1030 						"<br/>\n";
1031 				break;
1032 			case GOPHER_TYPE_CSO_SEARCH:
1033 				/* CSO search.
1034 				 * At least Lynx supports a cso:// URI scheme:
1035 				 * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html
1036 				 */
1037 				link = "cso://";
1038 				user = fields.StringAt(FIELD_SELECTOR);
1039 				if (user.FindLast('/') > -1) {
1040 					user.Remove(0, user.FindLast('/'));
1041 					link << user << "@";
1042 				}
1043 				link << fields.StringAt(FIELD_HOST);
1044 				if (fields.StringAt(FIELD_PORT) != "105")
1045 					link << ":" << fields.StringAt(FIELD_PORT);
1046 
1047 				item << "<a href=\"" << link << "\">"
1048 						"<span class=\"cso\">" << title << "</span></a>"
1049 						"<br/>\n";
1050 				break;
1051 			case GOPHER_TYPE_GIF:
1052 			case GOPHER_TYPE_IMAGE:
1053 			case GOPHER_TYPE_PNG:
1054 			case GOPHER_TYPE_BITMAP:
1055 				/* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */
1056 				if (kInlineImages) {
1057 					item << "<a href=\"" << link << "\">"
1058 							"<span class=\"img\">" << title << " "
1059 							"<img src=\"" << link << "\" "
1060 								"alt=\"" << title << "\"/>"
1061 							"</span></a>"
1062 							"<br/>\n";
1063 					break;
1064 				}
1065 				/* fallback to default, link them */
1066 				item << "<a href=\"" << link << "\">"
1067 						"<span class=\"img\">" << title << "</span></a>"
1068 						"<br/>\n";
1069 				break;
1070 			case GOPHER_TYPE_HTML:
1071 				/* cf. gopher://pineapple.vg/1 */
1072 				if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) {
1073 					link = fields.StringAt(FIELD_SELECTOR);
1074 					link.Remove(0, 4);
1075 				}
1076 				/* cf. gopher://sdf.org/1/sdf/classes/ */
1077 
1078 				item << "<a href=\"" << link << "\">"
1079 						"<span class=\"html\">" << title << "</span></a>"
1080 						"<br/>\n";
1081 				break;
1082 			case GOPHER_TYPE_INFO:
1083 				// TITLE resource, cf.
1084 				// gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt
1085 				if (fPosition == 0 && pageTitle.Length() == 0
1086 					&& fields.StringAt(FIELD_SELECTOR) == "TITLE") {
1087 						pageTitle = title;
1088 						break;
1089 				}
1090 				item << "<span class=\"info\">" << title << "</span>"
1091 						"<br/>\n";
1092 				break;
1093 			case GOPHER_TYPE_AUDIO:
1094 			case GOPHER_TYPE_SOUND:
1095 				item << "<a href=\"" << link << "\">"
1096 						"<span class=\"audio\">" << title << "</span></a>"
1097 						"<audio src=\"" << link << "\" "
1098 							//TODO:Fix crash in WebPositive with these
1099 							//"controls=\"controls\" "
1100 							//"width=\"300\" height=\"50\" "
1101 							"alt=\"" << title << "\"/>"
1102 						"<span>[player]</span></audio>"
1103 						"<br/>\n";
1104 				break;
1105 			case GOPHER_TYPE_PDF:
1106 			case GOPHER_TYPE_DOC:
1107 				/* generic case for known-to-work items */
1108 				item << "<a href=\"" << link << "\">"
1109 						"<span class=\"document\">" << title << "</span></a>"
1110 						"<br/>\n";
1111 				break;
1112 			case GOPHER_TYPE_MOVIE:
1113 				item << "<a href=\"" << link << "\">"
1114 						"<span class=\"video\">" << title << "</span></a>"
1115 						"<video src=\"" << link << "\" "
1116 							//TODO:Fix crash in WebPositive with these
1117 							//"controls=\"controls\" "
1118 							//"width=\"300\" height=\"300\" "
1119 							"alt=\"" << title << "\"/>"
1120 						"<span>[player]</span></audio>"
1121 						"<br/>\n";
1122 				break;
1123 			default:
1124 				_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
1125 					"Unknown gopher item (type 0x%02x '%c')", type, type);
1126 				item << "<a href=\"" << link << "\">"
1127 						"<span class=\"unknown\">" << title << "</span></a>"
1128 						"<br/>\n";
1129 				break;
1130 		}
1131 
1132 		if (fPosition == 0) {
1133 			if (pageTitle.Length() == 0)
1134 				pageTitle << "Index of " << Url();
1135 
1136 			const char *uplink = ".";
1137 			if (fPath.EndsWith("/"))
1138 				uplink = "..";
1139 
1140 			// emit header
1141 			BString header;
1142 			header <<
1143 				"<html>\n"
1144 				"<head>\n"
1145 				"<meta http-equiv=\"Content-Type\""
1146 					" content=\"text/html; charset=UTF-8\" />\n"
1147 				//FIXME: fix links
1148 				//"<link rel=\"icon\" type=\"image/png\""
1149 				//	" href=\"resource:icons/directory.png\">\n"
1150 				"<style type=\"text/css\">\n" << kStyleSheet << "</style>\n"
1151 				"<title>" << pageTitle << "</title>\n"
1152 				"</head>\n"
1153 				"<body id=\"gopher\">\n"
1154 				"<div class=\"uplink dontprint\">\n"
1155 				"<a href=" << uplink << ">[up]</a>\n"
1156 				"<a href=\"/\">[top]</a>\n"
1157 				"</div>\n"
1158 				"<h1>" << pageTitle << "</h1>\n";
1159 
1160 			if (fOutput != NULL) {
1161 				size_t written = 0;
1162 				status_t error = fOutput->WriteExactly(header.String(),
1163 					header.Length(), &written);
1164 				if (fListener != NULL && written > 0)
1165 					fListener->BytesWritten(this, written);
1166 				if (error != B_OK)
1167 					return error;
1168 			}
1169 
1170 			fPosition += header.Length();
1171 
1172 			if (fListener != NULL)
1173 				fListener->DownloadProgress(this, fPosition, 0);
1174 		}
1175 
1176 		if (item.Length()) {
1177 			if (fOutput != NULL) {
1178 				size_t written = 0;
1179 				status_t error = fOutput->WriteExactly(item.String(),
1180 					item.Length(), &written);
1181 				if (fListener != NULL && written > 0)
1182 					fListener->BytesWritten(this, written);
1183 				if (error != B_OK)
1184 					return error;
1185 			}
1186 
1187 			fPosition += item.Length();
1188 
1189 			if (fListener != NULL)
1190 				fListener->DownloadProgress(this, fPosition, 0);
1191 		}
1192 	}
1193 
1194 	if (last) {
1195 		// emit footer
1196 		BString footer =
1197 			"</div>\n"
1198 			"</body>\n"
1199 			"</html>\n";
1200 
1201 		if (fListener != NULL) {
1202 			size_t written = 0;
1203 			status_t error = fOutput->WriteExactly(footer.String(),
1204 				footer.Length(), &written);
1205 			if (fListener != NULL && written > 0)
1206 				fListener->BytesWritten(this, written);
1207 			if (error != B_OK)
1208 				return error;
1209 		}
1210 
1211 		fPosition += footer.Length();
1212 
1213 		if (fListener != NULL)
1214 			fListener->DownloadProgress(this, fPosition, 0);
1215 	}
1216 
1217 	return B_OK;
1218 }
1219 #endif
1220 
1221 
1222 BString&
1223 BGopherRequest::_HTMLEscapeString(BString &str)
1224 {
1225 	str.ReplaceAll("&", "&amp;");
1226 	str.ReplaceAll("<", "&lt;");
1227 	str.ReplaceAll(">", "&gt;");
1228 	return str;
1229 }
1230