mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2024-11-23 17:59:44 +00:00
Use HTTP digest mode for Web UI authentication (instead of Basic)
This commit is contained in:
parent
4522174555
commit
c7ca51f950
@ -19,6 +19,8 @@
|
|||||||
- BUGFIX: Use XDG folders (.cache, .local) instead of .qbittorrent
|
- BUGFIX: Use XDG folders (.cache, .local) instead of .qbittorrent
|
||||||
- BUGFIX: Added legal notice on startup that the user must accept
|
- BUGFIX: Added legal notice on startup that the user must accept
|
||||||
- BUGFIX: Protect Web UI authentication against brute forcing
|
- BUGFIX: Protect Web UI authentication against brute forcing
|
||||||
|
- BUGFIX: Use HTTP digest mode for Web UI authentication (instead of Basic)
|
||||||
|
- BUGFIX: Properly display torrents with one file in subfolder(s)
|
||||||
- COSMETIC: Use checkboxes to filter torrent content instead of comboboxes
|
- COSMETIC: Use checkboxes to filter torrent content instead of comboboxes
|
||||||
- COSMETIC: Use alternating row colors in transfer list (set in program preferences)
|
- COSMETIC: Use alternating row colors in transfer list (set in program preferences)
|
||||||
- COSMETIC: Added a spin box to speed limiting dialog for manual input
|
- COSMETIC: Added a spin box to speed limiting dialog for manual input
|
||||||
|
@ -137,14 +137,14 @@ void HttpConnection::respond() {
|
|||||||
write();
|
write();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QStringList auth = parser.value("Authorization").split(" ", QString::SkipEmptyParts);
|
QString auth = parser.value("Authorization");
|
||||||
if (auth.size() != 2 || QString::compare(auth[0], "Basic", Qt::CaseInsensitive) != 0 || !parent->isAuthorized(auth[1].toLocal8Bit())) {
|
if (QString::compare(auth.split(" ").first(), "Digest", Qt::CaseInsensitive) != 0 || !parent->isAuthorized(auth.toLocal8Bit(), parser.method())) {
|
||||||
// Update failed attempt counter
|
// Update failed attempt counter
|
||||||
parent->client_failed_attempts.insert(socket->peerAddress().toString(), nb_fail+1);
|
parent->client_failed_attempts.insert(socket->peerAddress().toString(), nb_fail+1);
|
||||||
qDebug("client IP: %s (%d failed attempts)", socket->peerAddress().toString().toLocal8Bit().data(), nb_fail);
|
qDebug("client IP: %s (%d failed attempts)", socket->peerAddress().toString().toLocal8Bit().data(), nb_fail);
|
||||||
// Return unauthorized header
|
// Return unauthorized header
|
||||||
generator.setStatusLine(401, "Unauthorized");
|
generator.setStatusLine(401, "Unauthorized");
|
||||||
generator.setValue("WWW-Authenticate", "Basic realm=\"you know what\"");
|
generator.setValue("WWW-Authenticate", "Digest realm=\""+QString(QBT_REALM)+"\", nonce=\""+parent->generateNonce()+"\", algorithm=\"MD5\", qop=\"auth\"");
|
||||||
write();
|
write();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -33,13 +33,14 @@
|
|||||||
#include "httpconnection.h"
|
#include "httpconnection.h"
|
||||||
#include "eventmanager.h"
|
#include "eventmanager.h"
|
||||||
#include "bittorrent.h"
|
#include "bittorrent.h"
|
||||||
#include "preferences.h"
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QTime>
|
||||||
|
#include <QRegExp>
|
||||||
|
|
||||||
HttpServer::HttpServer(Bittorrent *_BTSession, int msec, QObject* parent) : QTcpServer(parent) {
|
HttpServer::HttpServer(Bittorrent *_BTSession, int msec, QObject* parent) : QTcpServer(parent) {
|
||||||
username = Preferences::getWebUiUsername().toLocal8Bit();
|
username = Preferences::getWebUiUsername().toLocal8Bit();
|
||||||
password_md5 = Preferences::getWebUiPassword().toLocal8Bit();
|
password_ha1 = Preferences::getWebUiPassword().toLocal8Bit();
|
||||||
connect(this, SIGNAL(newConnection()), this, SLOT(newHttpConnection()));
|
connect(this, SIGNAL(newConnection()), this, SLOT(newHttpConnection()));
|
||||||
BTSession = _BTSession;
|
BTSession = _BTSession;
|
||||||
manager = new EventManager(this, BTSession);
|
manager = new EventManager(this, BTSession);
|
||||||
@ -118,21 +119,110 @@ void HttpServer::onTimer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HttpServer::setAuthorization(QString _username, QString _password_md5) {
|
QString HttpServer::generateNonce() const {
|
||||||
username = _username.toLocal8Bit();
|
QCryptographicHash md5(QCryptographicHash::Md5);
|
||||||
password_md5 = _password_md5.toLocal8Bit();
|
md5.addData(QTime::currentTime().toString("hhmmsszzz").toLocal8Bit());
|
||||||
|
md5.addData(":");
|
||||||
|
md5.addData(QBT_REALM);
|
||||||
|
return md5.result().toHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpServer::isAuthorized(QByteArray auth) const {
|
void HttpServer::setAuthorization(QString _username, QString _password_ha1) {
|
||||||
// Decode Auth
|
username = _username.toLocal8Bit();
|
||||||
QByteArray decoded = QByteArray::fromBase64(auth);
|
password_ha1 = _password_ha1.toLocal8Bit();
|
||||||
QList<QByteArray> creds = decoded.split(':');
|
}
|
||||||
if(creds.size() != 2) return false;
|
|
||||||
QByteArray prop_username = creds.first();
|
// AUTH string is: Digest username="chris",
|
||||||
if(prop_username != username) return false;
|
// realm="Web UI Access",
|
||||||
QCryptographicHash md5(QCryptographicHash::Md5);
|
// nonce="570d04de93444b7fd3eaeaecb00e635e",
|
||||||
md5.addData(creds.last());
|
// uri="/", algorithm=MD5,
|
||||||
return (password_md5 == md5.result().toHex());
|
// response="ba886766d19b45313c0e2195e4344264",
|
||||||
|
// qop=auth, nc=00000001, cnonce="e8ac970779c17075"
|
||||||
|
bool HttpServer::isAuthorized(QByteArray auth, QString method) const {
|
||||||
|
qDebug("AUTH string is %s", auth.data());
|
||||||
|
// Get user name
|
||||||
|
QRegExp regex_user(".*username=\"([^\"]+)\".*");
|
||||||
|
if(regex_user.indexIn(auth) < 0) return false;
|
||||||
|
QString prop_user = regex_user.cap(1);
|
||||||
|
qDebug("AUTH: Proposed username is %s, real username is %s", prop_user.toLocal8Bit().data(), username.data());
|
||||||
|
if(prop_user != username) {
|
||||||
|
// User name is invalid, we can reject already
|
||||||
|
qDebug("AUTH-PROB: Username is invalid");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Get realm
|
||||||
|
QRegExp regex_realm(".*realm=\"([^\"]+)\".*");
|
||||||
|
if(regex_realm.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: Missing realm");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_realm = regex_realm.cap(1).toLocal8Bit();
|
||||||
|
if(prop_realm != QBT_REALM) {
|
||||||
|
qDebug("AUTH-PROB: Wrong realm");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// get nonce
|
||||||
|
QRegExp regex_nonce(".*nonce=\"([^\"]+)\".*");
|
||||||
|
if(regex_nonce.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: missing nonce");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_nonce = regex_nonce.cap(1).toLocal8Bit();
|
||||||
|
qDebug("prop nonce is: %s", prop_nonce.data());
|
||||||
|
// get uri
|
||||||
|
QRegExp regex_uri(".*uri=\"([^\"]+)\".*");
|
||||||
|
if(regex_uri.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: Missing uri");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_uri = regex_uri.cap(1).toLocal8Bit();
|
||||||
|
qDebug("prop uri is: %s", prop_uri.data());
|
||||||
|
// get response
|
||||||
|
QRegExp regex_response(".*response=\"([^\"]+)\".*");
|
||||||
|
if(regex_response.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: Missing response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_response = regex_response.cap(1).toLocal8Bit();
|
||||||
|
qDebug("prop response is: %s", prop_response.data());
|
||||||
|
// Compute correct reponse
|
||||||
|
QCryptographicHash md5_ha2(QCryptographicHash::Md5);
|
||||||
|
md5_ha2.addData(method.toLocal8Bit() + ":" + prop_uri);
|
||||||
|
QByteArray ha2 = md5_ha2.result().toHex();
|
||||||
|
QByteArray response = "";
|
||||||
|
if(auth.contains("qop=")) {
|
||||||
|
QCryptographicHash md5_ha(QCryptographicHash::Md5);
|
||||||
|
// Get nc
|
||||||
|
QRegExp regex_nc(".*nc=(\\w+).*");
|
||||||
|
if(regex_nc.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: qop but missing nc");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_nc = regex_nc.cap(1).toLocal8Bit();
|
||||||
|
qDebug("prop nc is: %s", prop_nc.data());
|
||||||
|
QRegExp regex_cnonce(".*cnonce=\"([^\"]+)\".*");
|
||||||
|
if(regex_cnonce.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: qop but missing cnonce");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_cnonce = regex_cnonce.cap(1).toLocal8Bit();
|
||||||
|
qDebug("prop cnonce is: %s", prop_cnonce.data());
|
||||||
|
QRegExp regex_qop(".*qop=(\\w+).*");
|
||||||
|
if(regex_qop.indexIn(auth) < 0) {
|
||||||
|
qDebug("AUTH-PROB: missing qop");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray prop_qop = regex_qop.cap(1).toLocal8Bit();
|
||||||
|
qDebug("prop qop is: %s", prop_qop.data());
|
||||||
|
md5_ha.addData(password_ha1+":"+prop_nonce+":"+prop_nc+":"+prop_cnonce+":"+prop_qop+":"+ha2);
|
||||||
|
response = md5_ha.result().toHex();
|
||||||
|
} else {
|
||||||
|
QCryptographicHash md5_ha(QCryptographicHash::Md5);
|
||||||
|
md5_ha.addData(password_ha1+":"+prop_nonce+":"+ha2);
|
||||||
|
response = md5_ha.result().toHex();
|
||||||
|
}
|
||||||
|
qDebug("AUTH: comparing reponses");
|
||||||
|
return prop_response == response;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventManager* HttpServer::eventManager() const
|
EventManager* HttpServer::eventManager() const
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
#include <QTcpServer>
|
#include <QTcpServer>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include "preferences.h"
|
||||||
|
|
||||||
class Bittorrent;
|
class Bittorrent;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
@ -46,7 +47,7 @@ class HttpServer : public QTcpServer {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QByteArray username;
|
QByteArray username;
|
||||||
QByteArray password_md5;
|
QByteArray password_ha1;
|
||||||
Bittorrent *BTSession;
|
Bittorrent *BTSession;
|
||||||
EventManager *manager;
|
EventManager *manager;
|
||||||
QTimer *timer;
|
QTimer *timer;
|
||||||
@ -54,9 +55,10 @@ class HttpServer : public QTcpServer {
|
|||||||
public:
|
public:
|
||||||
HttpServer(Bittorrent *BTSession, int msec, QObject* parent = 0);
|
HttpServer(Bittorrent *BTSession, int msec, QObject* parent = 0);
|
||||||
~HttpServer();
|
~HttpServer();
|
||||||
void setAuthorization(QString username, QString password_md5);
|
void setAuthorization(QString username, QString password_ha1);
|
||||||
bool isAuthorized(QByteArray auth) const;
|
bool isAuthorized(QByteArray auth, QString method) const;
|
||||||
EventManager *eventManager() const;
|
EventManager *eventManager() const;
|
||||||
|
QString generateNonce() const;
|
||||||
QHash<QString, int> client_failed_attempts;
|
QHash<QString, int> client_failed_attempts;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
@ -36,6 +36,8 @@
|
|||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
||||||
|
#define QBT_REALM "Web UI Access"
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
public:
|
public:
|
||||||
// General options
|
// General options
|
||||||
@ -708,9 +710,10 @@ public:
|
|||||||
if(current_pass_md5 == new_password) return;
|
if(current_pass_md5 == new_password) return;
|
||||||
// Encode to md5 and save
|
// Encode to md5 and save
|
||||||
QCryptographicHash md5(QCryptographicHash::Md5);
|
QCryptographicHash md5(QCryptographicHash::Md5);
|
||||||
|
md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":");
|
||||||
md5.addData(new_password.toLocal8Bit());
|
md5.addData(new_password.toLocal8Bit());
|
||||||
QSettings settings("qBittorrent", "qBittorrent");
|
QSettings settings("qBittorrent", "qBittorrent");
|
||||||
settings.setValue("Preferences/WebUI/Password_md5", md5.result().toHex());
|
settings.setValue("Preferences/WebUI/Password_ha1", md5.result().toHex());
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString getWebUiPassword() {
|
static QString getWebUiPassword() {
|
||||||
@ -720,18 +723,20 @@ public:
|
|||||||
QString clear_pass = settings.value("Preferences/WebUI/Password", "adminadmin").toString();
|
QString clear_pass = settings.value("Preferences/WebUI/Password", "adminadmin").toString();
|
||||||
settings.remove("Preferences/WebUI/Password");
|
settings.remove("Preferences/WebUI/Password");
|
||||||
QCryptographicHash md5(QCryptographicHash::Md5);
|
QCryptographicHash md5(QCryptographicHash::Md5);
|
||||||
|
md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":");
|
||||||
md5.addData(clear_pass.toLocal8Bit());
|
md5.addData(clear_pass.toLocal8Bit());
|
||||||
QString pass_md5(md5.result().toHex());
|
QString pass_md5(md5.result().toHex());
|
||||||
settings.setValue("Preferences/WebUI/Password_md5", pass_md5);
|
settings.setValue("Preferences/WebUI/Password_ha1", pass_md5);
|
||||||
return pass_md5;
|
return pass_md5;
|
||||||
}
|
}
|
||||||
QString pass_md5 = settings.value("Preferences/WebUI/Password_md5", "").toString();
|
QString pass_ha1 = settings.value("Preferences/WebUI/Password_ha1", "").toString();
|
||||||
if(pass_md5.isEmpty()) {
|
if(pass_ha1.isEmpty()) {
|
||||||
QCryptographicHash md5(QCryptographicHash::Md5);
|
QCryptographicHash md5(QCryptographicHash::Md5);
|
||||||
|
md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":");
|
||||||
md5.addData("adminadmin");
|
md5.addData("adminadmin");
|
||||||
pass_md5 = md5.result().toHex();
|
pass_ha1 = md5.result().toHex();
|
||||||
}
|
}
|
||||||
return pass_md5;
|
return pass_ha1;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user