Add https support to localredirectserver

This commit is contained in:
Jonas Kvinge 2019-04-15 22:17:40 +02:00
parent e9bf04031b
commit 7f23b9b424
12 changed files with 290 additions and 66 deletions

View File

@ -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)

View File

@ -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 \

View File

@ -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}

View File

@ -283,4 +283,3 @@ void AlbumCoverFetcherSearch::Cancel() {
}
}

View File

@ -2,6 +2,7 @@
* This file was part of Clementine.
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2018-2019, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <QApplication>
#include <QBuffer>
#include <QFile>
#include <QRegExp>
#include <QStyle>
#include <QSslKey>
#include <QSslCertificate>
#include <QTcpServer>
#include <QAbstractSocket>
#include <QTcpSocket>
#include <QSslSocket>
#include <QSslConfiguration>
#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<uint64_t>(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<QSslError>)), this, SLOT(SSLErrors(QList<QSslError>)));
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<QSslError> &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<QByteArray> 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;
}

View File

@ -2,6 +2,7 @@
* This file was part of Clementine.
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2018-2019, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QByteArray>
#include <QObject>
#include <QSslCertificate>
#include <QSslKey>
#include <QTcpServer>
#include <QAbstractSocket>
#include <QTcpSocket>
#include <QSslSocket>
#include <QString>
#include <QUrl>
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<QSslError> &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

View File

@ -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);

View File

@ -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();

View File

@ -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?<br /><a href=\"%1\">%1</a><br />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) {

View File

@ -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_;

View File

@ -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);

View File

@ -115,6 +115,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_lastfm_https">
<property name="text">
<string>Use HTTPS for local redirectserver to bypass login problems</string>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="widget_lastfm_login_state" native="true"/>
</item>