diff --git a/autotests/httpserver_p.cpp b/autotests/httpserver_p.cpp index 4e3cb6d5..2682f1b4 100644 --- a/autotests/httpserver_p.cpp +++ b/autotests/httpserver_p.cpp @@ -1,295 +1,287 @@ /**************************************************************************** ** Copyright (C) 2010-2016 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. ** Author: David Faure ** All rights reserved. ** ** This file initially comes from the KD Soap library. ** ** This file may be distributed and/or modified under the terms of the ** GNU Lesser General Public License version 2.1 and version 3 as published by the ** Free Software Foundation and appearing in the file COPYING.LIB included. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ** Contact info@kdab.com if any conditions of this licensing are not ** clear to you. ** **********************************************************************/ #include "httpserver_p.h" #include #include #include static bool splitHeadersAndData(const QByteArray &request, QByteArray &header, QByteArray &data) { const int sep = request.indexOf("\r\n\r\n"); if (sep <= 0) { return false; } header = request.left(sep); data = request.mid(sep + 4); return true; } typedef QMap HeadersMap; static HeadersMap parseHeaders(const QByteArray &headerData) { HeadersMap headersMap; QBuffer sourceBuffer; sourceBuffer.setData(headerData); sourceBuffer.open(QIODevice::ReadOnly); // The first line is special, it's the GET or POST line const QList firstLine = sourceBuffer.readLine().split(' '); if (firstLine.count() < 3) { qDebug() << "Malformed HTTP request:" << firstLine; return headersMap; } const QByteArray request = firstLine[0]; const QByteArray path = firstLine[1]; const QByteArray httpVersion = firstLine[2]; if (request != "GET" && request != "POST") { qDebug() << "Unknown HTTP request:" << firstLine; return headersMap; } headersMap.insert("_path", path); headersMap.insert("_httpVersion", httpVersion); while (!sourceBuffer.atEnd()) { const QByteArray line = sourceBuffer.readLine(); const int pos = line.indexOf(':'); if (pos == -1) { qDebug() << "Malformed HTTP header:" << line; } const QByteArray header = line.left(pos); const QByteArray value = line.mid(pos + 1).trimmed(); // remove space before and \r\n after //qDebug() << "HEADER" << header << "VALUE" << value; headersMap.insert(header, value); } return headersMap; } enum Method { None, Basic, Plain, Login, Ntlm, CramMd5, DigestMd5 }; static void parseAuthLine(const QString &str, Method *method, QString *headerVal) { *method = None; // The code below (from QAuthenticatorPrivate::parseHttpResponse) // is supposed to be run in a loop, apparently // (multiple WWW-Authenticate lines? multiple values in the line?) //qDebug() << "parseAuthLine() " << str; if (*method < Basic && str.startsWith(QLatin1String("Basic"), Qt::CaseInsensitive)) { *method = Basic; *headerVal = str.mid(6); } else if (*method < Ntlm && str.startsWith(QLatin1String("NTLM"), Qt::CaseInsensitive)) { *method = Ntlm; *headerVal = str.mid(5); } else if (*method < DigestMd5 && str.startsWith(QLatin1String("Digest"), Qt::CaseInsensitive)) { *method = DigestMd5; *headerVal = str.mid(7); } } QByteArray HttpServerThread::makeHttpResponse(const QByteArray &responseData) const { QByteArray httpResponse; if (m_features & Error404) { httpResponse += "HTTP/1.1 404 Not Found\r\n"; } else { httpResponse += "HTTP/1.1 200 OK\r\n"; } if (!m_contentType.isEmpty()) { httpResponse += "Content-Type: " + m_contentType + "\r\n"; } httpResponse += "Mozilla/5.0 (X11; Linux x86_64) KHTML/5.20.0 (like Gecko) Konqueror/5.20\r\n"; httpResponse += "Content-Length: "; httpResponse += QByteArray::number(responseData.size()); httpResponse += "\r\n"; // We don't support multiple connections so let's ask the client // to close the connection every time. httpResponse += "Connection: close\r\n"; httpResponse += "\r\n"; httpResponse += responseData; return httpResponse; } void HttpServerThread::disableSsl() { m_server->disableSsl(); } void HttpServerThread::finish() { KIO::Job *job = KIO::get(QUrl(endPoint() + QLatin1String("/terminateThread"))); job->exec(); } void HttpServerThread::run() { m_server = new BlockingHttpServer(m_features & Ssl); m_server->listen(); QMutexLocker lock(&m_mutex); m_port = m_server->serverPort(); lock.unlock(); m_ready.release(); const bool doDebug = qEnvironmentVariableIsSet("HTTP_TEST_DEBUG"); if (doDebug) { qDebug() << "HttpServerThread listening on port" << m_port; } // Wait for first connection (we'll wait for further ones inside the loop) QTcpSocket *clientSocket = m_server->waitForNextConnectionSocket(); Q_ASSERT(clientSocket); Q_FOREVER { // get the "request" packet if (doDebug) { qDebug() << "HttpServerThread: waiting for read"; } if (clientSocket->state() == QAbstractSocket::UnconnectedState || !clientSocket->waitForReadyRead(2000)) { if (clientSocket->state() == QAbstractSocket::UnconnectedState) { delete clientSocket; if (doDebug) { qDebug() << "Waiting for next connection..."; } clientSocket = m_server->waitForNextConnectionSocket(); Q_ASSERT(clientSocket); continue; // go to "waitForReadyRead" } else { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto clientSocketError = clientSocket->error(); -#else - const auto clientSocketError = clientSocket->socketError(); -#endif qDebug() << "HttpServerThread:" << clientSocketError << "waiting for \"request\" packet"; break; } } const QByteArray request = m_partialRequest + clientSocket->readAll(); if (doDebug) { qDebug() << "HttpServerThread: request:" << request; } // Split headers and request xml lock.relock(); const bool splitOK = splitHeadersAndData(request, m_receivedHeaders, m_receivedData); if (!splitOK) { //if (doDebug) // qDebug() << "Storing partial request" << request; m_partialRequest = request; continue; } m_headers = parseHeaders(m_receivedHeaders); if (m_headers.value("Content-Length").toInt() > m_receivedData.size()) { //if (doDebug) // qDebug() << "Storing partial request" << request; m_partialRequest = request; continue; } m_partialRequest.clear(); if (m_headers.value("_path").endsWith("terminateThread")) { // we're asked to exit break; // normal exit } lock.unlock(); //qDebug() << "headers received:" << m_receivedHeaders; //qDebug() << headers; //qDebug() << "data received:" << m_receivedData; if (m_features & BasicAuth) { QByteArray authValue = m_headers.value("Authorization"); if (authValue.isEmpty()) { authValue = m_headers.value("authorization"); // as sent by Qt-4.5 } bool authOk = false; if (!authValue.isEmpty()) { //qDebug() << "got authValue=" << authValue; // looks like "Basic " Method method; QString headerVal; parseAuthLine(QString::fromLatin1(authValue.data(), authValue.size()), &method, &headerVal); //qDebug() << "method=" << method << "headerVal=" << headerVal; switch (method) { case None: // we want auth, so reject "None" break; case Basic: { const QByteArray userPass = QByteArray::fromBase64(headerVal.toLatin1()); //qDebug() << userPass; // TODO if (validateAuth(userPass)) { if (userPass == ("kdab:testpass")) { authOk = true; } break; } default: qWarning("Unsupported authentication mechanism %s", authValue.constData()); } } if (!authOk) { // send auth request (Qt supports basic, ntlm and digest) const QByteArray unauthorized = "HTTP/1.1 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"example\"\r\nContent-Length: 0\r\n\r\n"; clientSocket->write(unauthorized); if (!clientSocket->waitForBytesWritten(2000)) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto clientSocketError = clientSocket->error(); -#else - const auto clientSocketError = clientSocket->socketError(); -#endif qDebug() << "HttpServerThread:" << clientSocketError << "writing auth request"; break; } continue; } } // send response const QByteArray response = makeHttpResponse(m_dataToSend); if (doDebug) { qDebug() << "HttpServerThread: writing" << response; } clientSocket->write(response); clientSocket->flush(); } // all done... delete clientSocket; delete m_server; if (doDebug) { qDebug() << "HttpServerThread terminated"; } } void BlockingHttpServer::incomingConnection(qintptr socketDescriptor) { if (doSsl) { QSslSocket *serverSocket = new QSslSocket; serverSocket->setParent(this); serverSocket->setSocketDescriptor(socketDescriptor); connect(serverSocket, SIGNAL(sslErrors(QList)), this, SLOT(slotSslErrors(QList))); // TODO setupSslServer(serverSocket); //qDebug() << "Created QSslSocket, starting server encryption"; serverSocket->startServerEncryption(); sslSocket = serverSocket; // If startServerEncryption fails internally [and waitForEncrypted hangs], // then this is how to debug it. // A way to catch such errors is really missing in Qt.. //qDebug() << "startServerEncryption said:" << sslSocket->errorString(); bool ok = serverSocket->waitForEncrypted(); Q_ASSERT(ok); Q_UNUSED(ok); } else { QTcpServer::incomingConnection(socketDescriptor); } } diff --git a/src/core/ktcpsocket.cpp b/src/core/ktcpsocket.cpp index 6bc1982e..31800a16 100644 --- a/src/core/ktcpsocket.cpp +++ b/src/core/ktcpsocket.cpp @@ -1,1069 +1,1061 @@ /* This file is part of the KDE libraries Copyright (C) 2007, 2008 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ktcpsocket.h" #include "ksslerror_p.h" #include "kiocoredebug.h" #include #include #include #include #include #include #include #include static KTcpSocket::SslVersion kSslVersionFromQ(QSsl::SslProtocol protocol) { switch (protocol) { case QSsl::SslV2: return KTcpSocket::SslV2; case QSsl::SslV3: return KTcpSocket::SslV3; case QSsl::TlsV1_0: return KTcpSocket::TlsV1; case QSsl::TlsV1_1: return KTcpSocket::TlsV1_1; case QSsl::TlsV1_2: return KTcpSocket::TlsV1_2; case QSsl::TlsV1_3: return KTcpSocket::TlsV1_3; case QSsl::AnyProtocol: return KTcpSocket::AnySslVersion; case QSsl::TlsV1SslV3: return KTcpSocket::TlsV1SslV3; case QSsl::SecureProtocols: return KTcpSocket::SecureProtocols; default: return KTcpSocket::UnknownSslVersion; } } static QSsl::SslProtocol qSslProtocolFromK(KTcpSocket::SslVersion sslVersion) { //### this lowlevel bit-banging is a little dangerous and a likely source of bugs if (sslVersion == KTcpSocket::AnySslVersion) { return QSsl::AnyProtocol; } //does it contain any valid protocol? KTcpSocket::SslVersions validVersions(KTcpSocket::SslV2 | KTcpSocket::SslV3 | KTcpSocket::TlsV1); validVersions |= KTcpSocket::TlsV1_1; validVersions |= KTcpSocket::TlsV1_2; validVersions |= KTcpSocket::TlsV1_3; validVersions |= KTcpSocket::TlsV1SslV3; validVersions |= KTcpSocket::SecureProtocols; if (!(sslVersion & validVersions)) { return QSsl::UnknownProtocol; } switch (sslVersion) { case KTcpSocket::SslV2: return QSsl::SslV2; case KTcpSocket::SslV3: return QSsl::SslV3; case KTcpSocket::TlsV1_0: return QSsl::TlsV1_0; case KTcpSocket::TlsV1_1: return QSsl::TlsV1_1; case KTcpSocket::TlsV1_2: return QSsl::TlsV1_2; case KTcpSocket::TlsV1_3: return QSsl::TlsV1_3; case KTcpSocket::TlsV1SslV3: return QSsl::TlsV1SslV3; case KTcpSocket::SecureProtocols: return QSsl::SecureProtocols; default: //QSslSocket doesn't really take arbitrary combinations. It's one or all. return QSsl::AnyProtocol; } } static QString protocolString(QSsl::SslProtocol protocol) { switch (protocol) { case QSsl::SslV2: return QStringLiteral("SSLv2"); case QSsl::SslV3: return QStringLiteral("SSLv3"); case QSsl::TlsV1_0: return QStringLiteral("TLSv1.0"); case QSsl::TlsV1_1: return QStringLiteral("TLSv1.1"); case QSsl::TlsV1_2: return QStringLiteral("TLSv1.2"); case QSsl::TlsV1_3: return QStringLiteral("TLSv1.3"); default: return QStringLiteral("Unknown");; } } //cipher class converter KSslCipher -> QSslCipher class CipherCc { public: CipherCc() { const QList list = QSslConfiguration::supportedCiphers(); for (const QSslCipher &c : list) { allCiphers.insert(c.name(), c); } } QSslCipher converted(const KSslCipher &ksc) { return allCiphers.value(ksc.name()); } private: QHash allCiphers; }; KSslError::Error KSslErrorPrivate::errorFromQSslError(QSslError::SslError e) { switch (e) { case QSslError::NoError: return KSslError::NoError; case QSslError::UnableToGetLocalIssuerCertificate: case QSslError::InvalidCaCertificate: return KSslError::InvalidCertificateAuthorityCertificate; case QSslError::InvalidNotBeforeField: case QSslError::InvalidNotAfterField: case QSslError::CertificateNotYetValid: case QSslError::CertificateExpired: return KSslError::ExpiredCertificate; case QSslError::UnableToDecodeIssuerPublicKey: case QSslError::SubjectIssuerMismatch: case QSslError::AuthorityIssuerSerialNumberMismatch: return KSslError::InvalidCertificate; case QSslError::SelfSignedCertificate: case QSslError::SelfSignedCertificateInChain: return KSslError::SelfSignedCertificate; case QSslError::CertificateRevoked: return KSslError::RevokedCertificate; case QSslError::InvalidPurpose: return KSslError::InvalidCertificatePurpose; case QSslError::CertificateUntrusted: return KSslError::UntrustedCertificate; case QSslError::CertificateRejected: return KSslError::RejectedCertificate; case QSslError::NoPeerCertificate: return KSslError::NoPeerCertificate; case QSslError::HostNameMismatch: return KSslError::HostNameMismatch; case QSslError::UnableToVerifyFirstCertificate: case QSslError::UnableToDecryptCertificateSignature: case QSslError::UnableToGetIssuerCertificate: case QSslError::CertificateSignatureFailed: return KSslError::CertificateSignatureFailed; case QSslError::PathLengthExceeded: return KSslError::PathLengthExceeded; case QSslError::UnspecifiedError: case QSslError::NoSslSupport: default: return KSslError::UnknownError; } } QSslError::SslError KSslErrorPrivate::errorFromKSslError(KSslError::Error e) { switch (e) { case KSslError::NoError: return QSslError::NoError; case KSslError::InvalidCertificateAuthorityCertificate: return QSslError::InvalidCaCertificate; case KSslError::InvalidCertificate: return QSslError::UnableToDecodeIssuerPublicKey; case KSslError::CertificateSignatureFailed: return QSslError::CertificateSignatureFailed; case KSslError::SelfSignedCertificate: return QSslError::SelfSignedCertificate; case KSslError::ExpiredCertificate: return QSslError::CertificateExpired; case KSslError::RevokedCertificate: return QSslError::CertificateRevoked; case KSslError::InvalidCertificatePurpose: return QSslError::InvalidPurpose; case KSslError::RejectedCertificate: return QSslError::CertificateRejected; case KSslError::UntrustedCertificate: return QSslError::CertificateUntrusted; case KSslError::NoPeerCertificate: return QSslError::NoPeerCertificate; case KSslError::HostNameMismatch: return QSslError::HostNameMismatch; case KSslError::PathLengthExceeded: return QSslError::PathLengthExceeded; case KSslError::UnknownError: default: return QSslError::UnspecifiedError; } } KSslError::KSslError(Error errorCode, const QSslCertificate &certificate) : d(new KSslErrorPrivate()) { d->error = QSslError(d->errorFromKSslError(errorCode), certificate); } KSslError::KSslError(const QSslError &other) : d(new KSslErrorPrivate()) { d->error = other; } KSslError::KSslError(const KSslError &other) : d(new KSslErrorPrivate()) { *d = *other.d; } KSslError::~KSslError() { delete d; } KSslError &KSslError::operator=(const KSslError &other) { *d = *other.d; return *this; } KSslError::Error KSslError::error() const { return KSslErrorPrivate::errorFromQSslError(d->error.error()); } QString KSslError::errorString() const { return d->error.errorString(); } QSslCertificate KSslError::certificate() const { return d->error.certificate(); } QSslError KSslError::sslError() const { return d->error; } class KTcpSocketPrivate { public: explicit KTcpSocketPrivate(KTcpSocket *qq) : q(qq), certificatesLoaded(false), emittedReadyRead(false) { // create the instance, which sets Qt's static internal cert set to empty. KSslCertificateManager::self(); } KTcpSocket::State state(QAbstractSocket::SocketState s) { switch (s) { case QAbstractSocket::UnconnectedState: return KTcpSocket::UnconnectedState; case QAbstractSocket::HostLookupState: return KTcpSocket::HostLookupState; case QAbstractSocket::ConnectingState: return KTcpSocket::ConnectingState; case QAbstractSocket::ConnectedState: return KTcpSocket::ConnectedState; case QAbstractSocket::ClosingState: return KTcpSocket::ClosingState; case QAbstractSocket::BoundState: case QAbstractSocket::ListeningState: //### these two are not relevant as long as this can't be a server socket default: return KTcpSocket::UnconnectedState; //the closest to "error" } } KTcpSocket::EncryptionMode encryptionMode(QSslSocket::SslMode mode) { switch (mode) { case QSslSocket::SslClientMode: return KTcpSocket::SslClientMode; case QSslSocket::SslServerMode: return KTcpSocket::SslServerMode; default: return KTcpSocket::UnencryptedMode; } } KTcpSocket::Error errorFromAbsSocket(QAbstractSocket::SocketError e) { switch (e) { case QAbstractSocket::ConnectionRefusedError: return KTcpSocket::ConnectionRefusedError; case QAbstractSocket::RemoteHostClosedError: return KTcpSocket::RemoteHostClosedError; case QAbstractSocket::HostNotFoundError: return KTcpSocket::HostNotFoundError; case QAbstractSocket::SocketAccessError: return KTcpSocket::SocketAccessError; case QAbstractSocket::SocketResourceError: return KTcpSocket::SocketResourceError; case QAbstractSocket::SocketTimeoutError: return KTcpSocket::SocketTimeoutError; case QAbstractSocket::NetworkError: return KTcpSocket::NetworkError; case QAbstractSocket::UnsupportedSocketOperationError: return KTcpSocket::UnsupportedSocketOperationError; case QAbstractSocket::SslHandshakeFailedError: return KTcpSocket::SslHandshakeFailedError; case QAbstractSocket::DatagramTooLargeError: //we don't do UDP case QAbstractSocket::AddressInUseError: case QAbstractSocket::SocketAddressNotAvailableError: //### own values if/when we ever get server socket support case QAbstractSocket::ProxyAuthenticationRequiredError: //### maybe we need an enum value for this case QAbstractSocket::UnknownSocketError: default: return KTcpSocket::UnknownError; } } //private slots void reemitSocketError(QAbstractSocket::SocketError e) { q->setErrorString(sock.errorString()); emit q->error(errorFromAbsSocket(e)); } void reemitSslErrors(const QList &errors) { q->setErrorString(sock.errorString()); q->showSslErrors(); //H4X QList kErrors; kErrors.reserve(errors.size()); for (const QSslError &e : errors) { kErrors.append(KSslError(e)); } emit q->sslErrors(kErrors); } void reemitStateChanged(QAbstractSocket::SocketState s) { emit q->stateChanged(state(s)); } void reemitModeChanged(QSslSocket::SslMode m) { emit q->encryptionModeChanged(encryptionMode(m)); } // This method is needed because we might emit readyRead() due to this QIODevice // having some data buffered, so we need to care about blocking, too. //### useless ATM as readyRead() now just calls d->sock.readyRead(). void reemitReadyRead() { if (!emittedReadyRead) { emittedReadyRead = true; emit q->readyRead(); emittedReadyRead = false; } } void maybeLoadCertificates() { if (!certificatesLoaded) { q->setCaCertificates(KSslCertificateManager::self()->caCertificates()); } } KTcpSocket *const q; bool certificatesLoaded; bool emittedReadyRead; QSslSocket sock; QList ciphers; KTcpSocket::SslVersion advertisedSslVersion; CipherCc ccc; }; KTcpSocket::KTcpSocket(QObject *parent) : QIODevice(parent), d(new KTcpSocketPrivate(this)) { d->advertisedSslVersion = SslV3; connect(&d->sock, &QIODevice::aboutToClose, this, &QIODevice::aboutToClose); connect(&d->sock, &QIODevice::bytesWritten, this, &QIODevice::bytesWritten); connect(&d->sock, &QSslSocket::encryptedBytesWritten, this, &KTcpSocket::encryptedBytesWritten); connect(&d->sock, SIGNAL(readyRead()), this, SLOT(reemitReadyRead())); connect(&d->sock, &QAbstractSocket::connected, this, &KTcpSocket::connected); connect(&d->sock, &QSslSocket::encrypted, this, &KTcpSocket::encrypted); connect(&d->sock, &QAbstractSocket::disconnected, this, &KTcpSocket::disconnected); #ifndef QT_NO_NETWORKPROXY connect(&d->sock, &QAbstractSocket::proxyAuthenticationRequired, this, &KTcpSocket::proxyAuthenticationRequired); #endif connect(&d->sock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(reemitSocketError(QAbstractSocket::SocketError))); connect(&d->sock, SIGNAL(sslErrors(QList)), this, SLOT(reemitSslErrors(QList))); connect(&d->sock, &QAbstractSocket::hostFound, this, &KTcpSocket::hostFound); connect(&d->sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(reemitStateChanged(QAbstractSocket::SocketState))); connect(&d->sock, SIGNAL(modeChanged(QSslSocket::SslMode)), this, SLOT(reemitModeChanged(QSslSocket::SslMode))); } KTcpSocket::~KTcpSocket() { delete d; } ////////////////////////////// (mostly) virtuals from QIODevice bool KTcpSocket::atEnd() const { return d->sock.atEnd() && QIODevice::atEnd(); } qint64 KTcpSocket::bytesAvailable() const { return d->sock.bytesAvailable() + QIODevice::bytesAvailable(); } qint64 KTcpSocket::bytesToWrite() const { return d->sock.bytesToWrite(); } bool KTcpSocket::canReadLine() const { return d->sock.canReadLine() || QIODevice::canReadLine(); } void KTcpSocket::close() { d->sock.close(); QIODevice::close(); } bool KTcpSocket::isSequential() const { return true; } bool KTcpSocket::open(QIODevice::OpenMode open) { bool ret = d->sock.open(open); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); return ret; } bool KTcpSocket::waitForBytesWritten(int msecs) { return d->sock.waitForBytesWritten(msecs); } bool KTcpSocket::waitForReadyRead(int msecs) { return d->sock.waitForReadyRead(msecs); } qint64 KTcpSocket::readData(char *data, qint64 maxSize) { return d->sock.read(data, maxSize); } qint64 KTcpSocket::writeData(const char *data, qint64 maxSize) { return d->sock.write(data, maxSize); } ////////////////////////////// public methods from QAbstractSocket void KTcpSocket::abort() { d->sock.abort(); } void KTcpSocket::connectToHost(const QString &hostName, quint16 port, ProxyPolicy policy) { if (policy == AutoProxy) { //### } d->sock.connectToHost(hostName, port); // there are enough layers of buffers between us and the network, and there is a quirk // in QIODevice that can make it try to readData() twice per read() call if buffered and // reaData() does not deliver enough data the first time. like when the other side is // simply not sending any more data... // this can *apparently* lead to long delays sometimes which stalls applications. // do not want. setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } void KTcpSocket::connectToHost(const QHostAddress &hostAddress, quint16 port, ProxyPolicy policy) { if (policy == AutoProxy) { //### } d->sock.connectToHost(hostAddress, port); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } void KTcpSocket::connectToHost(const QUrl &url, ProxyPolicy policy) { if (policy == AutoProxy) { //### } d->sock.connectToHost(url.host(), url.port()); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } void KTcpSocket::disconnectFromHost() { d->sock.disconnectFromHost(); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } KTcpSocket::Error KTcpSocket::error() const { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = d->sock.error(); -#else - const auto networkError = d->sock.socketError(); -#endif return d->errorFromAbsSocket(networkError); } QList KTcpSocket::sslErrors() const { //### pretty slow; also consider throwing out duplicate error codes. We may get // duplicates even though there were none in the original list because KSslError // has a smallest common denominator range of SSL error codes. -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto qsslErrors = d->sock.sslErrors(); -#else - const auto qsslErrors = d->sock.sslHandshakeErrors(); -#endif QList ret; ret.reserve(qsslErrors.size()); for (const QSslError &e : qsslErrors) { ret.append(KSslError(e)); } return ret; } bool KTcpSocket::flush() { return d->sock.flush(); } bool KTcpSocket::isValid() const { return d->sock.isValid(); } QHostAddress KTcpSocket::localAddress() const { return d->sock.localAddress(); } QHostAddress KTcpSocket::peerAddress() const { return d->sock.peerAddress(); } QString KTcpSocket::peerName() const { return d->sock.peerName(); } quint16 KTcpSocket::peerPort() const { return d->sock.peerPort(); } #ifndef QT_NO_NETWORKPROXY QNetworkProxy KTcpSocket::proxy() const { return d->sock.proxy(); } #endif qint64 KTcpSocket::readBufferSize() const { return d->sock.readBufferSize(); } #ifndef QT_NO_NETWORKPROXY void KTcpSocket::setProxy(const QNetworkProxy &proxy) { d->sock.setProxy(proxy); } #endif void KTcpSocket::setReadBufferSize(qint64 size) { d->sock.setReadBufferSize(size); } KTcpSocket::State KTcpSocket::state() const { return d->state(d->sock.state()); } bool KTcpSocket::waitForConnected(int msecs) { bool ret = d->sock.waitForConnected(msecs); if (!ret) { setErrorString(d->sock.errorString()); } setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); return ret; } bool KTcpSocket::waitForDisconnected(int msecs) { bool ret = d->sock.waitForDisconnected(msecs); if (!ret) { setErrorString(d->sock.errorString()); } setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); return ret; } ////////////////////////////// public methods from QSslSocket void KTcpSocket::addCaCertificate(const QSslCertificate &certificate) { d->maybeLoadCertificates(); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) d->sock.addCaCertificate(certificate); #else d->sock.sslConfiguration().addCaCertificate(certificate); #endif } /* bool KTcpSocket::addCaCertificates(const QString &path, QSsl::EncodingFormat format, QRegExp::PatternSyntax syntax) { d->maybeLoadCertificates(); return d->sock.addCaCertificates(path, format, syntax); } */ void KTcpSocket::addCaCertificates(const QList &certificates) { d->maybeLoadCertificates(); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) d->sock.addCaCertificates(certificates); #else d->sock.sslConfiguration().addCaCertificates(certificates); #endif } QList KTcpSocket::caCertificates() const { d->maybeLoadCertificates(); return d->sock.sslConfiguration().caCertificates(); } QList KTcpSocket::ciphers() const { return d->ciphers; } void KTcpSocket::connectToHostEncrypted(const QString &hostName, quint16 port, OpenMode openMode) { d->maybeLoadCertificates(); d->sock.setProtocol(qSslProtocolFromK(d->advertisedSslVersion)); d->sock.connectToHostEncrypted(hostName, port, openMode); setOpenMode(d->sock.openMode() | QIODevice::Unbuffered); } QSslCertificate KTcpSocket::localCertificate() const { return d->sock.localCertificate(); } QList KTcpSocket::peerCertificateChain() const { return d->sock.peerCertificateChain(); } KSslKey KTcpSocket::privateKey() const { return KSslKey(d->sock.privateKey()); } KSslCipher KTcpSocket::sessionCipher() const { return KSslCipher(d->sock.sessionCipher()); } void KTcpSocket::setCaCertificates(const QList &certificates) { QSslConfiguration configuration = d->sock.sslConfiguration(); configuration.setCaCertificates(certificates); d->sock.setSslConfiguration(configuration); d->certificatesLoaded = true; } void KTcpSocket::setCiphers(const QList &ciphers) { d->ciphers = ciphers; QList cl; cl.reserve(d->ciphers.size()); for (const KSslCipher &c : ciphers) { cl.append(d->ccc.converted(c)); } QSslConfiguration configuration = d->sock.sslConfiguration(); configuration.setCiphers(cl); d->sock.setSslConfiguration(configuration); } void KTcpSocket::setLocalCertificate(const QSslCertificate &certificate) { d->sock.setLocalCertificate(certificate); } void KTcpSocket::setLocalCertificate(const QString &fileName, QSsl::EncodingFormat format) { d->sock.setLocalCertificate(fileName, format); } void KTcpSocket::setVerificationPeerName(const QString &hostName) { d->sock.setPeerVerifyName(hostName); } void KTcpSocket::setPrivateKey(const KSslKey &key) { // We cannot map KSslKey::Algorithm:Dh to anything in QSsl::KeyAlgorithm. if (key.algorithm() == KSslKey::Dh) { return; } QSslKey _key(key.toDer(), (key.algorithm() == KSslKey::Rsa) ? QSsl::Rsa : QSsl::Dsa, QSsl::Der, (key.secrecy() == KSslKey::PrivateKey) ? QSsl::PrivateKey : QSsl::PublicKey); d->sock.setPrivateKey(_key); } void KTcpSocket::setPrivateKey(const QString &fileName, KSslKey::Algorithm algorithm, QSsl::EncodingFormat format, const QByteArray &passPhrase) { // We cannot map KSslKey::Algorithm:Dh to anything in QSsl::KeyAlgorithm. if (algorithm == KSslKey::Dh) { return; } d->sock.setPrivateKey(fileName, (algorithm == KSslKey::Rsa) ? QSsl::Rsa : QSsl::Dsa, format, passPhrase); } bool KTcpSocket::waitForEncrypted(int msecs) { return d->sock.waitForEncrypted(msecs); } KTcpSocket::EncryptionMode KTcpSocket::encryptionMode() const { return d->encryptionMode(d->sock.mode()); } QVariant KTcpSocket::socketOption(QAbstractSocket::SocketOption options) const { return d->sock.socketOption(options); } void KTcpSocket::setSocketOption(QAbstractSocket::SocketOption options, const QVariant &value) { d->sock.setSocketOption(options, value); } QSslConfiguration KTcpSocket::sslConfiguration() const { return d->sock.sslConfiguration(); } void KTcpSocket::setSslConfiguration(const QSslConfiguration &configuration) { d->sock.setSslConfiguration(configuration); } //slot void KTcpSocket::ignoreSslErrors() { d->sock.ignoreSslErrors(); } //slot void KTcpSocket::startClientEncryption() { d->maybeLoadCertificates(); d->sock.setProtocol(qSslProtocolFromK(d->advertisedSslVersion)); d->sock.startClientEncryption(); } //debugging H4X void KTcpSocket::showSslErrors() { #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const QList list = d->sock.sslErrors(); #else const QList list = d->sock.sslHandshakeErrors(); #endif for (const QSslError &e : list) { qCDebug(KIO_CORE) << e.errorString(); } } void KTcpSocket::setAdvertisedSslVersion(KTcpSocket::SslVersion version) { d->advertisedSslVersion = version; } KTcpSocket::SslVersion KTcpSocket::advertisedSslVersion() const { return d->advertisedSslVersion; } KTcpSocket::SslVersion KTcpSocket::negotiatedSslVersion() const { if (!d->sock.isEncrypted()) { return UnknownSslVersion; } return kSslVersionFromQ(d->sock.sessionProtocol()); } QString KTcpSocket::negotiatedSslVersionName() const { if (!d->sock.isEncrypted()) { return QString(); } return protocolString(d->sock.sessionProtocol()); } ////////////////////////////// KSslKey class KSslKeyPrivate { public: KSslKey::Algorithm convertAlgorithm(QSsl::KeyAlgorithm a) { switch (a) { case QSsl::Dsa: return KSslKey::Dsa; default: return KSslKey::Rsa; } } KSslKey::Algorithm algorithm; KSslKey::KeySecrecy secrecy; bool isExportable; QByteArray der; }; KSslKey::KSslKey() : d(new KSslKeyPrivate) { d->algorithm = Rsa; d->secrecy = PublicKey; d->isExportable = true; } KSslKey::KSslKey(const KSslKey &other) : d(new KSslKeyPrivate) { *d = *other.d; } KSslKey::KSslKey(const QSslKey &qsk) : d(new KSslKeyPrivate) { d->algorithm = d->convertAlgorithm(qsk.algorithm()); d->secrecy = (qsk.type() == QSsl::PrivateKey) ? PrivateKey : PublicKey; d->isExportable = true; d->der = qsk.toDer(); } KSslKey::~KSslKey() { delete d; } KSslKey &KSslKey::operator=(const KSslKey &other) { *d = *other.d; return *this; } KSslKey::Algorithm KSslKey::algorithm() const { return d->algorithm; } bool KSslKey::isExportable() const { return d->isExportable; } KSslKey::KeySecrecy KSslKey::secrecy() const { return d->secrecy; } QByteArray KSslKey::toDer() const { return d->der; } ////////////////////////////// KSslCipher //nice-to-have: make implicitly shared class KSslCipherPrivate { public: QString authenticationMethod; QString encryptionMethod; QString keyExchangeMethod; QString name; bool isNull; int supportedBits; int usedBits; }; KSslCipher::KSslCipher() : d(new KSslCipherPrivate) { d->isNull = true; d->supportedBits = 0; d->usedBits = 0; } KSslCipher::KSslCipher(const KSslCipher &other) : d(new KSslCipherPrivate) { *d = *other.d; } KSslCipher::KSslCipher(const QSslCipher &qsc) : d(new KSslCipherPrivate) { d->authenticationMethod = qsc.authenticationMethod(); d->encryptionMethod = qsc.encryptionMethod(); //Qt likes to append the number of bits (usedBits?) to the algorithm, //for example "AES(256)". We only want the pure algorithm name, though. int parenIdx = d->encryptionMethod.indexOf(QLatin1Char('(')); if (parenIdx > 0) { d->encryptionMethod.truncate(parenIdx); } d->keyExchangeMethod = qsc.keyExchangeMethod(); d->name = qsc.name(); d->isNull = qsc.isNull(); d->supportedBits = qsc.supportedBits(); d->usedBits = qsc.usedBits(); } KSslCipher::~KSslCipher() { delete d; } KSslCipher &KSslCipher::operator=(const KSslCipher &other) { *d = *other.d; return *this; } bool KSslCipher::isNull() const { return d->isNull; } QString KSslCipher::authenticationMethod() const { return d->authenticationMethod; } QString KSslCipher::encryptionMethod() const { return d->encryptionMethod; } QString KSslCipher::keyExchangeMethod() const { return d->keyExchangeMethod; } QString KSslCipher::digestMethod() const { //### This is not really backend neutral. It works for OpenSSL and // for RFC compliant names, though. if (d->name.endsWith(QLatin1String("SHA"))) { return QStringLiteral("SHA-1"); } else if (d->name.endsWith(QLatin1String("MD5"))) { return QStringLiteral("MD5"); } else { return QString(); } } QString KSslCipher::name() const { return d->name; } int KSslCipher::supportedBits() const { return d->supportedBits; } int KSslCipher::usedBits() const { return d->usedBits; } //static QList KSslCipher::supportedCiphers() { QList ret; const QList candidates = QSslConfiguration::supportedCiphers(); ret.reserve(candidates.size()); for (const QSslCipher &c : candidates) { ret.append(KSslCipher(c)); } return ret; } #include "moc_ktcpsocket.cpp" diff --git a/src/core/tcpslavebase.cpp b/src/core/tcpslavebase.cpp index 1a15e579..8a2e733a 100644 --- a/src/core/tcpslavebase.cpp +++ b/src/core/tcpslavebase.cpp @@ -1,940 +1,936 @@ /* * Copyright (C) 2000 Alex Zepeda * Copyright (C) 2001-2003 George Staikos * Copyright (C) 2001 Dawit Alemayehu * Copyright (C) 2007,2008 Andreas Hartmetz * Copyright (C) 2008 Roland Harnau * Copyright (C) 2010 Richard Moore * * This file is part of the KDE project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "tcpslavebase.h" #include "kiocoredebug.h" #include #include #include #include #include #include #include using namespace KIO; //using namespace KNetwork; namespace KIO { Q_DECLARE_OPERATORS_FOR_FLAGS(TCPSlaveBase::SslResult) } //TODO Proxy support whichever way works; KPAC reportedly does *not* work. //NOTE kded_proxyscout may or may not be interesting //TODO resurrect SSL session recycling; this means save the session on disconnect and look //for a reusable session on connect. Consider how HTTP persistent connections interact with that. //TODO in case we support SSL-lessness we need static KTcpSocket::sslAvailable() and check it //in most places we ATM check for d->isSSL. //TODO check if d->isBlocking is honored everywhere it makes sense //TODO fold KSSLSetting and KSSLCertificateHome into KSslSettings and use that everywhere. //TODO recognize partially encrypted websites as "somewhat safe" /* List of dialogs/messageboxes we need to use (current code location in parentheses) - Can the "dontAskAgainName" thing be improved? - "SSLCertDialog" [select client cert] (SlaveInterface) - Enter password for client certificate (inline) - Password for client cert was wrong. Please reenter. (inline) - Setting client cert failed. [doesn't give reason] (inline) - "SSLInfoDialog" [mostly server cert info] (SlaveInterface) - You are about to enter secure mode. Security information/Display SSL information/Connect (inline) - You are about to leave secure mode. Security information/Continue loading/Abort (inline) - Hostname mismatch: Continue/Details/Cancel (inline) - IP address mismatch: Continue/Details/Cancel (inline) - Certificate failed authenticity check: Continue/Details/Cancel (inline) - Would you like to accept this certificate forever: Yes/No/Current sessions only (inline) */ /** @internal */ class Q_DECL_HIDDEN TCPSlaveBase::TcpSlaveBasePrivate { public: explicit TcpSlaveBasePrivate(TCPSlaveBase *qq) : q(qq) {} void setSslMetaData() { sslMetaData.insert(QStringLiteral("ssl_in_use"), QStringLiteral("TRUE")); QSslCipher cipher = socket.sessionCipher(); sslMetaData.insert(QStringLiteral("ssl_protocol_version"), cipher.protocolString()); sslMetaData.insert(QStringLiteral("ssl_cipher"), cipher.name()); sslMetaData.insert(QStringLiteral("ssl_cipher_used_bits"), QString::number(cipher.usedBits())); sslMetaData.insert(QStringLiteral("ssl_cipher_bits"), QString::number(cipher.supportedBits())); sslMetaData.insert(QStringLiteral("ssl_peer_ip"), ip); const QList peerCertificateChain = socket.peerCertificateChain(); // try to fill in the blanks, i.e. missing certificates, and just assume that // those belong to the peer (==website or similar) certificate. for (int i = 0; i < sslErrors.count(); i++) { if (sslErrors[i].certificate().isNull()) { sslErrors[i] = QSslError(sslErrors[i].error(), peerCertificateChain[0]); } } QString errorStr; // encode the two-dimensional numeric error list using '\n' and '\t' as outer and inner separators for (const QSslCertificate &cert : peerCertificateChain ) { for (const QSslError &error : qAsConst(sslErrors)) { if (error.certificate() == cert) { errorStr += QString::number(static_cast(error.error())) + QLatin1Char('\t'); } } if (errorStr.endsWith(QLatin1Char('\t'))) { errorStr.chop(1); } errorStr += QLatin1Char('\n'); } errorStr.chop(1); sslMetaData.insert(QStringLiteral("ssl_cert_errors"), errorStr); QString peerCertChain; for (const QSslCertificate &cert : peerCertificateChain) { peerCertChain += QString::fromUtf8(cert.toPem()) + QLatin1Char('\x01'); } peerCertChain.chop(1); sslMetaData.insert(QStringLiteral("ssl_peer_chain"), peerCertChain); sendSslMetaData(); } void clearSslMetaData() { sslMetaData.clear(); sslMetaData.insert(QStringLiteral("ssl_in_use"), QStringLiteral("FALSE")); sendSslMetaData(); } void sendSslMetaData() { MetaData::ConstIterator it = sslMetaData.constBegin(); for (; it != sslMetaData.constEnd(); ++it) { q->setMetaData(it.key(), it.value()); } } SslResult startTLSInternal(QSsl::SslProtocol sslVersion, int waitForEncryptedTimeout = -1); TCPSlaveBase * const q; bool isBlocking; QSslSocket socket; QString host; QString ip; quint16 port; QByteArray serviceName; KSSLSettings sslSettings; bool usingSSL; bool autoSSL; bool sslNoUi; // If true, we just drop the connection silently // if SSL certificate check fails in some way. QList sslErrors; MetaData sslMetaData; }; //### uh, is this a good idea?? QIODevice *TCPSlaveBase::socket() const { return &d->socket; } TCPSlaveBase::TCPSlaveBase(const QByteArray &protocol, const QByteArray &poolSocket, const QByteArray &appSocket, bool autoSSL) : SlaveBase(protocol, poolSocket, appSocket), d(new TcpSlaveBasePrivate(this)) { d->isBlocking = true; d->port = 0; d->serviceName = protocol; d->usingSSL = false; d->autoSSL = autoSSL; d->sslNoUi = false; // Limit the read buffer size to 14 MB (14*1024*1024) (based on the upload limit // in TransferJob::slotDataReq). See the docs for QAbstractSocket::setReadBufferSize // and the BR# 187876 to understand why setting this limit is necessary. d->socket.setReadBufferSize(14680064); } TCPSlaveBase::~TCPSlaveBase() { delete d; } ssize_t TCPSlaveBase::write(const char *data, ssize_t len) { ssize_t written = d->socket.write(data, len); if (written == -1) { /*qDebug() << "d->socket.write() returned -1! Socket error is" << d->socket.error() << ", Socket state is" << d->socket.state();*/ } bool success = false; if (d->isBlocking) { // Drain the tx buffer success = d->socket.waitForBytesWritten(-1); } else { // ### I don't know how to make sure that all data does get written at some point // without doing it now. There is no event loop to do it behind the scenes. // Polling in the dispatch() loop? Something timeout based? success = d->socket.waitForBytesWritten(0); } d->socket.flush(); //this is supposed to get the data on the wire faster if (d->socket.state() != QAbstractSocket::ConnectedState || !success) { /*qDebug() << "Write failed, will return -1! Socket error is" << d->socket.error() << ", Socket state is" << d->socket.state() << "Return value of waitForBytesWritten() is" << success;*/ return -1; } return written; } ssize_t TCPSlaveBase::read(char *data, ssize_t len) { if (d->usingSSL && (d->socket.mode() != QSslSocket::SslClientMode)) { d->clearSslMetaData(); //qDebug() << "lost SSL connection."; return -1; } if (!d->socket.bytesAvailable()) { const int timeout = d->isBlocking ? -1 : (readTimeout() * 1000); d->socket.waitForReadyRead(timeout); } #if 0 // Do not do this because its only benefit is to cause a nasty side effect // upstream in Qt. See BR# 260769. else if (d->socket.mode() != QSslSocket::SslClientMode || QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy) { // we only do this when it doesn't trigger Qt socket bugs. When it doesn't break anything // it seems to help performance. d->socket.waitForReadyRead(0); } #endif return d->socket.read(data, len); } ssize_t TCPSlaveBase::readLine(char *data, ssize_t len) { if (d->usingSSL && (d->socket.mode() != QSslSocket::SslClientMode)) { d->clearSslMetaData(); //qDebug() << "lost SSL connection."; return -1; } const int timeout = (d->isBlocking ? -1 : (readTimeout() * 1000)); ssize_t readTotal = 0; do { if (!d->socket.bytesAvailable()) { d->socket.waitForReadyRead(timeout); } ssize_t readStep = d->socket.readLine(&data[readTotal], len - readTotal); if (readStep == -1 || (readStep == 0 && d->socket.state() != QAbstractSocket::ConnectedState)) { return -1; } readTotal += readStep; } while (readTotal == 0 || data[readTotal - 1] != '\n'); return readTotal; } bool TCPSlaveBase::connectToHost(const QString &/*protocol*/, const QString &host, quint16 port) { QString errorString; const int errCode = connectToHost(host, port, &errorString); if (errCode == 0) { return true; } error(errCode, errorString); return false; } int TCPSlaveBase::connectToHost(const QString &host, quint16 port, QString *errorString) { d->clearSslMetaData(); //We have separate connection and SSL setup phases if (errorString) { errorString->clear(); // clear prior error messages. } d->socket.setPeerVerifyName(host); // Used for ssl certificate verification (SNI) // - leaving SSL - warn before we even connect //### see if it makes sense to move this into the HTTP ioslave which is the only // user. if (metaData(QStringLiteral("main_frame_request")) == QLatin1String("TRUE") //### this looks *really* unreliable && metaData(QStringLiteral("ssl_activate_warnings")) == QLatin1String("TRUE") && metaData(QStringLiteral("ssl_was_in_use")) == QLatin1String("TRUE") && !d->autoSSL) { if (d->sslSettings.warnOnLeave()) { int result = messageBox(i18n("You are about to leave secure " "mode. Transmissions will no " "longer be encrypted.\nThis " "means that a third party could " "observe your data in transit."), WarningContinueCancel, i18n("Security Information"), i18n("C&ontinue Loading"), QString(), QStringLiteral("WarnOnLeaveSSLMode")); if (result == SlaveBase::Cancel) { if (errorString) { *errorString = host; } return ERR_USER_CANCELED; } } } const int timeout = (connectTimeout() * 1000); // 20 sec timeout value disconnectFromHost(); //Reset some state, even if we are already disconnected d->host = host; d->socket.connectToHost(host, port); /*const bool connectOk = */d->socket.waitForConnected(timeout > -1 ? timeout : -1); /*qDebug() << "Socket: state=" << d->socket.state() << ", error=" << d->socket.error() << ", connected?" << connectOk;*/ if (d->socket.state() != QAbstractSocket::ConnectedState) { if (errorString) { *errorString = host + QLatin1String(": ") + d->socket.errorString(); } -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) switch (d->socket.error()) { -#else - switch (d->socket.socketError()) { -#endif case QAbstractSocket::UnsupportedSocketOperationError: return ERR_UNSUPPORTED_ACTION; case QAbstractSocket::RemoteHostClosedError: return ERR_CONNECTION_BROKEN; case QAbstractSocket::SocketTimeoutError: return ERR_SERVER_TIMEOUT; case QAbstractSocket::HostNotFoundError: return ERR_UNKNOWN_HOST; default: return ERR_CANNOT_CONNECT; } } //### check for proxyAuthenticationRequiredError d->ip = d->socket.peerAddress().toString(); d->port = d->socket.peerPort(); if (d->autoSSL) { const SslResult res = d->startTLSInternal(QSsl::SecureProtocols, timeout); if (res & ResultFailed) { if (errorString) { *errorString = i18nc("%1 is a host name", "%1: SSL negotiation failed", host); } return ERR_CANNOT_CONNECT; } } return 0; } void TCPSlaveBase::disconnectFromHost() { //qDebug(); d->host.clear(); d->ip.clear(); d->usingSSL = false; if (d->socket.state() == QAbstractSocket::UnconnectedState) { // discard incoming data - the remote host might have disconnected us in the meantime // but the visible effect of disconnectFromHost() should stay the same. d->socket.close(); return; } //### maybe save a session for reuse on SSL shutdown if and when QSslSocket // does that. QCA::TLS can do it apparently but that is not enough if // we want to present that as KDE API. Not a big loss in any case. d->socket.disconnectFromHost(); if (d->socket.state() != QAbstractSocket::UnconnectedState) { d->socket.waitForDisconnected(-1); // wait for unsent data to be sent } d->socket.close(); //whatever that means on a socket } bool TCPSlaveBase::isAutoSsl() const { return d->autoSSL; } bool TCPSlaveBase::isUsingSsl() const { return d->usingSSL; } quint16 TCPSlaveBase::port() const { return d->port; } bool TCPSlaveBase::atEnd() const { return d->socket.atEnd(); } bool TCPSlaveBase::startSsl() { if (d->usingSSL) { return false; } return d->startTLSInternal(QSsl::SecureProtocols) & ResultOk; } TCPSlaveBase::SslResult TCPSlaveBase::TcpSlaveBasePrivate::startTLSInternal(QSsl::SslProtocol sslVersion, int waitForEncryptedTimeout) { q->selectClientCertificate(); //setMetaData("ssl_session_id", d->kssl->session()->toString()); //### we don't support session reuse for now... usingSSL = true; // Set the SSL protocol version to use... socket.setProtocol(sslVersion); /* Usually ignoreSslErrors() would be called in the slot invoked by the sslErrors() signal but that would mess up the flow of control. We will check for errors anyway to decide if we want to continue connecting. Otherwise ignoreSslErrors() before connecting would be very insecure. */ socket.ignoreSslErrors(); socket.startClientEncryption(); const bool encryptionStarted = socket.waitForEncrypted(waitForEncryptedTimeout); //Set metadata, among other things for the "SSL Details" dialog QSslCipher cipher = socket.sessionCipher(); if (!encryptionStarted || socket.mode() != QSslSocket::SslClientMode || cipher.isNull() || cipher.usedBits() == 0 || socket.peerCertificateChain().isEmpty()) { usingSSL = false; clearSslMetaData(); /*qDebug() << "Initial SSL handshake failed. encryptionStarted is" << encryptionStarted << ", cipher.isNull() is" << cipher.isNull() << ", cipher.usedBits() is" << cipher.usedBits() << ", length of certificate chain is" << socket.peerCertificateChain().count() << ", the socket says:" << socket.errorString() << "and the list of SSL errors contains" << socket.sslErrors().count() << "items.";*/ /*for (const QSslError &sslError : socket.sslErrors()) { qDebug() << "SSL ERROR: (" << sslError.error() << ")" << sslError.errorString(); }*/ return ResultFailed | ResultFailedEarly; } /*qDebug() << "Cipher info - " << " advertised SSL protocol version" << socket.protocol() << " negotiated SSL protocol version" << socket.sessionProtocol() << " authenticationMethod:" << cipher.authenticationMethod() << " encryptionMethod:" << cipher.encryptionMethod() << " keyExchangeMethod:" << cipher.keyExchangeMethod() << " name:" << cipher.name() << " supportedBits:" << cipher.supportedBits() << " usedBits:" << cipher.usedBits();*/ #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) sslErrors = socket.sslErrors(); #else sslErrors = socket.sslHandshakeErrors(); #endif // TODO: review / rewrite / remove the comment // The app side needs the metadata now for the SSL error dialog (if any) but // the same metadata will be needed later, too. When "later" arrives the slave // may actually be connected to a different application that doesn't know // the metadata the slave sent to the previous application. // The quite important SSL indicator icon in Konqi's URL bar relies on metadata // from here, for example. And Konqi will be the second application to connect // to the slave. // Therefore we choose to have our metadata and send it, too :) setSslMetaData(); q->sendAndKeepMetaData(); SslResult rc = q->verifyServerCertificate(); if (rc & ResultFailed) { usingSSL = false; clearSslMetaData(); //qDebug() << "server certificate verification failed."; socket.disconnectFromHost(); //Make the connection fail (cf. ignoreSslErrors()) return ResultFailed; } else if (rc & ResultOverridden) { //qDebug() << "server certificate verification failed but continuing at user's request."; } //"warn" when starting SSL/TLS if (q->metaData(QStringLiteral("ssl_activate_warnings")) == QLatin1String("TRUE") && q->metaData(QStringLiteral("ssl_was_in_use")) == QLatin1String("FALSE") && sslSettings.warnOnEnter()) { int msgResult = q->messageBox(i18n("You are about to enter secure mode. " "All transmissions will be encrypted " "unless otherwise noted.\nThis means " "that no third party will be able to " "easily observe your data in transit."), WarningYesNo, i18n("Security Information"), i18n("Display SSL &Information"), i18n("C&onnect"), QStringLiteral("WarnOnEnterSSLMode")); if (msgResult == SlaveBase::Yes) { q->messageBox(SSLMessageBox /*==the SSL info dialog*/, host); } } return rc; } void TCPSlaveBase::selectClientCertificate() { #if 0 //hehe QString certname; // the cert to use this session bool send = false, prompt = false, save = false, forcePrompt = false; KSSLCertificateHome::KSSLAuthAction aa; setMetaData("ssl_using_client_cert", "FALSE"); // we change this if needed if (metaData("ssl_no_client_cert") == "TRUE") { return; } forcePrompt = (metaData("ssl_force_cert_prompt") == "TRUE"); // Delete the old cert since we're certainly done with it now if (d->pkcs) { delete d->pkcs; d->pkcs = NULL; } if (!d->kssl) { return; } // Look for a general certificate if (!forcePrompt) { certname = KSSLCertificateHome::getDefaultCertificateName(&aa); switch (aa) { case KSSLCertificateHome::AuthSend: send = true; prompt = false; break; case KSSLCertificateHome::AuthDont: send = false; prompt = false; certname.clear(); break; case KSSLCertificateHome::AuthPrompt: send = false; prompt = true; break; default: break; } } // Look for a certificate on a per-host basis as an override QString tmpcn = KSSLCertificateHome::getDefaultCertificateName(d->host, &aa); if (aa != KSSLCertificateHome::AuthNone) { // we must override switch (aa) { case KSSLCertificateHome::AuthSend: send = true; prompt = false; certname = tmpcn; break; case KSSLCertificateHome::AuthDont: send = false; prompt = false; certname.clear(); break; case KSSLCertificateHome::AuthPrompt: send = false; prompt = true; certname = tmpcn; break; default: break; } } // Finally, we allow the application to override anything. if (hasMetaData("ssl_demand_certificate")) { certname = metaData("ssl_demand_certificate"); if (!certname.isEmpty()) { forcePrompt = false; prompt = false; send = true; } } if (certname.isEmpty() && !prompt && !forcePrompt) { return; } // Ok, we're supposed to prompt the user.... if (prompt || forcePrompt) { QStringList certs = KSSLCertificateHome::getCertificateList(); QStringList::const_iterator it = certs.begin(); while (it != certs.end()) { KSSLPKCS12 *pkcs = KSSLCertificateHome::getCertificateByName(*it); if (pkcs && (!pkcs->getCertificate() || !pkcs->getCertificate()->x509V3Extensions().certTypeSSLClient())) { it = certs.erase(it); } else { ++it; } delete pkcs; } if (certs.isEmpty()) { return; // we had nothing else, and prompt failed } QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface(); if (!bus->isServiceRegistered("org.kde.kio.uiserver")) { bus->startService("org.kde.kuiserver"); } QDBusInterface uis("org.kde.kio.uiserver", "/UIServer", "org.kde.KIO.UIServer"); QDBusMessage retVal = uis.call("showSSLCertDialog", d->host, certs, metaData("window-id").toLongLong()); if (retVal.type() == QDBusMessage::ReplyMessage) { if (retVal.arguments().at(0).toBool()) { send = retVal.arguments().at(1).toBool(); save = retVal.arguments().at(2).toBool(); certname = retVal.arguments().at(3).toString(); } } } // The user may have said to not send the certificate, // but to save the choice if (!send) { if (save) { KSSLCertificateHome::setDefaultCertificate(certname, d->host, false, false); } return; } // We're almost committed. If we can read the cert, we'll send it now. KSSLPKCS12 *pkcs = KSSLCertificateHome::getCertificateByName(certname); if (!pkcs && KSSLCertificateHome::hasCertificateByName(certname)) { // We need the password KIO::AuthInfo ai; bool first = true; do { ai.prompt = i18n("Enter the certificate password:"); ai.caption = i18n("SSL Certificate Password"); ai.url.setScheme("kssl"); ai.url.setHost(certname); ai.username = certname; ai.keepPassword = true; bool showprompt; if (first) { showprompt = !checkCachedAuthentication(ai); } else { showprompt = true; } if (showprompt) { if (!openPasswordDialog(ai, first ? QString() : i18n("Unable to open the certificate. Try a new password?"))) { break; } } first = false; pkcs = KSSLCertificateHome::getCertificateByName(certname, ai.password); } while (!pkcs); } // If we could open the certificate, let's send it if (pkcs) { if (!d->kssl->setClientCertificate(pkcs)) { messageBox(Information, i18n("The procedure to set the " "client certificate for the session " "failed."), i18n("SSL")); delete pkcs; // we don't need this anymore pkcs = 0L; } else { //qDebug() << "Client SSL certificate is being used."; setMetaData("ssl_using_client_cert", "TRUE"); if (save) { KSSLCertificateHome::setDefaultCertificate(certname, d->host, true, false); } } d->pkcs = pkcs; } #endif } TCPSlaveBase::SslResult TCPSlaveBase::verifyServerCertificate() { d->sslNoUi = hasMetaData(QStringLiteral("ssl_no_ui")) && (metaData(QStringLiteral("ssl_no_ui")) != QLatin1String("FALSE")); if (d->sslErrors.isEmpty()) { return ResultOk; } else if (d->sslNoUi) { return ResultFailed; } const QList fatalErrors = KSslCertificateManager::nonIgnorableErrors(d->sslErrors); if (!fatalErrors.isEmpty()) { //TODO message "sorry, fatal error, you can't override it" return ResultFailed; } QList peerCertificationChain = d->socket.peerCertificateChain(); KSslCertificateManager *const cm = KSslCertificateManager::self(); KSslCertificateRule rule = cm->rule(peerCertificationChain.first(), d->host); // remove previously seen and acknowledged errors const QList remainingErrors = rule.filterErrors(d->sslErrors); if (remainingErrors.isEmpty()) { //qDebug() << "Error list empty after removing errors to be ignored. Continuing."; return ResultOk | ResultOverridden; } //### We don't ask to permanently reject the certificate QString message = i18n("The server failed the authenticity check (%1).\n\n", d->host); for (const QSslError &err : qAsConst(d->sslErrors)) { message += err.errorString() + QLatin1Char('\n'); } message = message.trimmed(); int msgResult; QDateTime ruleExpiry = QDateTime::currentDateTime(); do { msgResult = messageBox(WarningYesNoCancel, message, i18n("Server Authentication"), i18n("&Details"), i18n("Co&ntinue")); switch (msgResult) { case SlaveBase::Yes: //Details was chosen- show the certificate and error details messageBox(SSLMessageBox /*the SSL info dialog*/, d->host); break; case SlaveBase::No: { //fall through on SlaveBase::No const int result = messageBox(WarningYesNoCancel, i18n("Would you like to accept this " "certificate forever without " "being prompted?"), i18n("Server Authentication"), i18n("&Forever"), i18n("&Current Session only")); if (result == SlaveBase::Yes) { //accept forever ("for a very long time") ruleExpiry = ruleExpiry.addYears(1000); } else if (result == SlaveBase::No) { //accept "for a short time", half an hour. ruleExpiry = ruleExpiry.addSecs(30*60); } else { msgResult = SlaveBase::Yes; } break; } case SlaveBase::Cancel: return ResultFailed; default: qCWarning(KIO_CORE) << "Unexpected MessageBox response received:" << msgResult; return ResultFailed; } } while (msgResult == SlaveBase::Yes); //TODO special cases for wildcard domain name in the certificate! //rule = KSslCertificateRule(d->socket.peerCertificateChain().first(), whatever); rule.setExpiryDateTime(ruleExpiry); rule.setIgnoredErrors(d->sslErrors); cm->setRule(rule); return ResultOk | ResultOverridden; #if 0 //### need to do something like the old code about the main and subframe stuff //qDebug() << "SSL HTTP frame the parent? " << metaData("main_frame_request"); if (!hasMetaData("main_frame_request") || metaData("main_frame_request") == "TRUE") { // Since we're the parent, we need to teach the child. setMetaData("ssl_parent_ip", d->ip); setMetaData("ssl_parent_cert", pc.toString()); // - Read from cache and see if there is a policy for this KSSLCertificateCache::KSSLCertificatePolicy cp = d->certCache->getPolicyByCertificate(pc); // - validation code if (ksv != KSSLCertificate::Ok) { if (d->sslNoUi) { return -1; } if (cp == KSSLCertificateCache::Unknown || cp == KSSLCertificateCache::Ambiguous) { cp = KSSLCertificateCache::Prompt; } else { // A policy was already set so let's honor that. permacache = d->certCache->isPermanent(pc); } if (!_IPmatchesCN && cp == KSSLCertificateCache::Accept) { cp = KSSLCertificateCache::Prompt; // ksv = KSSLCertificate::Ok; } ////// SNIP SNIP ////////// // - cache the results d->certCache->addCertificate(pc, cp, permacache); if (doAddHost) { d->certCache->addHost(pc, d->host); } } else { // Child frame // - Read from cache and see if there is a policy for this KSSLCertificateCache::KSSLCertificatePolicy cp = d->certCache->getPolicyByCertificate(pc); isChild = true; // Check the cert and IP to make sure they're the same // as the parent frame bool certAndIPTheSame = (d->ip == metaData("ssl_parent_ip") && pc.toString() == metaData("ssl_parent_cert")); if (ksv == KSSLCertificate::Ok) { if (certAndIPTheSame) { // success rc = 1; setMetaData("ssl_action", "accept"); } else { /* if (d->sslNoUi) { return -1; } result = messageBox(WarningYesNo, i18n("The certificate is valid but does not appear to have been assigned to this server. Do you wish to continue loading?"), i18n("Server Authentication")); if (result == SlaveBase::Yes) { // success rc = 1; setMetaData("ssl_action", "accept"); } else { // fail rc = -1; setMetaData("ssl_action", "reject"); } */ setMetaData("ssl_action", "accept"); rc = 1; // Let's accept this now. It's bad, but at least the user // will see potential attacks in KDE3 with the pseudo-lock // icon on the toolbar, and can investigate with the RMB } } else { if (d->sslNoUi) { return -1; } if (cp == KSSLCertificateCache::Accept) { if (certAndIPTheSame) { // success rc = 1; setMetaData("ssl_action", "accept"); } else { // fail result = messageBox(WarningYesNo, i18n("You have indicated that you wish to accept this certificate, but it is not issued to the server who is presenting it. Do you wish to continue loading?"), i18n("Server Authentication")); if (result == SlaveBase::Yes) { rc = 1; setMetaData("ssl_action", "accept"); d->certCache->addHost(pc, d->host); } else { rc = -1; setMetaData("ssl_action", "reject"); } } } else if (cp == KSSLCertificateCache::Reject) { // fail messageBox(Information, i18n("SSL certificate is being rejected as requested. You can disable this in the KDE System Settings."), i18n("Server Authentication")); rc = -1; setMetaData("ssl_action", "reject"); } else { //////// SNIP SNIP ////////// return rc; } } } } #endif //#if 0 } bool TCPSlaveBase::isConnected() const { // QSslSocket::isValid() is shady... return d->socket.state() == QAbstractSocket::ConnectedState; } bool TCPSlaveBase::waitForResponse(int t) { if (d->socket.bytesAvailable()) { return true; } return d->socket.waitForReadyRead(t * 1000); } void TCPSlaveBase::setBlocking(bool b) { if (!b) { qCWarning(KIO_CORE) << "Caller requested non-blocking mode, but that doesn't work"; return; } d->isBlocking = b; } void TCPSlaveBase::virtual_hook(int id, void *data) { if (id == SlaveBase::AppConnectionMade) { d->sendSslMetaData(); } else { SlaveBase::virtual_hook(id, data); } } diff --git a/src/ioslaves/ftp/ftp.cpp b/src/ioslaves/ftp/ftp.cpp index ae9ff938..8177afc0 100644 --- a/src/ioslaves/ftp/ftp.cpp +++ b/src/ioslaves/ftp/ftp.cpp @@ -1,2763 +1,2755 @@ /* This file is part of the KDE libraries Copyright (C) 2000-2006 David Faure Copyright (C) 2019 Harald Sitter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* Recommended reading explaining FTP details and quirks: http://cr.yp.to/ftp.html (by D.J. Bernstein) RFC: RFC 959 "File Transfer Protocol (FTP)" RFC 1635 "How to Use Anonymous FTP" RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV) */ #include #define KIO_FTP_PRIVATE_INCLUDE #include "ftp.h" #ifdef Q_OS_WIN #include #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kioglobal_p.h" #include Q_DECLARE_LOGGING_CATEGORY(KIO_FTP) Q_LOGGING_CATEGORY(KIO_FTP, "kf5.kio.kio_ftp", QtWarningMsg) #if HAVE_STRTOLL #define charToLongLong(a) strtoll(a, nullptr, 10) #else #define charToLongLong(a) strtol(a, nullptr, 10) #endif #define FTP_LOGIN "anonymous" #define FTP_PASSWD "anonymous@" #define ENABLE_CAN_RESUME // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.ftp" FILE "ftp.json") }; static QString ftpCleanPath(const QString &path) { if (path.endsWith(QLatin1String(";type=A"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=I"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=D"), Qt::CaseInsensitive)) { return path.left((path.length() - qstrlen(";type=X"))); } return path; } static char ftpModeFromPath(const QString &path, char defaultMode = '\0') { const int index = path.lastIndexOf(QLatin1String(";type=")); if (index > -1 && (index + 6) < path.size()) { const QChar mode = path.at(index + 6); // kio_ftp supports only A (ASCII) and I(BINARY) modes. if (mode == QLatin1Char('A') || mode == QLatin1Char('a') || mode == QLatin1Char('I') || mode == QLatin1Char('i')) { return mode.toUpper().toLatin1(); } } return defaultMode; } static bool supportedProxyScheme(const QString &scheme) { return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks")); } // JPF: somebody should find a better solution for this or move this to KIO namespace KIO { enum buffersizes { /** * largest buffer size that should be used to transfer data between * KIO slaves using the data() function */ maximumIpcSize = 32 * 1024, /** * this is a reasonable value for an initial read() that a KIO slave * can do to obtain data via a slow network connection. */ initialIpcSize = 2 * 1024, /** * recommended size of a data block passed to findBufferFileType() */ minimumMimeSize = 1024 }; // JPF: this helper was derived from write_all in file.cc (FileProtocol). static // JPF: in ftp.cc we make it static /** * This helper handles some special issues (blocking and interrupted * system call) when writing to a file handle. * * @return 0 on success or an error code on failure (ERR_CANNOT_WRITE, * ERR_DISK_FULL, ERR_CONNECTION_BROKEN). */ int WriteToFile(int fd, const char *buf, size_t len) { while (len > 0) { // JPF: shouldn't there be a KDE_write? ssize_t written = write(fd, buf, len); if (written >= 0) { buf += written; len -= written; continue; } switch (errno) { case EINTR: continue; case EPIPE: return ERR_CONNECTION_BROKEN; case ENOSPC: return ERR_DISK_FULL; default: return ERR_CANNOT_WRITE; } } return 0; } } const KIO::filesize_t FtpInternal::UnknownSize = (KIO::filesize_t) - 1; using namespace KIO; extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); app.setApplicationName(QStringLiteral("kio_ftp")); qCDebug(KIO_FTP) << "Starting"; if (argc != 4) { fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n"); exit(-1); } Ftp slave(argv[2], argv[3]); slave.dispatchLoop(); qCDebug(KIO_FTP) << "Done"; return 0; } //=============================================================================== // FtpInternal //=============================================================================== /** * This closes a data connection opened by ftpOpenDataConnection(). */ void FtpInternal::ftpCloseDataConnection() { delete m_data; m_data = nullptr; delete m_server; m_server = nullptr; } /** * This closes a control connection opened by ftpOpenControlConnection() and reinits the * related states. This method gets called from the constructor with m_control = nullptr. */ void FtpInternal::ftpCloseControlConnection() { m_extControl = 0; delete m_control; m_control = nullptr; m_cDataMode = 0; m_bLoggedOn = false; // logon needs control connection m_bTextMode = false; m_bBusy = false; } /** * Returns the last response from the server (iOffset >= 0) -or- reads a new response * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0). */ const char *FtpInternal::ftpResponse(int iOffset) { Q_ASSERT(m_control); // must have control connection socket const char *pTxt = m_lastControlLine.data(); // read the next line ... if (iOffset < 0) { int iMore = 0; m_iRespCode = 0; if (!pTxt) { return nullptr; // avoid using a nullptr when calling atoi. } // If the server sends a multiline response starting with // "nnn-text" we loop here until a final "nnn text" line is // reached. Only data from the final line will be stored. do { while (!m_control->canReadLine() && m_control->waitForReadyRead((q->readTimeout() * 1000))) {} m_lastControlLine = m_control->readLine(); pTxt = m_lastControlLine.data(); int iCode = atoi(pTxt); if (iMore == 0) { // first line qCDebug(KIO_FTP) << " > " << pTxt; if (iCode >= 100) { m_iRespCode = iCode; if (pTxt[3] == '-') { // marker for a multiple line response iMore = iCode; } } else { qCWarning(KIO_FTP) << "Cannot parse valid code from line" << pTxt; } } else { // multi-line qCDebug(KIO_FTP) << " > " << pTxt; if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') { iMore = 0; } } } while (iMore != 0); qCDebug(KIO_FTP) << "resp> " << pTxt; m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0; } // return text with offset ... while (iOffset-- > 0 && pTxt[0]) { pTxt++; } return pTxt; } void FtpInternal::closeConnection() { if (m_control || m_data) qCDebug(KIO_FTP) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy; if (m_bBusy) { // ftpCloseCommand not called qCWarning(KIO_FTP) << "Abandoned data stream"; ftpCloseDataConnection(); } if (m_bLoggedOn) { // send quit if (!ftpSendCmd(QByteArrayLiteral("quit"), 0) || (m_iRespType != 2)) { qCWarning(KIO_FTP) << "QUIT returned error: " << m_iRespCode; } } // close the data and control connections ... ftpCloseDataConnection(); ftpCloseControlConnection(); } FtpInternal::FtpInternal(Ftp *qptr) : QObject() , q(qptr) { ftpCloseControlConnection(); } FtpInternal::~FtpInternal() { qCDebug(KIO_FTP); closeConnection(); } void FtpInternal::setHost(const QString &_host, quint16 _port, const QString &_user, const QString &_pass) { qCDebug(KIO_FTP) << _host << "port=" << _port << "user=" << _user; m_proxyURL.clear(); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) m_proxyUrls = q->mapConfig().value(QStringLiteral("ProxyUrls"), QString()).toString().split(QLatin1Char(','), QString::SkipEmptyParts); #else m_proxyUrls = q->mapConfig().value(QStringLiteral("ProxyUrls"), QString()).toString().split(QLatin1Char(','), Qt::SkipEmptyParts); #endif qCDebug(KIO_FTP) << "proxy urls:" << m_proxyUrls; if (m_host != _host || m_port != _port || m_user != _user || m_pass != _pass) { closeConnection(); } m_host = _host; m_port = _port; m_user = _user; m_pass = _pass; } Result FtpInternal::openConnection() { return ftpOpenConnection(LoginMode::Explicit); } Result FtpInternal::ftpOpenConnection(LoginMode loginMode) { // check for implicit login if we are already logged on ... if (loginMode == LoginMode::Implicit && m_bLoggedOn) { Q_ASSERT(m_control); // must have control connection socket return Result::pass(); } qCDebug(KIO_FTP) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]"; q->infoMessage(i18n("Opening connection to host %1", m_host)); if (m_host.isEmpty()) { return Result::fail(ERR_UNKNOWN_HOST); } Q_ASSERT(!m_bLoggedOn); m_initialPath.clear(); m_currentPath.clear(); const Result result = ftpOpenControlConnection(); if (!result.success) { return result; } q->infoMessage(i18n("Connected to host %1", m_host)); bool userNameChanged = false; if (loginMode != LoginMode::Deferred) { const Result result = ftpLogin(&userNameChanged); m_bLoggedOn = result.success; if (!m_bLoggedOn) { return result; } } m_bTextMode = q->configValue(QStringLiteral("textmode"), false); q->connected(); // Redirected due to credential change... if (userNameChanged && m_bLoggedOn) { QUrl realURL; realURL.setScheme(QStringLiteral("ftp")); if (m_user != QLatin1String(FTP_LOGIN)) { realURL.setUserName(m_user); } if (m_pass != QLatin1String(FTP_PASSWD)) { realURL.setPassword(m_pass); } realURL.setHost(m_host); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { realURL.setPort(m_port); } if (m_initialPath.isEmpty()) { m_initialPath = QStringLiteral("/"); } realURL.setPath(m_initialPath); qCDebug(KIO_FTP) << "User name changed! Redirecting to" << realURL; q->redirection(realURL); return Result::fail(); } return Result::pass(); } /** * Called by @ref openConnection. It opens the control connection to the ftp server. * * @return true on success. */ Result FtpInternal::ftpOpenControlConnection() { if (m_proxyUrls.isEmpty()) { return ftpOpenControlConnection(m_host, m_port); } Result result = Result::fail(); for (const QString &proxyUrl : qAsConst(m_proxyUrls)) { const QUrl url(proxyUrl); const QString scheme(url.scheme()); if (!supportedProxyScheme(scheme)) { // TODO: Need a new error code to indicate unsupported URL scheme. result = Result::fail(ERR_CANNOT_CONNECT, url.toString()); continue; } if (!isSocksProxyScheme(scheme)) { const Result result = ftpOpenControlConnection(url.host(), url.port()); if (result.success) { return Result::pass(); } continue; } qCDebug(KIO_FTP) << "Connecting to SOCKS proxy @" << url; m_proxyURL = url; result = ftpOpenControlConnection(m_host, m_port); if (result.success) { return result; } m_proxyURL.clear(); } return result; } Result FtpInternal::ftpOpenControlConnection(const QString &host, int port) { // implicitly close, then try to open a new connection ... closeConnection(); QString sErrorMsg; // now connect to the server and read the login message ... if (port == 0) { port = 21; // default FTP port } const auto connectionResult = synchronousConnectToHost(host, port); m_control = connectionResult.socket; int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_CANNOT_CONNECT; if (!connectionResult.result.success) { qDebug() << "overriding error code!!1" << connectionResult.result.error; iErrorCode = connectionResult.result.error; sErrorMsg = connectionResult.result.errorString; } // on connect success try to read the server message... if (iErrorCode == 0) { const char *psz = ftpResponse(-1); if (m_iRespType != 2) { // login not successful, do we have an message text? if (psz[0]) { sErrorMsg = i18n("%1 (Error %2)", host, q->remoteEncoding()->decode(psz).trimmed()); } iErrorCode = ERR_CANNOT_CONNECT; } } else { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto socketError = m_control->error(); -#else - const auto socketError = m_control->socketError(); -#endif if (socketError == QAbstractSocket::HostNotFoundError) { iErrorCode = ERR_UNKNOWN_HOST; } sErrorMsg = QStringLiteral("%1: %2").arg(host, m_control->errorString()); } // if there was a problem - report it ... if (iErrorCode == 0) { // OK, return success return Result::pass(); } closeConnection(); // clean-up on error return Result::fail(iErrorCode, sErrorMsg); } /** * Called by @ref openConnection. It logs us in. * @ref m_initialPath is set to the current working directory * if logging on was successful. * * @return true on success. */ Result FtpInternal::ftpLogin(bool *userChanged) { q->infoMessage(i18n("Sending login information")); Q_ASSERT(!m_bLoggedOn); QString user(m_user); QString pass(m_pass); if (q->configValue(QStringLiteral("EnableAutoLogin"), false)) { QString au = q->configValue(QStringLiteral("autoLoginUser")); if (!au.isEmpty()) { user = au; pass = q->configValue(QStringLiteral("autoLoginPass")); } } AuthInfo info; info.url.setScheme(QStringLiteral("ftp")); info.url.setHost(m_host); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { info.url.setPort(m_port); } if (!user.isEmpty()) { info.url.setUserName(user); } // Check for cached authentication first and fallback to // anonymous login when no stored credentials are found. if (!q->configValue(QStringLiteral("TryAnonymousLoginFirst"), false) && pass.isEmpty() && q->checkCachedAuthentication(info)) { user = info.username; pass = info.password; } // Try anonymous login if both username/password // information is blank. if (user.isEmpty() && pass.isEmpty()) { user = QStringLiteral(FTP_LOGIN); pass = QStringLiteral(FTP_PASSWD); } QByteArray tempbuf; QString lastServerResponse; int failedAuth = 0; bool promptForRetry = false; // Give the user the option to login anonymously... info.setExtraField(QStringLiteral("anonymous"), false); do { // Check the cache and/or prompt user for password if 1st // login attempt failed OR the user supplied a login name, // but no password. if (failedAuth > 0 || (!user.isEmpty() && pass.isEmpty())) { QString errorMsg; qCDebug(KIO_FTP) << "Prompting user for login info..."; // Ask user if we should retry after when login fails! if (failedAuth > 0 && promptForRetry) { errorMsg = i18n("Message sent:\nLogin using username=%1 and " "password=[hidden]\n\nServer replied:\n%2\n\n" , user, lastServerResponse); } if (user != QLatin1String(FTP_LOGIN)) { info.username = user; } info.prompt = i18n("You need to supply a username and a password " "to access this site."); info.commentLabel = i18n("Site:"); info.comment = i18n("%1", m_host); info.keepPassword = true; // Prompt the user for persistence as well. info.setModified(false); // Default the modified flag since we reuse authinfo. const bool disablePassDlg = q->configValue(QStringLiteral("DisablePassDlg"), false); if (disablePassDlg) { return Result::fail(ERR_USER_CANCELED, m_host); } const int errorCode = q->openPasswordDialogV2(info, errorMsg); if (errorCode) { return Result::fail(errorCode); } else { // User can decide go anonymous using checkbox if (info.getExtraField(QStringLiteral("anonymous")).toBool()) { user = QStringLiteral(FTP_LOGIN); pass = QStringLiteral(FTP_PASSWD); } else { user = info.username; pass = info.password; } promptForRetry = true; } } tempbuf = "USER " + user.toLatin1(); if (m_proxyURL.isValid()) { tempbuf += '@' + m_host.toLatin1(); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { tempbuf += ':' + QByteArray::number(m_port); } } qCDebug(KIO_FTP) << "Sending Login name: " << tempbuf; bool loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230)); bool needPass = (m_iRespCode == 331); // Prompt user for login info if we do not // get back a "230" or "331". if (!loggedIn && !needPass) { lastServerResponse = QString::fromUtf8(ftpResponse(0)); qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse; ++failedAuth; continue; // Well we failed, prompt the user please!! } if (needPass) { tempbuf = "PASS " + pass.toLatin1(); qCDebug(KIO_FTP) << "Sending Login password: " << "[protected]"; loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230)); } if (loggedIn) { // Make sure the user name changed flag is properly set. if (userChanged) { *userChanged = (!m_user.isEmpty() && (m_user != user)); } // Do not cache the default login!! if (user != QLatin1String(FTP_LOGIN) && pass != QLatin1String(FTP_PASSWD)) { // Update the username in case it was changed during login. if (!m_user.isEmpty()) { info.url.setUserName(user); m_user = user; } // Cache the password if the user requested it. if (info.keepPassword) { q->cacheAuthentication(info); } } failedAuth = -1; } else { // some servers don't let you login anymore // if you fail login once, so restart the connection here lastServerResponse = QString::fromUtf8(ftpResponse(0)); const Result result = ftpOpenControlConnection(); if (!result.success) { return result; } } } while (++failedAuth); qCDebug(KIO_FTP) << "Login OK"; q->infoMessage(i18n("Login OK")); // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix: // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint if (ftpSendCmd(QByteArrayLiteral("SYST")) && (m_iRespType == 2)) { if (!qstrncmp(ftpResponse(0), "215 Windows_NT", 14)) { // should do for any version (void) ftpSendCmd(QByteArrayLiteral("site dirstyle")); // Check if it was already in Unix style // Patch from Keith Refson if (!qstrncmp(ftpResponse(0), "200 MSDOS-like directory output is on", 37)) //It was in Unix style already! { (void) ftpSendCmd(QByteArrayLiteral("site dirstyle")); } // windows won't support chmod before KDE konquers their desktop... m_extControl |= chmodUnknown; } } else { qCWarning(KIO_FTP) << "SYST failed"; } if (q->configValue(QStringLiteral("EnableAutoLoginMacro"), false)) { ftpAutoLoginMacro(); } // Get the current working directory qCDebug(KIO_FTP) << "Searching for pwd"; if (!ftpSendCmd(QByteArrayLiteral("PWD")) || (m_iRespType != 2)) { qCDebug(KIO_FTP) << "Couldn't issue pwd command"; return Result::fail(ERR_CANNOT_LOGIN, i18n("Could not login to %1.", m_host)); // or anything better ? } QString sTmp = q->remoteEncoding()->decode(ftpResponse(3)); const int iBeg = sTmp.indexOf(QLatin1Char('"')); const int iEnd = sTmp.lastIndexOf(QLatin1Char('"')); if (iBeg > 0 && iBeg < iEnd) { m_initialPath = sTmp.mid(iBeg + 1, iEnd - iBeg - 1); if (m_initialPath[0] != QLatin1Char('/')) { m_initialPath.prepend(QLatin1Char('/')); } qCDebug(KIO_FTP) << "Initial path set to: " << m_initialPath; m_currentPath = m_initialPath; } return Result::pass(); } void FtpInternal::ftpAutoLoginMacro() { QString macro = q->metaData(QStringLiteral("autoLoginMacro")); if (macro.isEmpty()) { return; } #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList list = macro.split(QLatin1Char('\n'), QString::SkipEmptyParts); #else const QStringList list = macro.split(QLatin1Char('\n'), Qt::SkipEmptyParts); #endif for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it) { if ((*it).startsWith(QLatin1String("init"))) { #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList list2 = macro.split(QLatin1Char('\\'), QString::SkipEmptyParts); #else const QStringList list2 = macro.split(QLatin1Char('\\'), Qt::SkipEmptyParts); #endif it = list2.begin(); ++it; // ignore the macro name for (; it != list2.end(); ++it) { // TODO: Add support for arbitrary commands // besides simply changing directory!! if ((*it).startsWith(QLatin1String("cwd"))) { (void) ftpFolder((*it).mid(4)); } } break; } } } /** * ftpSendCmd - send a command (@p cmd) and read response * * @param maxretries number of time it should retry. Since it recursively * calls itself if it can't read the answer (this happens especially after * timeouts), we need to limit the recursiveness ;-) * * return true if any response received, false on error */ bool FtpInternal::ftpSendCmd(const QByteArray &cmd, int maxretries) { Q_ASSERT(m_control); // must have control connection socket if (cmd.indexOf('\r') != -1 || cmd.indexOf('\n') != -1) { qCWarning(KIO_FTP) << "Invalid command received (contains CR or LF):" << cmd.data(); return false; } // Don't print out the password... bool isPassCmd = (cmd.left(4).toLower() == "pass"); #if 0 if (!isPassCmd) { qDebug() << "send> " << cmd.data(); } else { qDebug() << "send> pass [protected]"; } #endif // Send the message... const QByteArray buf = cmd + "\r\n"; // Yes, must use CR/LF - see http://cr.yp.to/ftp/request.html int num = m_control->write(buf); while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) {} // If we were able to successfully send the command, then we will // attempt to read the response. Otherwise, take action to re-attempt // the login based on the maximum number of retries specified... if (num > 0) { ftpResponse(-1); } else { m_iRespType = m_iRespCode = 0; } // If respCh is NULL or the response is 421 (Timed-out), we try to re-send // the command based on the value of maxretries. if ((m_iRespType <= 0) || (m_iRespCode == 421)) { // We have not yet logged on... if (!m_bLoggedOn) { // The command was sent from the ftpLogin function, i.e. we are actually // attempting to login in. NOTE: If we already sent the username, we // return false and let the user decide whether (s)he wants to start from // the beginning... if (maxretries > 0 && !isPassCmd) { closeConnection(); const auto result = ftpOpenConnection(LoginMode::Deferred); if (result.success && ftpSendCmd(cmd, maxretries - 1)) { return true; } } return false; } else { if (maxretries < 1) { return false; } else { qCDebug(KIO_FTP) << "Was not able to communicate with " << m_host << "Attempting to re-establish connection."; closeConnection(); // Close the old connection... const Result openResult = openConnection(); // Attempt to re-establish a new connection... if (!openResult.success) { if (m_control) { // if openConnection succeeded ... qCDebug(KIO_FTP) << "Login failure, aborting"; closeConnection(); } return false; } qCDebug(KIO_FTP) << "Logged back in, re-issuing command"; // If we were able to login, resend the command... if (maxretries) { maxretries--; } return ftpSendCmd(cmd, maxretries); } } } return true; } /* * ftpOpenPASVDataConnection - set up data connection, using PASV mode * * return 0 if successful, ERR_INTERNAL otherwise * doesn't set error message, since non-pasv mode will always be tried if * this one fails */ int FtpInternal::ftpOpenPASVDataConnection() { Q_ASSERT(m_control); // must have control connection socket Q_ASSERT(!m_data); // ... but no data connection // Check that we can do PASV QHostAddress address = m_control->peerAddress(); if (address.protocol() != QAbstractSocket::IPv4Protocol && !isSocksProxy()) { return ERR_INTERNAL; // no PASV for non-PF_INET connections } if (m_extControl & pasvUnknown) { return ERR_INTERNAL; // already tried and got "unknown command" } m_bPasv = true; /* Let's PASsiVe*/ if (!ftpSendCmd(QByteArrayLiteral("PASV")) || (m_iRespType != 2)) { qCDebug(KIO_FTP) << "PASV attempt failed"; // unknown command? if (m_iRespType == 5) { qCDebug(KIO_FTP) << "disabling use of PASV"; m_extControl |= pasvUnknown; } return ERR_INTERNAL; } // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)' // but anonftpd gives '227 =160,39,200,55,6,245' int i[6]; const char *start = strchr(ftpResponse(3), '('); if (!start) { start = strchr(ftpResponse(3), '='); } if (!start || (sscanf(start, "(%d,%d,%d,%d,%d,%d)", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 && sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6)) { qCritical() << "parsing IP and port numbers failed. String parsed: " << start; return ERR_INTERNAL; } // we ignore the host part on purpose for two reasons // a) it might be wrong anyway // b) it would make us being susceptible to a port scanning attack // now connect the data socket ... quint16 port = i[4] << 8 | i[5]; const QString host = (isSocksProxy() ? m_host : address.toString()); const auto connectionResult = synchronousConnectToHost(host, port); m_data = connectionResult.socket; if (!connectionResult.result.success) { return connectionResult.result.error; } return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL; } /* * ftpOpenEPSVDataConnection - opens a data connection via EPSV */ int FtpInternal::ftpOpenEPSVDataConnection() { Q_ASSERT(m_control); // must have control connection socket Q_ASSERT(!m_data); // ... but no data connection QHostAddress address = m_control->peerAddress(); int portnum; if (m_extControl & epsvUnknown) { return ERR_INTERNAL; } m_bPasv = true; if (!ftpSendCmd(QByteArrayLiteral("EPSV")) || (m_iRespType != 2)) { // unknown command? if (m_iRespType == 5) { qCDebug(KIO_FTP) << "disabling use of EPSV"; m_extControl |= epsvUnknown; } return ERR_INTERNAL; } const char *start = strchr(ftpResponse(3), '|'); if (!start || sscanf(start, "|||%d|", &portnum) != 1) { return ERR_INTERNAL; } Q_ASSERT(portnum > 0); const QString host = (isSocksProxy() ? m_host : address.toString()); const auto connectionResult = synchronousConnectToHost(host, static_cast(portnum)); m_data = connectionResult.socket; if (!connectionResult.result.success) { return connectionResult.result.error; } return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL; } /* * ftpOpenDataConnection - set up data connection * * The routine calls several ftpOpenXxxxConnection() helpers to find * the best connection mode. If a helper cannot connect if returns * ERR_INTERNAL - so this is not really an error! All other error * codes are treated as fatal, e.g. they are passed back to the caller * who is responsible for calling error(). ftpOpenPortDataConnection * can be called as last try and it does never return ERR_INTERNAL. * * @return 0 if successful, err code otherwise */ int FtpInternal::ftpOpenDataConnection() { // make sure that we are logged on and have no data connection... Q_ASSERT(m_bLoggedOn); ftpCloseDataConnection(); int iErrCode = 0; int iErrCodePASV = 0; // Remember error code from PASV // First try passive (EPSV & PASV) modes if (!q->configValue(QStringLiteral("DisablePassiveMode"), false)) { iErrCode = ftpOpenPASVDataConnection(); if (iErrCode == 0) { return 0; // success } iErrCodePASV = iErrCode; ftpCloseDataConnection(); if (!q->configValue(QStringLiteral("DisableEPSV"), false)) { iErrCode = ftpOpenEPSVDataConnection(); if (iErrCode == 0) { return 0; // success } ftpCloseDataConnection(); } // if we sent EPSV ALL already and it was accepted, then we can't // use active connections any more if (m_extControl & epsvAllSent) { return iErrCodePASV; } } // fall back to port mode iErrCode = ftpOpenPortDataConnection(); if (iErrCode == 0) { return 0; // success } ftpCloseDataConnection(); // prefer to return the error code from PASV if any, since that's what should have worked in the first place return iErrCodePASV ? iErrCodePASV : iErrCode; } /* * ftpOpenPortDataConnection - set up data connection * * @return 0 if successful, err code otherwise (but never ERR_INTERNAL * because this is the last connection mode that is tried) */ int FtpInternal::ftpOpenPortDataConnection() { Q_ASSERT(m_control); // must have control connection socket Q_ASSERT(!m_data); // ... but no data connection m_bPasv = false; if (m_extControl & eprtUnknown) { return ERR_INTERNAL; } if (!m_server) { m_server = new QTcpServer; m_server->listen(QHostAddress::Any, 0); } if (!m_server->isListening()) { delete m_server; m_server = nullptr; return ERR_CANNOT_LISTEN; } m_server->setMaxPendingConnections(1); QString command; QHostAddress localAddress = m_control->localAddress(); if (localAddress.protocol() == QAbstractSocket::IPv4Protocol) { struct { quint32 ip4; quint16 port; } data; data.ip4 = localAddress.toIPv4Address(); data.port = m_server->serverPort(); unsigned char *pData = reinterpret_cast(&data); command = QStringLiteral("PORT %1,%2,%3,%4,%5,%6").arg(pData[3]).arg(pData[2]).arg(pData[1]).arg(pData[0]).arg(pData[5]).arg(pData[4]); } else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol) { command = QStringLiteral("EPRT |2|%2|%3|").arg(localAddress.toString()).arg(m_server->serverPort()); } if (ftpSendCmd(command.toLatin1()) && (m_iRespType == 2)) { return 0; } delete m_server; m_server = nullptr; return ERR_INTERNAL; } Result FtpInternal::ftpOpenCommand(const char *_command, const QString &_path, char _mode, int errorcode, KIO::fileoffset_t _offset) { int errCode = 0; if (!ftpDataMode(ftpModeFromPath(_path, _mode))) { errCode = ERR_CANNOT_CONNECT; } else { errCode = ftpOpenDataConnection(); } if (errCode != 0) { return Result::fail(errCode, m_host); } if (_offset > 0) { // send rest command if offset > 0, this applies to retr and stor commands char buf[100]; sprintf(buf, "rest %lld", _offset); if (!ftpSendCmd(buf)) { return Result::fail(); } if (m_iRespType != 3) { return Result::fail(ERR_CANNOT_RESUME, _path); // should never happen } } QByteArray tmp = _command; QString errormessage; if (!_path.isEmpty()) { tmp += ' ' + q->remoteEncoding()->encode(ftpCleanPath(_path)); } if (!ftpSendCmd(tmp) || (m_iRespType != 1)) { if (_offset > 0 && qstrcmp(_command, "retr") == 0 && (m_iRespType == 4)) { errorcode = ERR_CANNOT_RESUME; } // The error code here depends on the command errormessage = _path + i18n("\nThe server said: \"%1\"", QString::fromUtf8(ftpResponse(0)).trimmed()); } else { // Only now we know for sure that we can resume if (_offset > 0 && qstrcmp(_command, "retr") == 0) { q->canResume(); } if (m_server && !m_data) { qCDebug(KIO_FTP) << "waiting for connection from remote."; m_server->waitForNewConnection(q->connectTimeout() * 1000); m_data = m_server->nextPendingConnection(); } if (m_data) { qCDebug(KIO_FTP) << "connected with remote."; m_bBusy = true; // cleared in ftpCloseCommand return Result::pass(); } qCDebug(KIO_FTP) << "no connection received from remote."; errorcode = ERR_CANNOT_ACCEPT; errormessage = m_host; } if (errorcode != KJob::NoError) { return Result::fail(errorcode, errormessage); } return Result::fail(); } bool FtpInternal::ftpCloseCommand() { // first close data sockets (if opened), then read response that // we got for whatever was used in ftpOpenCommand ( should be 226 ) ftpCloseDataConnection(); if (!m_bBusy) { return true; } qCDebug(KIO_FTP) << "ftpCloseCommand: reading command result"; m_bBusy = false; if (!ftpResponse(-1) || (m_iRespType != 2)) { qCDebug(KIO_FTP) << "ftpCloseCommand: no transfer complete message"; return false; } return true; } Result FtpInternal::mkdir(const QUrl &url, int permissions) { auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } const QByteArray encodedPath(q->remoteEncoding()->encode(url)); const QString path = QString::fromLatin1(encodedPath.constData(), encodedPath.size()); if (!ftpSendCmd((QByteArrayLiteral("mkd ") + encodedPath)) || (m_iRespType != 2)) { QString currentPath(m_currentPath); // Check whether or not mkdir failed because // the directory already exists... if (ftpFolder(path)) { const QString &failedPath = path; // Change the directory back to what it was... (void) ftpFolder(currentPath); return Result::fail(ERR_DIR_ALREADY_EXIST, failedPath); } return Result::fail(ERR_CANNOT_MKDIR, path); } if (permissions != -1) { // chmod the dir we just created, ignoring errors. (void) ftpChmod(path, permissions); } return Result::pass(); } Result FtpInternal::rename(const QUrl &src, const QUrl &dst, KIO::JobFlags flags) { const auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } // The actual functionality is in ftpRename because put needs it return ftpRename(src.path(), dst.path(), flags); } Result FtpInternal::ftpRename(const QString &src, const QString &dst, KIO::JobFlags jobFlags) { Q_ASSERT(m_bLoggedOn); // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793). if (!(jobFlags & KIO::Overwrite)) { if (ftpFileExists(dst)) { return Result::fail(ERR_FILE_ALREADY_EXIST, dst); } } if (ftpFolder(dst)) { return Result::fail(ERR_DIR_ALREADY_EXIST, dst); } // CD into parent folder const int pos = src.lastIndexOf(QLatin1Char('/')); if (pos >= 0) { if (!ftpFolder(src.left(pos + 1))) { return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, src); } } const QByteArray from_cmd = "RNFR " + q->remoteEncoding()->encode(src.mid(pos + 1)); if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) { return Result::fail(ERR_CANNOT_RENAME, src); } const QByteArray to_cmd = "RNTO " + q->remoteEncoding()->encode(dst); if (!ftpSendCmd(to_cmd) || (m_iRespType != 2)) { return Result::fail(ERR_CANNOT_RENAME, src); } return Result::pass(); } Result FtpInternal::del(const QUrl &url, bool isfile) { auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } // When deleting a directory, we must exit from it first // The last command probably went into it (to stat it) if (!isfile) { (void) ftpFolder(q->remoteEncoding()->decode(q->remoteEncoding()->directory(url))); // ignore errors } const QByteArray cmd = (isfile ? "DELE " : "RMD ") + q->remoteEncoding()->encode(url); if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { return Result::fail(ERR_CANNOT_DELETE, url.path()); } return Result::pass(); } bool FtpInternal::ftpChmod(const QString &path, int permissions) { Q_ASSERT(m_bLoggedOn); if (m_extControl & chmodUnknown) { // previous errors? return false; } // we need to do bit AND 777 to get permissions, in case // we were sent a full mode (unlikely) const QByteArray cmd = "SITE CHMOD " + QByteArray::number(permissions & 0777/*octal*/, 8 /*octal*/) + ' ' + q->remoteEncoding()->encode(path); if (ftpSendCmd(cmd)) { qCDebug(KIO_FTP) << "ftpChmod: Failed to issue chmod"; return false; } if (m_iRespType == 2) { return true; } if (m_iRespCode == 500) { m_extControl |= chmodUnknown; qCDebug(KIO_FTP) << "ftpChmod: CHMOD not supported - disabling"; } return false; } Result FtpInternal::chmod(const QUrl &url, int permissions) { const auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } if (!ftpChmod(url.path(), permissions)) { return Result::fail(ERR_CANNOT_CHMOD, url.path()); } return Result::pass(); } void FtpInternal::ftpCreateUDSEntry(const QString &filename, const FtpEntry &ftpEnt, UDSEntry &entry, bool isDir) { Q_ASSERT(entry.count() == 0); // by contract :-) entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ftpEnt.size); entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, ftpEnt.date.toSecsSinceEpoch()); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, ftpEnt.access); entry.fastInsert(KIO::UDSEntry::UDS_USER, ftpEnt.owner); if (!ftpEnt.group.isEmpty()) { entry.fastInsert(KIO::UDSEntry::UDS_GROUP, ftpEnt.group); } if (!ftpEnt.link.isEmpty()) { entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, ftpEnt.link); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(QUrl(QLatin1String("ftp://host/") + filename)); // Links on ftp sites are often links to dirs, and we have no way to check // that. Let's do like Netscape : assume dirs generally. // But we do this only when the mimetype can't be known from the filename. // --> we do better than Netscape :-) if (mime.isDefault()) { qCDebug(KIO_FTP) << "Setting guessed mime type to inode/directory for " << filename; entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QStringLiteral("inode/directory")); isDir = true; } } entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : ftpEnt.type); // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime); // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime); } void FtpInternal::ftpShortStatAnswer(const QString &filename, bool isDir) { UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); if (isDir) { entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); } // No details about size, ownership, group, etc. q->statEntry(entry); } Result FtpInternal::ftpStatAnswerNotFound(const QString &path, const QString &filename) { // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source") // When e.g. uploading a file, we still need stat() to return "not found" // when the file doesn't exist. QString statSide = q->metaData(QStringLiteral("statSide")); qCDebug(KIO_FTP) << "statSide=" << statSide; if (statSide == QLatin1String("source")) { qCDebug(KIO_FTP) << "Not found, but assuming found, because some servers don't allow listing"; // MS Server is incapable of handling "list " in a case insensitive way // But "retr " works. So lie in stat(), to get going... // // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run // where listing permissions are denied, but downloading is still possible. ftpShortStatAnswer(filename, false /*file, not dir*/); return Result::pass(); } return Result::fail(ERR_DOES_NOT_EXIST, path); } Result FtpInternal::stat(const QUrl &url) { qCDebug(KIO_FTP) << "path=" << url.path(); auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } const QString path = ftpCleanPath(QDir::cleanPath(url.path())); qCDebug(KIO_FTP) << "cleaned path=" << path; // We can't stat root, but we know it's a dir. if (path.isEmpty() || path == QLatin1String("/")) { UDSEntry entry; //entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) ); entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral(".")); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); entry.fastInsert(KIO::UDSEntry::UDS_USER, QStringLiteral("root")); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QStringLiteral("root")); // no size q->statEntry(entry); return Result::pass(); } QUrl tempurl(url); tempurl.setPath(path); // take the clean one QString listarg; // = tempurl.directory(QUrl::ObeyTrailingSlash); QString parentDir; const QString filename = tempurl.fileName(); Q_ASSERT(!filename.isEmpty()); // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info) // if it doesn't work, it's a file (and then we'll use dir filename) bool isDir = ftpFolder(path); // if we're only interested in "file or directory", we should stop here QString sDetails = q->metaData(QStringLiteral("details")); int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); qCDebug(KIO_FTP) << "details=" << details; if (details == 0) { if (!isDir && !ftpFileExists(path)) { // ok, not a dir -> is it a file ? // no -> it doesn't exist at all return ftpStatAnswerNotFound(path, filename); } ftpShortStatAnswer(filename, isDir); return Result::pass(); // successfully found a dir or a file -> done } if (!isDir) { // It is a file or it doesn't exist, try going to parent directory parentDir = tempurl.adjusted(QUrl::RemoveFilename).path(); // With files we can do "LIST " to avoid listing the whole dir listarg = filename; } else { // --- New implementation: // Don't list the parent dir. Too slow, might not show it, etc. // Just return that it's a dir. UDSEntry entry; entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); // No clue about size, ownership, group, etc. q->statEntry(entry); return Result::pass(); } // Now cwd the parent dir, to prepare for listing if (!ftpFolder(parentDir)) { return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, parentDir); } result = ftpOpenCommand("list", listarg, 'I', ERR_DOES_NOT_EXIST); if (!result.success) { qCritical() << "COULD NOT LIST"; return result; } qCDebug(KIO_FTP) << "Starting of list was ok"; Q_ASSERT(!filename.isEmpty() && filename != QLatin1String("/")); bool bFound = false; QUrl linkURL; FtpEntry ftpEnt; QList ftpValidateEntList; while (ftpReadDir(ftpEnt)) { if (!ftpEnt.name.isEmpty() && ftpEnt.name.at(0).isSpace()) { ftpValidateEntList.append(ftpEnt); continue; } // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at) // return only the filename when doing "dir /full/path/to/file" if (!bFound) { bFound = maybeEmitStatEntry(ftpEnt, filename, isDir); } qCDebug(KIO_FTP) << ftpEnt.name; } for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) { FtpEntry &ftpEnt = ftpValidateEntList[i]; fixupEntryName(&ftpEnt); if (maybeEmitStatEntry(ftpEnt, filename, isDir)) { break; } } ftpCloseCommand(); // closes the data connection only if (!bFound) { return ftpStatAnswerNotFound(path, filename); } if (!linkURL.isEmpty()) { if (linkURL == url || linkURL == tempurl) { return Result::fail(ERR_CYCLIC_LINK, linkURL.toString()); } return FtpInternal::stat(linkURL); } qCDebug(KIO_FTP) << "stat : finished successfully";; return Result::pass(); } bool FtpInternal::maybeEmitStatEntry(FtpEntry &ftpEnt, const QString &filename, bool isDir) { if (filename == ftpEnt.name && !filename.isEmpty()) { UDSEntry entry; ftpCreateUDSEntry(filename, ftpEnt, entry, isDir); q->statEntry(entry); return true; } return false; } Result FtpInternal::listDir(const QUrl &url) { qCDebug(KIO_FTP) << url; auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } // No path specified ? QString path = url.path(); if (path.isEmpty()) { QUrl realURL; realURL.setScheme(QStringLiteral("ftp")); realURL.setUserName(m_user); realURL.setPassword(m_pass); realURL.setHost(m_host); if (m_port > 0 && m_port != DEFAULT_FTP_PORT) { realURL.setPort(m_port); } if (m_initialPath.isEmpty()) { m_initialPath = QStringLiteral("/"); } realURL.setPath(m_initialPath); qCDebug(KIO_FTP) << "REDIRECTION to " << realURL; q->redirection(realURL); return Result::pass(); } qCDebug(KIO_FTP) << "hunting for path" << path; result = ftpOpenDir(path); if (!result.success) { if (ftpFileExists(path)) { return Result::fail(ERR_IS_FILE, path); } // not sure which to emit //error( ERR_DOES_NOT_EXIST, path ); return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, path); } UDSEntry entry; FtpEntry ftpEnt; QList ftpValidateEntList; while (ftpReadDir(ftpEnt)) { qCDebug(KIO_FTP) << ftpEnt.name; //Q_ASSERT( !ftpEnt.name.isEmpty() ); if (!ftpEnt.name.isEmpty()) { if (ftpEnt.name.at(0).isSpace()) { ftpValidateEntList.append(ftpEnt); continue; } //if ( S_ISDIR( (mode_t)ftpEnt.type ) ) // qDebug() << "is a dir"; //if ( !ftpEnt.link.isEmpty() ) // qDebug() << "is a link to " << ftpEnt.link; ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false); q->listEntry(entry); entry.clear(); } } for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) { FtpEntry &ftpEnt = ftpValidateEntList[i]; fixupEntryName(&ftpEnt); ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false); q->listEntry(entry); entry.clear(); } ftpCloseCommand(); // closes the data connection only return Result::pass(); } void FtpInternal::slave_status() { qCDebug(KIO_FTP) << "Got slave_status host = " << (!m_host.toLatin1().isEmpty() ? m_host.toLatin1() : "[None]") << " [" << (m_bLoggedOn ? "Connected" : "Not connected") << "]"; q->slaveStatus(m_host, m_bLoggedOn); } Result FtpInternal::ftpOpenDir(const QString &path) { //QString path( _url.path(QUrl::RemoveTrailingSlash) ); // We try to change to this directory first to see whether it really is a directory. // (And also to follow symlinks) QString tmp = path.isEmpty() ? QStringLiteral("/") : path; // We get '550', whether it's a file or doesn't exist... if (!ftpFolder(tmp)) { return Result::fail(); } // Don't use the path in the list command: // We changed into this directory anyway - so it's enough just to send "list". // We use '-a' because the application MAY be interested in dot files. // The only way to really know would be to have a metadata flag for this... // Since some windows ftp server seems not to support the -a argument, we use a fallback here. // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com) // Pass KJob::NoError first because we don't want to emit error before we // have tried all commands. auto result = ftpOpenCommand("list -la", QString(), 'I', KJob::NoError); if (!result.success) { result = ftpOpenCommand("list", QString(), 'I', KJob::NoError); } if (!result.success) { // Servers running with Turkish locale having problems converting 'i' letter to upper case. // So we send correct upper case command as last resort. result = ftpOpenCommand("LIST -la", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY); } if (!result.success) { qCWarning(KIO_FTP) << "Can't open for listing"; return result; } qCDebug(KIO_FTP) << "Starting of list was ok"; return Result::pass(); } bool FtpInternal::ftpReadDir(FtpEntry &de) { Q_ASSERT(m_data); // get a line from the data connection ... while (true) { while (!m_data->canReadLine() && m_data->waitForReadyRead((q->readTimeout() * 1000))) {} QByteArray data = m_data->readLine(); if (data.size() == 0) { break; } const char *buffer = data.data(); qCDebug(KIO_FTP) << "dir > " << buffer; //Normally the listing looks like // -rw-r--r-- 1 dfaure dfaure 102 Nov 9 12:30 log // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442) // d [RWCEAFMS] Admin 512 Oct 13 2004 PSI // we should always get the following 5 fields ... const char *p_access, *p_junk, *p_owner, *p_group, *p_size; if ((p_access = strtok((char *)buffer, " ")) == nullptr) { continue; } if ((p_junk = strtok(nullptr, " ")) == nullptr) { continue; } if ((p_owner = strtok(nullptr, " ")) == nullptr) { continue; } if ((p_group = strtok(nullptr, " ")) == nullptr) { continue; } if ((p_size = strtok(nullptr, " ")) == nullptr) { continue; } qCDebug(KIO_FTP) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size; de.access = 0; if (qstrlen(p_access) == 1 && p_junk[0] == '[') { // Netware de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions } const char *p_date_1, *p_date_2, *p_date_3, *p_name; // A special hack for "/dev". A listing may look like this: // crw-rw-rw- 1 root root 1, 5 Jun 29 1997 zero // So we just ignore the number in front of the ",". Ok, it is a hack :-) if (strchr(p_size, ',') != nullptr) { qCDebug(KIO_FTP) << "Size contains a ',' -> reading size again (/dev hack)"; if ((p_size = strtok(nullptr, " ")) == nullptr) { continue; } } // This is needed for ftp servers with a directory listing like this (#375610): // drwxr-xr-x folder 0 Mar 15 15:50 directory_name if (strcmp(p_junk, "folder") == 0) { p_date_1 = p_group; p_date_2 = p_size; p_size = p_owner; p_group = nullptr; p_owner = nullptr; } // Check whether the size we just read was really the size // or a month (this happens when the server lists no group) // Used to be the case on sunsite.uio.no, but not anymore // This is needed for the Netware case, too. else if (!isdigit(*p_size)) { p_date_1 = p_size; p_date_2 = strtok(nullptr, " "); p_size = p_group; p_group = nullptr; qCDebug(KIO_FTP) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1; } else { p_date_1 = strtok(nullptr, " "); p_date_2 = strtok(nullptr, " "); qCDebug(KIO_FTP) << "Size has a digit -> ok. p_date_1=" << p_date_1; } if (p_date_1 != nullptr && p_date_2 != nullptr && (p_date_3 = strtok(nullptr, " ")) != nullptr && (p_name = strtok(nullptr, "\r\n")) != nullptr) { { QByteArray tmp(p_name); if (p_access[0] == 'l') { int i = tmp.lastIndexOf(" -> "); if (i != -1) { de.link = q->remoteEncoding()->decode(p_name + i + 4); tmp.truncate(i); } else { de.link.clear(); } } else { de.link.clear(); } if (tmp[0] == '/') { // listing on ftp://ftp.gnupg.org/ starts with '/' tmp.remove(0, 1); } if (tmp.indexOf('/') != -1) { continue; // Don't trick us! } de.name = q->remoteEncoding()->decode(tmp); } de.type = S_IFREG; switch (p_access[0]) { case 'd': de.type = S_IFDIR; break; case 's': de.type = S_IFSOCK; break; case 'b': de.type = S_IFBLK; break; case 'c': de.type = S_IFCHR; break; case 'l': de.type = S_IFREG; // we don't set S_IFLNK here. de.link says it. break; default: break; } if (p_access[1] == 'r') { de.access |= S_IRUSR; } if (p_access[2] == 'w') { de.access |= S_IWUSR; } if (p_access[3] == 'x' || p_access[3] == 's') { de.access |= S_IXUSR; } if (p_access[4] == 'r') { de.access |= S_IRGRP; } if (p_access[5] == 'w') { de.access |= S_IWGRP; } if (p_access[6] == 'x' || p_access[6] == 's') { de.access |= S_IXGRP; } if (p_access[7] == 'r') { de.access |= S_IROTH; } if (p_access[8] == 'w') { de.access |= S_IWOTH; } if (p_access[9] == 'x' || p_access[9] == 't') { de.access |= S_IXOTH; } if (p_access[3] == 's' || p_access[3] == 'S') { de.access |= S_ISUID; } if (p_access[6] == 's' || p_access[6] == 'S') { de.access |= S_ISGID; } if (p_access[9] == 't' || p_access[9] == 'T') { de.access |= S_ISVTX; } de.owner = q->remoteEncoding()->decode(p_owner); de.group = q->remoteEncoding()->decode(p_group); de.size = charToLongLong(p_size); // Parsing the date is somewhat tricky // Examples : "Oct 6 22:49", "May 13 1999" // First get current date - we need the current month and year QDate currentDate(QDate::currentDate()); int currentMonth = currentDate.month(); int day = currentDate.day(); int month = currentDate.month(); int year = currentDate.year(); int minute = 0; int hour = 0; // Get day number (always second field) if (p_date_2) { day = atoi(p_date_2); } // Get month from first field // NOTE : no, we don't want to use KLocale here // It seems all FTP servers use the English way qCDebug(KIO_FTP) << "Looking for month " << p_date_1; static const char s_months[][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; for (int c = 0; c < 12; c ++) if (!qstrcmp(p_date_1, s_months[c])) { qCDebug(KIO_FTP) << "Found month " << c << " for " << p_date_1; month = c + 1; break; } // Parse third field if (p_date_3 && !strchr(p_date_3, ':')) { // No colon, looks like a year year = atoi(p_date_3); } else { // otherwise, the year is implicit // according to man ls, this happens when it is between than 6 months // old and 1 hour in the future. // So the year is : current year if tm_mon <= currentMonth+1 // otherwise current year minus one // (The +1 is a security for the "+1 hour" at the end of the month issue) if (month > currentMonth + 1) { year--; } // and p_date_3 contains probably a time char *semicolon; if (p_date_3 && (semicolon = (char *)strchr(p_date_3, ':'))) { *semicolon = '\0'; minute = atoi(semicolon + 1); hour = atoi(p_date_3); } else { qCWarning(KIO_FTP) << "Can't parse third field " << p_date_3; } } de.date = QDateTime(QDate(year, month, day), QTime(hour, minute)); qCDebug(KIO_FTP) << de.date; return true; } } // line invalid, loop to get another line return false; } //=============================================================================== // public: get download file from server // helper: ftpGet called from get() and copy() //=============================================================================== Result FtpInternal::get(const QUrl &url) { qCDebug(KIO_FTP) << url; const Result result = ftpGet(-1, QString(), url, 0); ftpCloseCommand(); // must close command! return result; } Result FtpInternal::ftpGet(int iCopyFile, const QString &sCopyFile, const QUrl &url, KIO::fileoffset_t llOffset) { auto result = ftpOpenConnection(LoginMode::Implicit); if (!result.success) { return result; } // Try to find the size of the file (and check that it exists at // the same time). If we get back a 550, "File does not exist" // or "not a plain file", check if it is a directory. If it is a // directory, return an error; otherwise simply try to retrieve // the request... if (!ftpSize(url.path(), '?') && (m_iRespCode == 550) && ftpFolder(url.path())) { // Ok it's a dir in fact qCDebug(KIO_FTP) << "it is a directory in fact"; return Result::fail(ERR_IS_DIRECTORY); } QString resumeOffset = q->metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = q->metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { llOffset = resumeOffset.toLongLong(); qCDebug(KIO_FTP) << "got offset from metadata : " << llOffset; } result = ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset); if (!result.success) { qCWarning(KIO_FTP) << "Can't open for reading"; return result; } // Read the size from the response string if (m_size == UnknownSize) { const char *psz = strrchr(ftpResponse(4), '('); if (psz) { m_size = charToLongLong(psz + 1); } if (!m_size) { m_size = UnknownSize; } } // Send the mime-type... if (iCopyFile == -1) { const auto result = ftpSendMimeType(url); if (!result.success) { return result; } } KIO::filesize_t bytesLeft = 0; if (m_size != UnknownSize) { bytesLeft = m_size - llOffset; q->totalSize(m_size); // emit the total size... } qCDebug(KIO_FTP) << "starting with offset=" << llOffset; KIO::fileoffset_t processed_size = llOffset; QByteArray array; char buffer[maximumIpcSize]; // start with small data chunks in case of a slow data source (modem) // - unfortunately this has a negative impact on performance for large // - files - so we will increase the block size after a while ... int iBlockSize = initialIpcSize; int iBufferCur = 0; while (m_size == UnknownSize || bytesLeft > 0) { // let the buffer size grow if the file is larger 64kByte ... if (processed_size - llOffset > 1024 * 64) { iBlockSize = maximumIpcSize; } // read the data and detect EOF or error ... if (iBlockSize + iBufferCur > (int)sizeof(buffer)) { iBlockSize = sizeof(buffer) - iBufferCur; } if (m_data->bytesAvailable() == 0) { m_data->waitForReadyRead((q->readTimeout() * 1000)); } int n = m_data->read(buffer + iBufferCur, iBlockSize); if (n <= 0) { // this is how we detect EOF in case of unknown size if (m_size == UnknownSize && n == 0) { break; } // unexpected eof. Happens when the daemon gets killed. return Result::fail(ERR_CANNOT_READ); } processed_size += n; // collect very small data chunks in buffer before processing ... if (m_size != UnknownSize) { bytesLeft -= n; iBufferCur += n; if (iBufferCur < minimumMimeSize && bytesLeft > 0) { q->processedSize(processed_size); continue; } n = iBufferCur; iBufferCur = 0; } // write output file or pass to data pump ... int writeError = 0; if (iCopyFile == -1) { array = QByteArray::fromRawData(buffer, n); q->data(array); array.clear(); } else if ((writeError = WriteToFile(iCopyFile, buffer, n)) != 0) { return Result::fail(writeError, sCopyFile); } Q_ASSERT(processed_size >= 0); q->processedSize(static_cast(processed_size)); } qCDebug(KIO_FTP) << "done"; if (iCopyFile == -1) { // must signal EOF to data pump ... q->data(array); // array is empty and must be empty! } q->processedSize(m_size == UnknownSize ? processed_size : m_size); return Result::pass(); } #if 0 void FtpInternal::mimetype(const QUrl &url) { if (!ftpOpenConnection(loginImplicit)) { return; } if (!ftpOpenCommand("retr", url.path(), 'I', ERR_CANNOT_OPEN_FOR_READING, 0)) { qCWarning(KIO_FTP) << "Can't open for reading"; return; } char buffer[ 2048 ]; QByteArray array; // Get one chunk of data only and send it, KIO::Job will determine the // mimetype from it using KMimeMagic int n = m_data->read(buffer, 2048); array.setRawData(buffer, n); data(array); array.resetRawData(buffer, n); qCDebug(KIO_FTP) << "aborting"; ftpAbortTransfer(); qCDebug(KIO_FTP) << "finished"; finished(); qCDebug(KIO_FTP) << "after finished"; } void FtpInternal::ftpAbortTransfer() { // RFC 959, page 34-35 // IAC (interpret as command) = 255 ; IP (interrupt process) = 254 // DM = 242 (data mark) char msg[4]; // 1. User system inserts the Telnet "Interrupt Process" (IP) signal // in the Telnet stream. msg[0] = (char) 255; //IAC msg[1] = (char) 254; //IP (void) send(sControl, msg, 2, 0); // 2. User system sends the Telnet "Sync" signal. msg[0] = (char) 255; //IAC msg[1] = (char) 242; //DM if (send(sControl, msg, 2, MSG_OOB) != 2) ; // error... // Send ABOR qCDebug(KIO_FTP) << "send ABOR"; QCString buf = "ABOR\r\n"; if (KSocks::self()->write(sControl, buf.data(), buf.length()) <= 0) { error(ERR_CANNOT_WRITE, QString()); return; } // qCDebug(KIO_FTP) << "read resp"; if (readresp() != '2') { error(ERR_CANNOT_READ, QString()); return; } qCDebug(KIO_FTP) << "close sockets"; closeSockets(); } #endif //=============================================================================== // public: put upload file to server // helper: ftpPut called from put() and copy() //=============================================================================== Result FtpInternal::put(const QUrl &url, int permissions, KIO::JobFlags flags) { qCDebug(KIO_FTP) << url; const auto result = ftpPut(-1, url, permissions, flags); ftpCloseCommand(); // must close command! return result; } Result FtpInternal::ftpPut(int iCopyFile, const QUrl &dest_url, int permissions, KIO::JobFlags flags) { const auto openResult = ftpOpenConnection(LoginMode::Implicit); if (!openResult.success) { return openResult; } // Don't use mark partial over anonymous FTP. // My incoming dir allows put but not rename... bool bMarkPartial; if (m_user.isEmpty() || m_user == QLatin1String(FTP_LOGIN)) { bMarkPartial = false; } else { bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true); } QString dest_orig = dest_url.path(); const QString dest_part = dest_orig + QLatin1String(".part"); if (ftpSize(dest_orig, 'I')) { if (m_size == 0) { // delete files with zero size const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_orig); if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString()); } } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) { return Result::fail(ERR_FILE_ALREADY_EXIST, QString()); } else if (bMarkPartial) { // when using mark partial, append .part extension const auto result = ftpRename(dest_orig, dest_part, KIO::Overwrite); if (!result.success) { return Result::fail(ERR_CANNOT_RENAME_PARTIAL, QString()); } } // Don't chmod an existing file permissions = -1; } else if (bMarkPartial && ftpSize(dest_part, 'I')) { // file with extension .part exists if (m_size == 0) { // delete files with zero size const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_part); if (!ftpSendCmd(cmd) || (m_iRespType != 2)) { return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString()); } } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) { flags |= q->canResume(m_size) ? KIO::Resume : KIO::DefaultFlags; if (!(flags & KIO::Resume)) { return Result::fail(ERR_FILE_ALREADY_EXIST, QString()); } } } else { m_size = 0; } QString dest; // if we are using marking of partial downloads -> add .part extension if (bMarkPartial) { qCDebug(KIO_FTP) << "Adding .part extension to " << dest_orig; dest = dest_part; } else { dest = dest_orig; } KIO::fileoffset_t offset = 0; // set the mode according to offset if ((flags & KIO::Resume) && m_size > 0) { offset = m_size; if (iCopyFile != -1) { if (QT_LSEEK(iCopyFile, offset, SEEK_SET) < 0) { return Result::fail(ERR_CANNOT_RESUME, QString()); } } } const auto storResult = ftpOpenCommand("stor", dest, '?', ERR_CANNOT_WRITE, offset); if (!storResult.success) { return storResult; } qCDebug(KIO_FTP) << "ftpPut: starting with offset=" << offset; KIO::fileoffset_t processed_size = offset; QByteArray buffer; int result; int iBlockSize = initialIpcSize; int writeError = 0; // Loop until we got 'dataEnd' do { if (iCopyFile == -1) { q->dataReq(); // Request for data result = q->readData(buffer); } else { // let the buffer size grow if the file is larger 64kByte ... if (processed_size - offset > 1024 * 64) { iBlockSize = maximumIpcSize; } buffer.resize(iBlockSize); result = QT_READ(iCopyFile, buffer.data(), buffer.size()); if (result < 0) { writeError = ERR_CANNOT_READ; } else { buffer.resize(result); } } if (result > 0) { m_data->write(buffer); while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) {} processed_size += result; q->processedSize(processed_size); } } while (result > 0); if (result != 0) { // error ftpCloseCommand(); // don't care about errors qCDebug(KIO_FTP) << "Error during 'put'. Aborting."; if (bMarkPartial) { // Remove if smaller than minimum size if (ftpSize(dest, 'I') && (processed_size < q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE))) { const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest); (void) ftpSendCmd(cmd); } } return Result::fail(writeError, dest_url.toString()); } if (!ftpCloseCommand()) { return Result::fail(ERR_CANNOT_WRITE); } // after full download rename the file back to original name if (bMarkPartial) { qCDebug(KIO_FTP) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")"; const auto result = ftpRename(dest, dest_orig, KIO::Overwrite); if (!result.success) { return Result::fail(ERR_CANNOT_RENAME_PARTIAL); } } // set final permissions if (permissions != -1) { if (m_user == QLatin1String(FTP_LOGIN)) qCDebug(KIO_FTP) << "Trying to chmod over anonymous FTP ???"; // chmod the file we just put if (! ftpChmod(dest_orig, permissions)) { // To be tested //if ( m_user != FTP_LOGIN ) // warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) ); } } return Result::pass(); } /** Use the SIZE command to get the file size. Warning : the size depends on the transfer mode, hence the second arg. */ bool FtpInternal::ftpSize(const QString &path, char mode) { m_size = UnknownSize; if (!ftpDataMode(mode)) { return false; } const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path); if (!ftpSendCmd(buf) || (m_iRespType != 2)) { return false; } // skip leading "213 " (response code) QByteArray psz(ftpResponse(4)); if (psz.isEmpty()) { return false; } bool ok = false; m_size = psz.trimmed().toLongLong(&ok); if (!ok) { m_size = UnknownSize; } return true; } bool FtpInternal::ftpFileExists(const QString &path) { const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path); if (!ftpSendCmd(buf) || (m_iRespType != 2)) { return false; } // skip leading "213 " (response code) const char *psz = ftpResponse(4); return psz != nullptr; } // Today the differences between ASCII and BINARY are limited to // CR or CR/LF line terminators. Many servers ignore ASCII (like // win2003 -or- vsftp with default config). In the early days of // computing, when even text-files had structure, this stuff was // more important. // Theoretically "list" could return different results in ASCII // and BINARY mode. But again, most servers ignore ASCII here. bool FtpInternal::ftpDataMode(char cMode) { if (cMode == '?') { cMode = m_bTextMode ? 'A' : 'I'; } else if (cMode == 'a') { cMode = 'A'; } else if (cMode != 'A') { cMode = 'I'; } qCDebug(KIO_FTP) << "want" << cMode << "has" << m_cDataMode; if (m_cDataMode == cMode) { return true; } const QByteArray buf = QByteArrayLiteral("TYPE ") + cMode; if (!ftpSendCmd(buf) || (m_iRespType != 2)) { return false; } m_cDataMode = cMode; return true; } bool FtpInternal::ftpFolder(const QString &path) { QString newPath = path; int iLen = newPath.length(); if (iLen > 1 && newPath[iLen - 1] == QLatin1Char('/')) { newPath.chop(1); } qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath; if (m_currentPath == newPath) { return true; } const QByteArray tmp = "cwd " + q->remoteEncoding()->encode(newPath); if (!ftpSendCmd(tmp)) { return false; // connection failure } if (m_iRespType != 2) { return false; // not a folder } m_currentPath = newPath; return true; } //=============================================================================== // public: copy don't use kio data pump if one side is a local file // helper: ftpCopyPut called from copy() on upload // helper: ftpCopyGet called from copy() on download //=============================================================================== Result FtpInternal::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags) { int iCopyFile = -1; bool bSrcLocal = src.isLocalFile(); bool bDestLocal = dest.isLocalFile(); QString sCopyFile; Result result = Result::pass(); if (bSrcLocal && !bDestLocal) { // File -> Ftp sCopyFile = src.toLocalFile(); qCDebug(KIO_FTP) << "local file" << sCopyFile << "-> ftp" << dest.path(); result = ftpCopyPut(iCopyFile, sCopyFile, dest, permissions, flags); } else if (!bSrcLocal && bDestLocal) { // Ftp -> File sCopyFile = dest.toLocalFile(); qCDebug(KIO_FTP) << "ftp" << src.path() << "-> local file" << sCopyFile; result = ftpCopyGet(iCopyFile, sCopyFile, src, permissions, flags); } else { return Result::fail(ERR_UNSUPPORTED_ACTION, QString()); } // perform clean-ups and report error (if any) if (iCopyFile != -1) { QT_CLOSE(iCopyFile); } ftpCloseCommand(); // must close command! return result; } bool FtpInternal::isSocksProxyScheme(const QString &scheme) { return scheme == QLatin1String("socks") || scheme == QLatin1String("socks5"); } bool FtpInternal::isSocksProxy() const { return isSocksProxyScheme(m_proxyURL.scheme()); } Result FtpInternal::ftpCopyPut(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags) { // check if source is ok ... QFileInfo info(sCopyFile); bool bSrcExists = info.exists(); if (bSrcExists) { if (info.isDir()) { return Result::fail(ERR_IS_DIRECTORY); } } else { return Result::fail(ERR_DOES_NOT_EXIST); } iCopyFile = QT_OPEN(QFile::encodeName(sCopyFile).constData(), O_RDONLY); if (iCopyFile == -1) { return Result::fail(ERR_CANNOT_OPEN_FOR_READING); } // delegate the real work (iError gets status) ... q->totalSize(info.size()); #ifdef ENABLE_CAN_RESUME return ftpPut(iCopyFile, url, permissions, flags & ~KIO::Resume); #else return ftpPut(iCopyFile, url, permissions, flags | KIO::Resume); #endif } Result FtpInternal::ftpCopyGet(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags) { // check if destination is ok ... QFileInfo info(sCopyFile); const bool bDestExists = info.exists(); if (bDestExists) { if (info.isDir()) { return Result::fail(ERR_IS_DIRECTORY); } if (!(flags & KIO::Overwrite)) { return Result::fail(ERR_FILE_ALREADY_EXIST); } } // do we have a ".part" file? const QString sPart = sCopyFile + QLatin1String(".part"); bool bResume = false; QFileInfo sPartInfo(sPart); const bool bPartExists = sPartInfo.exists(); const bool bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true); const QString dest = bMarkPartial ? sPart : sCopyFile; if (bMarkPartial && bPartExists && sPartInfo.size() > 0) { // must not be a folder! please fix a similar bug in kio_file!! if (sPartInfo.isDir()) { return Result::fail(ERR_DIR_ALREADY_EXIST); } //doesn't work for copy? -> design flaw? #ifdef ENABLE_CAN_RESUME bResume = q->canResume(sPartInfo.size()); #else bResume = true; #endif } if (bPartExists && !bResume) { // get rid of an unwanted ".part" file QFile::remove(sPart); } // WABA: Make sure that we keep writing permissions ourselves, // otherwise we can be in for a surprise on NFS. mode_t initialMode; if (permissions >= 0) { initialMode = static_cast(permissions | S_IWUSR); } else { initialMode = 0666; } // open the output file ... KIO::fileoffset_t hCopyOffset = 0; if (bResume) { iCopyFile = QT_OPEN(QFile::encodeName(sPart).constData(), O_RDWR); // append if resuming hCopyOffset = QT_LSEEK(iCopyFile, 0, SEEK_END); if (hCopyOffset < 0) { return Result::fail(ERR_CANNOT_RESUME); } qCDebug(KIO_FTP) << "resuming at " << hCopyOffset; } else { iCopyFile = QT_OPEN(QFile::encodeName(dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); } if (iCopyFile == -1) { qCDebug(KIO_FTP) << "### COULD NOT WRITE " << sCopyFile; const int error = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING; return Result::fail(error); } // delegate the real work (iError gets status) ... auto result = ftpGet(iCopyFile, sCopyFile, url, hCopyOffset); if (QT_CLOSE(iCopyFile) == 0 && !result.success) { // If closing the file failed but there isn't an error yet, switch // into an error! result = Result::fail(ERR_CANNOT_WRITE); } iCopyFile = -1; // handle renaming or deletion of a partial file ... if (bMarkPartial) { if (result.success) { // rename ".part" on success if (!QFile::rename(sPart, sCopyFile)) { // If rename fails, try removing the destination first if it exists. if (!bDestExists || !(QFile::remove(sCopyFile) && QFile::rename(sPart, sCopyFile))) { qCDebug(KIO_FTP) << "cannot rename " << sPart << " to " << sCopyFile; result = Result::fail(ERR_CANNOT_RENAME_PARTIAL); } } } else { sPartInfo.refresh(); if (sPartInfo.exists()) { // should a very small ".part" be deleted? int size = q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE); if (sPartInfo.size() < size) { QFile::remove(sPart); } } } } if (result.success) { const QString mtimeStr = q->metaData(QStringLiteral("modified")); if (!mtimeStr.isEmpty()) { QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate); if (dt.isValid()) { qCDebug(KIO_FTP) << "Updating modified timestamp to" << mtimeStr; struct utimbuf utbuf; info.refresh(); utbuf.actime = info.lastRead().toSecsSinceEpoch(); // access time, unchanged utbuf.modtime = dt.toSecsSinceEpoch(); // modification time ::utime(QFile::encodeName(sCopyFile).constData(), &utbuf); } } } return result; } Result FtpInternal::ftpSendMimeType(const QUrl &url) { const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : static_cast(m_size)); QByteArray buffer(totalSize, '\0'); while (true) { // Wait for content to be available... if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead((q->readTimeout() * 1000))) { return Result::fail(ERR_CANNOT_READ, url.toString()); } const qint64 bytesRead = m_data->peek(buffer.data(), totalSize); // If we got a -1, it must be an error so return an error. if (bytesRead == -1) { return Result::fail(ERR_CANNOT_READ, url.toString()); } // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size // equal to the size we want, then break. if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) { break; } } if (!buffer.isEmpty()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForFileNameAndData(url.path(), buffer); qCDebug(KIO_FTP) << "Emitting mimetype" << mime.name(); q->mimeType(mime.name()); // emit the mime type... } return Result::pass(); } void FtpInternal::fixupEntryName(FtpEntry *e) { Q_ASSERT(e); if (e->type == S_IFDIR) { if (!ftpFolder(e->name)) { QString name(e->name.trimmed()); if (ftpFolder(name)) { e->name = name; qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name; } else { int index = 0; while (e->name.at(index).isSpace()) { index++; name = e->name.mid(index); if (ftpFolder(name)) { qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name; e->name = name; break; } } } } } else { if (!ftpFileExists(e->name)) { QString name(e->name.trimmed()); if (ftpFileExists(name)) { e->name = name; qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name; } else { int index = 0; while (e->name.at(index).isSpace()) { index++; name = e->name.mid(index); if (ftpFileExists(name)) { qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name; e->name = name; break; } } } } } } ConnectionResult FtpInternal::synchronousConnectToHost(const QString &host, quint16 port) { const QUrl proxyUrl = m_proxyURL; QNetworkProxy proxy; if (!proxyUrl.isEmpty()) { proxy = QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyUrl.host(), static_cast(proxyUrl.port(0)), proxyUrl.userName(), proxyUrl.password()); } QTcpSocket *socket = new QSslSocket; socket->setProxy(proxy); socket->connectToHost(host, port); socket->waitForConnected(q->connectTimeout() * 1000); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto socketError = socket->error(); -#else - const auto socketError = socket->socketError(); -#endif if (socketError == QAbstractSocket::ProxyAuthenticationRequiredError) { AuthInfo info; info.url = proxyUrl; info.verifyPath = true; //### whatever if (!q->checkCachedAuthentication(info)) { info.prompt = i18n("You need to supply a username and a password for " "the proxy server listed below before you are allowed " "to access any sites."); info.keepPassword = true; info.commentLabel = i18n("Proxy:"); info.comment = i18n("%1", proxy.hostName()); const int errorCode = q->openPasswordDialogV2(info, i18n("Proxy Authentication Failed.")); if (errorCode != KJob::NoError) { qCDebug(KIO_FTP) << "user canceled proxy authentication, or communication error." << errorCode; return ConnectionResult { socket, Result::fail(errorCode, proxyUrl.toString()) }; } } proxy.setUser(info.username); proxy.setPassword(info.password); delete socket; socket = new QSslSocket; socket->setProxy(proxy); socket->connectToHost(host, port); socket->waitForConnected(q->connectTimeout() * 1000); if (socket->state() == QAbstractSocket::ConnectedState) { // reconnect with credentials was successful -> save data q->cacheAuthentication(info); m_proxyURL.setUserName(info.username); m_proxyURL.setPassword(info.password); } } return ConnectionResult { socket, Result::pass() }; } //=============================================================================== // Ftp //=============================================================================== Ftp::Ftp(const QByteArray &pool, const QByteArray &app) : SlaveBase(QByteArrayLiteral("ftp"), pool, app) , d(new FtpInternal(this)) { } Ftp::~Ftp() = default; void Ftp::setHost(const QString &host, quint16 port, const QString &user, const QString &pass) { d->setHost(host, port, user, pass); } void Ftp::openConnection() { const auto result = d->openConnection(); if (!result.success) { error(result.error, result.errorString); return; } opened(); } void Ftp::closeConnection() { d->closeConnection(); } void Ftp::stat(const QUrl &url) { finalize(d->stat(url)); } void Ftp::listDir(const QUrl &url) { finalize(d->listDir(url)); } void Ftp::mkdir(const QUrl &url, int permissions) { finalize(d->mkdir(url, permissions)); } void Ftp::rename(const QUrl &src, const QUrl &dst, JobFlags flags) { finalize(d->rename(src, dst, flags)); } void Ftp::del(const QUrl &url, bool isfile) { finalize(d->del(url, isfile)); } void Ftp::chmod(const QUrl &url, int permissions) { finalize(d->chmod(url, permissions)); } void Ftp::get(const QUrl &url) { finalize(d->get(url)); } void Ftp::put(const QUrl &url, int permissions, JobFlags flags) { finalize(d->put(url, permissions, flags)); } void Ftp::slave_status() { d->slave_status(); } void Ftp::copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags) { finalize(d->copy(src, dest, permissions, flags)); } void Ftp::finalize(const Result &result) { if (!result.success) { error(result.error, result.errorString); return; } finished(); } QDebug operator<<(QDebug dbg, const Result &r) { QDebugStateSaver saver(dbg); dbg.nospace() << "Result(" << "success=" << r.success << ", err=" << r.error << ", str=" << r.errorString << ')'; return dbg; } // needed for JSON file embedding #include "ftp.moc" diff --git a/src/widgets/accessmanagerreply_p.cpp b/src/widgets/accessmanagerreply_p.cpp index 6ac09ae8..85849e14 100644 --- a/src/widgets/accessmanagerreply_p.cpp +++ b/src/widgets/accessmanagerreply_p.cpp @@ -1,553 +1,537 @@ /* * This file is part of the KDE project. * * Copyright (C) 2008 Alex Merry * Copyright (C) 2008 - 2009 Urs Wolfer * Copyright (C) 2009 - 2012 Dawit Alemayehu * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "accessmanagerreply_p.h" #include "accessmanager.h" #include "job.h" #include "scheduler.h" #include "kio_widgets_debug.h" #include #include #include #include #include #define QL1S(x) QLatin1String(x) #define QL1C(x) QLatin1Char(x) namespace KDEPrivate { AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op, const QNetworkRequest &request, KIO::SimpleJob *kioJob, bool emitReadyReadOnMetaDataChange, QObject *parent) : QNetworkReply(parent), m_offset(0), m_metaDataRead(false), m_ignoreContentDisposition(false), m_emitReadyReadOnMetaDataChange(emitReadyReadOnMetaDataChange), m_kioJob(kioJob) { setRequest(request); setOpenMode(QIODevice::ReadOnly); setUrl(request.url()); setOperation(op); setError(NoError, QString()); if (!request.sslConfiguration().isNull()) { setSslConfiguration(request.sslConfiguration()); } connect(kioJob, SIGNAL(redirection(KIO::Job*,QUrl)), SLOT(slotRedirection(KIO::Job*,QUrl))); connect(kioJob, QOverload::of(&KJob::percent), this, &AccessManagerReply::slotPercent); if (qobject_cast(kioJob)) { connect(kioJob, &KJob::result, this, &AccessManagerReply::slotStatResult); } else { connect(kioJob, &KJob::result, this, &AccessManagerReply::slotResult); connect(kioJob, SIGNAL(data(KIO::Job*,QByteArray)), SLOT(slotData(KIO::Job*,QByteArray))); connect(kioJob, SIGNAL(mimetype(KIO::Job*,QString)), SLOT(slotMimeType(KIO::Job*,QString))); } } AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &data, const QUrl &url, const KIO::MetaData &metaData, QObject *parent) : QNetworkReply(parent), m_data(data), m_offset(0), m_ignoreContentDisposition(false), m_emitReadyReadOnMetaDataChange(false) { setRequest(request); setOpenMode(QIODevice::ReadOnly); setUrl((url.isValid() ? url : request.url())); setOperation(op); setHeaderFromMetaData(metaData); if (!request.sslConfiguration().isNull()) { setSslConfiguration(request.sslConfiguration()); } setError(NoError, QString()); emitFinished(true, Qt::QueuedConnection); } AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op, const QNetworkRequest &request, QNetworkReply::NetworkError errorCode, const QString &errorMessage, QObject *parent) : QNetworkReply(parent), m_offset(0) { setRequest(request); setOpenMode(QIODevice::ReadOnly); setUrl(request.url()); setOperation(op); setError(static_cast(errorCode), errorMessage); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = error(); -#else - const auto networkError = this->networkError(); -#endif if (networkError != QNetworkReply::NoError) { QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, Q_ARG(QNetworkReply::NetworkError, networkError)); } emitFinished(true, Qt::QueuedConnection); } AccessManagerReply::~AccessManagerReply() { } void AccessManagerReply::abort() { if (m_kioJob) { m_kioJob.data()->disconnect(this); } m_kioJob.clear(); m_data.clear(); m_offset = 0; m_metaDataRead = false; } qint64 AccessManagerReply::bytesAvailable() const { return (QNetworkReply::bytesAvailable() + m_data.length() - m_offset); } qint64 AccessManagerReply::readData(char *data, qint64 maxSize) { const qint64 length = qMin(qint64(m_data.length() - m_offset), maxSize); if (length <= 0) { return 0; } memcpy(data, m_data.constData() + m_offset, length); m_offset += length; if (m_data.length() == m_offset) { m_data.clear(); m_offset = 0; } return length; } bool AccessManagerReply::ignoreContentDisposition(const KIO::MetaData &metaData) { if (m_ignoreContentDisposition) { return true; } if (!metaData.contains(QLatin1String("content-disposition-type"))) { return true; } bool ok = false; const int statusCode = attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&ok); if (!ok || statusCode < 200 || statusCode > 299) { return true; } return false; } void AccessManagerReply::setHeaderFromMetaData(const KIO::MetaData &_metaData) { if (_metaData.isEmpty()) { return; } KIO::MetaData metaData(_metaData); // Set the encryption attribute and values... QSslConfiguration sslConfig; const bool isEncrypted = KIO::Integration::sslConfigFromMetaData(metaData, sslConfig); setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, isEncrypted); if (isEncrypted) { setSslConfiguration(sslConfig); } // Set the raw header information... #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList httpHeaders(metaData.value(QStringLiteral("HTTP-Headers")).split(QL1C('\n'), QString::SkipEmptyParts)); #else const QStringList httpHeaders(metaData.value(QStringLiteral("HTTP-Headers")).split(QL1C('\n'), Qt::SkipEmptyParts)); #endif if (httpHeaders.isEmpty()) { const auto charSetIt = metaData.constFind(QStringLiteral("charset")); if (charSetIt != metaData.constEnd()) { QString mimeType = header(QNetworkRequest::ContentTypeHeader).toString(); mimeType += QLatin1String(" ; charset=") + *charSetIt; //qDebug() << "changed content-type to" << mimeType; setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8()); } } else { for (const QString &httpHeader : httpHeaders) { int index = httpHeader.indexOf(QL1C(':')); // Handle HTTP status line... if (index == -1) { // Except for the status line, all HTTP header must be an nvpair of // type ":" if (!httpHeader.startsWith(QLatin1String("HTTP/"), Qt::CaseInsensitive)) { continue; } #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList statusLineAttrs(httpHeader.split(QL1C(' '), QString::SkipEmptyParts)); #else QStringList statusLineAttrs(httpHeader.split(QL1C(' '), Qt::SkipEmptyParts)); #endif if (statusLineAttrs.count() > 1) { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusLineAttrs.at(1)); } if (statusLineAttrs.count() > 2) { setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, statusLineAttrs.at(2)); } continue; } const QStringRef headerName = httpHeader.leftRef(index); QString headerValue = httpHeader.mid(index + 1); // Ignore cookie header since it is handled by the http ioslave. if (headerName.startsWith(QLatin1String("set-cookie"), Qt::CaseInsensitive)) { continue; } if (headerName.startsWith(QLatin1String("content-disposition"), Qt::CaseInsensitive) && ignoreContentDisposition(metaData)) { continue; } // Without overriding the corrected mime-type sent by kio_http, add // back the "charset=" portion of the content-type header if present. if (headerName.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive)) { QString mimeType(header(QNetworkRequest::ContentTypeHeader).toString()); if (m_ignoreContentDisposition) { // If the server returned application/octet-stream, try to determine the // real content type from the disposition filename. if (mimeType == QLatin1String("application/octet-stream")) { const QString fileName(metaData.value(QStringLiteral("content-disposition-filename"))); QMimeDatabase db; QMimeType mime = db.mimeTypeForFile((fileName.isEmpty() ? url().path() : fileName), QMimeDatabase::MatchExtension); mimeType = mime.name(); } metaData.remove(QStringLiteral("content-disposition-type")); metaData.remove(QStringLiteral("content-disposition-filename")); } if (!headerValue.contains(mimeType, Qt::CaseInsensitive)) { index = headerValue.indexOf(QL1C(';')); if (index == -1) { headerValue = mimeType; } else { headerValue.replace(0, index, mimeType); } //qDebug() << "Changed mime-type from" << mimeType << "to" << headerValue; } } setRawHeader(headerName.trimmed().toUtf8(), headerValue.trimmed().toUtf8()); } } // Set the returned meta data as attribute... setAttribute(static_cast(KIO::AccessManager::MetaData), metaData.toVariant()); } void AccessManagerReply::setIgnoreContentDisposition(bool on) { //qDebug() << on; m_ignoreContentDisposition = on; } void AccessManagerReply::putOnHold() { if (!m_kioJob || isFinished()) { return; } //qDebug() << m_kioJob << m_data; m_kioJob.data()->disconnect(this); m_kioJob.data()->putOnHold(); m_kioJob.clear(); KIO::Scheduler::publishSlaveOnHold(); } bool AccessManagerReply::isLocalRequest(const QUrl &url) { const QString scheme(url.scheme()); return (KProtocolInfo::isKnownProtocol(scheme) && KProtocolInfo::protocolClass(scheme).compare(QStringLiteral(":local"), Qt::CaseInsensitive) == 0); } void AccessManagerReply::readHttpResponseHeaders(KIO::Job *job) { if (!job || m_metaDataRead) { return; } KIO::MetaData metaData(job->metaData()); if (metaData.isEmpty()) { // Allow handling of local resources such as man pages and file url... if (isLocalRequest(url())) { setHeader(QNetworkRequest::ContentLengthHeader, job->totalAmount(KJob::Bytes)); setAttribute(QNetworkRequest::HttpStatusCodeAttribute, QStringLiteral("200")); emit metaDataChanged(); } return; } setHeaderFromMetaData(metaData); m_metaDataRead = true; emit metaDataChanged(); } int AccessManagerReply::jobError(KJob *kJob) { const int errCode = kJob->error(); switch (errCode) { case 0: break; // No error; case KIO::ERR_SLAVE_DEFINED: case KIO::ERR_NO_CONTENT: // Sent by a 204 response is not an error condition. setError(QNetworkReply::NoError, kJob->errorText()); //qDebug() << "0 -> QNetworkReply::NoError"; break; case KIO::ERR_IS_DIRECTORY: // This error condition can happen if you click on an ftp link that points // to a directory instead of a file, e.g. ftp://ftp.kde.org/pub setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("inode/directory")); setError(QNetworkReply::NoError, kJob->errorText()); break; case KIO::ERR_CANNOT_CONNECT: setError(QNetworkReply::ConnectionRefusedError, kJob->errorText()); //qDebug() << "KIO::ERR_CANNOT_CONNECT -> QNetworkReply::ConnectionRefusedError"; break; case KIO::ERR_UNKNOWN_HOST: setError(QNetworkReply::HostNotFoundError, kJob->errorText()); //qDebug() << "KIO::ERR_UNKNOWN_HOST -> QNetworkReply::HostNotFoundError"; break; case KIO::ERR_SERVER_TIMEOUT: setError(QNetworkReply::TimeoutError, kJob->errorText()); //qDebug() << "KIO::ERR_SERVER_TIMEOUT -> QNetworkReply::TimeoutError"; break; case KIO::ERR_USER_CANCELED: case KIO::ERR_ABORTED: setError(QNetworkReply::OperationCanceledError, kJob->errorText()); //qDebug() << "KIO::ERR_ABORTED -> QNetworkReply::OperationCanceledError"; break; case KIO::ERR_UNKNOWN_PROXY_HOST: setError(QNetworkReply::ProxyNotFoundError, kJob->errorText()); //qDebug() << "KIO::UNKNOWN_PROXY_HOST -> QNetworkReply::ProxyNotFoundError"; break; case KIO::ERR_ACCESS_DENIED: setError(QNetworkReply::ContentAccessDenied, kJob->errorText()); //qDebug() << "KIO::ERR_ACCESS_DENIED -> QNetworkReply::ContentAccessDenied"; break; case KIO::ERR_WRITE_ACCESS_DENIED: setError(QNetworkReply::ContentOperationNotPermittedError, kJob->errorText()); //qDebug() << "KIO::ERR_WRITE_ACCESS_DENIED -> QNetworkReply::ContentOperationNotPermittedError"; break; case KIO::ERR_DOES_NOT_EXIST: setError(QNetworkReply::ContentNotFoundError, kJob->errorText()); //qDebug() << "KIO::ERR_DOES_NOT_EXIST -> QNetworkReply::ContentNotFoundError"; break; case KIO::ERR_CANNOT_AUTHENTICATE: setError(QNetworkReply::AuthenticationRequiredError, kJob->errorText()); //qDebug() << "KIO::ERR_CANNOT_AUTHENTICATE -> QNetworkReply::AuthenticationRequiredError"; break; case KIO::ERR_UNSUPPORTED_PROTOCOL: case KIO::ERR_NO_SOURCE_PROTOCOL: setError(QNetworkReply::ProtocolUnknownError, kJob->errorText()); //qDebug() << "KIO::ERR_UNSUPPORTED_PROTOCOL -> QNetworkReply::ProtocolUnknownError"; break; case KIO::ERR_CONNECTION_BROKEN: setError(QNetworkReply::RemoteHostClosedError, kJob->errorText()); //qDebug() << "KIO::ERR_CONNECTION_BROKEN -> QNetworkReply::RemoteHostClosedError"; break; case KIO::ERR_UNSUPPORTED_ACTION: setError(QNetworkReply::ProtocolInvalidOperationError, kJob->errorText()); //qDebug() << "KIO::ERR_UNSUPPORTED_ACTION -> QNetworkReply::ProtocolInvalidOperationError"; break; default: setError(QNetworkReply::UnknownNetworkError, kJob->errorText()); //qDebug() << KIO::rawErrorDetail(errCode, QString()) << "-> QNetworkReply::UnknownNetworkError"; } return errCode; } void AccessManagerReply::slotData(KIO::Job *kioJob, const QByteArray &data) { Q_UNUSED(kioJob); if (data.isEmpty()) { return; } qint64 newSizeWithOffset = m_data.size() + data.size(); if (newSizeWithOffset <= m_data.capacity()) { // Already enough space } else if (newSizeWithOffset - m_offset <= m_data.capacity()) { // We get enough space with ::remove. m_data.remove(0, m_offset); m_offset = 0; } else { // We have to resize the array, which implies an expensive memmove. // Do it ourselves to save m_offset bytes. QByteArray newData; // Leave some free space to avoid that every slotData call results in // a reallocation. qNextPowerOfTwo is what QByteArray does internally. newData.reserve(qNextPowerOfTwo(newSizeWithOffset - m_offset)); newData.append(m_data.constData() + m_offset, m_data.size() - m_offset); m_data = newData; m_offset = 0; } m_data += data; emit readyRead(); } void AccessManagerReply::slotMimeType(KIO::Job *kioJob, const QString &mimeType) { //qDebug() << kioJob << mimeType; setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8()); readHttpResponseHeaders(kioJob); if (m_emitReadyReadOnMetaDataChange) { emit readyRead(); } } void AccessManagerReply::slotResult(KJob *kJob) { const int errcode = jobError(kJob); const QUrl redirectUrl = attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (!redirectUrl.isValid()) { setAttribute(static_cast(KIO::AccessManager::KioError), errcode); if (errcode && errcode != KIO::ERR_NO_CONTENT) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = error(); -#else - const auto networkError = this->networkError(); -#endif emit error(networkError); } } // Make sure HTTP response headers are always set. if (!m_metaDataRead) { readHttpResponseHeaders(qobject_cast(kJob)); } emitFinished(true); } void AccessManagerReply::slotStatResult(KJob *kJob) { if (jobError(kJob)) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = error(); -#else - const auto networkError = this->networkError(); -#endif emit error(networkError); emitFinished(true); return; } KIO::StatJob *statJob = qobject_cast(kJob); Q_ASSERT(statJob); KIO::UDSEntry entry = statJob->statResult(); QString mimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE); if (mimeType.isEmpty() && entry.isDir()) { mimeType = QStringLiteral("inode/directory"); } if (!mimeType.isEmpty()) { setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8()); } emitFinished(true); } void AccessManagerReply::slotRedirection(KIO::Job *job, const QUrl &u) { if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), url(), u)) { qCWarning(KIO_WIDGETS) << "Redirection from" << url() << "to" << u << "REJECTED by policy!"; setError(QNetworkReply::ContentAccessDenied, u.toString()); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = error(); -#else - const auto networkError = this->networkError(); -#endif emit error(networkError); return; } setAttribute(QNetworkRequest::RedirectionTargetAttribute, QUrl(u)); if (job->queryMetaData(QStringLiteral("redirect-to-get")) == QL1S("true")) { setOperation(QNetworkAccessManager::GetOperation); } } void AccessManagerReply::slotPercent(KJob *job, unsigned long percent) { qulonglong bytesTotal = job->totalAmount(KJob::Bytes); qulonglong bytesProcessed = (bytesTotal * percent) / 100; if (operation() == QNetworkAccessManager::PutOperation || operation() == QNetworkAccessManager::PostOperation) { emit uploadProgress(bytesProcessed, bytesTotal); return; } emit downloadProgress(bytesProcessed, bytesTotal); } void AccessManagerReply::emitFinished(bool state, Qt::ConnectionType type) { setFinished(state); emit QMetaObject::invokeMethod(this, "finished", type); } }