From 7f23b9b424f2b8c545d1597bceed2e42d54c4598 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Mon, 15 Apr 2019 22:17:40 +0200 Subject: [PATCH] Add https support to localredirectserver --- CMakeLists.txt | 1 + Dockerfile | 2 +- src/CMakeLists.txt | 6 +- src/covermanager/albumcoverfetchersearch.cpp | 1 - src/internet/localredirectserver.cpp | 231 ++++++++++++++++--- src/internet/localredirectserver.h | 52 +++-- src/scrobbler/listenbrainzscrobbler.cpp | 10 +- src/scrobbler/listenbrainzscrobbler.h | 2 +- src/scrobbler/scrobblingapi20.cpp | 34 ++- src/scrobbler/scrobblingapi20.h | 4 +- src/settings/scrobblersettingspage.cpp | 6 +- src/settings/scrobblersettingspage.ui | 7 + 12 files changed, 290 insertions(+), 66 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d76a0b639..391c6032b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,7 @@ pkg_check_modules(GIO REQUIRED gio-2.0) pkg_check_modules(GOBJECT REQUIRED gobject-2.0) pkg_check_modules(CDIO libcdio) find_package(Threads) +find_package(OpenSSL) find_package(Boost REQUIRED) find_package(Protobuf REQUIRED) find_library(PROTOBUF_STATIC_LIBRARY libprotobuf.a libprotobuf) diff --git a/Dockerfile b/Dockerfile index c3ecc6d91..c07fdb270 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ run zypper --non-interactive --gpg-auto-import-keys dup -l -y run zypper --non-interactive --gpg-auto-import-keys install \ lsb-release git tar make cmake gcc gcc-c++ pkg-config gettext-tools \ - glibc-devel glib2-devel glib2-tools dbus-1-devel alsa-devel libpulse-devel libnotify-devel \ + glibc-devel glib2-devel glib2-tools dbus-1-devel alsa-devel libpulse-devel libnotify-devel libopenssl-devel \ boost-devel protobuf-devel sqlite3-devel taglib-devel \ gstreamer-devel gstreamer-plugins-base-devel libxine-devel vlc-devel \ libQt5Core-devel libQt5Gui-devel libQt5Widgets-devel libQt5Concurrent-devel libQt5Network-devel libQt5Sql-devel \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b439bb6ee..cad1eeb04 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,6 +43,7 @@ include_directories(${CMAKE_BINARY_DIR}) include_directories(${GLIB_INCLUDE_DIRS}) include_directories(${GLIBCONFIG_INCLUDE_DIRS}) include_directories(${GOBJECT_INCLUDE_DIRS}) +include_directories(${OPENSSL_INCLUDE_DIR}) include_directories(${Boost_INCLUDE_DIRS}) include_directories(${LIBXML_INCLUDE_DIRS}) include_directories(${CHROMAPRINT_INCLUDE_DIRS}) @@ -929,14 +930,15 @@ add_library(strawberry_lib STATIC target_link_libraries(strawberry_lib libstrawberry-common libstrawberry-tagreader + ${CMAKE_THREAD_LIBS_INIT} ${GLIB_LIBRARIES} ${GIO_LIBRARIES} - ${TAGLIB_LIBRARIES} ${GOBJECT_LIBRARIES} + ${OPENSSL_LIBRARIES} ${QT_LIBRARIES} ${CHROMAPRINT_LIBRARIES} ${SQLITE_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} + ${TAGLIB_LIBRARIES} ${SINGLEAPPLICATION_LIBRARIES} ${SINGLECOREAPPLICATION_LIBRARIES} ${QOCOA_LIBRARIES} diff --git a/src/covermanager/albumcoverfetchersearch.cpp b/src/covermanager/albumcoverfetchersearch.cpp index 178e16618..923cdc16d 100644 --- a/src/covermanager/albumcoverfetchersearch.cpp +++ b/src/covermanager/albumcoverfetchersearch.cpp @@ -283,4 +283,3 @@ void AlbumCoverFetcherSearch::Cancel() { } } - diff --git a/src/internet/localredirectserver.cpp b/src/internet/localredirectserver.cpp index 054039a1c..63cb1f301 100644 --- a/src/internet/localredirectserver.cpp +++ b/src/internet/localredirectserver.cpp @@ -2,6 +2,7 @@ * This file was part of Clementine. * Copyright 2012, 2014, John Maguire * Copyright 2014, Krzysztof Sobiecki + * Copyright 2018-2019, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,53 +21,225 @@ #include "localredirectserver.h" +#include +#include +#include +#include + #include #include #include #include #include +#include +#include #include +#include #include +#include +#include +#include "core/logging.h" #include "core/closure.h" -LocalRedirectServer::LocalRedirectServer(QObject* parent) - : QObject(parent), server_(new QTcpServer(this)) {} +LocalRedirectServer::LocalRedirectServer(const bool https, QObject *parent) + : QTcpServer(parent), + https_(https), + socket_(nullptr) + {} -void LocalRedirectServer::Listen() { +LocalRedirectServer::~LocalRedirectServer() {} - server_->listen(QHostAddress::LocalHost); - // We have to calculate this and store it now as the server port is cleared once we close the socket. - url_.setScheme("http"); +bool LocalRedirectServer::GenerateCertificate() { + + EVP_PKEY *pkey = nullptr; + RSA *rsa = nullptr; + X509 *x509 = nullptr; + X509_NAME *name = nullptr; + BIO *bp_public = nullptr, *bp_private = nullptr; + const char *buffer = nullptr; + long size = 0; + + pkey = EVP_PKEY_new(); + q_check_ptr(pkey); + + rsa = RSA_generate_key(2048, RSA_F4, nullptr, nullptr); + q_check_ptr(rsa); + + EVP_PKEY_assign_RSA(pkey, rsa); + + x509 = X509_new(); + q_check_ptr(x509); + + ASN1_INTEGER_set(X509_get_serialNumber(x509), static_cast(9999999 + qrand() % 1000000)); + + X509_gmtime_adj(X509_get_notBefore(x509), 0); + X509_gmtime_adj(X509_get_notAfter(x509), 31536000L); + X509_set_pubkey(x509, pkey); + + name = X509_get_subject_name(x509); + q_check_ptr(name); + + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *) "US", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *) "Strawberry Music Player", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *) "localhost", -1, -1, 0); + X509_set_issuer_name(x509, name); + X509_sign(x509, pkey, EVP_sha1()); + + bp_private = BIO_new(BIO_s_mem()); + q_check_ptr(bp_private); + + if (PEM_write_bio_PrivateKey(bp_private, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) { + EVP_PKEY_free(pkey); + X509_free(x509); + BIO_free_all(bp_private); + error_ = "PEM_write_bio_PrivateKey() failed."; + return false; + } + + bp_public = BIO_new(BIO_s_mem()); + q_check_ptr(bp_public); + + if (PEM_write_bio_X509(bp_public, x509) != 1) { + EVP_PKEY_free(pkey); + X509_free(x509); + BIO_free_all(bp_public); + BIO_free_all(bp_private); + error_ = "PEM_write_bio_X509() failed."; + return false; + } + + size = BIO_get_mem_data(bp_public, &buffer); + q_check_ptr(buffer); + + QSslCertificate ssl_certificate(QByteArray(buffer, size)); + if (ssl_certificate.isNull()) { + error_ = "Failed to generate a random client certificate."; + return false; + } + + size = BIO_get_mem_data(bp_private, &buffer); + q_check_ptr(buffer); + + QSslKey ssl_key(QByteArray(buffer, size), QSsl::Rsa); + if (ssl_key.isNull()) { + error_ = "Failed to generate a random private key."; + return false; + } + + EVP_PKEY_free(pkey); + X509_free(x509); + BIO_free_all(bp_public); + BIO_free_all(bp_private); + + ssl_certificate_ = ssl_certificate; + ssl_key_ = ssl_key; + + return true; + +} + +bool LocalRedirectServer::Listen() { + + if (https_) { + if (!GenerateCertificate()) return false; + } + if (!listen(QHostAddress::LocalHost)) { + error_ = errorString(); + return false; + } + + if (https_) url_.setScheme("https"); + else url_.setScheme("http"); url_.setHost("localhost"); - url_.setPort(server_->serverPort()); + url_.setPort(serverPort()); url_.setPath("/"); - connect(server_, SIGNAL(newConnection()), SLOT(NewConnection())); + connect(this, SIGNAL(newConnection()), this, SLOT(NewConnection())); + + return true; } void LocalRedirectServer::NewConnection() { - QTcpSocket* socket = server_->nextPendingConnection(); - server_->close(); - QByteArray buffer; - NewClosure(socket, SIGNAL(readyRead()), this, SLOT(ReadyRead(QTcpSocket*, QByteArray)), socket, buffer); + while (hasPendingConnections()) { + incomingConnection(nextPendingConnection()->socketDescriptor()); + } + } -void LocalRedirectServer::ReadyRead(QTcpSocket* socket, QByteArray buffer) { - buffer.append(socket->readAll()); - if (socket->atEnd() || buffer.endsWith("\r\n\r\n")) { - WriteTemplate(socket); - socket->deleteLater(); - request_url_ = ParseUrlFromRequest(buffer); +void LocalRedirectServer::incomingConnection(qintptr socket_descriptor) { + + if (socket_) { + if (socket_->state() == QAbstractSocket::ConnectedState) socket_->close(); + socket_->deleteLater(); + socket_ = nullptr; + } + buffer_.clear(); + + if (https_) { + QSslSocket *ssl_socket = new QSslSocket(this); + if (!ssl_socket->setSocketDescriptor(socket_descriptor)) { + delete ssl_socket; + error_ = "Unable to set socket descriptor"; + emit Finished(); + return; + } + ssl_socket->ignoreSslErrors({QSslError::SelfSignedCertificate}); + ssl_socket->setPrivateKey(ssl_key_); + ssl_socket->setLocalCertificate(ssl_certificate_); + ssl_socket->setProtocol(QSsl::TlsV1_2); + ssl_socket->startServerEncryption(); + + connect(ssl_socket, SIGNAL(sslErrors(QList)), this, SLOT(SSLErrors(QList))); + connect(ssl_socket, SIGNAL(encrypted()), this, SLOT(Encrypted(QSslSocket*))); + + socket_ = ssl_socket; + } + else { + QTcpSocket *tcp_socket = new QTcpSocket(this); + if (!tcp_socket->setSocketDescriptor(socket_descriptor)) { + delete tcp_socket; + error_ = "Unable to set socket descriptor"; + emit Finished(); + return; + } + socket_ = tcp_socket; + } + + connect(socket_, SIGNAL(connected()), this, SLOT(Connected())); + connect(socket_, SIGNAL(disconnected()), this, SLOT(Disconnected())); + connect(socket_, SIGNAL(readyRead()), this, SLOT(ReadyRead())); + +} + +void LocalRedirectServer::SSLErrors(const QList &errors) {} + +void LocalRedirectServer::Encrypted() {} + +void LocalRedirectServer::Connected() {} + +void LocalRedirectServer::Disconnected() {} + +void LocalRedirectServer::ReadyRead() { + + buffer_.append(socket_->readAll()); + if (socket_->atEnd() || buffer_.endsWith("\r\n\r\n")) { + WriteTemplate(); + socket_->close(); + socket_->deleteLater(); + socket_ = nullptr; + request_url_ = ParseUrlFromRequest(buffer_); + close(); emit Finished(); } else { - NewClosure(socket, SIGNAL(readyRead()), this, SLOT(ReadyReady(QTcpSocket*, QByteArray)), socket, buffer); + connect(socket_, SIGNAL(readyRead()), this, SLOT(ReadyRead())); } + } -void LocalRedirectServer::WriteTemplate(QTcpSocket* socket) const { +void LocalRedirectServer::WriteTemplate() const { QFile page_file(":/html/oauthsuccess.html"); page_file.open(QIODevice::ReadOnly); @@ -93,19 +266,21 @@ void LocalRedirectServer::WriteTemplate(QTcpSocket* socket) const { .save(&image_buffer, "PNG"); page_data.replace("@IMAGE_DATA@", image_buffer.data().toBase64()); - socket->write("HTTP/1.0 200 OK\r\n"); - socket->write("Content-type: text/html;charset=UTF-8\r\n"); - socket->write("\r\n\r\n"); - socket->write(page_data.toUtf8()); - socket->flush(); + socket_->write("HTTP/1.0 200 OK\r\n"); + socket_->write("Content-type: text/html;charset=UTF-8\r\n"); + socket_->write("\r\n\r\n"); + socket_->write(page_data.toUtf8()); + socket_->flush(); } -QUrl LocalRedirectServer::ParseUrlFromRequest(const QByteArray& request) const { +QUrl LocalRedirectServer::ParseUrlFromRequest(const QByteArray &request) const { + QList lines = request.split('\r'); - const QByteArray& request_line = lines[0]; + const QByteArray &request_line = lines[0]; QByteArray path = request_line.split(' ')[1]; - QUrl base_url = url(); + QUrl base_url = url_; QUrl request_url(base_url.toString() + path.mid(1), QUrl::StrictMode); return request_url; + } diff --git a/src/internet/localredirectserver.h b/src/internet/localredirectserver.h index e62086bac..4cdbc97de 100644 --- a/src/internet/localredirectserver.h +++ b/src/internet/localredirectserver.h @@ -2,6 +2,7 @@ * This file was part of Clementine. * Copyright 2012, 2014, John Maguire * Copyright 2014, Krzysztof Sobiecki + * Copyright 2018-2019, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,41 +24,54 @@ #include #include +#include +#include +#include +#include +#include +#include +#include #include -class QTcpServer; -class QTcpSocket; - -class LocalRedirectServer : public QObject { +class LocalRedirectServer : public QTcpServer { Q_OBJECT public: - explicit LocalRedirectServer(QObject* parent = nullptr); + explicit LocalRedirectServer(const bool https, QObject* parent = nullptr); + ~LocalRedirectServer(); - // Causes the server to listen for _one_ request. - void Listen(); - - // Returns the HTTP URL of this server. - const QUrl& url() const { return url_; } - - // Returns the URL requested by the OAuth redirect. - const QUrl& request_url() const { return request_url_; } + bool Listen(); + const QUrl &url() const { return url_; } + const QUrl &request_url() const { return request_url_; } + const QString &error() const { return error_; } signals: - void Finished(); + void Finished(QString error = QString()); - private slots: + public slots: void NewConnection(); - void ReadyRead(QTcpSocket* socket, QByteArray buffer); + void incomingConnection(qintptr socket_descriptor); + void SSLErrors(const QList &errors); + void Encrypted(); + void Connected(); + void Disconnected(); + void ReadyRead(); private: - void WriteTemplate(QTcpSocket* socket) const; - QUrl ParseUrlFromRequest(const QByteArray& request) const; + bool GenerateCertificate(); + void WriteTemplate() const; + QUrl ParseUrlFromRequest(const QByteArray &request) const; private: - QTcpServer* server_; + bool https_; QUrl url_; QUrl request_url_; + QSslCertificate ssl_certificate_; + QSslKey ssl_key_; + QAbstractSocket *socket_; + QByteArray buffer_; + QString error_; + }; #endif diff --git a/src/scrobbler/listenbrainzscrobbler.cpp b/src/scrobbler/listenbrainzscrobbler.cpp index 9df73736b..f5386f86b 100644 --- a/src/scrobbler/listenbrainzscrobbler.cpp +++ b/src/scrobbler/listenbrainzscrobbler.cpp @@ -121,12 +121,16 @@ void ListenBrainzScrobbler::Logout() { } -void ListenBrainzScrobbler::Authenticate() { +void ListenBrainzScrobbler::Authenticate(const bool https) { QUrl url(kAuthUrl); - LocalRedirectServer *server = new LocalRedirectServer(this); - server->Listen(); + LocalRedirectServer *server = new LocalRedirectServer(https, this); + if (!server->Listen()) { + AuthError(server->error()); + delete server; + return; + } NewClosure(server, SIGNAL(Finished()), this, &ListenBrainzScrobbler::RedirectArrived, server); QUrl redirect_url(kRedirectUrl); diff --git a/src/scrobbler/listenbrainzscrobbler.h b/src/scrobbler/listenbrainzscrobbler.h index 00b7e7f7b..b776dee5c 100644 --- a/src/scrobbler/listenbrainzscrobbler.h +++ b/src/scrobbler/listenbrainzscrobbler.h @@ -60,7 +60,7 @@ class ListenBrainzScrobbler : public ScrobblerService { void Submitted() { submitted_ = true; } QString user_token() const { return user_token_; } - void Authenticate(); + void Authenticate(const bool https = false); void Logout(); void ShowConfig(); void Submit(); diff --git a/src/scrobbler/scrobblingapi20.cpp b/src/scrobbler/scrobblingapi20.cpp index e064d07b2..4ba342977 100644 --- a/src/scrobbler/scrobblingapi20.cpp +++ b/src/scrobbler/scrobblingapi20.cpp @@ -83,6 +83,7 @@ void ScrobblingAPI20::ReloadSettings() { QSettings s; s.beginGroup(settings_group_); enabled_ = s.value("enabled", false).toBool(); + https_ = s.value("https", false).toBool(); s.endGroup(); } @@ -113,13 +114,17 @@ void ScrobblingAPI20::Logout() { } -void ScrobblingAPI20::Authenticate() { +void ScrobblingAPI20::Authenticate(const bool https) { QUrl url(auth_url_); - LocalRedirectServer *server = new LocalRedirectServer(this); - server->Listen(); - NewClosure(server, SIGNAL(Finished()), this, &ScrobblingAPI20::RedirectArrived, server); + LocalRedirectServer *server = new LocalRedirectServer(https, this); + if (!server->Listen()) { + AuthError(server->error()); + delete server; + return; + } + NewClosure(server, SIGNAL(Finished(QString)), this, &ScrobblingAPI20::RedirectArrived, server); QUrl redirect_url(kRedirectUrl); QUrlQuery redirect_url_query; @@ -129,7 +134,8 @@ void ScrobblingAPI20::Authenticate() { QUrlQuery url_query; url_query.addQueryItem("api_key", kApiKey); - url_query.addQueryItem("cb", redirect_url.toString()); + url_query.addQueryItem("cb", QUrl::toPercentEncoding(redirect_url.toString())); + qLog(Debug) << QUrl::toPercentEncoding(redirect_url.toString()); url.setQuery(url_query); QMessageBox messagebox(QMessageBox::Information, tr("%1 Scrobbler Authentication").arg(name_), tr("Open URL in web browser?
%1
Press \"Save\" to copy the URL to clipboard and manually open it in a web browser.").arg(url.toString()), QMessageBox::Open|QMessageBox::Save|QMessageBox::Cancel); @@ -149,7 +155,9 @@ void ScrobblingAPI20::Authenticate() { QApplication::clipboard()->setText(url.toString()); break; case QMessageBox::Cancel: - AuthError(tr("Authentication was cancelled.")); + server->close(); + server->deleteLater(); + emit AuthenticationComplete(false); break; default: break; @@ -162,7 +170,17 @@ void ScrobblingAPI20::RedirectArrived(LocalRedirectServer *server) { server->deleteLater(); QUrl url = server->request_url(); - RequestSession(QUrlQuery(url).queryItemValue("token").toUtf8()); + if (!url.isValid()) { + AuthError(tr("Invalid reply from web browser. Try using Chromium or Chrome instead.")); + return; + } + QUrlQuery url_query(url); + QString token = url_query.queryItemValue("token").toUtf8(); + if (token.isEmpty()) { + AuthError(tr("Invalid reply from web browser. Missing token.")); + return; + } + RequestSession(token); } @@ -373,7 +391,7 @@ QByteArray ScrobblingAPI20::GetReplyData(QNetworkReply *reply) { } return data; - + } void ScrobblingAPI20::UpdateNowPlaying(const Song &song) { diff --git a/src/scrobbler/scrobblingapi20.h b/src/scrobbler/scrobblingapi20.h index 588e95bd6..9748278e5 100644 --- a/src/scrobbler/scrobblingapi20.h +++ b/src/scrobbler/scrobblingapi20.h @@ -59,13 +59,14 @@ class ScrobblingAPI20 : public ScrobblerService { virtual ScrobblerCache *cache() = 0; bool IsEnabled() const { return enabled_; } + bool IsUseHTTPS() const { return https_; } bool IsAuthenticated() const { return !username_.isEmpty() && !session_key_.isEmpty(); } bool IsSubscriber() const { return subscriber_; } bool IsSubmitted() const { return submitted_; } void Submitted() { submitted_ = true; } QString username() const { return username_; } - void Authenticate(); + void Authenticate(const bool https = false); void Logout(); void UpdateNowPlaying(const Song &song); void Scrobble(const Song &song); @@ -143,6 +144,7 @@ class ScrobblingAPI20 : public ScrobblerService { Application *app_; bool enabled_; + bool https_; bool subscriber_; QString username_; diff --git a/src/settings/scrobblersettingspage.cpp b/src/settings/scrobblersettingspage.cpp index f88cdeb1b..46a46072d 100644 --- a/src/settings/scrobblersettingspage.cpp +++ b/src/settings/scrobblersettingspage.cpp @@ -85,6 +85,7 @@ void ScrobblerSettingsPage::Load() { ui_->spinbox_submit->setValue(scrobbler_->SubmitDelay()); ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->IsEnabled()); + ui_->checkbox_lastfm_https->setChecked(lastfmscrobbler_->IsUseHTTPS()); LastFM_RefreshControls(lastfmscrobbler_->IsAuthenticated()); ui_->checkbox_librefm_enable->setChecked(librefmscrobbler_->IsEnabled()); @@ -109,6 +110,7 @@ void ScrobblerSettingsPage::Save() { s.beginGroup(LastFMScrobbler::kSettingsGroup); s.setValue("enabled", ui_->checkbox_lastfm_enable->isChecked()); + s.setValue("https", ui_->checkbox_lastfm_https->isChecked()); s.endGroup(); s.beginGroup(LibreFMScrobbler::kSettingsGroup); @@ -128,7 +130,7 @@ void ScrobblerSettingsPage::LastFM_Login() { lastfm_waiting_for_auth_ = true; ui_->widget_lastfm_login_state->SetLoggedIn(LoginStateWidget::LoginInProgress); - lastfmscrobbler_->Authenticate(); + lastfmscrobbler_->Authenticate(ui_->checkbox_lastfm_https->isChecked()); } @@ -148,7 +150,7 @@ void ScrobblerSettingsPage::LastFM_AuthenticationComplete(const bool success, QS Save(); } else { - QMessageBox::warning(this, "Authentication failed", error); + if (!error.isEmpty()) QMessageBox::warning(this, "Authentication failed", error); } LastFM_RefreshControls(success); diff --git a/src/settings/scrobblersettingspage.ui b/src/settings/scrobblersettingspage.ui index eb0d4d723..24da14013 100644 --- a/src/settings/scrobblersettingspage.ui +++ b/src/settings/scrobblersettingspage.ui @@ -115,6 +115,13 @@ + + + + Use HTTPS for local redirectserver to bypass login problems + + +