From c85cb8799e0785f2658726e7b825191b827e6481 Mon Sep 17 00:00:00 2001 From: Christophe Dumez Date: Fri, 15 Apr 2011 13:02:39 +0000 Subject: [PATCH] FEATURE: qBittorrent can update dynamic DNS services (DynDNS, no-ip) --- Changelog | 1 + src/dnsupdater.cpp | 287 ++++++++++++++++++++++++++++++++ src/dnsupdater.h | 81 +++++++++ src/preferences/options.ui | 98 ++++++++++- src/preferences/options_imp.cpp | 23 +++ src/preferences/options_imp.h | 1 + src/preferences/preferences.h | 43 +++++ src/qtlibtorrent/qbtsession.cpp | 24 ++- src/qtlibtorrent/qbtsession.h | 3 + src/src.pro | 6 +- 10 files changed, 559 insertions(+), 8 deletions(-) create mode 100644 src/dnsupdater.cpp create mode 100644 src/dnsupdater.h diff --git a/Changelog b/Changelog index 04ed4f463..184468e6d 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,7 @@ - FEATURE: Added support for secure SMTP connection (SSL) - FEATURE: Added support for SMTP authentication - FEATURE: Added UPnP/NAT-PMP port forward for the Web UI port + - FEATURE: qBittorrent can update dynamic DNS services (DynDNS, no-ip) - BUGFIX: Change systray icon on the fly (no restart needed) - COSMETIC: Added monochrome icon for light themes diff --git a/src/dnsupdater.cpp b/src/dnsupdater.cpp new file mode 100644 index 000000000..9517a3b19 --- /dev/null +++ b/src/dnsupdater.cpp @@ -0,0 +1,287 @@ +/* + * Bittorrent Client using Qt4 and libtorrent. + * Copyright (C) 2011 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * Contact : chris@qbittorrent.org + */ + +#include +#include +#include +#include "dnsupdater.h" +#include "qbtsession.h" + +DNSUpdater::DNSUpdater(QObject *parent) : + QObject(parent), m_state(OK) +{ + updateCredentials(); + + // Load saved settings from previous session + QIniSettings settings("qBittorrent", "qBittorrent"); + m_lastIPCheckTime = settings.value("DNSUpdater/lastUpdateTime").toDateTime(); + m_lastIP = QHostAddress(settings.value("DNSUpdater/lastIP").toString()); + + // Start IP checking timer + m_ipCheckTimer.setInterval(IP_CHECK_INTERVAL_MS); + connect(&m_ipCheckTimer, SIGNAL(timeout()), SLOT(checkPublicIP())); + m_ipCheckTimer.start(); + + // Check lastUpdate to avoid flooding + if(!m_lastIPCheckTime.isValid() || + m_lastIPCheckTime.secsTo(QDateTime::currentDateTime())*1000 > IP_CHECK_INTERVAL_MS) { + checkPublicIP(); + } +} + +DNSUpdater::~DNSUpdater() { + // Save lastupdate time and last ip + QIniSettings settings("qBittorrent", "qBittorrent"); + settings.setValue("DNSUpdater/lastUpdateTime", m_lastIPCheckTime); + settings.setValue("DNSUpdater/lastIP", m_lastIP.toString()); +} + +void DNSUpdater::checkPublicIP() +{ + Q_ASSERT(m_state == OK); + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + connect(manager, SIGNAL(finished(QNetworkReply*)), + SLOT(ipRequestFinished(QNetworkReply*))); + m_lastIPCheckTime = QDateTime::currentDateTime(); + QNetworkRequest request; + request.setUrl(QUrl("http://checkip.dyndns.org")); + request.setRawHeader("User-Agent", "qBittorrent/"VERSION" chris@qbittorrent.org"); + manager->get(request); +} + +void DNSUpdater::ipRequestFinished(QNetworkReply *reply) +{ + qDebug() << Q_FUNC_INFO; + if(reply->error()) { + // Error + qWarning() << Q_FUNC_INFO << "Error:" << reply->errorString(); + } else { + // Parse response + QRegExp ipregex("Current IP Address:\\s+([^<]+)"); + QString ret = reply->readAll(); + if(ipregex.indexIn(ret) >= 0) { + QString ip_str = ipregex.cap(1); + qDebug() << Q_FUNC_INFO << "Regular expression captured the following IP:" << ip_str; + QHostAddress new_ip(ip_str); + if(!new_ip.isNull()) { + if(m_lastIP != new_ip) { + qDebug() << Q_FUNC_INFO << "The IP address changed, report the change to DynDNS..."; + m_lastIP = new_ip; + updateDNSService(); + } + } else { + qWarning() << Q_FUNC_INFO << "Failed to construct a QHostAddress from the IP string"; + } + } else { + qWarning() << Q_FUNC_INFO << "Regular expression failed ot capture the IP address"; + } + } + // Clean up + reply->deleteLater(); + sender()->deleteLater(); +} + +void DNSUpdater::updateDNSService() +{ + qDebug() << Q_FUNC_INFO; + // Prepare request + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + connect(manager, SIGNAL(finished(QNetworkReply*)), + SLOT(ipUpdateFinished(QNetworkReply*))); + m_lastIPCheckTime = QDateTime::currentDateTime(); + QNetworkRequest request; + request.setUrl(getUpdateUrl()); + request.setRawHeader("User-Agent", "qBittorrent/"VERSION" chris@qbittorrent.org"); + manager->get(request); +} + +QUrl DNSUpdater::getUpdateUrl() const +{ + QUrl url; +#ifdef QT_NO_OPENSSL + url.setScheme("http"); +#else + url.setScheme("https"); +#endif + url.setUserName(m_username); + url.setPassword(m_password); + + Q_ASSERT(!m_lastIP.isNull()); + // Service specific + switch(m_service) { + case DNS::DYNDNS: + url.setHost("members.dyndns.org"); + break; + case DNS::NOIP: + url.setHost("dynupdate.no-ip.com"); + break; + default: + qWarning() << "Unrecognized Dynamic DNS service!"; + Q_ASSERT(0); + } + url.setPath("/nic/update"); + url.addQueryItem("hostname", m_domain); + url.addQueryItem("myip", m_lastIP.toString()); + Q_ASSERT(url.isValid()); + qDebug() << Q_FUNC_INFO << url.toString(); + return url; +} + +void DNSUpdater::ipUpdateFinished(QNetworkReply *reply) +{ + if(reply->error()) { + // Error + qWarning() << Q_FUNC_INFO << "Error:" << reply->errorString(); + } else { + // Pase reply + processIPUpdateReply(reply->readAll()); + } + // Clean up + reply->deleteLater(); + sender()->deleteLater(); +} + +void DNSUpdater::processIPUpdateReply(const QString &reply) +{ + qDebug() << Q_FUNC_INFO << reply; + QString code = reply.split(" ").first(); + qDebug() << Q_FUNC_INFO << "Code:" << code; + if(code == "good" || code == "nochg") { + QBtSession::instance()->addConsoleMessage(tr("Your dynamic DNS was successfuly updated."), "green"); + return; + } + if(code == "911" || code == "dnserr") { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: The service is temporarily unavailable, it will be retried in 30 minutes."), + "red"); + m_lastIP.clear(); + // It will retry in 30 minutes because the timer was not stopped + return; + } + // Everything bellow is an error, stop updating until the user updates something + m_ipCheckTimer.stop(); + m_lastIP.clear(); + if(code == "nohost") { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: hostname supplied does not exist under specified account."), + "red"); + m_state = INVALID_CREDS; + return; + } + if(code == "badauth") { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: Invalid username/password."), "red"); + m_state = INVALID_CREDS; + return; + } + if(code == "badagent") { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: qBittorrent was blacklisted by the service, please report a bug at http://bugs.qbittorrent.org."), + "red"); + m_state = FATAL; + return; + } + if(code == "!donator") { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: %1 was returned by the service, please report a bug at http://bugs.qbittorrent.org.").arg("!donator"), + "red"); + m_state = FATAL; + return; + } + if(code == "abuse") { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: Your username was blocked due to abuse."), + "red"); + m_state = FATAL; + return; + } +} + +void DNSUpdater::updateCredentials() +{ + if(m_state == FATAL) return; + Preferences pref; + bool change = false; + // Get DNS service information + if(m_service != pref.getDynDNSService()) { + m_service = pref.getDynDNSService(); + change = true; + } + if(m_domain != pref.getDynDomainName()) { + m_domain = pref.getDynDomainName(); + QRegExp domain_regex("^(?:(?!\\d|-)[a-zA-Z0-9\\-]{1,63}\\.)+[a-zA-Z]{2,}$"); + if(domain_regex.indexIn(m_domain) < 0) { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: supplied domain name is invalid."), + "red"); + m_lastIP.clear(); + m_ipCheckTimer.stop(); + m_state = INVALID_CREDS; + return; + } + change = true; + } + if(m_username != pref.getDynDNSUsername()) { + m_username = pref.getDynDNSUsername(); + if(m_username.length() < 4) { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: supplied username is too short."), + "red"); + m_lastIP.clear(); + m_ipCheckTimer.stop(); + m_state = INVALID_CREDS; + return; + } + change = true; + } + if(m_password != pref.getDynDNSPassword()) { + m_password = pref.getDynDNSPassword(); + if(m_password.length() < 4) { + QBtSession::instance()->addConsoleMessage(tr("Dynamic DNS error: supplied password is too short."), + "red"); + m_lastIP.clear(); + m_ipCheckTimer.stop(); + m_state = INVALID_CREDS; + return; + } + change = true; + } + + if(m_state == INVALID_CREDS && change) { + m_state = OK; // Try again + m_ipCheckTimer.start(); + checkPublicIP(); + } +} + +QUrl DNSUpdater::getRegistrationUrl(int service) +{ + switch(service) { + case DNS::DYNDNS: + return QUrl("https://www.dyndns.com/account/services/hosts/add.html"); + case DNS::NOIP: + return QUrl("http://www.no-ip.com/services/managed_dns/free_dynamic_dns.html"); + default: + Q_ASSERT(0); + } + return QUrl(); +} diff --git a/src/dnsupdater.h b/src/dnsupdater.h new file mode 100644 index 000000000..3ea9a5cd3 --- /dev/null +++ b/src/dnsupdater.h @@ -0,0 +1,81 @@ +/* + * Bittorrent Client using Qt4 and libtorrent. + * Copyright (C) 2011 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * Contact : chris@qbittorrent.org + */ + +#ifndef DNSUPDATER_H +#define DNSUPDATER_H + +#include +#include +#include +#include +#include +#include "preferences.h" + +/*! + * Based on http://www.dyndns.com/developers/specs/ + */ +class DNSUpdater : public QObject +{ + Q_OBJECT +public: + explicit DNSUpdater(QObject *parent = 0); + ~DNSUpdater(); + static QUrl getRegistrationUrl(int service); + +public slots: + void updateCredentials(); + +private slots: + void checkPublicIP(); + void ipRequestFinished(QNetworkReply* reply); + void updateDNSService(); + void ipUpdateFinished(QNetworkReply* reply); + +private: + QUrl getUpdateUrl() const; + void processIPUpdateReply(const QString &reply); + +private: + QHostAddress m_lastIP; + QDateTime m_lastIPCheckTime; + QTimer m_ipCheckTimer; + int m_state; + // Service creds + DNS::Service m_service; + QString m_domain; + QString m_username; + QString m_password; + +private: + static const int IP_CHECK_INTERVAL_MS = 1800000; // 30 min + enum State { OK, INVALID_CREDS, FATAL }; +}; + +#endif // DNSUPDATER_H diff --git a/src/preferences/options.ui b/src/preferences/options.ui index 29d2d1ba2..206557de6 100644 --- a/src/preferences/options.ui +++ b/src/preferences/options.ui @@ -2281,11 +2281,103 @@ QGroupBox { + + + + Update my dynamic domain name + + + true + + + false + + + + + + Service: + + + + + + + + + + DynDNS + + + + + No-IP + + + + + + + + Register + + + + + + + + + Domain name: + + + + + + + changeme.dyndns.org + + + + + + + Username: + + + + + + + 50 + + + + + + + Password: + + + + + + + 50 + + + QLineEdit::Password + + + + + + - + Qt::Vertical @@ -2315,8 +2407,8 @@ QGroupBox { 0 0 - 86 - 16 + 504 + 384 diff --git a/src/preferences/options_imp.cpp b/src/preferences/options_imp.cpp index 18c616dce..17949152d 100644 --- a/src/preferences/options_imp.cpp +++ b/src/preferences/options_imp.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -50,6 +51,7 @@ #include "qinisettings.h" #include "qbtsession.h" #include "iconprovider.h" +#include "dnsupdater.h" using namespace libtorrent; @@ -203,6 +205,11 @@ options_imp::options_imp(QWidget *parent): connect(textWebUiUsername, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); connect(textWebUiPassword, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton())); connect(checkBypassLocalAuth, SIGNAL(toggled(bool)), this, SLOT(enableApplyButton())); + connect(checkDynDNS, SIGNAL(toggled(bool)), SLOT(enableApplyButton())); + connect(comboDNSService, SIGNAL(currentIndexChanged(int)), SLOT(enableApplyButton())); + connect(domainNameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); + connect(DNSUsernameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); + connect(DNSPasswordTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); // Disable apply Button applyButton->setEnabled(false); // Tab selection mecanism @@ -428,6 +435,12 @@ void options_imp::saveOptions(){ // FIXME: Check that the password is valid (not empty at least) pref.setWebUiPassword(webUiPassword()); pref.setWebUiLocalAuthEnabled(!checkBypassLocalAuth->isChecked()); + // DynDNS + pref.setDynDNSEnabled(checkDynDNS->isChecked()); + pref.setDynDNSService(comboDNSService->currentIndex()); + pref.setDynDomainName(domainNameTxt->text()); + pref.setDynDNSUsername(DNSUsernameTxt->text()); + pref.setDynDNSPassword(DNSPasswordTxt->text()); } // End Web UI // End preferences @@ -666,6 +679,12 @@ void options_imp::loadOptions(){ textWebUiUsername->setText(pref.getWebUiUsername()); textWebUiPassword->setText(pref.getWebUiPassword()); checkBypassLocalAuth->setChecked(!pref.isWebUiLocalAuthEnabled()); + // Dynamic DNS + checkDynDNS->setChecked(pref.isDynDNSEnabled()); + comboDNSService->setCurrentIndex((int)pref.getDynDNSService()); + domainNameTxt->setText(pref.getDynDomainName()); + DNSUsernameTxt->setText(pref.getDynDNSUsername()); + DNSPasswordTxt->setText(pref.getDynDNSPassword()); // End Web UI // Random stuff srand(time(0)); @@ -1090,6 +1109,10 @@ void options_imp::showConnectionTab() tabSelection->setCurrentRow(2); } +void options_imp::on_registerDNSBtn_clicked() { + QDesktopServices::openUrl(DNSUpdater::getRegistrationUrl(comboDNSService->currentIndex())); +} + void options_imp::on_IpFilterRefreshBtn_clicked() { if(m_refreshingIpFilter) return; m_refreshingIpFilter = true; diff --git a/src/preferences/options_imp.h b/src/preferences/options_imp.h index d7c4b8ce9..3dc28962d 100644 --- a/src/preferences/options_imp.h +++ b/src/preferences/options_imp.h @@ -80,6 +80,7 @@ private slots: void on_randomButton_clicked(); void on_addScanFolderButton_clicked(); void on_removeScanFolderButton_clicked(); + void on_registerDNSBtn_clicked(); void setLocale(const QString &locale); private: diff --git a/src/preferences/preferences.h b/src/preferences/preferences.h index ec4f27e4c..419d72f18 100644 --- a/src/preferences/preferences.h +++ b/src/preferences/preferences.h @@ -61,6 +61,9 @@ enum ProxyType {HTTP=1, SOCKS5=2, HTTP_PW=3, SOCKS5_PW=4, SOCKS4=5}; namespace TrayIcon { enum Style { NORMAL = 0, MONO_DARK, MONO_LIGHT }; } +namespace DNS { + enum Service { DYNDNS, NOIP }; +} class Preferences : public QIniSettings { Q_DISABLE_COPY(Preferences); @@ -746,6 +749,46 @@ public: return pass_ha1; } + bool isDynDNSEnabled() const { + return value("Preferences/DynDNS/Enabled", false).toBool(); + } + + void setDynDNSEnabled(bool enabled) { + setValue("Preferences/DynDNS/Enabled", enabled); + } + + DNS::Service getDynDNSService() const { + return DNS::Service(value("Preferences/DynDNS/Service", DNS::DYNDNS).toInt()); + } + + void setDynDNSService(int service) { + setValue("Preferences/DynDNS/Service", service); + } + + QString getDynDomainName() const { + return value("Preferences/DynDNS/DomainName", "changeme.dyndns.org").toString(); + } + + void setDynDomainName(const QString name) { + setValue("Preferences/DynDNS/DomainName", name); + } + + QString getDynDNSUsername() const { + return value("Preferences/DynDNS/Username").toString(); + } + + void setDynDNSUsername(const QString username) { + setValue("Preferences/DynDNS/Username", username); + } + + QString getDynDNSPassword() const { + return value("Preferences/DynDNS/Password").toString(); + } + + void setDynDNSPassword(const QString password) { + setValue("Preferences/DynDNS/Password", password); + } + // Advanced settings void setUILockPassword(const QString &clear_password) { diff --git a/src/qtlibtorrent/qbtsession.cpp b/src/qtlibtorrent/qbtsession.cpp index 1e49e06f7..c4bfbc9d3 100644 --- a/src/qtlibtorrent/qbtsession.cpp +++ b/src/qtlibtorrent/qbtsession.cpp @@ -78,6 +78,7 @@ #endif #include #include +#include "dnsupdater.h" using namespace libtorrent; @@ -98,7 +99,7 @@ QBtSession::QBtSession() , geoipDBLoaded(false), resolve_countries(false) #endif , m_tracker(0), m_shutdownAct(NO_SHUTDOWN), - m_upnp(0), m_natpmp(0) + m_upnp(0), m_natpmp(0), m_dynDNSUpdater(0) { BigRatioTimer = new QTimer(this); BigRatioTimer->setInterval(10000); @@ -612,8 +613,25 @@ void QBtSession::initWebUi() { else addConsoleMessage(tr("Web User Interface Error - Unable to bind Web UI to port %1").arg(port), "red"); } - } else if(httpServer) { - delete httpServer; + // DynDNS + if(pref.isDynDNSEnabled()) { + if(!m_dynDNSUpdater) + m_dynDNSUpdater = new DNSUpdater(this); + else + m_dynDNSUpdater->updateCredentials(); + } else { + if(m_dynDNSUpdater) { + delete m_dynDNSUpdater; + m_dynDNSUpdater = 0; + } + } + } else { + if(httpServer) + delete httpServer; + if(m_dynDNSUpdater) { + delete m_dynDNSUpdater; + m_dynDNSUpdater = 0; + } } } diff --git a/src/qtlibtorrent/qbtsession.h b/src/qtlibtorrent/qbtsession.h index 43aeafb2b..d4227696d 100644 --- a/src/qtlibtorrent/qbtsession.h +++ b/src/qtlibtorrent/qbtsession.h @@ -58,6 +58,7 @@ class HttpServer; class BandwidthScheduler; class ScanFoldersModel; class TorrentSpeedMonitor; +class DNSUpdater; class QBtSession : public QObject { Q_OBJECT @@ -272,6 +273,8 @@ private: // Port forwarding libtorrent::upnp *m_upnp; libtorrent::natpmp *m_natpmp; + // DynDNS + DNSUpdater *m_dynDNSUpdater; }; #endif diff --git a/src/src.pro b/src/src.pro index ec425c003..d299d810c 100644 --- a/src/src.pro +++ b/src/src.pro @@ -101,13 +101,15 @@ HEADERS += misc.h \ filesystemwatcher.h \ scannedfoldersmodel.h \ qinisettings.h \ - smtp.h + smtp.h \ + dnsupdater.h SOURCES += main.cpp \ downloadthread.cpp \ scannedfoldersmodel.cpp \ misc.cpp \ - smtp.cpp + smtp.cpp \ + dnsupdater.cpp nox { HEADERS += headlessloader.h