File indexing completed on 2024-03-24 05:00:47

0001 /*
0002  *   SPDX-FileCopyrightText: 2003-2008 Albert Astals Cid <aacid@kde.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kio_gopher.h"
0008 
0009 #include <QBuffer>
0010 #include <QCoreApplication>
0011 #include <QFile>
0012 #include <QMimeDatabase>
0013 #include <QMimeType>
0014 #include <QUrl>
0015 
0016 #include <klocalizedstring.h>
0017 
0018 using namespace KIO;
0019 
0020 class KIOPluginForMetaData : public QObject
0021 {
0022     Q_OBJECT
0023     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.gopher" FILE "gopher.json")
0024 };
0025 
0026 extern "C" {
0027 int Q_DECL_EXPORT kdemain(int argc, char **argv)
0028 {
0029     QCoreApplication app(argc, argv); // needed for QSocketNotifier
0030     app.setApplicationName(QLatin1String("kio_gopher"));
0031 
0032     if (argc != 4) {
0033         fprintf(stderr, "Usage: kio_gopher protocol domain-socket1 domain-socket2\n");
0034         exit(-1);
0035     }
0036 
0037     gopher worker(argv[2], argv[3]);
0038     worker.dispatchLoop();
0039     return 0;
0040 }
0041 }
0042 
0043 /* gopher */
0044 
0045 gopher::gopher(const QByteArray &pool_socket, const QByteArray &app_socket)
0046     : WorkerBase("gopher", pool_socket, app_socket)
0047 {
0048 }
0049 
0050 KIO::WorkerResult gopher::get(const QUrl &url)
0051 {
0052     // gopher urls are
0053     // gopher://<host>:<port>/<gopher-path>
0054     //
0055     //  where <gopher-path> is one of
0056     //
0057     //  <gophertype><selector>
0058     //  <gophertype><selector>%09<search>
0059     //  <gophertype><selector>%09<search>%09<gopher+_string>
0060     int port;
0061     QChar type;
0062     QString path(url.path());
0063     QString query(url.query());
0064 
0065     // determine the type
0066     if (path != "/" && path != "")
0067         type = path[1];
0068     else
0069         type = '1';
0070 
0071     // determine the port
0072     if (url.port() > 0)
0073         port = url.port();
0074     else
0075         port = 70;
0076 
0077     // connect to the host
0078     if (auto result = connectToHost("gopher", url.host(), port); !result.success())
0079         return result;
0080 
0081     if (type == '7' && query.isNull()) {
0082         disconnectFromHost();
0083         handleSearch(url.host(), path, port);
0084     } else {
0085         int i, bytes;
0086         char aux[10240];
0087         QBuffer received;
0088         received.open(QIODevice::WriteOnly);
0089 
0090         infoMessage(i18n("Connecting to %1...", url.host()));
0091         infoMessage(i18n("%1 contacted. Retrieving data...", url.host()));
0092         bytes = 0;
0093 
0094         // send the selector
0095         path.remove(0, 2);
0096         socketWrite(path.toLatin1(), path.length());
0097         socketWrite(query.toLatin1(), query.length());
0098         socketWrite("\r\n", 2);
0099 
0100         // read the data
0101         while ((i = socketRead(aux, 10240)) > 0) {
0102             bytes += i;
0103             received.write(aux, i);
0104             processedSize(bytes);
0105             infoMessage(i18n("Retrieved %1 bytes from %2...", bytes, url.host()));
0106         }
0107 
0108         if (type == '1' || type == '7')
0109             processDirectory(new QByteArray(received.buffer().data(), bytes + 1), url.host(), url.path());
0110         else {
0111             QMimeDatabase db;
0112             QMimeType result = db.mimeTypeForData(received.buffer());
0113             mimeType(result.name());
0114             data(received.buffer());
0115         }
0116         disconnectFromHost();
0117     }
0118     return KIO::WorkerResult::pass();
0119 }
0120 
0121 void gopher::processDirectory(QByteArray *received, const QString &host, const QString &path)
0122 {
0123     int i, remove;
0124     QString pathToShow;
0125     QByteArray show;
0126     QByteArray info;
0127     if (path == "/" || path == "/1")
0128         pathToShow = "";
0129     else
0130         pathToShow = path;
0131     mimeType("text/html");
0132     show.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n");
0133     show.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n");
0134     show.append("\t<head>\n");
0135     show.append("\t\t<title>");
0136     show.append(host.toUtf8());
0137     show.append(pathToShow.toUtf8());
0138     show.append("</title>\n");
0139     show.append("\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n");
0140     show.append("\t\t<style type=\"text/css\">\n");
0141     show.append("\t\t\t.info{ font-size : small; display : block; font-family : monospace; white-space : pre; margin-left : 18px; }\n");
0142     show.append("\t\t</style>\n");
0143     show.append("\t</head>\n");
0144     show.append("\t<body>\n");
0145     show.append("\t\t<h1>");
0146     show.append(host.toUtf8());
0147     show.append(pathToShow.toUtf8());
0148     show.append("</h1>\n");
0149     findLine(received, &i, &remove);
0150     while (i != -1) {
0151         processDirectoryLine(received->left(i), show, info);
0152         received->remove(0, i + remove);
0153         findLine(received, &i, &remove);
0154     }
0155     show.append("\t</body>\n");
0156     show.append("</html>\n");
0157     data(show);
0158     delete received;
0159 }
0160 
0161 void gopher::processDirectoryLine(const QByteArray &d, QByteArray &show, QByteArray &info)
0162 {
0163     // gopher <type><display><tab><selector><tab><server><tab><port><\r><\n>
0164     // gopher+ <type><display><tab><selector><tab><server><tab><port><tab><things><\r><\n>
0165     int i;
0166     QByteArray type, name, url, server, port;
0167     QByteArray data = d;
0168 
0169     type = data.left(1);
0170     data.remove(0, 1);
0171 
0172     i = data.indexOf("\t");
0173     name = data.left(i);
0174     data.remove(0, i + 1);
0175 
0176     i = data.indexOf("\t");
0177     url = data.left(i);
0178     data.remove(0, i + 1);
0179 
0180     i = data.indexOf("\t");
0181     server = data.left(i);
0182     data.remove(0, i + 1);
0183 
0184     port = parsePort(&data);
0185 
0186     if (type == "i") {
0187         if (!info.isEmpty())
0188             info.append("\n");
0189         info.append(name);
0190     } else {
0191         if (!info.isEmpty()) {
0192             show.append("\t\t<div class=\"info\">");
0193             show.append(info);
0194             show.append("</div>\n");
0195             info = "";
0196         }
0197         // it's the final line, ignore it
0198         if (type == ".")
0199             return;
0200         // those are the standard gopher types defined in the rfc
0201         //  0   Item is a file
0202         //  1   Item is a directory
0203         //  2   Item is a CSO phone-book server
0204         //  3   Error
0205         //  4   Item is a BinHexed Macintosh file.
0206         //  5   Item is DOS binary archive of some sort. Client must read until the TCP connection closes.  Beware.
0207         //  6   Item is a UNIX uuencoded file.
0208         //  7   Item is an Index-Search server.
0209         //  8   Item points to a text-based telnet session.
0210         //  9   Item is a binary file! Client must read until the TCP connection closes.  Beware.
0211         //  +   Item is a redundant server
0212         //  T   Item points to a text-based tn3270 session.
0213         //  g   Item is a GIF format graphics file.
0214         //  I   Item is some kind of image file.  Client decides how to display.
0215         show.append("\t\t\t<div>");
0216         // support the non-standard extension for URL to external sites
0217         // in this case, url begins with 'URL:'
0218         QByteArray finalUrl;
0219         QByteArray iconUrl;
0220         if (url.startsWith("URL:")) {
0221             finalUrl = url.mid(4);
0222             iconUrl = finalUrl;
0223         } else {
0224             finalUrl = "gopher://" + server;
0225             if (port != "70") {
0226                 finalUrl.append(":");
0227                 finalUrl.append(port);
0228             }
0229             finalUrl.append('/' + type + url);
0230             iconUrl = url;
0231         }
0232         show.append("\t\t\t\t<a href=\"");
0233         show.append(finalUrl);
0234         show.append("\">");
0235         addIcon(type, iconUrl, show);
0236         show.append(name);
0237         show.append("</a><br />\n");
0238         show.append("\t\t\t</div>");
0239     }
0240 }
0241 
0242 QByteArray gopher::parsePort(QByteArray *received)
0243 {
0244     int i = 0;
0245     QByteArray port;
0246     bool found = false;
0247     QChar c;
0248     while (!found && i < received->size()) {
0249         c = received->at(i);
0250         if (c.isDigit())
0251             i++;
0252         else
0253             found = true;
0254     }
0255     port = received->left(i);
0256     received->remove(0, i);
0257     return port;
0258 }
0259 
0260 void gopher::findLine(QByteArray *received, int *i, int *remove)
0261 {
0262     // it's not in the rfc but most servers don't follow the spec
0263     // find lines ending only in \n and in \r\n
0264     int aux, aux2;
0265     aux = received->indexOf("\r\n");
0266     aux2 = received->indexOf("\n");
0267 
0268     if (aux == -1) {
0269         *i = aux2;
0270         *remove = 1;
0271     } else {
0272         if (aux2 < aux) {
0273             *remove = 1;
0274             *i = aux2;
0275         } else {
0276             *remove = 2;
0277             *i = aux;
0278         }
0279     }
0280 }
0281 
0282 void gopher::handleSearch(const QString &host, const QString &path, int port)
0283 {
0284     QByteArray show;
0285     QString sPort;
0286     if (port != 70)
0287         sPort = ':' + QString::number(port);
0288     mimeType("text/html");
0289     show.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n");
0290     show.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n");
0291     show.append("\t<head>\n");
0292     show.append("\t\t<title>");
0293     show.append(host.toUtf8());
0294     show.append(path.toUtf8());
0295     show.append("</title>\n");
0296     show.append("\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n");
0297     show.append("\t\t<script type=\"text/javascript\">\n");
0298     show.append("\t\t\tfunction search()\n");
0299     show.append("\t\t\t{\n");
0300     show.append("\t\t\t\tdocument.location = 'gopher://");
0301     show.append(host.toUtf8());
0302     show.append(sPort.toUtf8());
0303     show.append(path.toUtf8());
0304     show.append("?' + document.getElementById('what').value;\n");
0305     show.append("\t\t\t}\n");
0306     show.append("\t\t</script>\n");
0307     show.append("\t</head>\n");
0308     show.append("\t<body>\n");
0309     show.append("\t\t<h1>");
0310     show.append(host.toUtf8());
0311     show.append(path.toUtf8());
0312     show.append("</h1>\n");
0313     show.append("\t\t");
0314     show.append(i18n("Enter a search term:").toUtf8());
0315     show.append("<br />\n");
0316     show.append("\t\t<input id=\"what\" type=\"text\">\n");
0317     show.append("\t\t<input type=\"button\" value=\"");
0318     show.append(i18nc("Text on a search button, like at a search engine", "Search").toUtf8());
0319     show.append("\" onClick=\"search()\">\n");
0320     show.append("\t</body>\n");
0321     show.append("</html>\n");
0322     data(show);
0323 }
0324 
0325 void gopher::addIcon(const QString &type, const QByteArray &url, QByteArray &show)
0326 {
0327     QString icon;
0328     QMimeDatabase db;
0329     if (type == "1")
0330         icon = "inode-directory";
0331     else if (type == "3")
0332         icon = "dialog-error";
0333     else if (type == "7")
0334         icon = "system-search";
0335     else if (type == "g")
0336         icon = "image-gif";
0337     else if (type == "I")
0338         icon = "image-x-generic";
0339     else {
0340         QMimeType mime = db.mimeTypeForFile(QUrl(url).path(), QMimeDatabase::MatchExtension);
0341         icon = mime.iconName();
0342     }
0343     QFile file(m_iconLoader.iconPath(icon, -16));
0344     file.open(QIODevice::ReadOnly);
0345     const QMimeType iconMime = db.mimeTypeForFile(file.fileName(), QMimeDatabase::MatchExtension);
0346     QByteArray ba = file.readAll();
0347     show.append("<img width=\"16\" height=\"16\" src=\"data:");
0348     show.append(iconMime.name().toLatin1());
0349     show.append(";base64,");
0350     show.append(ba.toBase64());
0351     show.append("\" /> ");
0352 }
0353 
0354 KIO::WorkerResult gopher::connectToHost(const QString & /*protocol*/, const QString &host, quint16 port)
0355 {
0356     QString errorString;
0357     const int errCode = connectToHost(host, port, &errorString);
0358     if (errCode == 0) {
0359         return WorkerResult::pass();
0360     }
0361 
0362     return WorkerResult::fail(errCode, errorString);
0363 }
0364 
0365 int gopher::connectToHost(const QString &host, quint16 port, QString *errorString)
0366 {
0367     if (errorString) {
0368         errorString->clear(); // clear prior error messages.
0369     }
0370 
0371     const int timeout = (connectTimeout() * 1000); // 20 sec timeout value
0372 
0373     disconnectFromHost(); // Reset some state, even if we are already disconnected
0374 
0375     socket.connectToHost(host, port);
0376     socket.waitForConnected(timeout > -1 ? timeout : -1);
0377 
0378     if (socket.state() != QAbstractSocket::ConnectedState) {
0379         if (errorString) {
0380             *errorString = host + QLatin1String(": ") + socket.errorString();
0381         }
0382         switch (socket.error()) {
0383         case QAbstractSocket::UnsupportedSocketOperationError:
0384             return ERR_UNSUPPORTED_ACTION;
0385         case QAbstractSocket::RemoteHostClosedError:
0386             return ERR_CONNECTION_BROKEN;
0387         case QAbstractSocket::SocketTimeoutError:
0388             return ERR_SERVER_TIMEOUT;
0389         case QAbstractSocket::HostNotFoundError:
0390             return ERR_UNKNOWN_HOST;
0391         default:
0392             return ERR_CANNOT_CONNECT;
0393         }
0394     }
0395 
0396     return 0;
0397 }
0398 
0399 void gopher::disconnectFromHost()
0400 {
0401     if (socket.state() == QAbstractSocket::UnconnectedState) {
0402         // discard incoming data - the remote host might have disconnected us in the meantime
0403         // but the visible effect of disconnectFromHost() should stay the same.
0404         socket.close();
0405         return;
0406     }
0407 
0408     socket.disconnectFromHost();
0409     if (socket.state() != QAbstractSocket::UnconnectedState) {
0410         socket.waitForDisconnected(-1); // wait for unsent data to be sent
0411     }
0412     socket.close(); // whatever that means on a socket
0413 }
0414 
0415 ssize_t gopher::socketWrite(const char *data, ssize_t len)
0416 {
0417     ssize_t written = socket.write(data, len);
0418 
0419     bool success = socket.waitForBytesWritten(-1);
0420 
0421     socket.flush(); // this is supposed to get the data on the wire faster
0422 
0423     if (socket.state() != QAbstractSocket::ConnectedState || !success) {
0424         return -1;
0425     }
0426 
0427     return written;
0428 }
0429 
0430 ssize_t gopher::socketRead(char *data, ssize_t len)
0431 {
0432     if (!socket.bytesAvailable()) {
0433         socket.waitForReadyRead(-1);
0434     }
0435     return socket.read(data, len);
0436 }
0437 
0438 #include "kio_gopher.moc"