File indexing completed on 2024-11-03 07:43:44
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"