aheinecke@10: #include "downloader.h" aheinecke@10: aheinecke@10: #ifndef DOWNLOAD_SERVER aheinecke@10: #define DOWNLOAD_SERVER "https://www.intevation.de" aheinecke@10: #endif aheinecke@10: aheinecke@10: #include aheinecke@15: #include aheinecke@10: #include aheinecke@15: #include andre@27: #include andre@27: #include andre@32: #include andre@32: #include andre@32: #include andre@27: andre@27: #include andre@27: #include andre@27: #include andre@27: #include andre@27: #include andre@27: #include andre@27: andre@27: #define MAX_SW_SIZE 10485760 andre@27: #define MAX_LIST_SIZE 1048576 andre@32: #define MAX_IO_TRIES 10 andre@32: andre@32: #define LIST_RESOURCE "/incoming/aheinecke/test" andre@32: #define SW_RESOURCE "/incoming/aheinecke/test" andre@32: andre@35: /* TODO: Wrap ssl_session in a class for reuse. andre@35: * see programs/ssl/ssl_client2.c for example of session reuse */ andre@32: andre@32: #ifdef CONNECTION_DEBUG andre@32: static void my_debug(void *ctx, int level, const char *str) andre@32: { andre@32: fprintf((FILE *) ctx, "%s", str); andre@32: fflush((FILE *) ctx); andre@32: } andre@32: #endif andre@27: andre@27: QString getErrorMsg(int ret) andre@27: { andre@27: char errbuf[255]; andre@27: polarssl_strerror(ret, errbuf, 255); andre@27: errbuf[254] = '\0'; /* Just to be sure */ andre@27: return QString::fromLatin1(errbuf); andre@27: } aheinecke@10: aheinecke@15: Downloader::Downloader(QObject* parent, const QString& url, aheinecke@15: const QByteArray& certificate, aheinecke@15: const QDateTime& newestSW, aheinecke@15: const QDateTime& newestList): aheinecke@12: QThread(parent), aheinecke@15: mUrl(url), andre@27: mPinnedCert(certificate), aheinecke@15: mLastModSW(newestSW), andre@27: mLastModList(newestList), andre@27: mErrorState(NoError), andre@27: mInitialized(false), andre@27: mServerFD(-1) aheinecke@10: { andre@27: int ret = -1; andre@27: andre@27: memset(&mSSL, 0, sizeof(ssl_context)); andre@27: aheinecke@15: if (certificate.isEmpty()) { aheinecke@18: QFile certResource(":certs/kolab.org"); aheinecke@15: certResource.open(QFile::ReadOnly); andre@27: mPinnedCert = certResource.readAll(); aheinecke@15: certResource.close(); aheinecke@15: } andre@27: andre@27: ret = init(); andre@27: if (ret == 0) { andre@27: mInitialized = true; andre@27: } else { andre@27: qDebug() << "Initialization error: " + getErrorMsg(ret); andre@27: } andre@27: } andre@27: andre@27: int Downloader::init() andre@27: { andre@27: int ret = -1; andre@27: QUuid uuid = QUuid::createUuid(); andre@27: QString personalString = QApplication::applicationName() + uuid.toString(); andre@27: QByteArray personalBa = personalString.toLocal8Bit(); andre@27: andre@27: x509_crt_init(&mX509PinnedCert); andre@27: entropy_init(&mEntropy); andre@27: andre@27: ret = ssl_init(&mSSL); andre@27: if (ret != 0) { andre@27: /* The only documented error is malloc failed */ andre@27: mErrorState = ErrUnknown; andre@27: return ret; andre@27: } andre@27: andre@27: /* andre@27: * Initialize random generator. andre@27: * Personalisation string, does not need to be random but andre@27: * should be unique according to documentation. andre@27: * andre@27: * the ctr_drbg structure does not need to be freed explicitly. andre@27: */ andre@27: ret = ctr_drbg_init(&mCtr_drbg, entropy_func, &mEntropy, andre@27: (const unsigned char*) personalBa.constData(), andre@27: personalBa.size()); andre@27: if (ret != 0) { andre@27: ssl_free(&mSSL); andre@27: mErrorState = ErrUnknown; andre@27: return ret; andre@27: } andre@27: andre@27: ret = x509_crt_parse(&mX509PinnedCert, andre@27: (const unsigned char*) mPinnedCert.constData(), andre@27: mPinnedCert.size()); andre@27: if (ret != 0){ andre@27: ssl_free(&mSSL); andre@27: mErrorState = InvalidPinnedCertificate; andre@27: return ret; andre@27: } andre@27: andre@27: ssl_set_endpoint(&mSSL, SSL_IS_CLIENT); andre@27: ssl_set_authmode(&mSSL, SSL_VERIFY_OPTIONAL); andre@27: ssl_set_ca_chain(&mSSL, &mX509PinnedCert, NULL, NULL); andre@27: ssl_set_renegotiation(&mSSL, SSL_RENEGOTIATION_DISABLED); andre@27: ssl_set_rng(&mSSL, ctr_drbg_random, &mCtr_drbg); andre@32: #ifdef RELEASE_BUILD andre@32: ssl_set_min_version(&mSSL, SSL_MAJOR_VERSION_3, SSL_MINOR_VERSION_3); andre@32: #endif andre@32: andre@32: #ifdef CONNECTION_DEBUG andre@32: ssl_set_dbg(&mSSL, my_debug, stdout); andre@32: #endif andre@27: andre@27: return 0; andre@27: } andre@27: andre@27: Downloader::~Downloader() { andre@27: x509_crt_free(&mX509PinnedCert); andre@27: entropy_free(&mEntropy); andre@27: if (mInitialized) { andre@27: ssl_free(&mSSL); andre@27: } aheinecke@10: } aheinecke@10: aheinecke@15: QString Downloader::getDataDirectory() aheinecke@10: { aheinecke@15: QString candidate = aheinecke@15: QStandardPaths::writableLocation(QStandardPaths::DataLocation); aheinecke@12: aheinecke@15: if (candidate.isEmpty()) { aheinecke@15: qDebug() << "Could not find writeable locaction for me"; aheinecke@15: return QString(); aheinecke@15: } aheinecke@15: aheinecke@15: QDir cDir(candidate); aheinecke@15: aheinecke@15: if (!cDir.exists()) { aheinecke@15: if (!cDir.mkpath(candidate)) { aheinecke@15: qDebug() << "Could not create path to: " << candidate; aheinecke@15: return QString(); aheinecke@15: } aheinecke@15: } aheinecke@15: return cDir.absolutePath(); aheinecke@10: } andre@27: andre@27: int Downloader::establishSSLConnection() { andre@27: int ret = -1; andre@27: const x509_crt *peerCert; andre@27: andre@32: if (mServerFD == -1 || !mInitialized) { andre@32: mErrorState = ErrUnknown; andre@27: return -1; andre@27: } andre@27: andre@27: ssl_set_bio(&mSSL, net_recv, &mServerFD, andre@27: net_send, &mServerFD); andre@27: andre@27: while ((ret = ssl_handshake(&mSSL)) != 0) { andre@27: if (ret != POLARSSL_ERR_NET_WANT_READ && andre@27: ret != POLARSSL_ERR_NET_WANT_WRITE) { andre@27: qDebug() << "SSL Handshake failed: " andre@27: << getErrorMsg(ret); andre@32: mErrorState = SSLHandshakeFailed; andre@27: return ret; andre@27: } andre@27: } andre@27: andre@32: /* we might want to set the verify function andre@32: * with ssl_set_verify before to archive the andre@32: * certificate pinning. */ andre@32: andre@27: ret = ssl_get_verify_result(&mSSL); andre@27: andre@27: if (ret != 0 ) { andre@32: if((ret & BADCERT_EXPIRED) != 0) andre@32: qDebug() << "server certificate has expired"; andre@32: if((ret & BADCERT_REVOKED) != 0) andre@32: qDebug() << "server certificate has been revoked"; andre@32: if((ret & BADCERT_CN_MISMATCH) != 0) andre@32: qDebug() << "CN mismatch"; andre@32: if((ret & BADCERT_NOT_TRUSTED) != 0) andre@27: qDebug() << "self-signed or not signed by a trusted CA"; andre@32: ret = -1; andre@27: #ifdef RELEASE_BUILD andre@32: mErrorState = InvalidCertificate; andre@27: return -1; andre@27: #endif andre@27: } andre@27: andre@27: peerCert = ssl_get_peer_cert(&mSSL); andre@27: andre@27: if (!peerCert) { andre@27: mErrorState = InvalidCertificate; andre@27: qDebug() << "Failed to get peer cert"; andre@27: return -1; andre@27: } andre@27: andre@27: if (peerCert->raw.len == 0 || andre@27: peerCert->raw.len != mX509PinnedCert.raw.len) { andre@27: mErrorState = InvalidCertificate; andre@27: qDebug() << "Certificate length mismatch"; andre@27: return -1; andre@27: } andre@27: andre@27: /* You can never be sure what those c++ operators do.. andre@27: if (mPinnedCert != QByteArray::fromRawData( andre@27: (const char*) peerCert->raw.p, andre@27: peerCert->raw.len)) { andre@27: qDebug() << "Certificate content mismatch"; andre@27: } andre@27: */ andre@27: andre@27: for (unsigned int i = 0; i < peerCert->raw.len; i++) { andre@27: if (peerCert->raw.p[i] != mX509PinnedCert.raw.p[i]) { andre@27: qDebug() << "Certificate content mismatch"; andre@27: mErrorState = InvalidCertificate; andre@27: return -1; andre@27: } andre@27: } andre@27: return 0; andre@27: } andre@27: andre@32: /* Helper around polarssl bare bone api */ andre@32: int polarSSLWrite (ssl_context *ssl, const QByteArray& request) andre@32: { andre@32: unsigned int tries = 0; andre@32: int ret = -1; andre@32: andre@32: const unsigned char *buf = (const unsigned char *) request.constData(); andre@32: size_t len = (size_t) request.size(); andre@32: andre@32: qDebug() << "Seinding request: " << request; andre@32: /* According to doc for ssl_write: andre@32: * andre@32: * When this function returns POLARSSL_ERR_NET_WANT_WRITE, andre@32: * it must be called later with the same arguments, andre@32: * until it returns a positive value. andre@32: */ andre@32: do { andre@32: ret = ssl_write(ssl, buf, len); andre@32: if (ret >= 0) { andre@32: if ((unsigned int) ret == len) { andre@32: return 0; andre@32: } else { andre@32: qDebug() << "Write failed to write everything"; andre@32: return -1; andre@32: } andre@32: } andre@32: andre@32: if (ret != POLARSSL_ERR_NET_WANT_WRITE) { andre@32: return ret; andre@32: } andre@32: tries++; andre@32: net_usleep(100000); /* sleep 100ms to give the socket a chance andre@32: to clean up. */ andre@32: } while (tries < MAX_IO_TRIES); andre@32: andre@32: return ret; andre@32: } andre@32: andre@32: /* Helper around polarssl bare bone api read at most len bytes andre@32: * and return them as a byte array returns a NULL byte array on error*/ andre@32: QByteArray polarSSLRead (ssl_context *ssl, size_t len) andre@32: { andre@32: unsigned char buf[len]; andre@32: QByteArray retval(""); andre@32: int ret = -1; andre@32: andre@32: do { andre@32: memset (buf, 0, sizeof(buf)); andre@32: ret = ssl_read(ssl, buf, len); andre@32: if (ret == 0 || andre@32: ret == POLARSSL_ERR_SSL_CONN_EOF) { andre@32: /* EOF */ andre@32: return retval; andre@32: } andre@32: if (ret <= 0) { andre@32: qDebug() << "Read failed: " << getErrorMsg(ret); andre@32: return QByteArray(); andre@32: } andre@32: if (len < (len - (unsigned int) ret)) { andre@32: /* Should never happen if ssl_read behaves */ andre@32: qDebug() << "integer overflow in polarSSLRead"; andre@32: return QByteArray(); andre@32: } andre@32: len -= (unsigned int) ret; andre@32: retval.append((const char *)buf, len); andre@32: } while (len > 0); andre@32: andre@32: return retval; andre@32: } andre@32: andre@32: andre@32: QDateTime Downloader::getLastModifiedHeader(const QString &resource) { andre@32: int ret = -1; andre@32: QByteArray response; andre@32: QTextStream responseStream(&response); andre@32: QLocale cLocale = QLocale::c(); andre@32: QString headRequest = andre@32: QString::fromLatin1("HEAD %1 HTTP/1.1\r\n" andre@32: "Connection: Keep-Alive\r\n" andre@32: "\r\n\r\n").arg(resource); andre@32: andre@32: ret = polarSSLWrite (&mSSL, headRequest.toUtf8()); andre@32: if (ret != 0) { andre@32: mErrorState = ConnectionLost; andre@32: return QDateTime(); andre@32: } andre@32: andre@32: response = polarSSLRead(&mSSL, 1024); andre@32: andre@32: if (response.isNull()) { andre@32: qDebug() << "No response"; andre@32: mErrorState = ConnectionLost; andre@32: return QDateTime(); andre@32: } andre@32: andre@32: while (1) { andre@32: QString line = responseStream.readLine(); andre@32: if (line.isNull()) { andre@32: break; andre@32: } andre@32: if (line.startsWith("Last-Modified:")) { andre@32: QDateTime candidate = cLocale.toDateTime(line, "'Last-Modified: 'ddd, dd MMM yyyy HH:mm:ss' GMT'"); andre@32: qDebug() << "Parsed line : " << line << " to " << candidate; andre@32: if (candidate.isValid()) { andre@32: return candidate; andre@32: } andre@32: } andre@32: } andre@32: andre@32: mErrorState = InvalidResponse; andre@32: return QDateTime(); andre@32: } andre@32: andre@32: bool Downloader::downloadFile(const QString &resource, andre@32: const QString &fileName, andre@32: size_t maxSize) andre@32: { andre@32: int ret = -1; andre@32: size_t bytesRead = 0; andre@32: QString getRequest = andre@32: QString::fromLatin1("GET %1 HTTP/1.1\r\n\r\n").arg(resource); andre@32: andre@32: QSaveFile outputFile(fileName); andre@32: andre@32: ret = polarSSLWrite (&mSSL, getRequest.toUtf8()); andre@32: andre@32: // Open / Create the file to write to. andre@32: if (!outputFile.open(QIODevice::WriteOnly)) { andre@32: qDebug() << "Failed to open file"; andre@32: return false; andre@32: } andre@32: andre@32: if (ret != 0) { andre@32: mErrorState = ConnectionLost; andre@32: return false; andre@32: } andre@32: andre@32: do { andre@32: QByteArray response = polarSSLRead(&mSSL, 8192); andre@32: if (response.isNull()) { andre@32: qDebug() << "Error reading response"; andre@32: mErrorState = ConnectionLost; andre@32: return false; andre@32: } andre@32: if (response.isEmpty()) { andre@32: /* We have read everything there is to read */ andre@32: break; andre@32: } andre@32: andre@32: outputFile.write(response); andre@32: bytesRead += response.size(); andre@32: } while (bytesRead < maxSize); andre@32: andre@32: return outputFile.commit(); andre@32: } andre@27: andre@27: void Downloader::run() { andre@27: int ret; andre@32: QDateTime remoteModList; andre@32: QDateTime remoteModSW; andre@27: andre@27: if (!mInitialized) { andre@27: emit error(tr("Failed to initialize SSL Module."), ErrUnknown); andre@27: return; andre@27: } andre@27: andre@27: ret = net_connect(&mServerFD, mUrl.host().toLatin1().constData(), andre@32: mUrl.port(443)); andre@32: andre@27: if (ret != 0) { andre@27: mErrorState = NoConnection; andre@27: emit error(tr("Failed to connect to %1.").arg(mUrl.host()), andre@27: mErrorState); andre@27: return; andre@27: } andre@27: andre@27: emit progress(tr("Connected"), 1, -1); andre@27: andre@27: ret = establishSSLConnection(); andre@27: if (ret != 0) { andre@27: qDebug() << "SSL conncetion failed: " << getErrorMsg(ret); andre@27: emit error(tr("Failed to connect to %1.").arg(mUrl.host()), andre@27: mErrorState); andre@27: return; andre@27: } andre@27: andre@32: remoteModSW = getLastModifiedHeader(QString::fromLatin1("/incoming/aheinecke/test")); andre@32: remoteModList = getLastModifiedHeader(QString::fromLatin1("/incoming/aheinecke/test")); andre@32: andre@32: if (!remoteModSW.isValid() || !remoteModList.isValid()) { andre@32: qDebug() << "Could not read headers"; andre@32: return; andre@32: } andre@32: andre@32: if (!mLastModSW.isValid() || remoteModSW > mLastModSW) { andre@32: QString dataDirectory = getDataDirectory(); andre@32: andre@32: if (dataDirectory.isEmpty()) { andre@32: qDebug() << "Failed to get data directory"; andre@32: return; andre@32: } andre@32: andre@32: QString fileName = dataDirectory.append("/SW-") andre@32: .append(remoteModSW.toString("yyyymmddHHmmss")) andre@32: .append(".exe"); andre@32: andre@32: qDebug() << "fileName: " << fileName; andre@32: andre@32: if (!downloadFile(QString::fromLatin1(SW_RESOURCE), andre@32: fileName, MAX_SW_SIZE)) { andre@32: return; andre@32: } andre@32: andre@32: emit newSoftwareAvailable(fileName, remoteModSW); andre@32: } else if (!mLastModList.isValid() || remoteModList > mLastModList) { andre@32: QString dataDirectory = getDataDirectory(); andre@32: andre@32: if (dataDirectory.isEmpty()) { andre@32: qDebug() << "Failed to get data directory"; andre@32: return; andre@32: } andre@32: andre@32: QString fileName = dataDirectory.append("/list-") andre@32: .append(remoteModSW.toString("yyyymmddHHmmss")) andre@32: .append(".txt"); andre@32: andre@32: qDebug() << "fileName: " << fileName; andre@32: andre@32: if (!downloadFile(QString::fromLatin1(LIST_RESOURCE), andre@32: fileName, MAX_LIST_SIZE)) { andre@32: return; andre@32: } andre@32: andre@32: emit newListAvailable(fileName, remoteModList); andre@32: } andre@27: andre@27: emit progress(tr("Closing"), 1, -1); andre@27: ssl_close_notify (&mSSL); andre@27: andre@27: if (mServerFD != -1) { andre@27: net_close(mServerFD); andre@27: mServerFD = -1; andre@27: } andre@27: }