This is the case for urls like gopher://gophernicus.org (without trailing /), which ended up being downloaded instead of displayed.
767 lines
20 KiB
C++
767 lines
20 KiB
C++
/*
|
|
* Copyright 2013-2014 Haiku Inc. All rights reserved.
|
|
* Distributed under the terms of the MIT License.
|
|
*
|
|
* Authors:
|
|
* François Revol, [email protected]
|
|
*/
|
|
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
#include <Directory.h>
|
|
#include <DynamicBuffer.h>
|
|
#include <File.h>
|
|
#include <GopherRequest.h>
|
|
#include <NodeInfo.h>
|
|
#include <Path.h>
|
|
#include <Socket.h>
|
|
#include <StackOrHeapArray.h>
|
|
#include <String.h>
|
|
#include <StringList.h>
|
|
|
|
/*
|
|
* TODO: fix for URLs without path like gopher://gophernicus.org
|
|
* TODO: fix '+' in selectors, cf. gopher://gophernicus.org/1/doc/gopher/
|
|
* TODO: add proper favicon
|
|
* TODO: add proper dir and document icons
|
|
* TODO: correctly eat the extraneous .\r\n at end of text files
|
|
* TODO: move parsing stuff to a translator?
|
|
*
|
|
* docs:
|
|
* gopher://gopher.floodgap.com/1/gopher/tech
|
|
* gopher://gopher.floodgap.com/0/overbite/dbrowse?pluginm%201
|
|
*
|
|
* tests:
|
|
* gopher://sdf.org/1/sdf/historical images
|
|
* gopher://gopher.r-36.net/1/ large photos
|
|
* gopher://sdf.org/1/sdf/classes binaries
|
|
* gopher://sdf.org/1/users/ long page
|
|
* gopher://jgw.mdns.org/1/ search items
|
|
* gopher://jgw.mdns.org/1/MISC/ 's' item (sound)
|
|
* gopher://gopher.floodgap.com/1/gopher broken link
|
|
* gopher://sdf.org/1/maps/m missing lines
|
|
* gopher://sdf.org/1/foo gophernicus reports errors incorrectly
|
|
* gopher://gopher.floodgap.com/1/foo correct error report
|
|
*/
|
|
|
|
/** Type of Gopher items */
|
|
typedef enum {
|
|
GOPHER_TYPE_NONE = 0, /**< none set */
|
|
GOPHER_TYPE_ENDOFPAGE = '.', /**< a dot alone on a line */
|
|
/* these come from http://tools.ietf.org/html/rfc1436 */
|
|
GOPHER_TYPE_TEXTPLAIN = '0', /**< text/plain */
|
|
GOPHER_TYPE_DIRECTORY = '1', /**< gopher directory */
|
|
GOPHER_TYPE_CSO_SEARCH = '2', /**< CSO search */
|
|
GOPHER_TYPE_ERROR = '3', /**< error message */
|
|
GOPHER_TYPE_BINHEX = '4', /**< binhex encoded text */
|
|
GOPHER_TYPE_BINARCHIVE = '5', /**< binary archive file */
|
|
GOPHER_TYPE_UUENCODED = '6', /**< uuencoded text */
|
|
GOPHER_TYPE_QUERY = '7', /**< gopher search query */
|
|
GOPHER_TYPE_TELNET = '8', /**< telnet link */
|
|
GOPHER_TYPE_BINARY = '9', /**< generic binary */
|
|
GOPHER_TYPE_DUPSERV = '+', /**< duplicated server */
|
|
GOPHER_TYPE_GIF = 'g', /**< GIF image */
|
|
GOPHER_TYPE_IMAGE = 'I', /**< image (depends, usually jpeg) */
|
|
GOPHER_TYPE_TN3270 = 'T', /**< tn3270 session */
|
|
/* not standardized but widely used,
|
|
* cf. http://en.wikipedia.org/wiki/Gopher_%28protocol%29#Gopher_item_types
|
|
*/
|
|
GOPHER_TYPE_HTML = 'h', /**< HTML file or URL */
|
|
GOPHER_TYPE_INFO = 'i', /**< information text */
|
|
GOPHER_TYPE_AUDIO = 's', /**< audio (wav?) */
|
|
/* not standardized, some servers use them */
|
|
GOPHER_TYPE_DOC = 'd', /**< gophernicus uses it for PS and PDF */
|
|
GOPHER_TYPE_PNG = 'p', /**< PNG image */
|
|
/* cf. gopher://namcub.accelera-labs.com/1/pics */
|
|
GOPHER_TYPE_MIME = 'M', /**< multipart/mixed MIME data */
|
|
/* cf. http://www.pms.ifi.lmu.de/mitarbeiter/ohlbach/multimedia/IT/IBMtutorial/3376c61.html */
|
|
/* cf. http://nofixedpoint.motd.org/2011/02/22/an-introduction-to-the-gopher-protocol/ */
|
|
GOPHER_TYPE_PDF = 'P', /**< PDF file */
|
|
GOPHER_TYPE_BITMAP = ':', /**< Bitmap image (Gopher+) */
|
|
GOPHER_TYPE_MOVIE = ';', /**< Movie (Gopher+) */
|
|
GOPHER_TYPE_SOUND = '<', /**< Sound (Gopher+) */
|
|
GOPHER_TYPE_CALENDAR = 'c', /**< Calendar */
|
|
GOPHER_TYPE_EVENT = 'e', /**< Event */
|
|
GOPHER_TYPE_MBOX = 'm', /**< mbox file */
|
|
} gopher_item_type;
|
|
|
|
/** Types of fields in a line */
|
|
typedef enum {
|
|
FIELD_NAME,
|
|
FIELD_SELECTOR,
|
|
FIELD_HOST,
|
|
FIELD_PORT,
|
|
FIELD_GPFLAG,
|
|
FIELD_EOL,
|
|
FIELD_COUNT = FIELD_EOL
|
|
} gopher_field;
|
|
|
|
/** Map of gopher types to MIME types */
|
|
static struct {
|
|
gopher_item_type type;
|
|
const char *mime;
|
|
} gopher_type_map[] = {
|
|
/* these come from http://tools.ietf.org/html/rfc1436 */
|
|
{ GOPHER_TYPE_TEXTPLAIN, "text/plain" },
|
|
{ GOPHER_TYPE_DIRECTORY, "text/html;charset=UTF-8" },
|
|
{ GOPHER_TYPE_QUERY, "text/html;charset=UTF-8" },
|
|
{ GOPHER_TYPE_GIF, "image/gif" },
|
|
{ GOPHER_TYPE_HTML, "text/html" },
|
|
/* those are not standardized */
|
|
{ GOPHER_TYPE_PDF, "application/pdf" },
|
|
{ GOPHER_TYPE_PNG, "image/png"},
|
|
{ GOPHER_TYPE_NONE, NULL }
|
|
};
|
|
|
|
static const char *kStyleSheet = "\n"
|
|
"/*\n"
|
|
" * gopher listing style\n"
|
|
" */\n"
|
|
"\n"
|
|
"body#gopher {\n"
|
|
" /* margin: 10px;*/\n"
|
|
" background-color: Window;\n"
|
|
" color: WindowText;\n"
|
|
" font-size: 100%;\n"
|
|
" padding-bottom: 2em; }\n"
|
|
"\n"
|
|
"body#gopher div.uplink {\n"
|
|
" padding: 0;\n"
|
|
" margin: 0;\n"
|
|
" position: fixed;\n"
|
|
" top: 5px;\n"
|
|
" right: 5px; }\n"
|
|
"\n"
|
|
"body#gopher h1 {\n"
|
|
" padding: 5mm;\n"
|
|
" margin: 0;\n"
|
|
" border-bottom: 2px solid #777; }\n"
|
|
"\n"
|
|
"body#gopher span {\n"
|
|
" margin-left: 1em;\n"
|
|
" padding-left: 2em;\n"
|
|
" font-family: 'DejaVu Sans Mono', Courier, monospace;\n"
|
|
" word-wrap: break-word;\n"
|
|
" white-space: pre-wrap; }\n"
|
|
"\n"
|
|
"body#gopher span.error {\n"
|
|
" color: #f00; }\n"
|
|
"\n"
|
|
"body#gopher span.unknown {\n"
|
|
" color: #800; }\n"
|
|
"\n"
|
|
"body#gopher span.dir {\n"
|
|
" background-image: url('resource:icons/directory.png');\n"
|
|
" background-repeat: no-repeat;\n"
|
|
" background-position: bottom left; }\n"
|
|
"\n"
|
|
"body#gopher span.text {\n"
|
|
" background-image: url('resource:icons/content.png');\n"
|
|
" background-repeat: no-repeat;\n"
|
|
" background-position: bottom left; }\n"
|
|
"\n"
|
|
"body#gopher span.query {\n"
|
|
" background-image: url('resource:icons/search.png');\n"
|
|
" background-repeat: no-repeat;\n"
|
|
" background-position: bottom left; }\n"
|
|
"\n"
|
|
"body#gopher span.img img {\n"
|
|
" display: block;\n"
|
|
" margin-left:auto;\n"
|
|
" margin-right:auto; }\n";
|
|
|
|
static const int32 kGopherBufferSize = 4096;
|
|
|
|
static const bool kInlineImages = true;
|
|
|
|
|
|
BGopherRequest::BGopherRequest(const BUrl& url, BUrlProtocolListener* listener,
|
|
BUrlContext* context)
|
|
:
|
|
BUrlRequest(url, listener, context, "BUrlProtocol.Gopher", "gopher"),
|
|
fItemType(GOPHER_TYPE_NONE),
|
|
fPosition(0),
|
|
fResult()
|
|
{
|
|
fSocket = new(std::nothrow) BSocket();
|
|
|
|
fUrl.UrlDecode();
|
|
// the first part of the path is actually the document type
|
|
|
|
fPath = Url().Path();
|
|
if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") {
|
|
// default entry
|
|
fItemType = GOPHER_TYPE_DIRECTORY;
|
|
fPath = "";
|
|
} else if (fPath.Length() > 1 && fPath[0] == '/') {
|
|
fItemType = fPath[1];
|
|
fPath.Remove(0, 2);
|
|
}
|
|
}
|
|
|
|
|
|
BGopherRequest::~BGopherRequest()
|
|
{
|
|
Stop();
|
|
|
|
delete fSocket;
|
|
}
|
|
|
|
|
|
status_t
|
|
BGopherRequest::Stop()
|
|
{
|
|
if (fSocket != NULL) {
|
|
fSocket->Disconnect();
|
|
// Unlock any pending connect, read or write operation.
|
|
}
|
|
return BUrlRequest::Stop();
|
|
}
|
|
|
|
|
|
const BUrlResult&
|
|
BGopherRequest::Result() const
|
|
{
|
|
return fResult;
|
|
}
|
|
|
|
|
|
status_t
|
|
BGopherRequest::_ProtocolLoop()
|
|
{
|
|
if (fSocket == NULL)
|
|
return B_NO_MEMORY;
|
|
|
|
if (!_ResolveHostName()) {
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR,
|
|
"Unable to resolve hostname (%s), aborting.",
|
|
fUrl.Host().String());
|
|
return B_SERVER_NOT_FOUND;
|
|
}
|
|
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.",
|
|
fUrl.Authority().String(), fRemoteAddr.Port());
|
|
status_t connectError = fSocket->Connect(fRemoteAddr);
|
|
|
|
if (connectError != B_OK) {
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s",
|
|
strerror(connectError));
|
|
return connectError;
|
|
}
|
|
|
|
//! ProtocolHook:ConnectionOpened
|
|
if (fListener != NULL)
|
|
fListener->ConnectionOpened(this);
|
|
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
|
|
"Connection opened, sending request.");
|
|
|
|
_SendRequest();
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent.");
|
|
|
|
// Receive loop
|
|
bool receiveEnd = false;
|
|
status_t readError = B_OK;
|
|
ssize_t bytesRead = 0;
|
|
//ssize_t bytesReceived = 0;
|
|
//ssize_t bytesTotal = 0;
|
|
bool dataValidated = false;
|
|
BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize);
|
|
|
|
while (!fQuit && !receiveEnd) {
|
|
fSocket->WaitForReadable();
|
|
bytesRead = fSocket->Read(chunk, kGopherBufferSize);
|
|
|
|
if (bytesRead < 0) {
|
|
readError = bytesRead;
|
|
break;
|
|
} else if (bytesRead == 0)
|
|
receiveEnd = true;
|
|
|
|
fInputBuffer.AppendData(chunk, bytesRead);
|
|
|
|
if (!dataValidated) {
|
|
size_t i;
|
|
// on error (file doesn't exist, ...) the server sends
|
|
// a faked directory entry with an error message
|
|
if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') {
|
|
int tabs = 0;
|
|
bool crlf = false;
|
|
|
|
// make sure the buffer only contains printable characters
|
|
// and has at least 3 tabs before a CRLF
|
|
for (i = 0; i < fInputBuffer.Size(); i++) {
|
|
char c = fInputBuffer.Data()[i];
|
|
if (c == '\t') {
|
|
if (!crlf)
|
|
tabs++;
|
|
} else if (c == '\r' || c == '\n') {
|
|
if (tabs < 3)
|
|
break;
|
|
crlf = true;
|
|
} else if (!isprint(fInputBuffer.Data()[i])) {
|
|
crlf = false;
|
|
break;
|
|
}
|
|
}
|
|
if (crlf && tabs > 2 && tabs < 5) {
|
|
// TODO:
|
|
//if enough data
|
|
// else continue
|
|
fItemType = GOPHER_TYPE_DIRECTORY;
|
|
readError = B_RESOURCE_NOT_FOUND;
|
|
// continue parsing the error text anyway
|
|
}
|
|
}
|
|
// special case for buggy(?) Gophernicus/1.5
|
|
static const char *buggy = "Error: File or directory not found!";
|
|
if (fInputBuffer.Size() > strlen(buggy)
|
|
&& !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) {
|
|
fItemType = GOPHER_TYPE_DIRECTORY;
|
|
readError = B_RESOURCE_NOT_FOUND;
|
|
// continue parsing the error text anyway
|
|
// but it won't look good
|
|
}
|
|
|
|
|
|
// now we probably have correct data
|
|
dataValidated = true;
|
|
|
|
//! ProtocolHook:ResponseStarted
|
|
if (fListener != NULL)
|
|
fListener->ResponseStarted(this);
|
|
|
|
// we don't really have headers but well...
|
|
//! ProtocolHook:HeadersReceived
|
|
if (fListener != NULL)
|
|
fListener->HeadersReceived(this);
|
|
|
|
// now we can assign MIME type if we know it
|
|
const char *mime = "application/octet-stream";
|
|
for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) {
|
|
if (gopher_type_map[i].type == fItemType) {
|
|
mime = gopher_type_map[i].mime;
|
|
break;
|
|
}
|
|
}
|
|
fResult.SetContentType(mime);
|
|
}
|
|
|
|
if (_NeedsParsing())
|
|
_ParseInput(receiveEnd);
|
|
else if (fInputBuffer.Size()) {
|
|
// send input directly
|
|
fListener->DataReceived(this, (const char *)fInputBuffer.Data(),
|
|
fPosition, fInputBuffer.Size());
|
|
|
|
fPosition += fInputBuffer.Size();
|
|
|
|
// XXX: this is plain stupid, we already copied the data
|
|
// and just want to drop it...
|
|
char *inputTempBuffer = new(std::nothrow) char[bytesRead];
|
|
if (inputTempBuffer == NULL) {
|
|
readError = B_NO_MEMORY;
|
|
break;
|
|
}
|
|
fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size());
|
|
delete[] inputTempBuffer;
|
|
}
|
|
}
|
|
|
|
if (fPosition > 0) {
|
|
fResult.SetLength(fPosition);
|
|
fListener->DownloadProgress(this, fPosition, fPosition);
|
|
}
|
|
|
|
fSocket->Disconnect();
|
|
|
|
if (readError != B_OK)
|
|
return readError;
|
|
|
|
return fQuit ? B_INTERRUPTED : B_OK;
|
|
}
|
|
|
|
|
|
bool
|
|
BGopherRequest::_ResolveHostName()
|
|
{
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Resolving %s",
|
|
fUrl.UrlString().String());
|
|
|
|
uint16_t port;
|
|
if (fUrl.HasPort())
|
|
port = fUrl.Port();
|
|
else
|
|
port = 70;
|
|
|
|
// FIXME stop forcing AF_INET, when BNetworkAddress stops giving IPv6
|
|
// addresses when there isn't an IPv6 link available.
|
|
fRemoteAddr = BNetworkAddress(AF_INET, fUrl.Host(), port);
|
|
|
|
if (fRemoteAddr.InitCheck() != B_OK)
|
|
return false;
|
|
|
|
//! ProtocolHook:HostnameResolved
|
|
if (fListener != NULL)
|
|
fListener->HostnameResolved(this, fRemoteAddr.ToString().String());
|
|
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Hostname resolved to: %s",
|
|
fRemoteAddr.ToString().String());
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void
|
|
BGopherRequest::_SendRequest()
|
|
{
|
|
BString request;
|
|
|
|
request << fPath;
|
|
|
|
if (Url().HasRequest())
|
|
request << '\t' << Url().Request();
|
|
|
|
request << "\r\n";
|
|
|
|
fSocket->Write(request.String(), request.Length());
|
|
}
|
|
|
|
|
|
bool
|
|
BGopherRequest::_NeedsParsing()
|
|
{
|
|
if (fItemType == GOPHER_TYPE_DIRECTORY
|
|
|| fItemType == GOPHER_TYPE_QUERY)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
|
|
bool
|
|
BGopherRequest::_NeedsLastDotStrip()
|
|
{
|
|
if (fItemType == GOPHER_TYPE_DIRECTORY
|
|
|| fItemType == GOPHER_TYPE_QUERY
|
|
|| fItemType == GOPHER_TYPE_TEXTPLAIN)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
|
|
void
|
|
BGopherRequest::_ParseInput(bool last)
|
|
{
|
|
BString line;
|
|
|
|
while (_GetLine(line) == B_OK) {
|
|
char type = GOPHER_TYPE_NONE;
|
|
BStringList fields;
|
|
|
|
line.MoveInto(&type, 0, 1);
|
|
|
|
line.Split("\t", false, fields);
|
|
|
|
if (type != GOPHER_TYPE_ENDOFPAGE
|
|
&& fields.CountStrings() < FIELD_GPFLAG)
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
|
|
"Unterminated gopher item (type '%c')", type);
|
|
|
|
BString pageTitle;
|
|
BString item;
|
|
BString title = fields.StringAt(FIELD_NAME);
|
|
BString link("gopher://");
|
|
BString user;
|
|
if (fields.CountStrings() > 3) {
|
|
link << fields.StringAt(FIELD_HOST);
|
|
if (fields.StringAt(FIELD_PORT).Length())
|
|
link << ":" << fields.StringAt(FIELD_PORT);
|
|
link << "/" << type;
|
|
//if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/')
|
|
// link << "/";
|
|
link << fields.StringAt(FIELD_SELECTOR);
|
|
}
|
|
_HTMLEscapeString(title);
|
|
_HTMLEscapeString(link);
|
|
|
|
switch (type) {
|
|
case GOPHER_TYPE_ENDOFPAGE:
|
|
/* end of the page */
|
|
break;
|
|
case GOPHER_TYPE_TEXTPLAIN:
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"text\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_BINARY:
|
|
case GOPHER_TYPE_BINHEX:
|
|
case GOPHER_TYPE_BINARCHIVE:
|
|
case GOPHER_TYPE_UUENCODED:
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"binary\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_DIRECTORY:
|
|
/*
|
|
* directory link
|
|
*/
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"dir\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_ERROR:
|
|
item << "<span class=\"error\">" << title << "</span>"
|
|
"<br/>\n";
|
|
if (fPosition == 0 && pageTitle.Length() == 0)
|
|
pageTitle << "Error: " << title;
|
|
break;
|
|
case GOPHER_TYPE_QUERY:
|
|
/* TODO: handle search better.
|
|
* For now we use an unnamed input field and accept sending ?=foo
|
|
* as it seems at least Veronica-2 ignores the = but it's unclean.
|
|
*/
|
|
item << "<form method=\"get\" action=\"" << link << "\" "
|
|
"onsubmit=\"window.location = this.action + '?' + "
|
|
"this.elements['q'].value; return false;\">"
|
|
"<span class=\"query\">"
|
|
"<label>" << title << " "
|
|
"<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />"
|
|
"</label>"
|
|
"</span></form>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_TELNET:
|
|
/* telnet: links
|
|
* cf. gopher://78.80.30.202/1/ps3
|
|
* -> gopher://78.80.30.202:23/8/ps3/new -> [email protected]
|
|
*/
|
|
link = "telnet://";
|
|
user = fields.StringAt(FIELD_SELECTOR);
|
|
if (user.FindLast('/') > -1) {
|
|
user.Remove(0, user.FindLast('/'));
|
|
link << user << "@";
|
|
}
|
|
link << fields.StringAt(FIELD_HOST);
|
|
if (fields.StringAt(FIELD_PORT) != "23")
|
|
link << ":" << fields.StringAt(FIELD_PORT);
|
|
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"telnet\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_TN3270:
|
|
/* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */
|
|
link = "tn3270://";
|
|
user = fields.StringAt(FIELD_SELECTOR);
|
|
if (user.FindLast('/') > -1) {
|
|
user.Remove(0, user.FindLast('/'));
|
|
link << user << "@";
|
|
}
|
|
link << fields.StringAt(FIELD_HOST);
|
|
if (fields.StringAt(FIELD_PORT) != "23")
|
|
link << ":" << fields.StringAt(FIELD_PORT);
|
|
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"telnet\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_CSO_SEARCH:
|
|
/* CSO search.
|
|
* At least Lynx supports a cso:// URI scheme:
|
|
* http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html
|
|
*/
|
|
link = "cso://";
|
|
user = fields.StringAt(FIELD_SELECTOR);
|
|
if (user.FindLast('/') > -1) {
|
|
user.Remove(0, user.FindLast('/'));
|
|
link << user << "@";
|
|
}
|
|
link << fields.StringAt(FIELD_HOST);
|
|
if (fields.StringAt(FIELD_PORT) != "105")
|
|
link << ":" << fields.StringAt(FIELD_PORT);
|
|
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"cso\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_GIF:
|
|
case GOPHER_TYPE_IMAGE:
|
|
case GOPHER_TYPE_PNG:
|
|
case GOPHER_TYPE_BITMAP:
|
|
/* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */
|
|
if (kInlineImages) {
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"img\">" << title << " "
|
|
"<img src=\"" << link << "\" "
|
|
"alt=\"" << title << "\"/>"
|
|
"</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
}
|
|
/* fallback to default, link them */
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"img\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_HTML:
|
|
/* cf. gopher://pineapple.vg/1 */
|
|
if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) {
|
|
link = fields.StringAt(FIELD_SELECTOR);
|
|
link.Remove(0, 4);
|
|
}
|
|
/* cf. gopher://sdf.org/1/sdf/classes/ */
|
|
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"html\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_INFO:
|
|
// TITLE resource, cf.
|
|
// gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt
|
|
if (fPosition == 0 && pageTitle.Length() == 0
|
|
&& fields.StringAt(FIELD_SELECTOR) == "TITLE") {
|
|
pageTitle = title;
|
|
break;
|
|
}
|
|
item << "<span class=\"info\">" << title << "</span>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_AUDIO:
|
|
case GOPHER_TYPE_SOUND:
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"audio\">" << title << "</span></a>"
|
|
"<audio src=\"" << link << "\" "
|
|
//TODO:Fix crash in WebPositive with these
|
|
//"controls=\"controls\" "
|
|
//"width=\"300\" height=\"50\" "
|
|
"alt=\"" << title << "\"/>"
|
|
"<span>[player]</span></audio>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_PDF:
|
|
case GOPHER_TYPE_DOC:
|
|
/* generic case for known-to-work items */
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"document\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
case GOPHER_TYPE_MOVIE:
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"video\">" << title << "</span></a>"
|
|
"<video src=\"" << link << "\" "
|
|
//TODO:Fix crash in WebPositive with these
|
|
//"controls=\"controls\" "
|
|
//"width=\"300\" height=\"300\" "
|
|
"alt=\"" << title << "\"/>"
|
|
"<span>[player]</span></audio>"
|
|
"<br/>\n";
|
|
break;
|
|
default:
|
|
_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
|
|
"Unknown gopher item (type 0x%02x '%c')", type, type);
|
|
item << "<a href=\"" << link << "\">"
|
|
"<span class=\"unknown\">" << title << "</span></a>"
|
|
"<br/>\n";
|
|
break;
|
|
}
|
|
|
|
if (fPosition == 0) {
|
|
if (pageTitle.Length() == 0)
|
|
pageTitle << "Index of " << Url();
|
|
|
|
const char *uplink = ".";
|
|
if (fPath.EndsWith("/"))
|
|
uplink = "..";
|
|
|
|
// emit header
|
|
BString header;
|
|
header <<
|
|
"<html>\n"
|
|
"<head>\n"
|
|
"<meta http-equiv=\"Content-Type\""
|
|
" content=\"text/html; charset=UTF-8\" />\n"
|
|
//FIXME: fix links
|
|
//"<link rel=\"icon\" type=\"image/png\""
|
|
// " href=\"resource:icons/directory.png\">\n"
|
|
"<style type=\"text/css\">\n" << kStyleSheet << "</style>\n"
|
|
"<title>" << pageTitle << "</title>\n"
|
|
"</head>\n"
|
|
"<body id=\"gopher\">\n"
|
|
"<div class=\"uplink dontprint\">\n"
|
|
"<a href=" << uplink << ">[up]</a>\n"
|
|
"<a href=\"/\">[top]</a>\n"
|
|
"</div>\n"
|
|
"<h1>" << pageTitle << "</h1>\n";
|
|
|
|
fListener->DataReceived(this, header.String(), fPosition,
|
|
header.Length());
|
|
|
|
fPosition += header.Length();
|
|
}
|
|
|
|
if (item.Length()) {
|
|
fListener->DataReceived(this, item.String(), fPosition,
|
|
item.Length());
|
|
|
|
fPosition += item.Length();
|
|
}
|
|
}
|
|
|
|
if (last) {
|
|
// emit footer
|
|
BString footer =
|
|
"</div>\n"
|
|
"</body>\n"
|
|
"</html>\n";
|
|
|
|
fListener->DataReceived(this, footer.String(), fPosition,
|
|
footer.Length());
|
|
|
|
fPosition += footer.Length();
|
|
}
|
|
}
|
|
|
|
|
|
status_t
|
|
BGopherRequest::_GetLine(BString& destString)
|
|
{
|
|
// Find a complete line in inputBuffer
|
|
uint32 characterIndex = 0;
|
|
|
|
while ((characterIndex < fInputBuffer.Size())
|
|
&& ((fInputBuffer.Data())[characterIndex] != '\n'))
|
|
characterIndex++;
|
|
|
|
if (characterIndex == fInputBuffer.Size())
|
|
return B_ERROR;
|
|
|
|
char* temporaryBuffer = new(std::nothrow) char[characterIndex + 1];
|
|
if (temporaryBuffer == NULL)
|
|
return B_NO_MEMORY;
|
|
|
|
fInputBuffer.RemoveData(temporaryBuffer, characterIndex + 1);
|
|
|
|
// Strip end-of-line character(s)
|
|
if (temporaryBuffer[characterIndex - 1] == '\r')
|
|
destString.SetTo(temporaryBuffer, characterIndex - 1);
|
|
else
|
|
destString.SetTo(temporaryBuffer, characterIndex);
|
|
|
|
delete[] temporaryBuffer;
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
BString&
|
|
BGopherRequest::_HTMLEscapeString(BString &str)
|
|
{
|
|
str.ReplaceAll("&", "&");
|
|
str.ReplaceAll("<", "<");
|
|
str.ReplaceAll(">", ">");
|
|
return str;
|
|
}
|