From 25a3ae50ac0d9835283e4f5f10fcfcc10ed5575d Mon Sep 17 00:00:00 2001 From: Shane Synan Date: Mon, 5 Sep 2016 14:19:03 -0500 Subject: [PATCH] Reload SSL certificates on signal SIGHUP Catch SIGHUP, use it to reload configuration (SSL certs), similar to nginx and other server programs. This allows easy automation of reloading certificates, an important factor with services such as Let's Encrypt. If reloading certificates fails, the old certificates are kept to avoid disrupting new connections until the situation is sorted out. Resolves GH-208. --- src/common/quassel.cpp | 19 +++++++++ src/common/quassel.h | 10 +++++ src/core/core.cpp | 12 ++++++ src/core/core.h | 8 ++++ src/core/coreapplication.cpp | 21 ++++++++++ src/core/coreapplication.h | 18 ++++++++ src/core/sslserver.cpp | 81 ++++++++++++++++++++++++++---------- src/core/sslserver.h | 24 +++++++++++ src/qtui/monoapplication.cpp | 10 +++++ src/qtui/monoapplication.h | 9 ++++ 10 files changed, 191 insertions(+), 21 deletions(-) diff --git a/src/common/quassel.cpp b/src/common/quassel.cpp index 6671bc47..28fb2b08 100644 --- a/src/common/quassel.cpp +++ b/src/common/quassel.cpp @@ -72,6 +72,11 @@ Quassel::Quassel() // We catch SIGTERM and SIGINT (caused by Ctrl+C) to graceful shutdown Quassel. signal(SIGTERM, handleSignal); signal(SIGINT, handleSignal); +#ifndef Q_OS_WIN + // SIGHUP is used to reload configuration (i.e. SSL certificates) + // Windows does not support SIGHUP + signal(SIGHUP, handleSignal); +#endif } @@ -323,6 +328,20 @@ void Quassel::handleSignal(int sig) else QCoreApplication::quit(); break; +#ifndef Q_OS_WIN +// Windows does not support SIGHUP + case SIGHUP: + // Most applications use this as the 'configuration reload' command, e.g. nginx uses it for + // graceful reloading of processes. + if (_instance) { + // If the instance exists, reload the configuration + quInfo() << "Caught signal" << SIGHUP <<"- reloading configuration"; + if (_instance->reloadConfig()) { + quInfo() << "Successfully reloaded configuration"; + } + } + break; +#endif case SIGABRT: case SIGSEGV: #ifndef Q_OS_WIN diff --git a/src/common/quassel.h b/src/common/quassel.h index a2a573cc..b705d595 100644 --- a/src/common/quassel.h +++ b/src/common/quassel.h @@ -144,6 +144,16 @@ protected: virtual bool init(); virtual void quit(); + /** + * Requests a reload of relevant runtime configuration. + * + * Does not reload all configuration, but could catch things such as SSL certificates. Unless + * overridden, simply does nothing. + * + * @return True if configuration reload successful, otherwise false + */ + virtual bool reloadConfig() { return true; } + inline void setRunMode(RunMode mode); inline void setDataDirPaths(const QStringList &paths); QStringList findDataDirPaths() const; diff --git a/src/core/core.cpp b/src/core/core.cpp index 3eb31e44..0faf7693 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -428,6 +428,18 @@ bool Core::sslSupported() } +bool Core::reloadCerts() +{ +#ifdef HAVE_SSL + SslServer *sslServer = qobject_cast(&instance()->_server); + return sslServer->reloadCerts(); +#else + // SSL not supported, don't mark configuration reload as failed + return true; +#endif +} + + bool Core::startListening() { // in mono mode we only start a local port if a port is specified in the cli call diff --git a/src/core/core.h b/src/core/core.h index 8901542b..2015f70a 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -494,6 +494,14 @@ public: static inline QDateTime startTime() { return instance()->_startTime; } static inline bool isConfigured() { return instance()->_configured; } static bool sslSupported(); + + /** + * Reloads SSL certificates used for connection with clients + * + * @return True if certificates reloaded successfully, otherwise false. + */ + static bool reloadCerts(); + static QVariantList backendInfo(); static QString setup(const QString &adminUser, const QString &adminPassword, const QString &backend, const QVariantMap &setupData); diff --git a/src/core/coreapplication.cpp b/src/core/coreapplication.cpp index b9422b8b..7426f177 100644 --- a/src/core/coreapplication.cpp +++ b/src/core/coreapplication.cpp @@ -62,6 +62,17 @@ bool CoreApplicationInternal::init() } +bool CoreApplicationInternal::reloadConfig() +{ + if (_coreCreated) { + // Currently, only reloading SSL certificates is supported + return Core::reloadCerts(); + } else { + return false; + } +} + + /*****************************************************************************/ CoreApplication::CoreApplication(int &argc, char **argv) @@ -94,3 +105,13 @@ bool CoreApplication::init() } return false; } + + +bool CoreApplication::reloadConfig() +{ + if (_internal) { + return _internal->reloadConfig(); + } else { + return false; + } +} diff --git a/src/core/coreapplication.h b/src/core/coreapplication.h index dffe9059..b350d52f 100644 --- a/src/core/coreapplication.h +++ b/src/core/coreapplication.h @@ -38,6 +38,15 @@ public: bool init(); + /** + * Requests a reload of relevant runtime configuration. + * + * In particular, signals to the Core to reload SSL certificates. + * + * @return True if configuration reload successful, otherwise false + */ + bool reloadConfig(); + private: bool _coreCreated; }; @@ -52,6 +61,15 @@ public: bool init(); + /** + * Requests a reload of relevant runtime configuration. + * + * @see Quassel::reloadConfig() + * + * @return True if configuration reload successful, otherwise false + */ + bool reloadConfig(); + private: CoreApplicationInternal *_internal; }; diff --git a/src/core/sslserver.cpp b/src/core/sslserver.cpp index 2fc502e9..b6becaf6 100644 --- a/src/core/sslserver.cpp +++ b/src/core/sslserver.cpp @@ -36,24 +36,23 @@ SslServer::SslServer(QObject *parent) : QTcpServer(parent), _isCertValid(false) { + // Keep track if the SSL warning has been mentioned at least once before static bool sslWarningShown = false; - QString ssl_cert; - QString ssl_key; - if(Quassel::isOptionSet("ssl-cert")) { - ssl_cert = Quassel::optionValue("ssl-cert"); + _sslCertPath = Quassel::optionValue("ssl-cert"); } else { - ssl_cert = Quassel::configDirPath() + "quasselCert.pem"; + _sslCertPath = Quassel::configDirPath() + "quasselCert.pem"; } if(Quassel::isOptionSet("ssl-key")) { - ssl_key = Quassel::optionValue("ssl-key"); + _sslKeyPath = Quassel::optionValue("ssl-key"); } else { - ssl_key = ssl_cert; + _sslKeyPath = _sslCertPath; } - if (!setCertificate(ssl_cert, ssl_key)) { + // Initialize the certificates for first-time usage + if (!loadCerts()) { if (!sslWarningShown) { quWarning() << "SslServer: Unable to set certificate file\n" @@ -95,9 +94,44 @@ void SslServer::incomingConnection(int socketDescriptor) } +bool SslServer::loadCerts() +{ + // Load the certificates specified in the path. If needed, other prep work can be done here. + return setCertificate(_sslCertPath, _sslKeyPath); +} + + +bool SslServer::reloadCerts() +{ + if (loadCerts()) { + return true; + } else { + // Reloading certificates currently only occur in response to a request. Always print an + // error if something goes wrong, in order to simplify checking if it's working. + if (isCertValid()) { + quWarning() + << "SslServer: Unable to reload certificate file, reverting\n" + << " Quassel Core will use the previous key to provide SSL for client connections.\n" + << " Please see http://quassel-irc.org/faq/cert to learn how to enable SSL support."; + } else { + quWarning() + << "SslServer: Unable to reload certificate file\n" + << " Quassel Core will still work, but cannot provide SSL for client connections.\n" + << " Please see http://quassel-irc.org/faq/cert to learn how to enable SSL support."; + } + return false; + } +} + + bool SslServer::setCertificate(const QString &path, const QString &keyPath) { - _isCertValid = false; + // Don't reset _isCertValid here, in case an older but valid certificate is still loaded. + // Use temporary variables in order to avoid overwriting the existing certificates until + // everything is confirmed good. + QSslCertificate untestedCert; + QList untestedCA; + QSslKey untestedKey; if (path.isEmpty()) return false; @@ -122,11 +156,11 @@ bool SslServer::setCertificate(const QString &path, const QString &keyPath) return false; } - _cert = certList[0]; + untestedCert = certList[0]; certList.removeFirst(); // remove server cert // store CA and intermediates certs - _ca = certList; + untestedCA = certList; if (!certFile.reset()) { quWarning() << "SslServer: IO error reading certificate file"; @@ -148,42 +182,47 @@ bool SslServer::setCertificate(const QString &path, const QString &keyPath) return false; } - _key = QSslKey(&keyFile, QSsl::Rsa); + untestedKey = QSslKey(&keyFile, QSsl::Rsa); keyFile.close(); } else { - _key = QSslKey(&certFile, QSsl::Rsa); + untestedKey = QSslKey(&certFile, QSsl::Rsa); } certFile.close(); - if (_cert.isNull()) { + if (untestedCert.isNull()) { quWarning() << "SslServer:" << qPrintable(path) << "contains no certificate data"; return false; } // We allow the core to offer SSL anyway, so no "return false" here. Client will warn about the cert being invalid. const QDateTime now = QDateTime::currentDateTime(); - if (now < _cert.effectiveDate()) - quWarning() << "SslServer: Certificate won't be valid before" << _cert.effectiveDate().toString(); + if (now < untestedCert.effectiveDate()) + quWarning() << "SslServer: Certificate won't be valid before" << untestedCert.effectiveDate().toString(); - else if (now > _cert.expiryDate()) - quWarning() << "SslServer: Certificate expired on" << _cert.expiryDate().toString(); + else if (now > untestedCert.expiryDate()) + quWarning() << "SslServer: Certificate expired on" << untestedCert.expiryDate().toString(); else { // Qt4's isValid() checks for time range and blacklist; avoid a double warning, hence the else block #if QT_VERSION < 0x050000 - if (!_cert.isValid()) + if (!untestedCert.isValid()) #else - if (_cert.isBlacklisted()) + if (untestedCert.isBlacklisted()) #endif quWarning() << "SslServer: Certificate blacklisted"; } - if (_key.isNull()) { + if (untestedKey.isNull()) { quWarning() << "SslServer:" << qPrintable(keyPath) << "contains no key data"; return false; } _isCertValid = true; + // All keys are valid, update the externally visible copy used for new connections. + _cert = untestedCert; + _ca = untestedCA; + _key = untestedKey; + return _isCertValid; } diff --git a/src/core/sslserver.h b/src/core/sslserver.h index 2b63abd7..1f43cc43 100644 --- a/src/core/sslserver.h +++ b/src/core/sslserver.h @@ -42,6 +42,16 @@ public: virtual inline const QSslKey &key() const { return _key; } virtual inline bool isCertValid() const { return _isCertValid; } + /** + * Reloads SSL certificates used for connections + * + * If this command fails, it will try to maintain the most recent working certificate. Error + * conditions are automatically written to the log. + * + * @return True if certificates reloaded successfully, otherwise false. + */ + bool reloadCerts(); + protected: #if QT_VERSION >= 0x050000 virtual void incomingConnection(qintptr socketDescriptor); @@ -52,11 +62,25 @@ protected: virtual bool setCertificate(const QString &path, const QString &keyPath); private: + /** + * Loads SSL certificates used for connections + * + * If this command fails, it will try to maintain the most recent working certificate. Will log + * specific failure points, but does not offer verbose guidance. + * + * @return True if certificates loaded successfully, otherwise false. + */ + bool loadCerts(); + QLinkedList _pendingConnections; QSslCertificate _cert; QSslKey _key; QList _ca; bool _isCertValid; + + // Used when reloading certificates later + QString _sslCertPath; /// Path to the certificate file + QString _sslKeyPath; /// Path to the private key file (may be in same file as above) }; diff --git a/src/qtui/monoapplication.cpp b/src/qtui/monoapplication.cpp index d6c1bcd2..5eb3a5cb 100644 --- a/src/qtui/monoapplication.cpp +++ b/src/qtui/monoapplication.cpp @@ -73,3 +73,13 @@ void MonolithicApplication::startInternalCore() connect(connection, SIGNAL(connectToInternalCore(InternalPeer*)), core, SLOT(setupInternalClientSession(InternalPeer*))); connect(core, SIGNAL(sessionState(Protocol::SessionState)), connection, SLOT(internalSessionStateReceived(Protocol::SessionState))); } + + +bool MonolithicApplication::reloadConfig() +{ + if (_internal) { + return _internal->reloadConfig(); + } else { + return false; + } +} diff --git a/src/qtui/monoapplication.h b/src/qtui/monoapplication.h index a96051fc..6e7b9022 100644 --- a/src/qtui/monoapplication.h +++ b/src/qtui/monoapplication.h @@ -34,6 +34,15 @@ public: bool init(); + /** + * Requests a reload of relevant runtime configuration. + * + * @see Quassel::reloadConfig() + * + * @return True if configuration reload successful, otherwise false + */ + bool reloadConfig(); + private slots: void startInternalCore(); -- 2.20.1