local http server for nonwebengine oauth.

This commit is contained in:
Martin Rotter 2017-10-26 13:14:44 +02:00
parent 9d8bbe0410
commit 497a469a8b
15 changed files with 434 additions and 42 deletions

@ -1 +1 @@
Subproject commit 4a01edaec7d67d3b2ae81aeea2a3c876216fbab8
Subproject commit ae7084718c41afc01919779e58cd449e0eebd401

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -9,7 +9,6 @@
<file>graphics/rssguard_plain.png</file>
<file>graphics/misc/adblock.png</file>
<file>graphics/misc/adblock-disabled.png</file>
<file>graphics/misc/feedly.png</file>
<file>graphics/misc/gmail.png</file>
<file>graphics/misc/image-placeholder.png</file>
<file>graphics/misc/inoreader.png</file>

View File

@ -563,11 +563,13 @@ equals(USE_WEBENGINE, true) {
else {
HEADERS += src/gui/messagepreviewer.h \
src/gui/messagetextbrowser.h \
src/gui/newspaperpreviewer.h
src/gui/newspaperpreviewer.h \
src/network-web/oauthhttphandler.h
SOURCES += src/gui/messagepreviewer.cpp \
src/gui/messagetextbrowser.cpp \
src/gui/newspaperpreviewer.cpp
src/gui/newspaperpreviewer.cpp \
src/network-web/oauthhttphandler.cpp
FORMS += src/gui/messagepreviewer.ui \
src/gui/newspaperpreviewer.ui

View File

@ -15,6 +15,20 @@ class StyledItemDelegateWithoutFocus : public QStyledItemDelegate {
explicit StyledItemDelegateWithoutFocus(QObject* parent = 0);
virtual ~StyledItemDelegateWithoutFocus();
QSize sizeHint ( const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
QSize siz = QStyledItemDelegate::sizeHint(option, index);
/* QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
QStyle* style = widget ? widget->style() : QApplication::style();
return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), widget);*/
return siz;
}
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
};

View File

@ -26,10 +26,15 @@
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include "network-web/webfactory.h"
#include "services/inoreader/definitions.h"
#if defined(USE_WEBENGINE)
#include "gui/dialogs/oauthlogin.h"
#else
#include "network-web/oauthhttphandler.h"
Q_GLOBAL_STATIC(OAuthHttpHandler, qz_silent_acmanager)
#endif
#include <QDebug>
@ -38,21 +43,34 @@
#include <QNetworkReply>
#include <QNetworkRequest>
OAuth2Service::OAuth2Service(QString authUrl, QString tokenUrl, QString clientId,
QString clientSecret, QString scope, QObject* parent)
OAuth2Service::OAuth2Service(const QString& id_string, const QString& auth_url, const QString& token_url, const QString& client_id,
const QString& client_secret, const QString& scope, QObject* parent)
: QObject(parent), m_timerId(-1), m_tokensExpireIn(QDateTime()) {
if (id_string.isEmpty()) {
m_id = "somerandomstring";
}
else {
m_id = id_string;
}
m_redirectUrl = QSL(LOCALHOST_ADDRESS);
m_tokenGrantType = QSL("authorization_code");
m_tokenUrl = QUrl(tokenUrl);
m_authUrl = authUrl;
m_tokenUrl = QUrl(token_url);
m_authUrl = auth_url;
m_clientId = clientId;
m_clientSecret = clientSecret;
m_clientId = client_id;
m_clientSecret = client_secret;
m_scope = scope;
connect(&m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(tokenRequestFinished(QNetworkReply*)));
connect(this, &OAuth2Service::authCodeObtained, this, &OAuth2Service::retrieveAccessToken);
#if !defined(USE_WEBENGINE)
connect(handler(), &OAuthHttpHandler::authGranted, this, &OAuth2Service::retrieveAccessToken);
connect(handler(), &OAuthHttpHandler::authRejected, [this](const QString& error_description) {
emit authFailed();
});
#endif
}
QString OAuth2Service::bearer() {
@ -105,6 +123,21 @@ void OAuth2Service::timerEvent(QTimerEvent* event) {
QObject::timerEvent(event);
}
QString OAuth2Service::id() const {
return m_id;
}
void OAuth2Service::setId(const QString& id) {
m_id = id;
}
#if !defined(USE_WEBENGINE)
OAuthHttpHandler* OAuth2Service::handler() {
return qz_silent_acmanager();
}
#endif
void OAuth2Service::retrieveAccessToken(QString auth_code) {
QNetworkRequest networkRequest;
@ -155,7 +188,6 @@ void OAuth2Service::tokenRequestFinished(QNetworkReply* network_reply) {
QByteArray repl = network_reply->readAll();
QJsonDocument json_document = QJsonDocument::fromJson(repl);
QJsonObject root_obj = json_document.object();
auto cod = network_reply->error();
qDebug() << "Token response:" << json_document.toJson();
@ -276,15 +308,16 @@ void OAuth2Service::killRefreshTimer() {
void OAuth2Service::retrieveAuthCode() {
QString auth_url = m_authUrl + QString("?client_id=%1&scope=%2&"
"redirect_uri=%3&response_type=code&state=abcdef&"
"redirect_uri=%3&response_type=code&state=%4&"
"prompt=consent&access_type=offline").arg(m_clientId,
m_scope,
m_redirectUrl);
m_redirectUrl,
m_id);
#if defined(USE_WEBENGINE)
OAuthLogin login_page(qApp->mainFormWidget());
connect(&login_page, &OAuthLogin::authGranted, this, &OAuth2Service::authCodeObtained);
connect(&login_page, &OAuthLogin::authGranted, this, &OAuth2Service::retrieveAccessToken);
connect(&login_page, &OAuthLogin::authRejected, [this]() {
logout();
emit authFailed();
@ -295,7 +328,9 @@ void OAuth2Service::retrieveAuthCode() {
QSystemTrayIcon::MessageIcon::Information);
login_page.login(auth_url, m_redirectUrl);
#endif
#else
// TODO: For non-webengine version, user http-server and login via external browser.
// We run login URL in external browser, response is caught by light HTTP server.
qApp->web()->openUrlInExternalBrowser(auth_url);
#endif
}

View File

@ -29,12 +29,16 @@
#include "network-web/silentnetworkaccessmanager.h"
#if !defined(USE_WEBENGINE)
#include "network-web/oauthhttphandler.h"
#endif
class OAuth2Service : public QObject {
Q_OBJECT
public:
explicit OAuth2Service(QString authUrl, QString tokenUrl, QString clientId,
QString clientSecret, QString scope, QObject* parent = 0);
explicit OAuth2Service(const QString& id_string, const QString& auth_url, const QString& token_url,
const QString& client_id, const QString& client_secret, const QString& scope, QObject* parent = 0);
// Returns bearer HTTP header value.
// NOTE: Only call this if isFullyLoggedIn()
@ -65,6 +69,9 @@ class OAuth2Service : public QObject {
QString accessToken() const;
void setAccessToken(const QString& access_token);
QString id() const;
void setId(const QString& id);
signals:
void tokensReceived(QString access_token, QString refresh_token, int expires_in);
void tokensRetrieveError(QString error, QString error_description);
@ -72,9 +79,6 @@ class OAuth2Service : public QObject {
// User failed to authenticate or rejected it.
void authFailed();
// User enabled access.
void authCodeObtained(QString auth_code);
public slots:
void retrieveAuthCode();
void retrieveAccessToken(QString auth_code);
@ -100,6 +104,7 @@ class OAuth2Service : public QObject {
void timerEvent(QTimerEvent* event);
private:
QString m_id;
int m_timerId;
QDateTime m_tokensExpireIn;
QString m_accessToken;
@ -112,6 +117,12 @@ class OAuth2Service : public QObject {
QString m_authUrl;
QString m_scope;
SilentNetworkAccessManager m_networkManager;
#if !defined(USE_WEBENGINE)
// Returns pointer to global silent network manager
static OAuthHttpHandler* handler();
#endif
};
#endif // OAUTH2SERVICE_H

View File

@ -0,0 +1,276 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "network-web/oauthhttphandler.h"
#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include <cctype>
#include <QTcpSocket>
#include <QUrlQuery>
OAuthHttpHandler::OAuthHttpHandler(QObject* parent) : QObject(parent) {
m_text = tr("You can close this window now. Go back to %1").arg(APP_NAME);
connect(&m_httpServer, &QTcpServer::newConnection, this, &OAuthHttpHandler::clientConnected);
if (!m_httpServer.listen(m_listenAddress, 80)) {
qCritical("OAuth HTTP handler: Failed to start listening.");
}
}
OAuthHttpHandler::~OAuthHttpHandler() {
if (m_httpServer.isListening()) {
m_httpServer.close();
}
}
void OAuthHttpHandler::clientConnected() {
QTcpSocket* socket = m_httpServer.nextPendingConnection();
QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
QObject::connect(socket, &QTcpSocket::readyRead, [this, socket]() {
readReceivedData(socket);
});
}
void OAuthHttpHandler::handleRedirection(const QVariantMap& data) {
if (data.isEmpty()) {
return;
}
const QString error = data.value(QSL("error")).toString();
const QString code = data.value(QSL("code")).toString();
const QString received_state = data.value(QSL("state")).toString();
if (error.size()) {
const QString uri = data.value(QSL("error_uri")).toString();
const QString description = data.value(QSL("error_description")).toString();
qWarning("OAuth HTTP handler: AuthenticationError: %s(%s): %s", qPrintable(error), qPrintable(uri), qPrintable(description));
emit authRejected(description);
}
else if (code.isEmpty()) {
qWarning("OAuth HTTP handler: AuthenticationError: Code not received");
emit authRejected(QSL("AuthenticationError: Code not received"));
}
else if (received_state.isEmpty()) {
qWarning("OAuth HTTP handler: State not received");
emit authRejected(QSL("State not received"));
}
else {
emit authGranted(code);
}
}
void OAuthHttpHandler::answerClient(QTcpSocket* socket, const QUrl& url) {
if (!url.path().remove(QL1C('/')).isEmpty()) {
qWarning("OAuth HTTP handler: Invalid request: %s", qPrintable(url.toString()));
}
else {
QVariantMap received_data;
const QUrlQuery query(url.query());
const auto items = query.queryItems();
for (auto it = items.begin(), end = items.end(); it != end; ++it) {
received_data.insert(it->first, it->second);
}
handleRedirection(received_data);
const QByteArray html = QByteArrayLiteral("<html><head><title>") +
qApp->applicationName().toUtf8() +
QByteArrayLiteral("</title></head><body>") +
m_text.toUtf8() +
QByteArrayLiteral("</body></html>");
const QByteArray html_size = QString::number(html.size()).toUtf8();
const QByteArray reply_message = QByteArrayLiteral("HTTP/1.0 200 OK \r\n"
"Content-Type: text/html; "
"charset=\"utf-8\"\r\n"
"Content-Length: ") + html_size +
QByteArrayLiteral("\r\n\r\n") + html;
socket->write(reply_message);
}
socket->disconnectFromHost();
}
void OAuthHttpHandler::readReceivedData(QTcpSocket* socket) {
if (!m_connectedClients.contains(socket)) {
m_connectedClients[socket].m_port = m_httpServer.serverPort();
}
QHttpRequest* request = &m_connectedClients[socket];
bool error = false;
if (Q_LIKELY(request->m_state == QHttpRequest::State::ReadingMethod)) {
if (Q_UNLIKELY(error = !request->readMethod(socket))) {
qWarning("OAuth HTTP handler: Invalid dethod");
}
}
if (Q_LIKELY(!error && request->m_state == QHttpRequest::State::ReadingUrl)) {
if (Q_UNLIKELY(error = !request->readUrl(socket))) {
qWarning("OAuth HTTP handler: Invalid URL");
}
}
if (Q_LIKELY(!error && request->m_state == QHttpRequest::State::ReadingStatus)) {
if (Q_UNLIKELY(error = !request->readStatus(socket))) {
qWarning("OAuth HTTP handler: Invalid status");
}
}
if (Q_LIKELY(!error && request->m_state == QHttpRequest::State::ReadingHeader)) {
if (Q_UNLIKELY(error = !request->readHeader(socket))) {
qWarning("OAuth HTTP handler: Invalid header");
}
}
if (error) {
socket->disconnectFromHost();
m_connectedClients.remove(socket);
}
else if (!request->m_url.isEmpty()) {
Q_ASSERT(request->m_state != QHttpRequest::State::ReadingUrl);
answerClient(socket, request->m_url);
m_connectedClients.remove(socket);
}
}
bool OAuthHttpHandler::QHttpRequest::readMethod(QTcpSocket* socket) {
bool finished = false;
while (socket->bytesAvailable() && !finished) {
const auto c = socket->read(1).at(0);
if (std::isupper(c) && m_fragment.size() < 6) {
m_fragment += c;
}
else {
finished = true;
}
}
if (finished) {
if (m_fragment == "HEAD") {
m_method = Method::Head;
}
else if (m_fragment == "GET") {
m_method = Method::Get;
}
else if (m_fragment == "PUT") {
m_method = Method::Put;
}
else if (m_fragment == "POST") {
m_method = Method::Post;
}
else if (m_fragment == "DELETE") {
m_method = Method::Delete;
}
else {
qWarning("OAuth HTTP handler: Invalid operation %s", m_fragment.data());
}
m_state = State::ReadingUrl;
m_fragment.clear();
return m_method != Method::Unknown;
}
return true;
}
bool OAuthHttpHandler::QHttpRequest::readUrl(QTcpSocket* socket) {
bool finished = false;
while (socket->bytesAvailable() && !finished) {
const auto c = socket->read(1).at(0);
if (std::isspace(c)) {
finished = true;
}
else {
m_fragment += c;
}
}
if (finished) {
if (!m_fragment.startsWith("/")) {
qWarning("OAuth HTTP handler: Invalid URL path %s", m_fragment.constData());
return false;
}
m_url.setUrl(QStringLiteral("http://localhost:") + QString::number(m_port) + QString::fromUtf8(m_fragment));
m_state = State::ReadingStatus;
if (!m_url.isValid()) {
qWarning("OAuth HTTP handler: Invalid URL %s", m_fragment.constData());
return false;
}
m_fragment.clear();
return true;
}
return true;
}
bool OAuthHttpHandler::QHttpRequest::readStatus(QTcpSocket* socket) {
bool finished = false;
while (socket->bytesAvailable() && !finished) {
m_fragment += socket->read(1);
if (m_fragment.endsWith("\r\n")) {
finished = true;
m_fragment.resize(m_fragment.size() - 2);
}
}
if (finished) {
if (!std::isdigit(m_fragment.at(m_fragment.size() - 3)) || !std::isdigit(m_fragment.at(m_fragment.size() - 1))) {
qWarning("OAuth HTTP handler: Invalid version");
return false;
}
m_version = qMakePair(m_fragment.at(m_fragment.size() - 3) - '0', m_fragment.at(m_fragment.size() - 1) - '0');
m_state = State::ReadingHeader;
m_fragment.clear();
}
return true;
}
bool OAuthHttpHandler::QHttpRequest::readHeader(QTcpSocket* socket) {
while (socket->bytesAvailable()) {
m_fragment += socket->read(1);
if (m_fragment.endsWith("\r\n")) {
if (m_fragment == "\r\n") {
m_state = State::ReadingBody;
m_fragment.clear();
return true;
}
else {
m_fragment.chop(2);
const int index = m_fragment.indexOf(':');
if (index == -1) {
return false;
}
const QByteArray key = m_fragment.mid(0, index).trimmed();
const QByteArray value = m_fragment.mid(index + 1).trimmed();
m_headers.insert(key, value);
m_fragment.clear();
}
}
}
return false;
}

View File

@ -0,0 +1,69 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef OAUTHHTTPHANDLER_H
#define OAUTHHTTPHANDLER_H
#include <QObject>
#include <QTcpServer>
#include <QUrl>
class OAuthHttpHandler : public QObject {
Q_OBJECT
public:
explicit OAuthHttpHandler(QObject* parent = nullptr);
virtual ~OAuthHttpHandler();
signals:
void authRejected(const QString& error_description);
void authGranted(const QString& auth_code);
private slots:
void clientConnected();
private:
void handleRedirection(const QVariantMap& data);
void answerClient(QTcpSocket* socket, const QUrl& url);
void readReceivedData(QTcpSocket* socket);
private:
struct QHttpRequest {
bool readMethod(QTcpSocket* socket);
bool readUrl(QTcpSocket* socket);
bool readStatus(QTcpSocket* socket);
bool readHeader(QTcpSocket* socket);
enum class State {
ReadingMethod,
ReadingUrl,
ReadingStatus,
ReadingHeader,
ReadingBody,
AllDone
} m_state = State::ReadingMethod;
enum class Method {
Unknown,
Head,
Get,
Put,
Post,
Delete,
} m_method = Method::Unknown;
quint16 m_port = 0;
QByteArray m_fragment;
QUrl m_url;
QPair<quint8, quint8> m_version;
QMap<QByteArray, QByteArray> m_headers;
};
QMap<QTcpSocket*, QHttpRequest> m_connectedClients;
QTcpServer m_httpServer;
QHostAddress m_listenAddress = QHostAddress::LocalHost;
QString m_text;
};
#endif // OAUTHHTTPHANDLER_H

View File

@ -13,16 +13,9 @@
#include <QMessageBox>
ServiceRoot* GmailEntryPoint::createNewRoot() const {
#if defined(USE_WEBENGINE)
FormEditGmailAccount form_acc(qApp->mainFormWidget());
return form_acc.execForCreate();
#else
QMessageBox::warning(qApp->mainFormWidget(),
QObject::tr("Not supported"),
QObject::tr("This plugin is not supported in NonWebEngine variant of this program."));
return nullptr;
#endif
}
QList<ServiceRoot*> GmailEntryPoint::initializeSubtree() const {

View File

@ -10,7 +10,7 @@
#include "services/gmail/gmailserviceroot.h"
FormEditGmailAccount::FormEditGmailAccount(QWidget* parent) : QDialog(parent),
m_oauth(new OAuth2Service(GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL,
m_oauth(new OAuth2Service(QString(), GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL,
QString(), QString(), GMAIL_OAUTH_SCOPE)), m_editableRoot(nullptr) {
m_ui.setupUi(this);

View File

@ -26,7 +26,7 @@
GmailNetworkFactory::GmailNetworkFactory(QObject* parent) : QObject(parent),
m_service(nullptr), m_username(QString()), m_batchSize(GMAIL_DEFAULT_BATCH_SIZE),
m_oauth2(new OAuth2Service(GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL,
m_oauth2(new OAuth2Service(QString(), GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL,
QString(), QString(), GMAIL_OAUTH_SCOPE)) {
initializeOauth();
}
@ -328,7 +328,7 @@ void GmailNetworkFactory::markMessagesStarred(RootItem::Importance importance, c
void GmailNetworkFactory::onTokensError(const QString& error, const QString& error_description) {
Q_UNUSED(error)
qApp->showGuiMessage(tr("Inoreader: authentication error"),
qApp->showGuiMessage(tr("Gmail: authentication error"),
tr("Click this to login again. Error is: '%1'").arg(error_description),
QSystemTrayIcon::Critical,
nullptr, false,
@ -338,7 +338,7 @@ void GmailNetworkFactory::onTokensError(const QString& error, const QString& err
}
void GmailNetworkFactory::onAuthFailed() {
qApp->showGuiMessage(tr("Inoreader: authorization denied"),
qApp->showGuiMessage(tr("Gmail: authorization denied"),
tr("Click this to login again."),
QSystemTrayIcon::Critical,
nullptr, false,

View File

@ -10,7 +10,7 @@
#include "services/inoreader/inoreaderserviceroot.h"
FormEditInoreaderAccount::FormEditInoreaderAccount(QWidget* parent) : QDialog(parent),
m_oauth(new OAuth2Service(INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL,
m_oauth(new OAuth2Service(QString(), INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL,
INOREADER_OAUTH_CLI_ID, INOREADER_OAUTH_CLI_KEY, INOREADER_OAUTH_SCOPE)), m_editableRoot(nullptr) {
m_ui.setupUi(this);

View File

@ -14,16 +14,9 @@
#include <QMessageBox>
ServiceRoot* InoreaderEntryPoint::createNewRoot() const {
#if defined(USE_WEBENGINE)
FormEditInoreaderAccount form_acc(qApp->mainFormWidget());
return form_acc.execForCreate();
#else
QMessageBox::warning(qApp->mainFormWidget(),
QObject::tr("Not supported"),
QObject::tr("This plugin is not supported in NonWebEngine variant of this program."));
return nullptr;
#endif
}
QList<ServiceRoot*> InoreaderEntryPoint::initializeSubtree() const {

View File

@ -24,7 +24,7 @@
InoreaderNetworkFactory::InoreaderNetworkFactory(QObject* parent) : QObject(parent),
m_service(nullptr), m_username(QString()), m_batchSize(INOREADER_DEFAULT_BATCH_SIZE),
m_oauth2(new OAuth2Service(INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL,
m_oauth2(new OAuth2Service(QString(), INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL,
INOREADER_OAUTH_CLI_ID, INOREADER_OAUTH_CLI_KEY, INOREADER_OAUTH_SCOPE)) {
initializeOauth();
}