hrev47571 adds 9 changesets to branch 'master' old head: 1b4510eebfb4aa1975fef0c46f90f4a1dfd71159 new head: e95d0f00ceeeb436c9f279824c44da038db9d246 overview: http://cgit.haiku-os.org/haiku/log/?qt=range&q=e95d0f0+%5E1b4510e ---------------------------------------------------------------------------- 0c1a4eb: Preliminary support for Gopher Currently parses information and text items and retrives files. 2e8b8fd: gopher: Handle binary, directory and error types f74e08f: gopher: Handle info resources and add proper title We now create a proper title from the error message, or the TITLE resource if present. 0e48c9a: gopher: Handle some more item types ec0e815: gopher: Handle audio and video types, add a default case 6983b35: gopher: Add a stylesheet Modified version from my attempt at adding gopher to NetSurf. 0716bfd: gopher: remove debug printfs cf2bf30: gopher: Add TODOs e95d0f0: gopher: Set a default MIME type to force downloading [ François Revol <revol@xxxxxxx> ] ---------------------------------------------------------------------------- 4 files changed, 805 insertions(+) headers/os/net/GopherRequest.h | 55 ++ src/kits/network/libnetapi/GopherRequest.cpp | 744 +++++++++++++++++++ src/kits/network/libnetapi/Jamfile | 3 + src/kits/network/libnetapi/UrlProtocolRoster.cpp | 3 + ############################################################################ Commit: 0c1a4ebf8b65b69d3a6606b3578ba83155f47b58 URL: http://cgit.haiku-os.org/haiku/commit/?id=0c1a4eb Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 20:08:59 2014 UTC Preliminary support for Gopher Currently parses information and text items and retrives files. ---------------------------------------------------------------------------- diff --git a/headers/os/net/GopherRequest.h b/headers/os/net/GopherRequest.h new file mode 100644 index 0000000..9173349 --- /dev/null +++ b/headers/os/net/GopherRequest.h @@ -0,0 +1,55 @@ +/* + * Copyright 2014 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + */ +#ifndef _B_GOPHER_REQUEST_H_ +#define _B_GOPHER_REQUEST_H_ + + +#include <deque> + +#include <NetBuffer.h> +#include <NetworkAddress.h> +#include <UrlRequest.h> + + +class BAbstractSocket; + + +class BGopherRequest : public BUrlRequest { +public: + BGopherRequest(const BUrl& url, + BUrlProtocolListener* listener = NULL, + BUrlContext* context = NULL); + virtual ~BGopherRequest(); + + status_t Stop(); + const BUrlResult& Result() const; + void SetDisableListener(bool disable); + +private: + status_t _ProtocolLoop(); + bool _ResolveHostName(); + void _SendRequest(); + + bool _NeedsParsing(); + bool _NeedsLastDotStrip(); + void _ParseInput(bool last); + + status_t _GetLine(BString& destString); + BString& _HTMLEscapeString(BString &str); + +private: + char fItemType; + BString fPath; + BAbstractSocket* fSocket; + BNetworkAddress fRemoteAddr; + + BNetBuffer fInputBuffer; + ssize_t fPosition; + + BUrlResult fResult; +}; + + +#endif // _B_GOPHER_REQUEST_H_ diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp new file mode 100644 index 0000000..1d193f9 --- /dev/null +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -0,0 +1,531 @@ +/* + * Copyright 2013-2014 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * François Revol, revol@xxxxxxx + */ + + +#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 <String.h> +#include <StringList.h> + +/* + * 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 + */ + +/** 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_PDF_ALT = 'd', /**< seems to be only for PDF files */ + 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_ALT, "application/pdf" }, + { GOPHER_TYPE_PDF, "application/pdf" }, + { GOPHER_TYPE_PNG, "image/png"}, + { GOPHER_TYPE_NONE, NULL } +}; + +static const int32 kGopherBufferSize = 4096; + + + + +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() == 1 && fPath[0] == '/')) { + // default entry + fItemType = GOPHER_TYPE_DIRECTORY; + fPath = ""; + } else if (fPath.Length() > 1 && fPath[0] == '/') { + fItemType = fPath[1]; + fPath.Remove(0, 2); + } + fprintf(stderr, "t: '%c' p:'%s'\n", fItemType, fPath.String()); +} + + +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; + + while (!fQuit && !receiveEnd) { + fSocket->WaitForReadable(); + BNetBuffer chunk(kGopherBufferSize); + bytesRead = fSocket->Read(chunk.Data(), kGopherBufferSize); + fprintf(stderr, "Read: %d\n", (int)bytesRead); + + if (bytesRead < 0) { + readError = bytesRead; + break; + } else if (bytesRead == 0) + receiveEnd = true; + + fInputBuffer.AppendData(chunk.Data(), 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') { + fprintf(stderr, "tab\n"); + if (!crlf) + tabs++; + } else if (c == '\r' || c == '\n') { + fprintf(stderr, "crlf\n"); + if (tabs < 3) + break; + crlf = true; + } else if (!isprint(fInputBuffer.Data()[i])) { + fprintf(stderr, "!isprint at %lu\n", i); + crlf = false; + break; + } + } + if (crlf && tabs > 2 && tabs < 5) { + fprintf(stderr, "error\n"); + // TODO: + //if enough data + // else continue + fItemType = GOPHER_TYPE_DIRECTORY; + //readError = B_RESOURCE_NOT_FOUND; + // continue parsing the error text anyway + } + } + + // 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 + for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) { + if (gopher_type_map[i].type == fItemType) { + fprintf(stderr, "MIME:'%s'\n", gopher_type_map[i].mime); + fResult.SetContentType(gopher_type_map[i].mime); + break; + } + } + } + + 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); + + fprintf(stderr, "type: '%c' name: '%s'\n", type, fields.StringAt(FIELD_NAME).String()); + //fields.PrintToStream(); + + BString item; + BString title = fields.StringAt(FIELD_NAME); + BString link("gopher://";); + 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); + } + fprintf(stderr, "link: '%s'\n", link.String()); + _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; + default: + item << "<div>" << fields.StringAt(FIELD_NAME) << "</div>"; + break; + } + + if (fPosition == 0) { + BString title = "TITLE"; + 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=\"stylesheet\" title=\"Standard\" " + "type=\"text/css\" href=\"resource:internal.css\">\n" + "<link rel=\"icon\" type=\"image/png\"" + " href=\"resource:icons/directory.png\">\n" + "<title>" << title << "</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>" << title << "</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; +} diff --git a/src/kits/network/libnetapi/Jamfile b/src/kits/network/libnetapi/Jamfile index ad74ae7..fb8da38 100644 --- a/src/kits/network/libnetapi/Jamfile +++ b/src/kits/network/libnetapi/Jamfile @@ -70,6 +70,9 @@ for architectureObject in [ MultiArchSubDirSetup ] { # TODO: another add-on for file:// (a much simpler one) FileRequest.cpp + # TODO: another add-on for gopher:// + GopherRequest.cpp + notifications.cpp $(md5Sources) diff --git a/src/kits/network/libnetapi/UrlProtocolRoster.cpp b/src/kits/network/libnetapi/UrlProtocolRoster.cpp index 4b44c99..54e27e2 100644 --- a/src/kits/network/libnetapi/UrlProtocolRoster.cpp +++ b/src/kits/network/libnetapi/UrlProtocolRoster.cpp @@ -14,6 +14,7 @@ #include <DataRequest.h> #include <Debug.h> #include <FileRequest.h> +#include <GopherRequest.h> #include <HttpRequest.h> #include <UrlRequest.h> @@ -45,6 +46,8 @@ BUrlProtocolRoster::MakeRequest(const BUrl& url, return new(std::nothrow) BFileRequest(url, listener, context); } else if (url.Protocol() == "data") { return new(std::nothrow) BDataRequest(url, listener, context); + } else if (url.Protocol() == "gopher") { + return new(std::nothrow) BGopherRequest(url, listener, context); } return NULL; ############################################################################ Commit: 2e8b8fd0468ecd67b0b22d753172c571b1600e78 URL: http://cgit.haiku-os.org/haiku/commit/?id=2e8b8fd Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 20:16:29 2014 UTC gopher: Handle binary, directory and error types ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index 1d193f9..2686618 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -430,6 +430,26 @@ BGopherRequest::_ParseInput(bool last) "<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"; + break; default: item << "<div>" << fields.StringAt(FIELD_NAME) << "</div>"; break; ############################################################################ Commit: f74e08fca8197c3b3227b6d5c1c9a4052cc6ad2b URL: http://cgit.haiku-os.org/haiku/commit/?id=f74e08f Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 20:35:31 2014 UTC gopher: Handle info resources and add proper title We now create a proper title from the error message, or the TITLE resource if present. ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index 2686618..cbfaa61 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -405,6 +405,7 @@ BGopherRequest::_ParseInput(bool last) fprintf(stderr, "type: '%c' name: '%s'\n", type, fields.StringAt(FIELD_NAME).String()); //fields.PrintToStream(); + BString pageTitle; BString item; BString title = fields.StringAt(FIELD_NAME); BString link("gopher://";); @@ -449,6 +450,19 @@ BGopherRequest::_ParseInput(bool last) 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_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; default: item << "<div>" << fields.StringAt(FIELD_NAME) << "</div>"; @@ -456,7 +470,9 @@ BGopherRequest::_ParseInput(bool last) } if (fPosition == 0) { - BString title = "TITLE"; + if (pageTitle.Length() == 0) + pageTitle << "Index of " << Url(); + const char *uplink = "."; if (fPath.EndsWith("/")) uplink = ".."; @@ -473,14 +489,14 @@ BGopherRequest::_ParseInput(bool last) "type=\"text/css\" href=\"resource:internal.css\">\n" "<link rel=\"icon\" type=\"image/png\"" " href=\"resource:icons/directory.png\">\n" - "<title>" << title << "</title>\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>" << title << "</h1>\n"; + "<h1>" << pageTitle << "</h1>\n"; fListener->DataReceived(this, header.String(), fPosition, header.Length()); ############################################################################ Commit: 0e48c9aecd39191b20a991517534cba7c5b471a7 URL: http://cgit.haiku-os.org/haiku/commit/?id=0e48c9a Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 21:34:52 2014 UTC gopher: Handle some more item types ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index cbfaa61..887ad3c 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -112,7 +112,7 @@ static struct { static const int32 kGopherBufferSize = 4096; - +static const bool kInlineImages = true; BGopherRequest::BGopherRequest(const BUrl& url, BUrlProtocolListener* listener, @@ -409,6 +409,7 @@ BGopherRequest::_ParseInput(bool last) 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()) @@ -453,6 +454,104 @@ BGopherRequest::_ParseInput(bool last) 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 << "\">" + "<span class=\"query\">" + "<label>" << title << " " + "<input 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 -> new@78.80.30.202 + */ + 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 ############################################################################ Commit: ec0e815354f9b7d53e00b4105cfe2bf87bd0bef1 URL: http://cgit.haiku-os.org/haiku/commit/?id=ec0e815 Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 21:56:04 2014 UTC gopher: Handle audio and video types, add a default case ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index 887ad3c..d4a53ff 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -563,8 +563,36 @@ BGopherRequest::_ParseInput(bool last) 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 << "\" " + "alt=\"" << title << "\"/>" + "<span>[player]</span></audio>" + "<br/>\n"; + break; + case GOPHER_TYPE_PDF: + case GOPHER_TYPE_PDF_ALT: + /* generic case for known-to-work items */ + item << "<a href=\"" << link << "\">" + "<span class=\"other\">" << title << "</span></a>" + "<br/>\n"; + break; + case GOPHER_TYPE_MOVIE: + item << "<a href=\"" << link << "\">" + "<span class=\"video\">" << title << "</span></a>" + "<video src=\"" << link << "\" " + "alt=\"" << title << "\"/>" + "<span>[player]</span></audio>" + "<br/>\n"; + break; default: - item << "<div>" << fields.StringAt(FIELD_NAME) << "</div>"; + _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; } ############################################################################ Commit: 6983b35de56a9a25b3aff10eac19e69fbed8b0af URL: http://cgit.haiku-os.org/haiku/commit/?id=6983b35 Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 23:08:07 2014 UTC gopher: Add a stylesheet Modified version from my attempt at adding gopher to NetSurf. ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index d4a53ff..8a13313 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -110,6 +110,63 @@ static struct { { 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; @@ -612,10 +669,9 @@ BGopherRequest::_ParseInput(bool last) "<meta http-equiv=\"Content-Type\"" " content=\"text/html; charset=UTF-8\" />\n" //FIXME: fix links - "<link rel=\"stylesheet\" title=\"Standard\" " - "type=\"text/css\" href=\"resource:internal.css\">\n" - "<link rel=\"icon\" type=\"image/png\"" - " href=\"resource:icons/directory.png\">\n" + //"<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" ############################################################################ Commit: 0716bfd63ce42d0100af811b79cb8b42102589d7 URL: http://cgit.haiku-os.org/haiku/commit/?id=0716bfd Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 23:23:24 2014 UTC gopher: remove debug printfs ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index 8a13313..e1764cf 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -194,7 +194,6 @@ BGopherRequest::BGopherRequest(const BUrl& url, BUrlProtocolListener* listener, fItemType = fPath[1]; fPath.Remove(0, 2); } - fprintf(stderr, "t: '%c' p:'%s'\n", fItemType, fPath.String()); } @@ -269,7 +268,6 @@ BGopherRequest::_ProtocolLoop() fSocket->WaitForReadable(); BNetBuffer chunk(kGopherBufferSize); bytesRead = fSocket->Read(chunk.Data(), kGopherBufferSize); - fprintf(stderr, "Read: %d\n", (int)bytesRead); if (bytesRead < 0) { readError = bytesRead; @@ -292,22 +290,18 @@ BGopherRequest::_ProtocolLoop() for (i = 0; i < fInputBuffer.Size(); i++) { char c = fInputBuffer.Data()[i]; if (c == '\t') { - fprintf(stderr, "tab\n"); if (!crlf) tabs++; } else if (c == '\r' || c == '\n') { - fprintf(stderr, "crlf\n"); if (tabs < 3) break; crlf = true; } else if (!isprint(fInputBuffer.Data()[i])) { - fprintf(stderr, "!isprint at %lu\n", i); crlf = false; break; } } if (crlf && tabs > 2 && tabs < 5) { - fprintf(stderr, "error\n"); // TODO: //if enough data // else continue @@ -332,7 +326,6 @@ BGopherRequest::_ProtocolLoop() // now we can assign MIME type if we know it for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) { if (gopher_type_map[i].type == fItemType) { - fprintf(stderr, "MIME:'%s'\n", gopher_type_map[i].mime); fResult.SetContentType(gopher_type_map[i].mime); break; } @@ -459,9 +452,6 @@ BGopherRequest::_ParseInput(bool last) _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Unterminated gopher item (type '%c')", type); - fprintf(stderr, "type: '%c' name: '%s'\n", type, fields.StringAt(FIELD_NAME).String()); - //fields.PrintToStream(); - BString pageTitle; BString item; BString title = fields.StringAt(FIELD_NAME); @@ -476,7 +466,6 @@ BGopherRequest::_ParseInput(bool last) // link << "/"; link << fields.StringAt(FIELD_SELECTOR); } - fprintf(stderr, "link: '%s'\n", link.String()); _HTMLEscapeString(title); _HTMLEscapeString(link); ############################################################################ Commit: cf2bf30633ed24c948164536be1407a9e2342b4c URL: http://cgit.haiku-os.org/haiku/commit/?id=cf2bf30 Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 23:24:48 2014 UTC gopher: Add TODOs ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index e1764cf..64b8013 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -23,6 +23,9 @@ #include <StringList.h> /* + * 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: ############################################################################ Revision: hrev47571 Commit: e95d0f00ceeeb436c9f279824c44da038db9d246 URL: http://cgit.haiku-os.org/haiku/commit/?id=e95d0f0 Author: François Revol <revol@xxxxxxx> Date: Fri Jul 25 23:35:19 2014 UTC gopher: Set a default MIME type to force downloading ---------------------------------------------------------------------------- diff --git a/src/kits/network/libnetapi/GopherRequest.cpp b/src/kits/network/libnetapi/GopherRequest.cpp index 64b8013..70d44af 100644 --- a/src/kits/network/libnetapi/GopherRequest.cpp +++ b/src/kits/network/libnetapi/GopherRequest.cpp @@ -327,12 +327,14 @@ BGopherRequest::_ProtocolLoop() 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) { - fResult.SetContentType(gopher_type_map[i].mime); + mime = gopher_type_map[i].mime; break; } } + fResult.SetContentType(mime); } if (_NeedsParsing())