NOISSUE bulk addition of code from Katabasis
This commit is contained in:
parent
2568752af5
commit
dd13368085
@ -278,6 +278,7 @@ add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions
|
|||||||
add_subdirectory(libraries/classparser) # google analytics library
|
add_subdirectory(libraries/classparser) # google analytics library
|
||||||
add_subdirectory(libraries/optional-bare)
|
add_subdirectory(libraries/optional-bare)
|
||||||
add_subdirectory(libraries/tomlc99) # toml parser
|
add_subdirectory(libraries/tomlc99) # toml parser
|
||||||
|
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
|
||||||
|
|
||||||
############################### Built Artifacts ###############################
|
############################### Built Artifacts ###############################
|
||||||
|
|
||||||
|
26
COPYING.md
26
COPYING.md
@ -276,3 +276,29 @@
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
# O2 (Katabasis fork)
|
||||||
|
|
||||||
|
Copyright (c) 2012, Akos Polster
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
9
api/logic/minecraft/auth-msa/BuildConfig.cpp.in
Normal file
9
api/logic/minecraft/auth-msa/BuildConfig.cpp.in
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#include "BuildConfig.h"
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
const Config BuildConfig;
|
||||||
|
|
||||||
|
Config::Config()
|
||||||
|
{
|
||||||
|
CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
|
||||||
|
}
|
11
api/logic/minecraft/auth-msa/BuildConfig.h
Normal file
11
api/logic/minecraft/auth-msa/BuildConfig.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class Config
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Config();
|
||||||
|
QString CLIENT_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const Config BuildConfig;
|
28
api/logic/minecraft/auth-msa/CMakeLists.txt
Normal file
28
api/logic/minecraft/auth-msa/CMakeLists.txt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
find_package(Qt5 COMPONENTS Core Gui Network Widgets REQUIRED)
|
||||||
|
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
||||||
|
|
||||||
|
|
||||||
|
set(MOJANGDEMO_CLIENT_ID "" CACHE STRING "Client ID used for OAuth2 in mojangdemo")
|
||||||
|
|
||||||
|
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp")
|
||||||
|
|
||||||
|
set(mojang_SRCS
|
||||||
|
main.cpp
|
||||||
|
context.cpp
|
||||||
|
context.h
|
||||||
|
|
||||||
|
mainwindow.cpp
|
||||||
|
mainwindow.h
|
||||||
|
mainwindow.ui
|
||||||
|
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp
|
||||||
|
BuildConfig.h
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable( mojangdemo ${mojang_SRCS} )
|
||||||
|
target_link_libraries( mojangdemo Katabasis Qt5::Gui Qt5::Widgets )
|
||||||
|
target_include_directories(mojangdemo PRIVATE logic)
|
938
api/logic/minecraft/auth-msa/context.cpp
Normal file
938
api/logic/minecraft/auth-msa/context.cpp
Normal file
@ -0,0 +1,938 @@
|
|||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QMetaEnum>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
#include "context.h"
|
||||||
|
#include "katabasis/Globals.h"
|
||||||
|
#include "katabasis/StoreQSettings.h"
|
||||||
|
#include "katabasis/Requestor.h"
|
||||||
|
#include "BuildConfig.h"
|
||||||
|
|
||||||
|
using OAuth2 = Katabasis::OAuth2;
|
||||||
|
using Requestor = Katabasis::Requestor;
|
||||||
|
using Activity = Katabasis::Activity;
|
||||||
|
|
||||||
|
Context::Context(QObject *parent) :
|
||||||
|
QObject(parent)
|
||||||
|
{
|
||||||
|
mgr = new QNetworkAccessManager(this);
|
||||||
|
|
||||||
|
Katabasis::OAuth2::Options opts;
|
||||||
|
opts.scope = "XboxLive.signin offline_access";
|
||||||
|
opts.clientIdentifier = BuildConfig.CLIENT_ID;
|
||||||
|
opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
|
||||||
|
opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
|
||||||
|
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
|
||||||
|
|
||||||
|
oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr);
|
||||||
|
|
||||||
|
connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed);
|
||||||
|
connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded);
|
||||||
|
connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser);
|
||||||
|
connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser);
|
||||||
|
connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::beginActivity(Activity activity) {
|
||||||
|
if(isBusy()) {
|
||||||
|
throw 0;
|
||||||
|
}
|
||||||
|
activity_ = activity;
|
||||||
|
emit activityChanged(activity_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::finishActivity() {
|
||||||
|
if(!isBusy()) {
|
||||||
|
throw 0;
|
||||||
|
}
|
||||||
|
activity_ = Katabasis::Activity::Idle;
|
||||||
|
m_account.validity_ = m_account.minecraftProfile.validity;
|
||||||
|
emit activityChanged(activity_);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Context::gameToken() {
|
||||||
|
return m_account.minecraftToken.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Context::userId() {
|
||||||
|
return m_account.minecraftProfile.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Context::userName() {
|
||||||
|
return m_account.minecraftProfile.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Context::silentSignIn() {
|
||||||
|
if(isBusy()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
beginActivity(Activity::Refreshing);
|
||||||
|
if(!oauth2->refresh()) {
|
||||||
|
finishActivity();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestsDone = 0;
|
||||||
|
xboxProfileSucceeded = false;
|
||||||
|
mcAuthSucceeded = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Context::signIn() {
|
||||||
|
if(isBusy()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestsDone = 0;
|
||||||
|
xboxProfileSucceeded = false;
|
||||||
|
mcAuthSucceeded = false;
|
||||||
|
|
||||||
|
beginActivity(Activity::LoggingIn);
|
||||||
|
oauth2->unlink();
|
||||||
|
m_account = AccountData();
|
||||||
|
oauth2->link();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Context::signOut() {
|
||||||
|
if(isBusy()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
beginActivity(Activity::LoggingOut);
|
||||||
|
oauth2->unlink();
|
||||||
|
m_account = AccountData();
|
||||||
|
finishActivity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Context::onOpenBrowser(const QUrl &url) {
|
||||||
|
QDesktopServices::openUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onCloseBrowser() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onLinkingFailed() {
|
||||||
|
finishActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onLinkingSucceeded() {
|
||||||
|
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
||||||
|
if (!o2t->linked()) {
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantMap extraTokens = o2t->extraTokens();
|
||||||
|
if (!extraTokens.isEmpty()) {
|
||||||
|
qDebug() << "Extra tokens in response:";
|
||||||
|
foreach (QString key, extraTokens.keys()) {
|
||||||
|
qDebug() << "\t" << key << ":" << extraTokens.value(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doUserAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onOAuthActivityChanged(Katabasis::Activity activity) {
|
||||||
|
// respond to activity change here
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::doUserAuth() {
|
||||||
|
QString xbox_auth_template = R"XXX(
|
||||||
|
{
|
||||||
|
"Properties": {
|
||||||
|
"AuthMethod": "RPS",
|
||||||
|
"SiteName": "user.auth.xboxlive.com",
|
||||||
|
"RpsTicket": "d=%1"
|
||||||
|
},
|
||||||
|
"RelyingParty": "http://auth.xboxlive.com",
|
||||||
|
"TokenType": "JWT"
|
||||||
|
}
|
||||||
|
)XXX";
|
||||||
|
auto xbox_auth_data = xbox_auth_template.arg(m_account.msaToken.token);
|
||||||
|
|
||||||
|
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setRawHeader("Accept", "application/json");
|
||||||
|
auto *requestor = new Katabasis::Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone);
|
||||||
|
requestor->post(request, xbox_auth_data.toUtf8());
|
||||||
|
qDebug() << "First layer of XBox auth ... commencing.";
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool getDateTime(QJsonValue value, QDateTime & out) {
|
||||||
|
if(!value.isString()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = QDateTime::fromString(value.toString(), Qt::ISODateWithMs);
|
||||||
|
return out.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getString(QJsonValue value, QString & out) {
|
||||||
|
if(!value.isString()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = value.toString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getNumber(QJsonValue value, double & out) {
|
||||||
|
if(!value.isDouble()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = value.toDouble();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"IssueInstant":"2020-12-07T19:52:08.4463796Z",
|
||||||
|
"NotAfter":"2020-12-21T19:52:08.4463796Z",
|
||||||
|
"Token":"token",
|
||||||
|
"DisplayClaims":{
|
||||||
|
"xui":[
|
||||||
|
{
|
||||||
|
"uhs":"userhash"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// TODO: handle error responses ...
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"Identity":"0",
|
||||||
|
"XErr":2148916238,
|
||||||
|
"Message":"",
|
||||||
|
"Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
|
||||||
|
}
|
||||||
|
// 2148916233 = missing XBox account
|
||||||
|
// 2148916238 = child account not linked to a family
|
||||||
|
*/
|
||||||
|
|
||||||
|
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) {
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||||
|
if(jsonError.error) {
|
||||||
|
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = doc.object();
|
||||||
|
if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) {
|
||||||
|
qWarning() << "User IssueInstant is not a timestamp";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!getDateTime(obj.value("NotAfter"), output.notAfter)) {
|
||||||
|
qWarning() << "User NotAfter is not a timestamp";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!getString(obj.value("Token"), output.token)) {
|
||||||
|
qWarning() << "User Token is not a timestamp";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto arrayVal = obj.value("DisplayClaims").toObject().value("xui");
|
||||||
|
if(!arrayVal.isArray()) {
|
||||||
|
qWarning() << "Missing xui claims array";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool foundUHS = false;
|
||||||
|
for(auto item: arrayVal.toArray()) {
|
||||||
|
if(!item.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto obj = item.toObject();
|
||||||
|
if(obj.contains("uhs")) {
|
||||||
|
foundUHS = true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// consume all 'display claims' ... whatever that means
|
||||||
|
for(auto iter = obj.begin(); iter != obj.end(); iter++) {
|
||||||
|
QString claim;
|
||||||
|
if(!getString(obj.value(iter.key()), claim)) {
|
||||||
|
qWarning() << "display claim " << iter.key() << " is not a string...";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
output.extra[iter.key()] = claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!foundUHS) {
|
||||||
|
qWarning() << "Missing uhs";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
output.validity = Katabasis::Validity::Certain;
|
||||||
|
qDebug() << data;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onUserAuthDone(
|
||||||
|
int requestId,
|
||||||
|
QNetworkReply::NetworkError error,
|
||||||
|
QByteArray replyData,
|
||||||
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
|
) {
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
qWarning() << "Reply error:" << error;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Katabasis::Token temp;
|
||||||
|
if(!parseXTokenResponse(replyData, temp)) {
|
||||||
|
qWarning() << "Could not parse user authentication response...";
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_account.userToken = temp;
|
||||||
|
|
||||||
|
doSTSAuthMinecraft();
|
||||||
|
doSTSAuthGeneric();
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
url = "https://xsts.auth.xboxlive.com/xsts/authorize"
|
||||||
|
headers = {"x-xbl-contract-version": "1"}
|
||||||
|
data = {
|
||||||
|
"RelyingParty": relying_party,
|
||||||
|
"TokenType": "JWT",
|
||||||
|
"Properties": {
|
||||||
|
"UserTokens": [self.user_token.token],
|
||||||
|
"SandboxId": "RETAIL",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
void Context::doSTSAuthMinecraft() {
|
||||||
|
QString xbox_auth_template = R"XXX(
|
||||||
|
{
|
||||||
|
"Properties": {
|
||||||
|
"SandboxId": "RETAIL",
|
||||||
|
"UserTokens": [
|
||||||
|
"%1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||||
|
"TokenType": "JWT"
|
||||||
|
}
|
||||||
|
)XXX";
|
||||||
|
auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
|
||||||
|
|
||||||
|
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setRawHeader("Accept", "application/json");
|
||||||
|
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone);
|
||||||
|
requestor->post(request, xbox_auth_data.toUtf8());
|
||||||
|
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onSTSAuthMinecraftDone(
|
||||||
|
int requestId,
|
||||||
|
QNetworkReply::NetworkError error,
|
||||||
|
QByteArray replyData,
|
||||||
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
|
) {
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
qWarning() << "Reply error:" << error;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Katabasis::Token temp;
|
||||||
|
if(!parseXTokenResponse(replyData, temp)) {
|
||||||
|
qWarning() << "Could not parse authorization response for access to mojang services...";
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
|
||||||
|
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||||
|
qDebug() << replyData;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_account.mojangservicesToken = temp;
|
||||||
|
|
||||||
|
doMinecraftAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::doSTSAuthGeneric() {
|
||||||
|
QString xbox_auth_template = R"XXX(
|
||||||
|
{
|
||||||
|
"Properties": {
|
||||||
|
"SandboxId": "RETAIL",
|
||||||
|
"UserTokens": [
|
||||||
|
"%1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RelyingParty": "http://xboxlive.com",
|
||||||
|
"TokenType": "JWT"
|
||||||
|
}
|
||||||
|
)XXX";
|
||||||
|
auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
|
||||||
|
|
||||||
|
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setRawHeader("Accept", "application/json");
|
||||||
|
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone);
|
||||||
|
requestor->post(request, xbox_auth_data.toUtf8());
|
||||||
|
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onSTSAuthGenericDone(
|
||||||
|
int requestId,
|
||||||
|
QNetworkReply::NetworkError error,
|
||||||
|
QByteArray replyData,
|
||||||
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
|
) {
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
qWarning() << "Reply error:" << error;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Katabasis::Token temp;
|
||||||
|
if(!parseXTokenResponse(replyData, temp)) {
|
||||||
|
qWarning() << "Could not parse authorization response for access to xbox API...";
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
|
||||||
|
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||||
|
qDebug() << replyData;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_account.xboxApiToken = temp;
|
||||||
|
|
||||||
|
doXBoxProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Context::doMinecraftAuth() {
|
||||||
|
QString mc_auth_template = R"XXX(
|
||||||
|
{
|
||||||
|
"identityToken": "XBL3.0 x=%1;%2"
|
||||||
|
}
|
||||||
|
)XXX";
|
||||||
|
auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.mojangservicesToken.token);
|
||||||
|
|
||||||
|
QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox"));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setRawHeader("Accept", "application/json");
|
||||||
|
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone);
|
||||||
|
requestor->post(request, data.toUtf8());
|
||||||
|
qDebug() << "Getting Minecraft access token...";
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||||
|
if(jsonError.error) {
|
||||||
|
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = doc.object();
|
||||||
|
double expires_in = 0;
|
||||||
|
if(!getNumber(obj.value("expires_in"), expires_in)) {
|
||||||
|
qWarning() << "expires_in is not a valid number";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto currentTime = QDateTime::currentDateTimeUtc();
|
||||||
|
output.issueInstant = currentTime;
|
||||||
|
output.notAfter = currentTime.addSecs(expires_in);
|
||||||
|
|
||||||
|
QString username;
|
||||||
|
if(!getString(obj.value("username"), username)) {
|
||||||
|
qWarning() << "username is not valid";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: it's a JWT... validate it?
|
||||||
|
if(!getString(obj.value("access_token"), output.token)) {
|
||||||
|
qWarning() << "access_token is not valid";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
output.validity = Katabasis::Validity::Certain;
|
||||||
|
qDebug() << data;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onMinecraftAuthDone(
|
||||||
|
int requestId,
|
||||||
|
QNetworkReply::NetworkError error,
|
||||||
|
QByteArray replyData,
|
||||||
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
|
) {
|
||||||
|
requestsDone++;
|
||||||
|
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
qWarning() << "Reply error:" << error;
|
||||||
|
qDebug() << replyData;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!parseMojangResponse(replyData, m_account.minecraftToken)) {
|
||||||
|
qWarning() << "Could not parse login_with_xbox response...";
|
||||||
|
qDebug() << replyData;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mcAuthSucceeded = true;
|
||||||
|
|
||||||
|
checkResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::doXBoxProfile() {
|
||||||
|
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
|
||||||
|
QUrlQuery q;
|
||||||
|
q.addQueryItem(
|
||||||
|
"settings",
|
||||||
|
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
|
||||||
|
"PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
|
||||||
|
"UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
|
||||||
|
"PreferredColor,Location,Bio,Watermarks,"
|
||||||
|
"RealName,RealNameOverride,IsQuarantined"
|
||||||
|
);
|
||||||
|
url.setQuery(q);
|
||||||
|
|
||||||
|
QNetworkRequest request = QNetworkRequest(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setRawHeader("Accept", "application/json");
|
||||||
|
request.setRawHeader("x-xbl-contract-version", "3");
|
||||||
|
request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8());
|
||||||
|
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone);
|
||||||
|
requestor->get(request);
|
||||||
|
qDebug() << "Getting Xbox profile...";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onXBoxProfileDone(
|
||||||
|
int requestId,
|
||||||
|
QNetworkReply::NetworkError error,
|
||||||
|
QByteArray replyData,
|
||||||
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
|
) {
|
||||||
|
requestsDone ++;
|
||||||
|
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
qWarning() << "Reply error:" << error;
|
||||||
|
qDebug() << replyData;
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "XBox profile: " << replyData;
|
||||||
|
|
||||||
|
xboxProfileSucceeded = true;
|
||||||
|
checkResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::checkResult() {
|
||||||
|
if(requestsDone != 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(mcAuthSucceeded && xboxProfileSucceeded) {
|
||||||
|
doMinecraftProfile();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
finishActivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||||
|
if(jsonError.error) {
|
||||||
|
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = doc.object();
|
||||||
|
if(!getString(obj.value("id"), output.id)) {
|
||||||
|
qWarning() << "minecraft profile id is not a string";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!getString(obj.value("name"), output.name)) {
|
||||||
|
qWarning() << "minecraft profile name is not a string";
|
||||||
|
qDebug() << data;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto skinsArray = obj.value("skins").toArray();
|
||||||
|
for(auto skin: skinsArray) {
|
||||||
|
auto skinObj = skin.toObject();
|
||||||
|
Skin skinOut;
|
||||||
|
if(!getString(skinObj.value("id"), skinOut.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString state;
|
||||||
|
if(!getString(skinObj.value("state"), state)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(state != "ACTIVE") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!getString(skinObj.value("url"), skinOut.url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!getString(skinObj.value("variant"), skinOut.variant)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// we deal with only the active skin
|
||||||
|
output.skin = skinOut;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto capesArray = obj.value("capes").toArray();
|
||||||
|
int i = -1;
|
||||||
|
int currentCape = -1;
|
||||||
|
for(auto cape: capesArray) {
|
||||||
|
i++;
|
||||||
|
auto capeObj = cape.toObject();
|
||||||
|
Cape capeOut;
|
||||||
|
if(!getString(capeObj.value("id"), capeOut.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString state;
|
||||||
|
if(!getString(capeObj.value("state"), state)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(state == "ACTIVE") {
|
||||||
|
currentCape = i;
|
||||||
|
}
|
||||||
|
if(!getString(capeObj.value("url"), capeOut.url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!getString(capeObj.value("alias"), capeOut.alias)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we deal with only the active skin
|
||||||
|
output.capes.push_back(capeOut);
|
||||||
|
}
|
||||||
|
output.currentCape = currentCape;
|
||||||
|
output.validity = Katabasis::Validity::Certain;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::doMinecraftProfile() {
|
||||||
|
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
|
||||||
|
QNetworkRequest request = QNetworkRequest(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
// request.setRawHeader("Accept", "application/json");
|
||||||
|
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_account.minecraftToken.token).toUtf8());
|
||||||
|
|
||||||
|
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone);
|
||||||
|
requestor->get(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
|
||||||
|
qDebug() << data;
|
||||||
|
if (error == QNetworkReply::ContentNotFoundError) {
|
||||||
|
m_account.minecraftProfile = MinecraftProfile();
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!parseMinecraftProfile(data, m_account.minecraftProfile)) {
|
||||||
|
m_account.minecraftProfile = MinecraftProfile();
|
||||||
|
finishActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doGetSkin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::doGetSkin() {
|
||||||
|
auto url = QUrl(m_account.minecraftProfile.skin.url);
|
||||||
|
QNetworkRequest request = QNetworkRequest(url);
|
||||||
|
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||||
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
connect(requestor, &Requestor::finished, this, &Context::onSkinDone);
|
||||||
|
requestor->get(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
|
||||||
|
if (error == QNetworkReply::NoError) {
|
||||||
|
m_account.minecraftProfile.skin.data = data;
|
||||||
|
}
|
||||||
|
finishActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
|
||||||
|
if(t.validity == Katabasis::Validity::None || !t.persistent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonObject out;
|
||||||
|
if(t.issueInstant.isValid()) {
|
||||||
|
out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(t.notAfter.isValid()) {
|
||||||
|
out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!t.token.isEmpty()) {
|
||||||
|
out["token"] = QJsonValue(t.token);
|
||||||
|
}
|
||||||
|
if(!t.refresh_token.isEmpty()) {
|
||||||
|
out["refresh_token"] = QJsonValue(t.refresh_token);
|
||||||
|
}
|
||||||
|
if(t.extra.size()) {
|
||||||
|
out["extra"] = QJsonObject::fromVariantMap(t.extra);
|
||||||
|
}
|
||||||
|
if(out.size()) {
|
||||||
|
parent[tokenName] = out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) {
|
||||||
|
Katabasis::Token out;
|
||||||
|
auto tokenObject = parent.value(tokenName).toObject();
|
||||||
|
if(tokenObject.isEmpty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
auto issueInstant = tokenObject.value("iat");
|
||||||
|
if(issueInstant.isDouble()) {
|
||||||
|
out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto notAfter = tokenObject.value("exp");
|
||||||
|
if(notAfter.isDouble()) {
|
||||||
|
out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto token = tokenObject.value("token");
|
||||||
|
if(token.isString()) {
|
||||||
|
out.token = token.toString();
|
||||||
|
out.validity = Katabasis::Validity::Assumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto refresh_token = tokenObject.value("refresh_token");
|
||||||
|
if(refresh_token.isString()) {
|
||||||
|
out.refresh_token = refresh_token.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extra = tokenObject.value("extra");
|
||||||
|
if(extra.isObject()) {
|
||||||
|
out.extra = extra.toObject().toVariantMap();
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
|
||||||
|
if(p.id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonObject out;
|
||||||
|
out["id"] = QJsonValue(p.id);
|
||||||
|
out["name"] = QJsonValue(p.name);
|
||||||
|
if(p.currentCape != -1) {
|
||||||
|
out["cape"] = p.capes[p.currentCape].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QJsonObject skinObj;
|
||||||
|
skinObj["id"] = p.skin.id;
|
||||||
|
skinObj["url"] = p.skin.url;
|
||||||
|
skinObj["variant"] = p.skin.variant;
|
||||||
|
if(p.skin.data.size()) {
|
||||||
|
skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
|
||||||
|
}
|
||||||
|
out["skin"] = skinObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray capesArray;
|
||||||
|
for(auto & cape: p.capes) {
|
||||||
|
QJsonObject capeObj;
|
||||||
|
capeObj["id"] = cape.id;
|
||||||
|
capeObj["url"] = cape.url;
|
||||||
|
capeObj["alias"] = cape.alias;
|
||||||
|
if(cape.data.size()) {
|
||||||
|
capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
|
||||||
|
}
|
||||||
|
capesArray.push_back(capeObj);
|
||||||
|
}
|
||||||
|
out["capes"] = capesArray;
|
||||||
|
parent[tokenName] = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) {
|
||||||
|
MinecraftProfile out;
|
||||||
|
auto tokenObject = parent.value(tokenName).toObject();
|
||||||
|
if(tokenObject.isEmpty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto idV = tokenObject.value("id");
|
||||||
|
auto nameV = tokenObject.value("name");
|
||||||
|
if(!idV.isString() || !nameV.isString()) {
|
||||||
|
qWarning() << "mandatory profile attributes are missing or of unexpected type";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
out.name = nameV.toString();
|
||||||
|
out.id = idV.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto skinV = tokenObject.value("skin");
|
||||||
|
if(!skinV.isObject()) {
|
||||||
|
qWarning() << "skin is missing";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
auto skinObj = skinV.toObject();
|
||||||
|
auto idV = skinObj.value("id");
|
||||||
|
auto urlV = skinObj.value("url");
|
||||||
|
auto variantV = skinObj.value("variant");
|
||||||
|
if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
|
||||||
|
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
out.skin.id = idV.toString();
|
||||||
|
out.skin.url = urlV.toString();
|
||||||
|
out.skin.variant = variantV.toString();
|
||||||
|
|
||||||
|
// data for skin is optional
|
||||||
|
auto dataV = skinObj.value("data");
|
||||||
|
if(dataV.isString()) {
|
||||||
|
// TODO: validate base64
|
||||||
|
out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
||||||
|
}
|
||||||
|
else if (!dataV.isUndefined()) {
|
||||||
|
qWarning() << "skin data is something unexpected";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto capesV = tokenObject.value("capes");
|
||||||
|
if(!capesV.isArray()) {
|
||||||
|
qWarning() << "capes is not an array!";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
auto capesArray = capesV.toArray();
|
||||||
|
for(auto capeV: capesArray) {
|
||||||
|
if(!capeV.isObject()) {
|
||||||
|
qWarning() << "cape is not an object!";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
auto capeObj = capeV.toObject();
|
||||||
|
auto idV = capeObj.value("id");
|
||||||
|
auto urlV = capeObj.value("url");
|
||||||
|
auto aliasV = capeObj.value("alias");
|
||||||
|
if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
|
||||||
|
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
Cape cape;
|
||||||
|
cape.id = idV.toString();
|
||||||
|
cape.url = urlV.toString();
|
||||||
|
cape.alias = aliasV.toString();
|
||||||
|
|
||||||
|
// data for cape is optional.
|
||||||
|
auto dataV = capeObj.value("data");
|
||||||
|
if(dataV.isString()) {
|
||||||
|
// TODO: validate base64
|
||||||
|
cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
||||||
|
}
|
||||||
|
else if (!dataV.isUndefined()) {
|
||||||
|
qWarning() << "cape data is something unexpected";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
out.capes.push_back(cape);
|
||||||
|
}
|
||||||
|
out.validity = Katabasis::Validity::Assumed;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Context::resumeFromState(QByteArray data) {
|
||||||
|
QJsonParseError error;
|
||||||
|
auto doc = QJsonDocument::fromJson(data, &error);
|
||||||
|
if(error.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "Failed to parse account data as JSON.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto docObject = doc.object();
|
||||||
|
m_account.msaToken = tokenFromJSON(docObject, "msa");
|
||||||
|
m_account.userToken = tokenFromJSON(docObject, "utoken");
|
||||||
|
m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main");
|
||||||
|
m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc");
|
||||||
|
m_account.minecraftToken = tokenFromJSON(docObject, "ygg");
|
||||||
|
|
||||||
|
m_account.minecraftProfile = profileFromJSON(docObject, "profile");
|
||||||
|
|
||||||
|
m_account.validity_ = m_account.minecraftProfile.validity;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Context::saveState() {
|
||||||
|
QJsonDocument doc;
|
||||||
|
QJsonObject output;
|
||||||
|
tokenToJSON(output, m_account.msaToken, "msa");
|
||||||
|
tokenToJSON(output, m_account.userToken, "utoken");
|
||||||
|
tokenToJSON(output, m_account.xboxApiToken, "xrp-main");
|
||||||
|
tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc");
|
||||||
|
tokenToJSON(output, m_account.minecraftToken, "ygg");
|
||||||
|
profileToJSON(output, m_account.minecraftProfile, "profile");
|
||||||
|
doc.setObject(output);
|
||||||
|
return doc.toJson(QJsonDocument::Indented);
|
||||||
|
}
|
128
api/logic/minecraft/auth-msa/context.h
Normal file
128
api/logic/minecraft/auth-msa/context.h
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
#include <katabasis/OAuth2.h>
|
||||||
|
|
||||||
|
struct Skin {
|
||||||
|
QString id;
|
||||||
|
QString url;
|
||||||
|
QString variant;
|
||||||
|
|
||||||
|
QByteArray data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cape {
|
||||||
|
QString id;
|
||||||
|
QString url;
|
||||||
|
QString alias;
|
||||||
|
|
||||||
|
QByteArray data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MinecraftProfile {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
Skin skin;
|
||||||
|
int currentCape = -1;
|
||||||
|
QVector<Cape> capes;
|
||||||
|
Katabasis::Validity validity = Katabasis::Validity::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AccountType {
|
||||||
|
MSA,
|
||||||
|
Mojang
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AccountData {
|
||||||
|
AccountType type = AccountType::MSA;
|
||||||
|
|
||||||
|
Katabasis::Token msaToken;
|
||||||
|
Katabasis::Token userToken;
|
||||||
|
Katabasis::Token xboxApiToken;
|
||||||
|
Katabasis::Token mojangservicesToken;
|
||||||
|
Katabasis::Token minecraftToken;
|
||||||
|
|
||||||
|
MinecraftProfile minecraftProfile;
|
||||||
|
Katabasis::Validity validity_ = Katabasis::Validity::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Context : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Context(QObject *parent = 0);
|
||||||
|
|
||||||
|
QByteArray saveState();
|
||||||
|
bool resumeFromState(QByteArray data);
|
||||||
|
|
||||||
|
bool isBusy() {
|
||||||
|
return activity_ != Katabasis::Activity::Idle;
|
||||||
|
};
|
||||||
|
Katabasis::Validity validity() {
|
||||||
|
return m_account.validity_;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool signIn();
|
||||||
|
bool silentSignIn();
|
||||||
|
bool signOut();
|
||||||
|
|
||||||
|
QString userName();
|
||||||
|
QString userId();
|
||||||
|
QString gameToken();
|
||||||
|
signals:
|
||||||
|
void succeeded();
|
||||||
|
void failed();
|
||||||
|
void activityChanged(Katabasis::Activity activity);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onLinkingSucceeded();
|
||||||
|
void onLinkingFailed();
|
||||||
|
void onOpenBrowser(const QUrl &url);
|
||||||
|
void onCloseBrowser();
|
||||||
|
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void doUserAuth();
|
||||||
|
Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
|
||||||
|
void doSTSAuthMinecraft();
|
||||||
|
Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
void doMinecraftAuth();
|
||||||
|
Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
|
||||||
|
void doSTSAuthGeneric();
|
||||||
|
Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
void doXBoxProfile();
|
||||||
|
Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
|
||||||
|
void doMinecraftProfile();
|
||||||
|
Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
|
||||||
|
void doGetSkin();
|
||||||
|
Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
|
||||||
|
void checkResult();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void beginActivity(Katabasis::Activity activity);
|
||||||
|
void finishActivity();
|
||||||
|
void clearTokens();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Katabasis::OAuth2 *oauth2 = nullptr;
|
||||||
|
|
||||||
|
int requestsDone = 0;
|
||||||
|
bool xboxProfileSucceeded = false;
|
||||||
|
bool mcAuthSucceeded = false;
|
||||||
|
Katabasis::Activity activity_ = Katabasis::Activity::Idle;
|
||||||
|
|
||||||
|
AccountData m_account;
|
||||||
|
|
||||||
|
QNetworkAccessManager *mgr = nullptr;
|
||||||
|
};
|
100
api/logic/minecraft/auth-msa/main.cpp
Normal file
100
api/logic/minecraft/auth-msa/main.cpp
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
#include <QApplication>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QSaveFile>
|
||||||
|
|
||||||
|
#include "context.h"
|
||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
|
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||||
|
{
|
||||||
|
QByteArray localMsg = msg.toLocal8Bit();
|
||||||
|
const char *file = context.file ? context.file : "";
|
||||||
|
const char *function = context.function ? context.function : "";
|
||||||
|
switch (type) {
|
||||||
|
case QtDebugMsg:
|
||||||
|
fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||||
|
break;
|
||||||
|
case QtInfoMsg:
|
||||||
|
fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||||
|
break;
|
||||||
|
case QtWarningMsg:
|
||||||
|
fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||||
|
break;
|
||||||
|
case QtCriticalMsg:
|
||||||
|
fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||||
|
break;
|
||||||
|
case QtFatalMsg:
|
||||||
|
fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Helper : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Helper(Context * context) : QObject(), context_(context), msg_(QString()) {
|
||||||
|
QFile tokenCache("usercache.dat");
|
||||||
|
if(tokenCache.open(QIODevice::ReadOnly)) {
|
||||||
|
context_->resumeFromState(tokenCache.readAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void run() {
|
||||||
|
connect(context_, &Context::activityChanged, this, &Helper::onActivityChanged);
|
||||||
|
context_->silentSignIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFailed() {
|
||||||
|
qDebug() << "Login failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onActivityChanged(Katabasis::Activity activity) {
|
||||||
|
if(activity == Katabasis::Activity::Idle) {
|
||||||
|
switch(context_->validity()) {
|
||||||
|
case Katabasis::Validity::None: {
|
||||||
|
// account is gone, remove it.
|
||||||
|
QFile::remove("usercache.dat");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Validity::Assumed: {
|
||||||
|
// this is basically a soft-failed refresh. do nothing.
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Validity::Certain: {
|
||||||
|
// stuff got refreshed / signed in. Save.
|
||||||
|
auto data = context_->saveState();
|
||||||
|
QSaveFile tokenCache("usercache.dat");
|
||||||
|
if(tokenCache.open(QIODevice::WriteOnly)) {
|
||||||
|
tokenCache.write(context_->saveState());
|
||||||
|
tokenCache.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Context *context_;
|
||||||
|
QString msg_;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
qInstallMessageHandler(myMessageOutput);
|
||||||
|
QApplication a(argc, argv);
|
||||||
|
QCoreApplication::setOrganizationName("MultiMC");
|
||||||
|
QCoreApplication::setApplicationName("MultiMC");
|
||||||
|
Context c;
|
||||||
|
Helper helper(&c);
|
||||||
|
MainWindow window(&c);
|
||||||
|
window.show();
|
||||||
|
QTimer::singleShot(0, &helper, &Helper::run);
|
||||||
|
return a.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "main.moc"
|
97
api/logic/minecraft/auth-msa/mainwindow.cpp
Normal file
97
api/logic/minecraft/auth-msa/mainwindow.cpp
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
#include "mainwindow.h"
|
||||||
|
#include "ui_mainwindow.h"
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
|
||||||
|
#include "BuildConfig.h"
|
||||||
|
|
||||||
|
MainWindow::MainWindow(Context * context, QWidget *parent) :
|
||||||
|
QMainWindow(parent),
|
||||||
|
m_context(context),
|
||||||
|
m_ui(new Ui::MainWindow)
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
connect(m_ui->signInButton_MSA, &QPushButton::clicked, this, &MainWindow::SignInMSAClicked);
|
||||||
|
connect(m_ui->signInButton_Mojang, &QPushButton::clicked, this, &MainWindow::SignInMojangClicked);
|
||||||
|
connect(m_ui->signOutButton, &QPushButton::clicked, this, &MainWindow::SignOutClicked);
|
||||||
|
connect(m_ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshClicked);
|
||||||
|
|
||||||
|
// connect(m_context, &Context::linkingSucceeded, this, &MainWindow::SignInSucceeded);
|
||||||
|
// connect(m_context, &Context::linkingFailed, this, &MainWindow::SignInFailed);
|
||||||
|
connect(m_context, &Context::activityChanged, this, &MainWindow::ActivityChanged);
|
||||||
|
ActivityChanged(Katabasis::Activity::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow::~MainWindow() = default;
|
||||||
|
|
||||||
|
void MainWindow::ActivityChanged(Katabasis::Activity activity) {
|
||||||
|
switch(activity) {
|
||||||
|
case Katabasis::Activity::Idle: {
|
||||||
|
if(m_context->validity() != Katabasis::Validity::None) {
|
||||||
|
m_ui->signInButton_Mojang->setEnabled(false);
|
||||||
|
m_ui->signInButton_MSA->setEnabled(false);
|
||||||
|
m_ui->signOutButton->setEnabled(true);
|
||||||
|
m_ui->refreshButton->setEnabled(true);
|
||||||
|
m_ui->statusBar->showMessage(QString("Hello %1!").arg(m_context->userName()));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_ui->signInButton_Mojang->setEnabled(true);
|
||||||
|
m_ui->signInButton_MSA->setEnabled(true);
|
||||||
|
m_ui->signOutButton->setEnabled(false);
|
||||||
|
m_ui->refreshButton->setEnabled(false);
|
||||||
|
m_ui->statusBar->showMessage("Press the login button to start.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Activity::LoggingIn: {
|
||||||
|
m_ui->signInButton_Mojang->setEnabled(false);
|
||||||
|
m_ui->signInButton_MSA->setEnabled(false);
|
||||||
|
m_ui->signOutButton->setEnabled(false);
|
||||||
|
m_ui->refreshButton->setEnabled(false);
|
||||||
|
m_ui->statusBar->showMessage("Logging in...");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Activity::LoggingOut: {
|
||||||
|
m_ui->signInButton_Mojang->setEnabled(false);
|
||||||
|
m_ui->signInButton_MSA->setEnabled(false);
|
||||||
|
m_ui->signOutButton->setEnabled(false);
|
||||||
|
m_ui->refreshButton->setEnabled(false);
|
||||||
|
m_ui->statusBar->showMessage("Logging out...");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Activity::Refreshing: {
|
||||||
|
m_ui->signInButton_Mojang->setEnabled(false);
|
||||||
|
m_ui->signInButton_MSA->setEnabled(false);
|
||||||
|
m_ui->signOutButton->setEnabled(false);
|
||||||
|
m_ui->refreshButton->setEnabled(false);
|
||||||
|
m_ui->statusBar->showMessage("Refreshing login...");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::SignInMSAClicked() {
|
||||||
|
qDebug() << "Sign In MSA";
|
||||||
|
// signIn({{"prompt", "select_account"}})
|
||||||
|
// FIXME: wrong. very wrong. this should not be operating on the current context
|
||||||
|
m_context->signIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::SignInMojangClicked() {
|
||||||
|
qDebug() << "Sign In Mojang";
|
||||||
|
// signIn({{"prompt", "select_account"}})
|
||||||
|
// FIXME: wrong. very wrong. this should not be operating on the current context
|
||||||
|
m_context->signIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MainWindow::SignOutClicked() {
|
||||||
|
qDebug() << "Sign Out";
|
||||||
|
m_context->signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::RefreshClicked() {
|
||||||
|
qDebug() << "Refresh";
|
||||||
|
m_context->silentSignIn();
|
||||||
|
}
|
34
api/logic/minecraft/auth-msa/mainwindow.h
Normal file
34
api/logic/minecraft/auth-msa/mainwindow.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QtNetwork>
|
||||||
|
#include <katabasis/Bits.h>
|
||||||
|
|
||||||
|
#include "context.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class MainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainWindow : public QMainWindow {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MainWindow(Context * context, QWidget *parent = nullptr);
|
||||||
|
~MainWindow() override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void SignInMojangClicked();
|
||||||
|
void SignInMSAClicked();
|
||||||
|
|
||||||
|
void SignOutClicked();
|
||||||
|
void RefreshClicked();
|
||||||
|
|
||||||
|
void ActivityChanged(Katabasis::Activity activity);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Context* m_context;
|
||||||
|
QScopedPointer<Ui::MainWindow> m_ui;
|
||||||
|
};
|
||||||
|
|
72
api/logic/minecraft/auth-msa/mainwindow.ui
Normal file
72
api/logic/minecraft/auth-msa/mainwindow.ui
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1037</width>
|
||||||
|
<height>511</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>SmartMapsClient</string>
|
||||||
|
</property>
|
||||||
|
<property name="dockNestingEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralWidget">
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="1" column="3">
|
||||||
|
<widget class="QPushButton" name="signInButton_Mojang">
|
||||||
|
<property name="text">
|
||||||
|
<string>SignIn Mojang</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="3">
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" rowspan="7" colspan="3">
|
||||||
|
<widget class="QTreeView" name="accountView"/>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="3">
|
||||||
|
<widget class="QPushButton" name="refreshButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Refresh</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="3">
|
||||||
|
<widget class="QPushButton" name="signInButton_MSA">
|
||||||
|
<property name="text">
|
||||||
|
<string>SignIn MSA</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="3">
|
||||||
|
<widget class="QPushButton" name="signOutButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>SignOut</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="3">
|
||||||
|
<widget class="QPushButton" name="makeActiveButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Make Active</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusBar"/>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
2
libraries/katabasis/.gitignore
vendored
Normal file
2
libraries/katabasis/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
*.kdev4
|
60
libraries/katabasis/CMakeLists.txt
Normal file
60
libraries/katabasis/CMakeLists.txt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
|
||||||
|
if(IS_IN_SOURCE_BUILD)
|
||||||
|
message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR
|
||||||
|
CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*"
|
||||||
|
)
|
||||||
|
message(FATAL_ERROR "Building Katabasis is not supported in Linux-on-Windows distributions. Use a real Linux machine, not a fraudulent one.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
project(Katabasis)
|
||||||
|
enable_testing()
|
||||||
|
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED true)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED true)
|
||||||
|
set(CMAKE_CXX_STANDARD 11)
|
||||||
|
set(CMAKE_C_STANDARD 11)
|
||||||
|
|
||||||
|
find_package(Qt5 COMPONENTS Core Network REQUIRED)
|
||||||
|
|
||||||
|
set( katabasis_PRIVATE
|
||||||
|
src/OAuth2.cpp
|
||||||
|
|
||||||
|
src/JsonResponse.cpp
|
||||||
|
src/JsonResponse.h
|
||||||
|
src/PollServer.cpp
|
||||||
|
src/Reply.cpp
|
||||||
|
src/ReplyServer.cpp
|
||||||
|
src/Requestor.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set( katabasis_PUBLIC
|
||||||
|
include/katabasis/OAuth2.h
|
||||||
|
|
||||||
|
include/katabasis/Globals.h
|
||||||
|
include/katabasis/PollServer.h
|
||||||
|
include/katabasis/Reply.h
|
||||||
|
include/katabasis/ReplyServer.h
|
||||||
|
|
||||||
|
include/katabasis/Requestor.h
|
||||||
|
include/katabasis/RequestParameter.h
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} )
|
||||||
|
target_link_libraries(Katabasis Qt5::Core Qt5::Network)
|
||||||
|
|
||||||
|
# needed for statically linked Katabasis in shared libs on x86_64
|
||||||
|
set_target_properties(Katabasis
|
||||||
|
PROPERTIES POSITION_INDEPENDENT_CODE TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis)
|
23
libraries/katabasis/LICENSE
Normal file
23
libraries/katabasis/LICENSE
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Copyright (c) 2012, Akos Polster
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
36
libraries/katabasis/README.md
Normal file
36
libraries/katabasis/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Katabasis - MS-flavoerd OAuth for Qt, derived from the O2 library
|
||||||
|
|
||||||
|
This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful.
|
||||||
|
|
||||||
|
It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored.
|
||||||
|
|
||||||
|
[You can find the original library's git repository here.](https://github.com/pipacs/o2)
|
||||||
|
|
||||||
|
Notes to contributors:
|
||||||
|
|
||||||
|
* Please follow the coding style of the existing source, where reasonable
|
||||||
|
* Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
|
||||||
|
* If you are interested in working on this, come to the MultiMC Discord server and talk first
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Clone the Github repository, integrate the it into your CMake build system.
|
||||||
|
|
||||||
|
The library is static only, dynamic linking and system-wide installation are out of scope and undesirable.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
At this stage, don't, unless you want to help with the library itself.
|
||||||
|
|
||||||
|
This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features:
|
||||||
|
|
||||||
|
* Multiple accounts
|
||||||
|
* Multi-stage authentication/authorization schemes
|
||||||
|
* Tighter control over token chains and their storage
|
||||||
|
* Talking to complex APIs and individually authorized microservices
|
||||||
|
* Token lifetime management, 'offline mode' and resilience in face of network failures
|
||||||
|
* Token and claims/entitlements validation
|
||||||
|
* Caching of some API results
|
||||||
|
* XBox magic
|
||||||
|
* Mojang magic
|
||||||
|
* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available)
|
110
libraries/katabasis/acknowledgements.md
Normal file
110
libraries/katabasis/acknowledgements.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# O2 library by Akos Polster and contributors
|
||||||
|
|
||||||
|
[The origin of this fork.](https://github.com/pipacs/o2)
|
||||||
|
|
||||||
|
> Copyright (c) 2012, Akos Polster
|
||||||
|
> All rights reserved.
|
||||||
|
>
|
||||||
|
> Redistribution and use in source and binary forms, with or without
|
||||||
|
> modification, are permitted provided that the following conditions are met:
|
||||||
|
>
|
||||||
|
> * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
> list of conditions and the following disclaimer.
|
||||||
|
>
|
||||||
|
> * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
> this list of conditions and the following disclaimer in the documentation
|
||||||
|
> and/or other materials provided with the distribution.
|
||||||
|
>
|
||||||
|
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
# SimpleCrypt by Andre Somers
|
||||||
|
|
||||||
|
Cryptographic methods for Qt.
|
||||||
|
|
||||||
|
> Copyright (c) 2011, Andre Somers
|
||||||
|
> All rights reserved.
|
||||||
|
>
|
||||||
|
> Redistribution and use in source and binary forms, with or without
|
||||||
|
> modification, are permitted provided that the following conditions are met:
|
||||||
|
>
|
||||||
|
> * Redistributions of source code must retain the above copyright
|
||||||
|
> notice, this list of conditions and the following disclaimer.
|
||||||
|
> * Redistributions in binary form must reproduce the above copyright
|
||||||
|
> notice, this list of conditions and the following disclaimer in the
|
||||||
|
> documentation and/or other materials provided with the distribution.
|
||||||
|
> * Neither the name of the Rathenau Instituut, Andre Somers nor the
|
||||||
|
> names of its contributors may be used to endorse or promote products
|
||||||
|
> derived from this software without specific prior written permission.
|
||||||
|
>
|
||||||
|
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY
|
||||||
|
> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
# Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
|
||||||
|
|
||||||
|
Configurable settings storage, Twitter XAuth specialization, new demos, cleanups.
|
||||||
|
|
||||||
|
> "Hi Akos,
|
||||||
|
>
|
||||||
|
> I'm writing this mail to confirm that my contributions to the O2 library, available here https://github.com/pipacs/o2, can be freely distributed according to the project's license (as shown in the LICENSE file).
|
||||||
|
>
|
||||||
|
> Regards,
|
||||||
|
> -mandeep"
|
||||||
|
|
||||||
|
# Sergey Gavrushkin <https://github.com/ncux>
|
||||||
|
|
||||||
|
FreshBooks specialization
|
||||||
|
|
||||||
|
# Theofilos Intzoglou <https://github.com/parapente>
|
||||||
|
|
||||||
|
Hubic specialization
|
||||||
|
|
||||||
|
# Dimitar
|
||||||
|
|
||||||
|
SurveyMonkey specialization
|
||||||
|
|
||||||
|
# David Brooks <https://github.com/dbrnz>
|
||||||
|
|
||||||
|
CMake related fixes and improvements.
|
||||||
|
|
||||||
|
# Lukas Vogel <https://github.com/lukedirtwalker>
|
||||||
|
|
||||||
|
Spotify support
|
||||||
|
|
||||||
|
# Alan Garny <https://github.com/agarny>
|
||||||
|
|
||||||
|
Windows DLL build support
|
||||||
|
|
||||||
|
# MartinMikita <https://github.com/MartinMikita>
|
||||||
|
|
||||||
|
Bug fixes
|
||||||
|
|
||||||
|
# Larry Shaffer <https://github.com/dakcarto>
|
||||||
|
|
||||||
|
Versioning, shared lib, install target and header support
|
||||||
|
|
||||||
|
# Gilmanov Ildar <https://github.com/gilmanov-ildar>
|
||||||
|
|
||||||
|
Bug fixes, support for ```qml``` module
|
||||||
|
|
||||||
|
# Fabian Vogt <https://github.com/Vogtinator>
|
||||||
|
|
||||||
|
Bug fixes, support for building without Qt keywords enabled
|
||||||
|
|
33
libraries/katabasis/include/katabasis/Bits.h
Normal file
33
libraries/katabasis/include/katabasis/Bits.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
enum class Activity {
|
||||||
|
Idle,
|
||||||
|
LoggingIn,
|
||||||
|
LoggingOut,
|
||||||
|
Refreshing
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Validity {
|
||||||
|
None,
|
||||||
|
Assumed,
|
||||||
|
Certain
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Token {
|
||||||
|
QDateTime issueInstant;
|
||||||
|
QDateTime notAfter;
|
||||||
|
QString token;
|
||||||
|
QString refresh_token;
|
||||||
|
QVariantMap extra;
|
||||||
|
|
||||||
|
Validity validity = Validity::None;
|
||||||
|
bool persistent = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
59
libraries/katabasis/include/katabasis/Globals.h
Normal file
59
libraries/katabasis/include/katabasis/Globals.h
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
// Common constants
|
||||||
|
const char ENCRYPTION_KEY[] = "12345678";
|
||||||
|
const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded";
|
||||||
|
const char MIME_TYPE_JSON[] = "application/json";
|
||||||
|
|
||||||
|
// OAuth 1/1.1 Request Parameters
|
||||||
|
const char OAUTH_CALLBACK[] = "oauth_callback";
|
||||||
|
const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key";
|
||||||
|
const char OAUTH_NONCE[] = "oauth_nonce";
|
||||||
|
const char OAUTH_SIGNATURE[] = "oauth_signature";
|
||||||
|
const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method";
|
||||||
|
const char OAUTH_TIMESTAMP[] = "oauth_timestamp";
|
||||||
|
const char OAUTH_VERSION[] = "oauth_version";
|
||||||
|
// OAuth 1/1.1 Response Parameters
|
||||||
|
const char OAUTH_TOKEN[] = "oauth_token";
|
||||||
|
const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret";
|
||||||
|
const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed";
|
||||||
|
const char OAUTH_VERFIER[] = "oauth_verifier";
|
||||||
|
|
||||||
|
// OAuth 2 Request Parameters
|
||||||
|
const char OAUTH2_RESPONSE_TYPE[] = "response_type";
|
||||||
|
const char OAUTH2_CLIENT_ID[] = "client_id";
|
||||||
|
const char OAUTH2_CLIENT_SECRET[] = "client_secret";
|
||||||
|
const char OAUTH2_USERNAME[] = "username";
|
||||||
|
const char OAUTH2_PASSWORD[] = "password";
|
||||||
|
const char OAUTH2_REDIRECT_URI[] = "redirect_uri";
|
||||||
|
const char OAUTH2_SCOPE[] = "scope";
|
||||||
|
const char OAUTH2_GRANT_TYPE_CODE[] = "code";
|
||||||
|
const char OAUTH2_GRANT_TYPE_TOKEN[] = "token";
|
||||||
|
const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password";
|
||||||
|
const char OAUTH2_GRANT_TYPE_DEVICE[] = "urn:ietf:params:oauth:grant-type:device_code";
|
||||||
|
const char OAUTH2_GRANT_TYPE[] = "grant_type";
|
||||||
|
const char OAUTH2_API_KEY[] = "api_key";
|
||||||
|
const char OAUTH2_STATE[] = "state";
|
||||||
|
const char OAUTH2_CODE[] = "code";
|
||||||
|
|
||||||
|
// OAuth 2 Response Parameters
|
||||||
|
const char OAUTH2_ACCESS_TOKEN[] = "access_token";
|
||||||
|
const char OAUTH2_REFRESH_TOKEN[] = "refresh_token";
|
||||||
|
const char OAUTH2_EXPIRES_IN[] = "expires_in";
|
||||||
|
const char OAUTH2_DEVICE_CODE[] = "device_code";
|
||||||
|
const char OAUTH2_USER_CODE[] = "user_code";
|
||||||
|
const char OAUTH2_VERIFICATION_URI[] = "verification_uri";
|
||||||
|
const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in
|
||||||
|
const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete";
|
||||||
|
const char OAUTH2_INTERVAL[] = "interval";
|
||||||
|
|
||||||
|
// Parameter values
|
||||||
|
const char AUTHORIZATION_CODE[] = "authorization_code";
|
||||||
|
|
||||||
|
// Standard HTTP headers
|
||||||
|
const char HTTP_HTTP_HEADER[] = "HTTP";
|
||||||
|
const char HTTP_AUTHORIZATION_HEADER[] = "Authorization";
|
||||||
|
|
||||||
|
}
|
233
libraries/katabasis/include/katabasis/OAuth2.h
Normal file
233
libraries/katabasis/include/katabasis/OAuth2.h
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QPair>
|
||||||
|
|
||||||
|
#include "Reply.h"
|
||||||
|
#include "RequestParameter.h"
|
||||||
|
#include "Bits.h"
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
class ReplyServer;
|
||||||
|
class PollServer;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FIXME: this is not as simple as it should be. it squishes 4 different grant flows into one big ball of mud
|
||||||
|
* This serves no practical purpose and simply makes the code less readable / maintainable.
|
||||||
|
*
|
||||||
|
* Therefore: Split this into the 4 different OAuth2 flows that people can use as authentication steps. Write tests/examples for all of them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Simple OAuth2 authenticator.
|
||||||
|
class OAuth2: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Q_ENUMS(GrantFlow)
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
struct Options {
|
||||||
|
QString userAgent = QStringLiteral("Katabasis/1.0");
|
||||||
|
QString redirectionUrl = QStringLiteral("http://localhost:%1");
|
||||||
|
QString responseType = QStringLiteral("code");
|
||||||
|
QString scope;
|
||||||
|
QString clientIdentifier;
|
||||||
|
QString clientSecret;
|
||||||
|
QUrl authorizationUrl;
|
||||||
|
QUrl accessTokenUrl;
|
||||||
|
QVector<quint16> listenerPorts = { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Authorization flow types.
|
||||||
|
enum GrantFlow {
|
||||||
|
GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
|
||||||
|
GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2
|
||||||
|
GrantFlowResourceOwnerPasswordCredentials,
|
||||||
|
GrantFlowDevice ///< @see https://tools.ietf.org/html/rfc8628#section-1
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Authorization flow.
|
||||||
|
GrantFlow grantFlow();
|
||||||
|
void setGrantFlow(GrantFlow value);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Are we authenticated?
|
||||||
|
bool linked();
|
||||||
|
|
||||||
|
/// Authentication token.
|
||||||
|
QString token();
|
||||||
|
|
||||||
|
/// Provider-specific extra tokens, available after a successful authentication
|
||||||
|
QVariantMap extraTokens();
|
||||||
|
|
||||||
|
/// Page content on local host after successful oauth.
|
||||||
|
/// Provide it in case you do not want to close the browser, but display something
|
||||||
|
QByteArray replyContent() const;
|
||||||
|
void setReplyContent(const QByteArray &value);
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
/// Resource owner username.
|
||||||
|
/// instances with the same (username, password) share the same "linked" and "token" properties.
|
||||||
|
QString username();
|
||||||
|
void setUsername(const QString &value);
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
/// Resource owner password.
|
||||||
|
/// instances with the same (username, password) share the same "linked" and "token" properties.
|
||||||
|
QString password();
|
||||||
|
void setPassword(const QString &value);
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
/// API key.
|
||||||
|
QString apiKey();
|
||||||
|
void setApiKey(const QString &value);
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
/// Allow ignoring SSL errors?
|
||||||
|
/// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem
|
||||||
|
bool ignoreSslErrors();
|
||||||
|
void setIgnoreSslErrors(bool ignoreSslErrors);
|
||||||
|
|
||||||
|
// TODO: put in `Options`
|
||||||
|
/// User-defined extra parameters to append to request URL
|
||||||
|
QVariantMap extraRequestParams();
|
||||||
|
void setExtraRequestParams(const QVariantMap &value);
|
||||||
|
|
||||||
|
// TODO: split up the class into multiple, each implementing one OAuth2 flow
|
||||||
|
/// Grant type (if non-standard)
|
||||||
|
QString grantType();
|
||||||
|
void setGrantType(const QString &value);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Constructor.
|
||||||
|
/// @param parent Parent object.
|
||||||
|
explicit OAuth2(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0);
|
||||||
|
|
||||||
|
/// Get refresh token.
|
||||||
|
QString refreshToken();
|
||||||
|
|
||||||
|
/// Get token expiration time
|
||||||
|
QDateTime expires();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/// Authenticate.
|
||||||
|
virtual void link();
|
||||||
|
|
||||||
|
/// De-authenticate.
|
||||||
|
virtual void unlink();
|
||||||
|
|
||||||
|
/// Refresh token.
|
||||||
|
bool refresh();
|
||||||
|
|
||||||
|
/// Handle situation where reply server has opted to close its connection
|
||||||
|
void serverHasClosed(bool paramsfound = false);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
/// Emitted when a token refresh has been completed or failed.
|
||||||
|
void refreshFinished(QNetworkReply::NetworkError error);
|
||||||
|
|
||||||
|
/// Emitted when client needs to open a web browser window, with the given URL.
|
||||||
|
void openBrowser(const QUrl &url);
|
||||||
|
|
||||||
|
/// Emitted when client can close the browser window.
|
||||||
|
void closeBrowser();
|
||||||
|
|
||||||
|
/// Emitted when client needs to show a verification uri and user code
|
||||||
|
void showVerificationUriAndCode(const QUrl &uri, const QString &code);
|
||||||
|
|
||||||
|
/// Emitted when authentication/deauthentication succeeded.
|
||||||
|
void linkingSucceeded();
|
||||||
|
|
||||||
|
/// Emitted when authentication/deauthentication failed.
|
||||||
|
void linkingFailed();
|
||||||
|
|
||||||
|
void activityChanged(Activity activity);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/// Handle verification response.
|
||||||
|
virtual void onVerificationReceived(QMap<QString, QString>);
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
/// Handle completion of a token request.
|
||||||
|
virtual void onTokenReplyFinished();
|
||||||
|
|
||||||
|
/// Handle failure of a token request.
|
||||||
|
virtual void onTokenReplyError(QNetworkReply::NetworkError error);
|
||||||
|
|
||||||
|
/// Handle completion of a refresh request.
|
||||||
|
virtual void onRefreshFinished();
|
||||||
|
|
||||||
|
/// Handle failure of a refresh request.
|
||||||
|
virtual void onRefreshError(QNetworkReply::NetworkError error);
|
||||||
|
|
||||||
|
/// Handle completion of a Device Authorization Request
|
||||||
|
virtual void onDeviceAuthReplyFinished();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/// Build HTTP request body.
|
||||||
|
QByteArray buildRequestBody(const QMap<QString, QString> ¶meters);
|
||||||
|
|
||||||
|
/// Set refresh token.
|
||||||
|
void setRefreshToken(const QString &v);
|
||||||
|
|
||||||
|
/// Set token expiration time.
|
||||||
|
void setExpires(QDateTime v);
|
||||||
|
|
||||||
|
/// Start polling authorization server
|
||||||
|
void startPollServer(const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
/// Set authentication token.
|
||||||
|
void setToken(const QString &v);
|
||||||
|
|
||||||
|
/// Set the linked state
|
||||||
|
void setLinked(bool v);
|
||||||
|
|
||||||
|
/// Set extra tokens found in OAuth response
|
||||||
|
void setExtraTokens(QVariantMap extraTokens);
|
||||||
|
|
||||||
|
/// Set local reply server
|
||||||
|
void setReplyServer(ReplyServer *server);
|
||||||
|
|
||||||
|
ReplyServer * replyServer() const;
|
||||||
|
|
||||||
|
/// Set local poll server
|
||||||
|
void setPollServer(PollServer *server);
|
||||||
|
|
||||||
|
PollServer * pollServer() const;
|
||||||
|
|
||||||
|
void updateActivity(Activity activity);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QString username_;
|
||||||
|
QString password_;
|
||||||
|
|
||||||
|
Options options_;
|
||||||
|
|
||||||
|
QVariantMap extraReqParams_;
|
||||||
|
QString apiKey_;
|
||||||
|
QNetworkAccessManager *manager_ = nullptr;
|
||||||
|
ReplyList timedReplies_;
|
||||||
|
GrantFlow grantFlow_;
|
||||||
|
QString grantType_;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QString redirectUri_;
|
||||||
|
Token &token_;
|
||||||
|
|
||||||
|
// this should be part of the reply server impl
|
||||||
|
QByteArray replyContent_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ReplyServer *replyServer_ = nullptr;
|
||||||
|
PollServer *pollServer_ = nullptr;
|
||||||
|
Activity activity_ = Activity::Idle;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
48
libraries/katabasis/include/katabasis/PollServer.h
Normal file
48
libraries/katabasis/include/katabasis/PollServer.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
/// Poll an authorization server for token
|
||||||
|
class PollServer : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PollServer(QNetworkAccessManager * manager, const QNetworkRequest &request, const QByteArray & payload, int expiresIn, QObject *parent = 0);
|
||||||
|
|
||||||
|
/// Seconds to wait between polling requests
|
||||||
|
Q_PROPERTY(int interval READ interval WRITE setInterval)
|
||||||
|
int interval() const;
|
||||||
|
void setInterval(int interval);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void verificationReceived(QMap<QString, QString>);
|
||||||
|
void serverClosed(bool); // whether it has found parameters
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void startPolling();
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void onPollTimeout();
|
||||||
|
void onExpiration();
|
||||||
|
void onReplyFinished();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QNetworkAccessManager *manager_;
|
||||||
|
const QNetworkRequest request_;
|
||||||
|
const QByteArray payload_;
|
||||||
|
const int expiresIn_;
|
||||||
|
QTimer expirationTimer;
|
||||||
|
QTimer pollTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
60
libraries/katabasis/include/katabasis/Reply.h
Normal file
60
libraries/katabasis/include/katabasis/Reply.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
/// A network request/reply pair that can time out.
|
||||||
|
class Reply: public QTimer {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Reply(QNetworkReply *reply, int timeOut = 60 * 1000, QObject *parent = 0);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void error(QNetworkReply::NetworkError);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/// When time out occurs, the QNetworkReply's error() signal is triggered.
|
||||||
|
void onTimeOut();
|
||||||
|
|
||||||
|
public:
|
||||||
|
QNetworkReply *reply;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// List of O2Replies.
|
||||||
|
class ReplyList {
|
||||||
|
public:
|
||||||
|
ReplyList() { ignoreSslErrors_ = false; }
|
||||||
|
|
||||||
|
/// Destructor.
|
||||||
|
/// Deletes all O2Reply instances in the list.
|
||||||
|
virtual ~ReplyList();
|
||||||
|
|
||||||
|
/// Create a new O2Reply from a QNetworkReply, and add it to this list.
|
||||||
|
void add(QNetworkReply *reply);
|
||||||
|
|
||||||
|
/// Add an O2Reply to the list, while taking ownership of it.
|
||||||
|
void add(Reply *reply);
|
||||||
|
|
||||||
|
/// Remove item from the list that corresponds to a QNetworkReply.
|
||||||
|
void remove(QNetworkReply *reply);
|
||||||
|
|
||||||
|
/// Find an O2Reply in the list, corresponding to a QNetworkReply.
|
||||||
|
/// @return Matching O2Reply or NULL.
|
||||||
|
Reply *find(QNetworkReply *reply);
|
||||||
|
|
||||||
|
bool ignoreSslErrors();
|
||||||
|
void setIgnoreSslErrors(bool ignoreSslErrors);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QList<Reply *> replies_;
|
||||||
|
bool ignoreSslErrors_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
53
libraries/katabasis/include/katabasis/ReplyServer.h
Normal file
53
libraries/katabasis/include/katabasis/ReplyServer.h
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
/// HTTP server to process authentication response.
|
||||||
|
class ReplyServer: public QTcpServer {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ReplyServer(QObject *parent = 0);
|
||||||
|
|
||||||
|
/// Page content on local host after successful oauth - in case you do not want to close the browser, but display something
|
||||||
|
Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent)
|
||||||
|
QByteArray replyContent();
|
||||||
|
void setReplyContent(const QByteArray &value);
|
||||||
|
|
||||||
|
/// Seconds to keep listening *after* first response for a callback with token content
|
||||||
|
Q_PROPERTY(int timeout READ timeout WRITE setTimeout)
|
||||||
|
int timeout();
|
||||||
|
void setTimeout(int timeout);
|
||||||
|
|
||||||
|
/// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.)
|
||||||
|
Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries)
|
||||||
|
int callbackTries();
|
||||||
|
void setCallbackTries(int maxtries);
|
||||||
|
|
||||||
|
QString uniqueState();
|
||||||
|
void setUniqueState(const QString &state);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void verificationReceived(QMap<QString, QString>);
|
||||||
|
void serverClosed(bool); // whether it has found parameters
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onIncomingConnection();
|
||||||
|
void onBytesReady();
|
||||||
|
QMap<QString, QString> parseQueryParams(QByteArray *data);
|
||||||
|
void closeServer(QTcpSocket *socket = 0, bool hasparameters = false);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QByteArray replyContent_;
|
||||||
|
int timeout_;
|
||||||
|
int maxtries_;
|
||||||
|
int tries_;
|
||||||
|
QString uniqueState_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
15
libraries/katabasis/include/katabasis/RequestParameter.h
Normal file
15
libraries/katabasis/include/katabasis/RequestParameter.h
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
/// Request parameter (name-value pair) participating in authentication.
|
||||||
|
struct RequestParameter {
|
||||||
|
RequestParameter(const QByteArray &n, const QByteArray &v): name(n), value(v) {}
|
||||||
|
bool operator <(const RequestParameter &other) const {
|
||||||
|
return (name == other.name)? (value < other.value): (name < other.name);
|
||||||
|
}
|
||||||
|
QByteArray name;
|
||||||
|
QByteArray value;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
116
libraries/katabasis/include/katabasis/Requestor.h
Normal file
116
libraries/katabasis/include/katabasis/Requestor.h
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QHttpMultiPart>
|
||||||
|
|
||||||
|
#include "Reply.h"
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
class OAuth2;
|
||||||
|
|
||||||
|
/// Makes authenticated requests.
|
||||||
|
class Requestor: public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent = 0);
|
||||||
|
~Requestor();
|
||||||
|
|
||||||
|
|
||||||
|
/// Some services require the access token to be sent as a Authentication HTTP header
|
||||||
|
/// and refuse requests with the access token in the query.
|
||||||
|
/// This function allows to use or ignore the access token in the query.
|
||||||
|
/// The default value of `true` means that the query will contain the access token.
|
||||||
|
/// By setting the value to false, the query will not contain the access token.
|
||||||
|
/// See:
|
||||||
|
/// https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-4.3
|
||||||
|
/// https://tools.ietf.org/html/rfc6750#section-2.3
|
||||||
|
|
||||||
|
void setAddAccessTokenInQuery(bool value);
|
||||||
|
|
||||||
|
/// Some services require the access token to be sent as a Authentication HTTP header.
|
||||||
|
/// This is the case for Twitch and Mixer.
|
||||||
|
/// When the access token expires and is refreshed, O2Requestor::retry() needs to update the Authentication HTTP header.
|
||||||
|
/// In order to do so, O2Requestor needs to know the format of the Authentication HTTP header.
|
||||||
|
void setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/// Make a GET request.
|
||||||
|
/// @return Request ID or -1 if there are too many requests in the queue.
|
||||||
|
int get(const QNetworkRequest &req, int timeout = 60*1000);
|
||||||
|
|
||||||
|
/// Make a POST request.
|
||||||
|
/// @return Request ID or -1 if there are too many requests in the queue.
|
||||||
|
int post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000);
|
||||||
|
int post(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000);
|
||||||
|
|
||||||
|
/// Make a PUT request.
|
||||||
|
/// @return Request ID or -1 if there are too many requests in the queue.
|
||||||
|
int put(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000);
|
||||||
|
int put(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000);
|
||||||
|
|
||||||
|
/// Make a HEAD request.
|
||||||
|
/// @return Request ID or -1 if there are too many requests in the queue.
|
||||||
|
int head(const QNetworkRequest &req, int timeout = 60*1000);
|
||||||
|
|
||||||
|
/// Make a custom request.
|
||||||
|
/// @return Request ID or -1 if there are too many requests in the queue.
|
||||||
|
int customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout = 60*1000);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
|
||||||
|
/// Emitted when a request has been completed or failed.
|
||||||
|
void finished(int id, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
|
||||||
|
|
||||||
|
/// Emitted when an upload has progressed.
|
||||||
|
void uploadProgress(int id, qint64 bytesSent, qint64 bytesTotal);
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
/// Handle refresh completion.
|
||||||
|
void onRefreshFinished(QNetworkReply::NetworkError error);
|
||||||
|
|
||||||
|
/// Handle request finished.
|
||||||
|
void onRequestFinished();
|
||||||
|
|
||||||
|
/// Handle request error.
|
||||||
|
void onRequestError(QNetworkReply::NetworkError error);
|
||||||
|
|
||||||
|
/// Re-try request (after successful token refresh).
|
||||||
|
void retry();
|
||||||
|
|
||||||
|
/// Finish the request, emit finished() signal.
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
/// Handle upload progress.
|
||||||
|
void onUploadProgress(qint64 uploaded, qint64 total);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray());
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle, Requesting, ReRequesting
|
||||||
|
};
|
||||||
|
|
||||||
|
QNetworkAccessManager *manager_;
|
||||||
|
OAuth2 *authenticator_;
|
||||||
|
QNetworkRequest request_;
|
||||||
|
QByteArray data_;
|
||||||
|
QHttpMultiPart* multipartData_;
|
||||||
|
QNetworkReply *reply_;
|
||||||
|
Status status_;
|
||||||
|
int id_;
|
||||||
|
QNetworkAccessManager::Operation operation_;
|
||||||
|
QUrl url_;
|
||||||
|
ReplyList timedReplies_;
|
||||||
|
QNetworkReply::NetworkError error_;
|
||||||
|
bool addAccessTokenInQuery_;
|
||||||
|
QString accessTokenInAuthenticationHTTPHeaderFormat_;
|
||||||
|
bool rawData_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
26
libraries/katabasis/src/JsonResponse.cpp
Normal file
26
libraries/katabasis/src/JsonResponse.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#include "JsonResponse.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
QVariantMap parseJsonResponse(const QByteArray &data) {
|
||||||
|
QJsonParseError err;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
|
||||||
|
if (err.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString();
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
qWarning() << "parseTokenResponse: Token response is not an object";
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.object().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
libraries/katabasis/src/JsonResponse.h
Normal file
12
libraries/katabasis/src/JsonResponse.h
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
class QByteArray;
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
/// Parse JSON data into a QVariantMap
|
||||||
|
QVariantMap parseJsonResponse(const QByteArray &data);
|
||||||
|
|
||||||
|
}
|
668
libraries/katabasis/src/OAuth2.cpp
Normal file
668
libraries/katabasis/src/OAuth2.cpp
Normal file
@ -0,0 +1,668 @@
|
|||||||
|
#include <QList>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QDataStream>
|
||||||
|
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "katabasis/OAuth2.h"
|
||||||
|
#include "katabasis/PollServer.h"
|
||||||
|
#include "katabasis/ReplyServer.h"
|
||||||
|
#include "katabasis/Globals.h"
|
||||||
|
|
||||||
|
#include "JsonResponse.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// ref: https://tools.ietf.org/html/rfc8628#section-3.2
|
||||||
|
// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both.
|
||||||
|
bool hasMandatoryDeviceAuthParams(const QVariantMap& params)
|
||||||
|
{
|
||||||
|
if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!params.contains(Katabasis::OAUTH2_USER_CODE))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray createQueryParameters(const QList<Katabasis::RequestParameter> ¶meters) {
|
||||||
|
QByteArray ret;
|
||||||
|
bool first = true;
|
||||||
|
for( auto & h: parameters) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
ret.append("&");
|
||||||
|
}
|
||||||
|
ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
OAuth2::OAuth2(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) {
|
||||||
|
manager_ = manager ? manager : new QNetworkAccessManager(this);
|
||||||
|
grantFlow_ = GrantFlowAuthorizationCode;
|
||||||
|
qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
|
||||||
|
options_ = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OAuth2::linked() {
|
||||||
|
return token_.validity != Validity::None;
|
||||||
|
}
|
||||||
|
void OAuth2::setLinked(bool v) {
|
||||||
|
qDebug() << "OAuth2::setLinked:" << (v? "true": "false");
|
||||||
|
token_.validity = v ? Validity::Certain : Validity::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuth2::token() {
|
||||||
|
return token_.token;
|
||||||
|
}
|
||||||
|
void OAuth2::setToken(const QString &v) {
|
||||||
|
token_.token = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray OAuth2::replyContent() const {
|
||||||
|
return replyContent_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setReplyContent(const QByteArray &value) {
|
||||||
|
replyContent_ = value;
|
||||||
|
if (replyServer_) {
|
||||||
|
replyServer_->setReplyContent(replyContent_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap OAuth2::extraTokens() {
|
||||||
|
return token_.extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setExtraTokens(QVariantMap extraTokens) {
|
||||||
|
token_.extra = extraTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setReplyServer(ReplyServer * server)
|
||||||
|
{
|
||||||
|
delete replyServer_;
|
||||||
|
|
||||||
|
replyServer_ = server;
|
||||||
|
replyServer_->setReplyContent(replyContent_);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyServer * OAuth2::replyServer() const
|
||||||
|
{
|
||||||
|
return replyServer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setPollServer(PollServer *server)
|
||||||
|
{
|
||||||
|
if (pollServer_)
|
||||||
|
pollServer_->deleteLater();
|
||||||
|
|
||||||
|
pollServer_ = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
PollServer *OAuth2::pollServer() const
|
||||||
|
{
|
||||||
|
return pollServer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
OAuth2::GrantFlow OAuth2::grantFlow() {
|
||||||
|
return grantFlow_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setGrantFlow(OAuth2::GrantFlow value) {
|
||||||
|
grantFlow_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuth2::username() {
|
||||||
|
return username_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setUsername(const QString &value) {
|
||||||
|
username_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuth2::password() {
|
||||||
|
return password_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setPassword(const QString &value) {
|
||||||
|
password_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap OAuth2::extraRequestParams()
|
||||||
|
{
|
||||||
|
return extraReqParams_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setExtraRequestParams(const QVariantMap &value)
|
||||||
|
{
|
||||||
|
extraReqParams_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuth2::grantType()
|
||||||
|
{
|
||||||
|
if (!grantType_.isEmpty())
|
||||||
|
return grantType_;
|
||||||
|
|
||||||
|
switch (grantFlow_) {
|
||||||
|
case GrantFlowAuthorizationCode:
|
||||||
|
return OAUTH2_GRANT_TYPE_CODE;
|
||||||
|
case GrantFlowImplicit:
|
||||||
|
return OAUTH2_GRANT_TYPE_TOKEN;
|
||||||
|
case GrantFlowResourceOwnerPasswordCredentials:
|
||||||
|
return OAUTH2_GRANT_TYPE_PASSWORD;
|
||||||
|
case GrantFlowDevice:
|
||||||
|
return OAUTH2_GRANT_TYPE_DEVICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setGrantType(const QString &value)
|
||||||
|
{
|
||||||
|
grantType_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::updateActivity(Activity activity)
|
||||||
|
{
|
||||||
|
if(activity_ != activity) {
|
||||||
|
activity_ = activity;
|
||||||
|
emit activityChanged(activity_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::link() {
|
||||||
|
qDebug() << "OAuth2::link";
|
||||||
|
|
||||||
|
// Create the reply server if it doesn't exist
|
||||||
|
if(replyServer() == NULL) {
|
||||||
|
ReplyServer * replyServer = new ReplyServer(this);
|
||||||
|
connect(replyServer, &ReplyServer::verificationReceived, this, &OAuth2::onVerificationReceived);
|
||||||
|
connect(replyServer, &ReplyServer::serverClosed, this, &OAuth2::serverHasClosed);
|
||||||
|
setReplyServer(replyServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linked()) {
|
||||||
|
qDebug() << "OAuth2::link: Linked already";
|
||||||
|
emit linkingSucceeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLinked(false);
|
||||||
|
setToken("");
|
||||||
|
setExtraTokens(QVariantMap());
|
||||||
|
setRefreshToken(QString());
|
||||||
|
setExpires(QDateTime());
|
||||||
|
|
||||||
|
if (grantFlow_ == GrantFlowAuthorizationCode || grantFlow_ == GrantFlowImplicit) {
|
||||||
|
|
||||||
|
QString uniqueState = QUuid::createUuid().toString().remove(QRegExp("([^a-zA-Z0-9]|[-])"));
|
||||||
|
|
||||||
|
// FIXME: this should be part of a 'redirection handler' that would get injected into O2
|
||||||
|
{
|
||||||
|
quint16 foundPort = 0;
|
||||||
|
// Start listening to authentication replies
|
||||||
|
if (!replyServer()->isListening()) {
|
||||||
|
auto ports = options_.listenerPorts;
|
||||||
|
for(auto & port: ports) {
|
||||||
|
if (replyServer()->listen(QHostAddress::Any, port)) {
|
||||||
|
foundPort = replyServer()->serverPort();
|
||||||
|
qDebug() << "OAuth2::link: Reply server listening on port " << foundPort;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(foundPort == 0) {
|
||||||
|
qWarning() << "OAuth2::link: Reply server failed to start listening on any port out of " << ports;
|
||||||
|
emit linkingFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save redirect URI, as we have to reuse it when requesting the access token
|
||||||
|
redirectUri_ = options_.redirectionUrl.arg(foundPort);
|
||||||
|
replyServer()->setUniqueState(uniqueState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble intial authentication URL
|
||||||
|
QUrl url(options_.authorizationUrl);
|
||||||
|
QUrlQuery query(url);
|
||||||
|
QList<QPair<QString, QString> > parameters;
|
||||||
|
query.addQueryItem(OAUTH2_RESPONSE_TYPE, (grantFlow_ == GrantFlowAuthorizationCode)? OAUTH2_GRANT_TYPE_CODE: OAUTH2_GRANT_TYPE_TOKEN);
|
||||||
|
query.addQueryItem(OAUTH2_CLIENT_ID, options_.clientIdentifier);
|
||||||
|
query.addQueryItem(OAUTH2_REDIRECT_URI, redirectUri_);
|
||||||
|
query.addQueryItem(OAUTH2_SCOPE, options_.scope.replace( " ", "+" ));
|
||||||
|
query.addQueryItem(OAUTH2_STATE, uniqueState);
|
||||||
|
if (!apiKey_.isEmpty()) {
|
||||||
|
query.addQueryItem(OAUTH2_API_KEY, apiKey_);
|
||||||
|
}
|
||||||
|
for(auto iter = extraReqParams_.begin(); iter != extraReqParams_.end(); iter++) {
|
||||||
|
query.addQueryItem(iter.key(), iter.value().toString());
|
||||||
|
}
|
||||||
|
url.setQuery(query);
|
||||||
|
|
||||||
|
// Show authentication URL with a web browser
|
||||||
|
qDebug() << "OAuth2::link: Emit openBrowser" << url.toString();
|
||||||
|
emit openBrowser(url);
|
||||||
|
updateActivity(Activity::LoggingIn);
|
||||||
|
} else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) {
|
||||||
|
QList<RequestParameter> parameters;
|
||||||
|
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
|
||||||
|
if ( !options_.clientSecret.isEmpty() ) {
|
||||||
|
parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8()));
|
||||||
|
}
|
||||||
|
parameters.append(RequestParameter(OAUTH2_USERNAME, username_.toUtf8()));
|
||||||
|
parameters.append(RequestParameter(OAUTH2_PASSWORD, password_.toUtf8()));
|
||||||
|
parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, OAUTH2_GRANT_TYPE_PASSWORD));
|
||||||
|
parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
|
||||||
|
if ( !apiKey_.isEmpty() )
|
||||||
|
parameters.append(RequestParameter(OAUTH2_API_KEY, apiKey_.toUtf8()));
|
||||||
|
foreach (QString key, extraRequestParams().keys()) {
|
||||||
|
parameters.append(RequestParameter(key.toUtf8(), extraRequestParams().value(key).toByteArray()));
|
||||||
|
}
|
||||||
|
QByteArray payload = createQueryParameters(parameters);
|
||||||
|
|
||||||
|
qDebug() << "OAuth2::link: Sending token request for resource owner flow";
|
||||||
|
QUrl url(options_.accessTokenUrl);
|
||||||
|
QNetworkRequest tokenRequest(url);
|
||||||
|
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
QNetworkReply *tokenReply = manager_->post(tokenRequest, payload);
|
||||||
|
|
||||||
|
connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection);
|
||||||
|
connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
updateActivity(Activity::LoggingIn);
|
||||||
|
}
|
||||||
|
else if (grantFlow_ == GrantFlowDevice) {
|
||||||
|
QList<RequestParameter> parameters;
|
||||||
|
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
|
||||||
|
parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
|
||||||
|
QByteArray payload = createQueryParameters(parameters);
|
||||||
|
|
||||||
|
QUrl url(options_.authorizationUrl);
|
||||||
|
QNetworkRequest deviceRequest(url);
|
||||||
|
deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
QNetworkReply *tokenReply = manager_->post(deviceRequest, payload);
|
||||||
|
|
||||||
|
connect(tokenReply, SIGNAL(finished()), this, SLOT(onDeviceAuthReplyFinished()), Qt::QueuedConnection);
|
||||||
|
connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
updateActivity(Activity::LoggingIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::unlink() {
|
||||||
|
qDebug() << "OAuth2::unlink";
|
||||||
|
updateActivity(Activity::LoggingOut);
|
||||||
|
// FIXME: implement logout flows... if they exist
|
||||||
|
token_ = Token();
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::onVerificationReceived(const QMap<QString, QString> response) {
|
||||||
|
qDebug() << "OAuth2::onVerificationReceived: Emitting closeBrowser()";
|
||||||
|
emit closeBrowser();
|
||||||
|
|
||||||
|
if (response.contains("error")) {
|
||||||
|
qWarning() << "OAuth2::onVerificationReceived: Verification failed:" << response;
|
||||||
|
emit linkingFailed();
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grantFlow_ == GrantFlowAuthorizationCode) {
|
||||||
|
// NOTE: access code is temporary and should never be saved anywhere!
|
||||||
|
auto access_code = response.value(QString(OAUTH2_GRANT_TYPE_CODE));
|
||||||
|
|
||||||
|
// Exchange access code for access/refresh tokens
|
||||||
|
QString query;
|
||||||
|
if(!apiKey_.isEmpty())
|
||||||
|
query = QString("?" + QString(OAUTH2_API_KEY) + "=" + apiKey_);
|
||||||
|
QNetworkRequest tokenRequest(QUrl(options_.accessTokenUrl.toString() + query));
|
||||||
|
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM);
|
||||||
|
tokenRequest.setRawHeader("Accept", MIME_TYPE_JSON);
|
||||||
|
QMap<QString, QString> parameters;
|
||||||
|
parameters.insert(OAUTH2_GRANT_TYPE_CODE, access_code);
|
||||||
|
parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
|
||||||
|
if ( !options_.clientSecret.isEmpty() ) {
|
||||||
|
parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
|
||||||
|
}
|
||||||
|
parameters.insert(OAUTH2_REDIRECT_URI, redirectUri_);
|
||||||
|
parameters.insert(OAUTH2_GRANT_TYPE, AUTHORIZATION_CODE);
|
||||||
|
QByteArray data = buildRequestBody(parameters);
|
||||||
|
|
||||||
|
qDebug() << QString("OAuth2::onVerificationReceived: Exchange access code data:\n%1").arg(QString(data));
|
||||||
|
|
||||||
|
QNetworkReply *tokenReply = manager_->post(tokenRequest, data);
|
||||||
|
timedReplies_.add(tokenReply);
|
||||||
|
connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection);
|
||||||
|
connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
} else if (grantFlow_ == GrantFlowImplicit || grantFlow_ == GrantFlowDevice) {
|
||||||
|
// Check for mandatory tokens
|
||||||
|
if (response.contains(OAUTH2_ACCESS_TOKEN)) {
|
||||||
|
qDebug() << "OAuth2::onVerificationReceived: Access token returned for implicit or device flow";
|
||||||
|
setToken(response.value(OAUTH2_ACCESS_TOKEN));
|
||||||
|
if (response.contains(OAUTH2_EXPIRES_IN)) {
|
||||||
|
bool ok = false;
|
||||||
|
int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
qDebug() << "OAuth2::onVerificationReceived: Token expires in" << expiresIn << "seconds";
|
||||||
|
setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response.contains(OAUTH2_REFRESH_TOKEN)) {
|
||||||
|
setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
|
||||||
|
}
|
||||||
|
setLinked(true);
|
||||||
|
emit linkingSucceeded();
|
||||||
|
} else {
|
||||||
|
qWarning() << "OAuth2::onVerificationReceived: Access token missing from response for implicit or device flow";
|
||||||
|
emit linkingFailed();
|
||||||
|
}
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
} else {
|
||||||
|
setToken(response.value(OAUTH2_ACCESS_TOKEN));
|
||||||
|
setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::onTokenReplyFinished() {
|
||||||
|
qDebug() << "OAuth2::onTokenReplyFinished";
|
||||||
|
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!tokenReply)
|
||||||
|
{
|
||||||
|
qDebug() << "OAuth2::onTokenReplyFinished: reply is null";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tokenReply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray replyData = tokenReply->readAll();
|
||||||
|
|
||||||
|
// Dump replyData
|
||||||
|
// SENSITIVE DATA in RelWithDebInfo or Debug builds
|
||||||
|
//qDebug() << "OAuth2::onTokenReplyFinished: replyData\n";
|
||||||
|
//qDebug() << QString( replyData );
|
||||||
|
|
||||||
|
QVariantMap tokens = parseJsonResponse(replyData);
|
||||||
|
|
||||||
|
// Dump tokens
|
||||||
|
qDebug() << "OAuth2::onTokenReplyFinished: Tokens returned:\n";
|
||||||
|
foreach (QString key, tokens.keys()) {
|
||||||
|
// SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first
|
||||||
|
qDebug() << key << ": "<< tokens.value( key ).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mandatory tokens
|
||||||
|
if (tokens.contains(OAUTH2_ACCESS_TOKEN)) {
|
||||||
|
qDebug() << "OAuth2::onTokenReplyFinished: Access token returned";
|
||||||
|
setToken(tokens.take(OAUTH2_ACCESS_TOKEN).toString());
|
||||||
|
bool ok = false;
|
||||||
|
int expiresIn = tokens.take(OAUTH2_EXPIRES_IN).toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
qDebug() << "OAuth2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds";
|
||||||
|
setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
|
||||||
|
}
|
||||||
|
setRefreshToken(tokens.take(OAUTH2_REFRESH_TOKEN).toString());
|
||||||
|
setExtraTokens(tokens);
|
||||||
|
timedReplies_.remove(tokenReply);
|
||||||
|
setLinked(true);
|
||||||
|
emit linkingSucceeded();
|
||||||
|
} else {
|
||||||
|
qWarning() << "OAuth2::onTokenReplyFinished: Access token missing from response";
|
||||||
|
emit linkingFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokenReply->deleteLater();
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::onTokenReplyError(QNetworkReply::NetworkError error) {
|
||||||
|
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!tokenReply)
|
||||||
|
{
|
||||||
|
qDebug() << "OAuth2::onTokenReplyError: reply is null";
|
||||||
|
} else {
|
||||||
|
qWarning() << "OAuth2::onTokenReplyError: " << error << ": " << tokenReply->errorString();
|
||||||
|
qDebug() << "OAuth2::onTokenReplyError: " << tokenReply->readAll();
|
||||||
|
timedReplies_.remove(tokenReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
setToken(QString());
|
||||||
|
setRefreshToken(QString());
|
||||||
|
emit linkingFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray OAuth2::buildRequestBody(const QMap<QString, QString> ¶meters) {
|
||||||
|
QByteArray body;
|
||||||
|
bool first = true;
|
||||||
|
foreach (QString key, parameters.keys()) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
body.append("&");
|
||||||
|
}
|
||||||
|
QString value = parameters.value(key);
|
||||||
|
body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value));
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime OAuth2::expires() {
|
||||||
|
return token_.notAfter;
|
||||||
|
}
|
||||||
|
void OAuth2::setExpires(QDateTime v) {
|
||||||
|
token_.notAfter = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::startPollServer(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
qWarning() << "OAuth2::startPollServer: No expired_in parameter";
|
||||||
|
emit linkingFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds";
|
||||||
|
|
||||||
|
QUrl url(options_.accessTokenUrl);
|
||||||
|
QNetworkRequest authRequest(url);
|
||||||
|
authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString();
|
||||||
|
const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_;
|
||||||
|
|
||||||
|
QList<RequestParameter> parameters;
|
||||||
|
parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8()));
|
||||||
|
if ( !options_.clientSecret.isEmpty() ) {
|
||||||
|
parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8()));
|
||||||
|
}
|
||||||
|
parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8()));
|
||||||
|
parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8()));
|
||||||
|
QByteArray payload = createQueryParameters(parameters);
|
||||||
|
|
||||||
|
PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this);
|
||||||
|
if (params.contains(OAUTH2_INTERVAL)) {
|
||||||
|
int interval = params[OAUTH2_INTERVAL].toInt(&ok);
|
||||||
|
if (ok)
|
||||||
|
pollServer->setInterval(interval);
|
||||||
|
}
|
||||||
|
connect(pollServer, SIGNAL(verificationReceived(QMap<QString,QString>)), this, SLOT(onVerificationReceived(QMap<QString,QString>)));
|
||||||
|
connect(pollServer, SIGNAL(serverClosed(bool)), this, SLOT(serverHasClosed(bool)));
|
||||||
|
setPollServer(pollServer);
|
||||||
|
pollServer->startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuth2::refreshToken() {
|
||||||
|
return token_.refresh_token;
|
||||||
|
}
|
||||||
|
void OAuth2::setRefreshToken(const QString &v) {
|
||||||
|
qDebug() << "OAuth2::setRefreshToken" << v << "...";
|
||||||
|
token_.refresh_token = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OAuth2::refresh() {
|
||||||
|
qDebug() << "OAuth2::refresh: Token: ..." << refreshToken().right(7);
|
||||||
|
|
||||||
|
if (refreshToken().isEmpty()) {
|
||||||
|
qWarning() << "OAuth2::refresh: No refresh token";
|
||||||
|
onRefreshError(QNetworkReply::AuthenticationRequiredError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (options_.accessTokenUrl.isEmpty()) {
|
||||||
|
qWarning() << "OAuth2::refresh: Refresh token URL not set";
|
||||||
|
onRefreshError(QNetworkReply::AuthenticationRequiredError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActivity(Activity::Refreshing);
|
||||||
|
|
||||||
|
QNetworkRequest refreshRequest(options_.accessTokenUrl);
|
||||||
|
refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM);
|
||||||
|
QMap<QString, QString> parameters;
|
||||||
|
parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
|
||||||
|
if ( !options_.clientSecret.isEmpty() ) {
|
||||||
|
parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
|
||||||
|
}
|
||||||
|
parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken());
|
||||||
|
parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN);
|
||||||
|
|
||||||
|
QByteArray data = buildRequestBody(parameters);
|
||||||
|
QNetworkReply *refreshReply = manager_->post(refreshRequest, data);
|
||||||
|
timedReplies_.add(refreshReply);
|
||||||
|
connect(refreshReply, SIGNAL(finished()), this, SLOT(onRefreshFinished()), Qt::QueuedConnection);
|
||||||
|
connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::onRefreshFinished() {
|
||||||
|
QNetworkReply *refreshReply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
|
||||||
|
if (refreshReply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray reply = refreshReply->readAll();
|
||||||
|
QVariantMap tokens = parseJsonResponse(reply);
|
||||||
|
setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString());
|
||||||
|
setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt()));
|
||||||
|
QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString();
|
||||||
|
if(!refreshToken.isEmpty()) {
|
||||||
|
setRefreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qDebug() << "No new refresh token. Keep the old one.";
|
||||||
|
}
|
||||||
|
timedReplies_.remove(refreshReply);
|
||||||
|
setLinked(true);
|
||||||
|
emit linkingSucceeded();
|
||||||
|
emit refreshFinished(QNetworkReply::NoError);
|
||||||
|
qDebug() << " New token expires in" << expires() << "seconds";
|
||||||
|
} else {
|
||||||
|
qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString();
|
||||||
|
}
|
||||||
|
refreshReply->deleteLater();
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::onRefreshError(QNetworkReply::NetworkError error) {
|
||||||
|
QNetworkReply *refreshReply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
qWarning() << "OAuth2::onRefreshError: " << error;
|
||||||
|
unlink();
|
||||||
|
timedReplies_.remove(refreshReply);
|
||||||
|
emit refreshFinished(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::onDeviceAuthReplyFinished()
|
||||||
|
{
|
||||||
|
qDebug() << "OAuth2::onDeviceAuthReplyFinished";
|
||||||
|
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!tokenReply)
|
||||||
|
{
|
||||||
|
qDebug() << "OAuth2::onDeviceAuthReplyFinished: reply is null";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tokenReply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray replyData = tokenReply->readAll();
|
||||||
|
|
||||||
|
// Dump replyData
|
||||||
|
// SENSITIVE DATA in RelWithDebInfo or Debug builds
|
||||||
|
//qDebug() << "OAuth2::onDeviceAuthReplyFinished: replyData\n";
|
||||||
|
//qDebug() << QString( replyData );
|
||||||
|
|
||||||
|
QVariantMap params = parseJsonResponse(replyData);
|
||||||
|
|
||||||
|
// Dump tokens
|
||||||
|
qDebug() << "OAuth2::onDeviceAuthReplyFinished: Tokens returned:\n";
|
||||||
|
foreach (QString key, params.keys()) {
|
||||||
|
// SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first
|
||||||
|
qDebug() << key << ": "<< params.value( key ).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mandatory parameters
|
||||||
|
if (hasMandatoryDeviceAuthParams(params)) {
|
||||||
|
qDebug() << "OAuth2::onDeviceAuthReplyFinished: Device auth request response";
|
||||||
|
|
||||||
|
const QString userCode = params.take(OAUTH2_USER_CODE).toString();
|
||||||
|
QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl();
|
||||||
|
if (uri.isEmpty())
|
||||||
|
uri = params.take(OAUTH2_VERIFICATION_URL).toUrl();
|
||||||
|
|
||||||
|
if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
|
||||||
|
emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl());
|
||||||
|
|
||||||
|
emit showVerificationUriAndCode(uri, userCode);
|
||||||
|
|
||||||
|
startPollServer(params);
|
||||||
|
} else {
|
||||||
|
qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response";
|
||||||
|
emit linkingFailed();
|
||||||
|
updateActivity(Activity::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokenReply->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::serverHasClosed(bool paramsfound)
|
||||||
|
{
|
||||||
|
if ( !paramsfound ) {
|
||||||
|
// server has probably timed out after receiving first response
|
||||||
|
emit linkingFailed();
|
||||||
|
}
|
||||||
|
// poll server is not re-used for later auth requests
|
||||||
|
setPollServer(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OAuth2::apiKey() {
|
||||||
|
return apiKey_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setApiKey(const QString &value) {
|
||||||
|
apiKey_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OAuth2::ignoreSslErrors() {
|
||||||
|
return timedReplies_.ignoreSslErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OAuth2::setIgnoreSslErrors(bool ignoreSslErrors) {
|
||||||
|
timedReplies_.setIgnoreSslErrors(ignoreSslErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
123
libraries/katabasis/src/PollServer.cpp
Normal file
123
libraries/katabasis/src/PollServer.cpp
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "katabasis/PollServer.h"
|
||||||
|
#include "JsonResponse.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QMap<QString, QString> toVerificationParams(const QVariantMap &map)
|
||||||
|
{
|
||||||
|
QMap<QString, QString> params;
|
||||||
|
for (QVariantMap::const_iterator i = map.constBegin();
|
||||||
|
i != map.constEnd(); ++i)
|
||||||
|
{
|
||||||
|
params[i.key()] = i.value().toString();
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
PollServer::PollServer(QNetworkAccessManager *manager, const QNetworkRequest &request, const QByteArray &payload, int expiresIn, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, manager_(manager)
|
||||||
|
, request_(request)
|
||||||
|
, payload_(payload)
|
||||||
|
, expiresIn_(expiresIn)
|
||||||
|
{
|
||||||
|
expirationTimer.setTimerType(Qt::VeryCoarseTimer);
|
||||||
|
expirationTimer.setInterval(expiresIn * 1000);
|
||||||
|
expirationTimer.setSingleShot(true);
|
||||||
|
connect(&expirationTimer, SIGNAL(timeout()), this, SLOT(onExpiration()));
|
||||||
|
expirationTimer.start();
|
||||||
|
|
||||||
|
pollTimer.setTimerType(Qt::VeryCoarseTimer);
|
||||||
|
pollTimer.setInterval(5 * 1000);
|
||||||
|
pollTimer.setSingleShot(true);
|
||||||
|
connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int PollServer::interval() const
|
||||||
|
{
|
||||||
|
return pollTimer.interval() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollServer::setInterval(int interval)
|
||||||
|
{
|
||||||
|
pollTimer.setInterval(interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollServer::startPolling()
|
||||||
|
{
|
||||||
|
if (expirationTimer.isActive()) {
|
||||||
|
pollTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollServer::onPollTimeout()
|
||||||
|
{
|
||||||
|
qDebug() << "PollServer::onPollTimeout: retrying";
|
||||||
|
QNetworkReply * reply = manager_->post(request_, payload_);
|
||||||
|
connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollServer::onExpiration()
|
||||||
|
{
|
||||||
|
pollTimer.stop();
|
||||||
|
emit serverClosed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollServer::onReplyFinished()
|
||||||
|
{
|
||||||
|
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
|
||||||
|
if (!reply) {
|
||||||
|
qDebug() << "PollServer::onReplyFinished: reply is null";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray replyData = reply->readAll();
|
||||||
|
QMap<QString, QString> params = toVerificationParams(parseJsonResponse(replyData));
|
||||||
|
|
||||||
|
// Dump replyData
|
||||||
|
// SENSITIVE DATA in RelWithDebInfo or Debug builds
|
||||||
|
// qDebug() << "PollServer::onReplyFinished: replyData\n";
|
||||||
|
// qDebug() << QString( replyData );
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::TimeoutError) {
|
||||||
|
// rfc8628#section-3.2
|
||||||
|
// "On encountering a connection timeout, clients MUST unilaterally
|
||||||
|
// reduce their polling frequency before retrying. The use of an
|
||||||
|
// exponential backoff algorithm to achieve this, such as doubling the
|
||||||
|
// polling interval on each such connection timeout, is RECOMMENDED."
|
||||||
|
setInterval(interval() * 2);
|
||||||
|
pollTimer.start();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QString error = params.value("error");
|
||||||
|
if (error == "slow_down") {
|
||||||
|
// rfc8628#section-3.2
|
||||||
|
// "A variant of 'authorization_pending', the authorization request is
|
||||||
|
// still pending and polling should continue, but the interval MUST
|
||||||
|
// be increased by 5 seconds for this and all subsequent requests."
|
||||||
|
setInterval(interval() + 5);
|
||||||
|
pollTimer.start();
|
||||||
|
}
|
||||||
|
else if (error == "authorization_pending") {
|
||||||
|
// keep trying - rfc8628#section-3.2
|
||||||
|
// "The authorization request is still pending as the end user hasn't
|
||||||
|
// yet completed the user-interaction steps (Section 3.3)."
|
||||||
|
pollTimer.start();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expirationTimer.stop();
|
||||||
|
emit serverClosed(true);
|
||||||
|
// let O2 handle the other cases
|
||||||
|
emit verificationReceived(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
62
libraries/katabasis/src/Reply.cpp
Normal file
62
libraries/katabasis/src/Reply.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#include <QTimer>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "katabasis/Reply.h"
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
Reply::Reply(QNetworkReply *r, int timeOut, QObject *parent): QTimer(parent), reply(r) {
|
||||||
|
setSingleShot(true);
|
||||||
|
connect(this, SIGNAL(error(QNetworkReply::NetworkError)), reply, SIGNAL(error(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(this, SIGNAL(timeout()), this, SLOT(onTimeOut()), Qt::QueuedConnection);
|
||||||
|
start(timeOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reply::onTimeOut() {
|
||||||
|
emit error(QNetworkReply::TimeoutError);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyList::~ReplyList() {
|
||||||
|
foreach (Reply *timedReply, replies_) {
|
||||||
|
delete timedReply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyList::add(QNetworkReply *reply) {
|
||||||
|
if (reply && ignoreSslErrors())
|
||||||
|
reply->ignoreSslErrors();
|
||||||
|
add(new Reply(reply));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyList::add(Reply *reply) {
|
||||||
|
replies_.append(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyList::remove(QNetworkReply *reply) {
|
||||||
|
Reply *o2Reply = find(reply);
|
||||||
|
if (o2Reply) {
|
||||||
|
o2Reply->stop();
|
||||||
|
(void)replies_.removeOne(o2Reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Reply *ReplyList::find(QNetworkReply *reply) {
|
||||||
|
foreach (Reply *timedReply, replies_) {
|
||||||
|
if (timedReply->reply == reply) {
|
||||||
|
return timedReply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReplyList::ignoreSslErrors()
|
||||||
|
{
|
||||||
|
return ignoreSslErrors_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors)
|
||||||
|
{
|
||||||
|
ignoreSslErrors_ = ignoreSslErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
182
libraries/katabasis/src/ReplyServer.cpp
Executable file
182
libraries/katabasis/src/ReplyServer.cpp
Executable file
@ -0,0 +1,182 @@
|
|||||||
|
#include <QTcpServer>
|
||||||
|
#include <QTcpSocket>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "katabasis/Globals.h"
|
||||||
|
#include "katabasis/ReplyServer.h"
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
ReplyServer::ReplyServer(QObject *parent): QTcpServer(parent),
|
||||||
|
timeout_(15), maxtries_(3), tries_(0) {
|
||||||
|
qDebug() << "O2ReplyServer: Starting";
|
||||||
|
connect(this, SIGNAL(newConnection()), this, SLOT(onIncomingConnection()));
|
||||||
|
replyContent_ = "<HTML></HTML>";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::onIncomingConnection() {
|
||||||
|
qDebug() << "O2ReplyServer::onIncomingConnection: Receiving...";
|
||||||
|
QTcpSocket *socket = nextPendingConnection();
|
||||||
|
connect(socket, SIGNAL(readyRead()), this, SLOT(onBytesReady()), Qt::UniqueConnection);
|
||||||
|
connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater()));
|
||||||
|
|
||||||
|
// Wait for a bit *after* first response, then close server if no useable data has arrived
|
||||||
|
// Helps with implicit flow, where a URL fragment may need processed by local user-agent and
|
||||||
|
// sent as secondary query string callback, or additional requests make it through first,
|
||||||
|
// like for favicons, etc., before such secondary callbacks are fired
|
||||||
|
QTimer *timer = new QTimer(socket);
|
||||||
|
timer->setObjectName("timeoutTimer");
|
||||||
|
connect(timer, SIGNAL(timeout()), this, SLOT(closeServer()));
|
||||||
|
timer->setSingleShot(true);
|
||||||
|
timer->setInterval(timeout() * 1000);
|
||||||
|
connect(socket, SIGNAL(readyRead()), timer, SLOT(start()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::onBytesReady() {
|
||||||
|
if (!isListening()) {
|
||||||
|
// server has been closed, stop processing queued connections
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qDebug() << "O2ReplyServer::onBytesReady: Processing request";
|
||||||
|
// NOTE: on first call, the timeout timer is started
|
||||||
|
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
|
||||||
|
if (!socket) {
|
||||||
|
qWarning() << "O2ReplyServer::onBytesReady: No socket available";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QByteArray reply;
|
||||||
|
reply.append("HTTP/1.0 200 OK \r\n");
|
||||||
|
reply.append("Content-Type: text/html; charset=\"utf-8\"\r\n");
|
||||||
|
reply.append(QString("Content-Length: %1\r\n\r\n").arg(replyContent_.size()).toLatin1());
|
||||||
|
reply.append(replyContent_);
|
||||||
|
socket->write(reply);
|
||||||
|
qDebug() << "O2ReplyServer::onBytesReady: Sent reply";
|
||||||
|
|
||||||
|
QByteArray data = socket->readAll();
|
||||||
|
QMap<QString, QString> queryParams = parseQueryParams(&data);
|
||||||
|
if (queryParams.isEmpty()) {
|
||||||
|
if (tries_ < maxtries_ ) {
|
||||||
|
qDebug() << "O2ReplyServer::onBytesReady: No query params found, waiting for more callbacks";
|
||||||
|
++tries_;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
tries_ = 0;
|
||||||
|
qWarning() << "O2ReplyServer::onBytesReady: No query params found, maximum callbacks received";
|
||||||
|
closeServer(socket, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!uniqueState_.isEmpty() && !queryParams.contains(QString(OAUTH2_STATE))) {
|
||||||
|
qDebug() << "O2ReplyServer::onBytesReady: Malicious or service request";
|
||||||
|
closeServer(socket, true);
|
||||||
|
return; // Malicious or service (e.g. favicon.ico) request
|
||||||
|
}
|
||||||
|
qDebug() << "O2ReplyServer::onBytesReady: Query params found, closing server";
|
||||||
|
closeServer(socket, true);
|
||||||
|
emit verificationReceived(queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
QMap<QString, QString> ReplyServer::parseQueryParams(QByteArray *data) {
|
||||||
|
qDebug() << "O2ReplyServer::parseQueryParams";
|
||||||
|
|
||||||
|
//qDebug() << QString("O2ReplyServer::parseQueryParams data:\n%1").arg(QString(*data));
|
||||||
|
|
||||||
|
QString splitGetLine = QString(*data).split("\r\n").first();
|
||||||
|
splitGetLine.remove("GET ");
|
||||||
|
splitGetLine.remove("HTTP/1.1");
|
||||||
|
splitGetLine.remove("\r\n");
|
||||||
|
splitGetLine.prepend("http://localhost");
|
||||||
|
QUrl getTokenUrl(splitGetLine);
|
||||||
|
|
||||||
|
QList< QPair<QString, QString> > tokens;
|
||||||
|
QUrlQuery query(getTokenUrl);
|
||||||
|
tokens = query.queryItems();
|
||||||
|
QMap<QString, QString> queryParams;
|
||||||
|
QPair<QString, QString> tokenPair;
|
||||||
|
foreach (tokenPair, tokens) {
|
||||||
|
// FIXME: We are decoding key and value again. This helps with Google OAuth, but is it mandated by the standard?
|
||||||
|
QString key = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.first.trimmed().toLatin1()));
|
||||||
|
QString value = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.second.trimmed().toLatin1()));
|
||||||
|
queryParams.insert(key, value);
|
||||||
|
}
|
||||||
|
return queryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::closeServer(QTcpSocket *socket, bool hasparameters)
|
||||||
|
{
|
||||||
|
if (!isListening()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "O2ReplyServer::closeServer: Initiating";
|
||||||
|
int port = serverPort();
|
||||||
|
|
||||||
|
if (!socket && sender()) {
|
||||||
|
QTimer *timer = qobject_cast<QTimer*>(sender());
|
||||||
|
if (timer) {
|
||||||
|
qWarning() << "O2ReplyServer::closeServer: Closing due to timeout";
|
||||||
|
timer->stop();
|
||||||
|
socket = qobject_cast<QTcpSocket *>(timer->parent());
|
||||||
|
timer->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (socket) {
|
||||||
|
QTimer *timer = socket->findChild<QTimer*>("timeoutTimer");
|
||||||
|
if (timer) {
|
||||||
|
qDebug() << "O2ReplyServer::closeServer: Stopping socket's timeout timer";
|
||||||
|
timer->stop();
|
||||||
|
}
|
||||||
|
socket->disconnectFromHost();
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
qDebug() << "O2ReplyServer::closeServer: Closed, no longer listening on port" << port;
|
||||||
|
emit serverClosed(hasparameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray ReplyServer::replyContent() {
|
||||||
|
return replyContent_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::setReplyContent(const QByteArray &value) {
|
||||||
|
replyContent_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReplyServer::timeout()
|
||||||
|
{
|
||||||
|
return timeout_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::setTimeout(int timeout)
|
||||||
|
{
|
||||||
|
timeout_ = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReplyServer::callbackTries()
|
||||||
|
{
|
||||||
|
return maxtries_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::setCallbackTries(int maxtries)
|
||||||
|
{
|
||||||
|
maxtries_ = maxtries;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ReplyServer::uniqueState()
|
||||||
|
{
|
||||||
|
return uniqueState_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyServer::setUniqueState(const QString &state)
|
||||||
|
{
|
||||||
|
uniqueState_ = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
304
libraries/katabasis/src/Requestor.cpp
Normal file
304
libraries/katabasis/src/Requestor.cpp
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "katabasis/Requestor.h"
|
||||||
|
#include "katabasis/OAuth2.h"
|
||||||
|
#include "katabasis/Globals.h"
|
||||||
|
|
||||||
|
namespace Katabasis {
|
||||||
|
|
||||||
|
Requestor::Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent): QObject(parent), reply_(NULL), status_(Idle), addAccessTokenInQuery_(true), rawData_(false) {
|
||||||
|
manager_ = manager;
|
||||||
|
authenticator_ = authenticator;
|
||||||
|
if (authenticator) {
|
||||||
|
timedReplies_.setIgnoreSslErrors(authenticator->ignoreSslErrors());
|
||||||
|
}
|
||||||
|
qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
|
||||||
|
connect(authenticator, &OAuth2::refreshFinished, this, &Requestor::onRefreshFinished, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
Requestor::~Requestor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::setAddAccessTokenInQuery(bool value) {
|
||||||
|
addAccessTokenInQuery_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value) {
|
||||||
|
accessTokenInAuthenticationHTTPHeaderFormat_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) {
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::GetOperation)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
reply_ = manager_->get(request_);
|
||||||
|
timedReplies_.add(new Reply(reply_, timeout));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) {
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::PostOperation)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rawData_ = true;
|
||||||
|
data_ = data;
|
||||||
|
reply_ = manager_->post(request_, data_);
|
||||||
|
timedReplies_.add(new Reply(reply_, timeout));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::post(const QNetworkRequest & req, QHttpMultiPart* data, int timeout/* = 60*1000*/)
|
||||||
|
{
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::PostOperation)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rawData_ = false;
|
||||||
|
multipartData_ = data;
|
||||||
|
reply_ = manager_->post(request_, multipartData_);
|
||||||
|
multipartData_->setParent(reply_);
|
||||||
|
timedReplies_.add(new Reply(reply_, timeout));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::put(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) {
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::PutOperation)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rawData_ = true;
|
||||||
|
data_ = data;
|
||||||
|
reply_ = manager_->put(request_, data_);
|
||||||
|
timedReplies_.add(new Reply(reply_, timeout));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::put(const QNetworkRequest & req, QHttpMultiPart* data, int timeout/* = 60*1000*/)
|
||||||
|
{
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::PutOperation)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rawData_ = false;
|
||||||
|
multipartData_ = data;
|
||||||
|
reply_ = manager_->put(request_, multipartData_);
|
||||||
|
multipartData_->setParent(reply_);
|
||||||
|
timedReplies_.add(new Reply(reply_, timeout));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout/* = 60*1000*/)
|
||||||
|
{
|
||||||
|
(void)timeout;
|
||||||
|
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::CustomOperation, verb)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
data_ = data;
|
||||||
|
QBuffer * buffer = new QBuffer;
|
||||||
|
buffer->setData(data_);
|
||||||
|
reply_ = manager_->sendCustomRequest(request_, verb, buffer);
|
||||||
|
buffer->setParent(reply_);
|
||||||
|
timedReplies_.add(new Reply(reply_));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::head(const QNetworkRequest &req, int timeout/* = 60*1000*/)
|
||||||
|
{
|
||||||
|
if (-1 == setup(req, QNetworkAccessManager::HeadOperation)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
reply_ = manager_->head(request_);
|
||||||
|
timedReplies_.add(new Reply(reply_, timeout));
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::onRefreshFinished(QNetworkReply::NetworkError error) {
|
||||||
|
if (status_ != Requesting) {
|
||||||
|
qWarning() << "O2Requestor::onRefreshFinished: No pending request";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (QNetworkReply::NoError == error) {
|
||||||
|
QTimer::singleShot(100, this, &Requestor::retry);
|
||||||
|
} else {
|
||||||
|
error_ = error;
|
||||||
|
QTimer::singleShot(10, this, &Requestor::finish);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::onRequestFinished() {
|
||||||
|
if (status_ == Idle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply_->error() == QNetworkReply::NoError) {
|
||||||
|
QTimer::singleShot(10, this, SLOT(finish()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::onRequestError(QNetworkReply::NetworkError error) {
|
||||||
|
qWarning() << "O2Requestor::onRequestError: Error" << (int)error;
|
||||||
|
if (status_ == Idle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
qWarning() << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
|
||||||
|
if ((status_ == Requesting) && (httpStatus == 401)) {
|
||||||
|
// Call OAuth2::refresh. Note the O2 instance might live in a different thread
|
||||||
|
if (QMetaObject::invokeMethod(authenticator_, "refresh")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qCritical() << "O2Requestor::onRequestError: Invoking remote refresh failed";
|
||||||
|
}
|
||||||
|
error_ = error;
|
||||||
|
QTimer::singleShot(10, this, SLOT(finish()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::onUploadProgress(qint64 uploaded, qint64 total) {
|
||||||
|
if (status_ == Idle) {
|
||||||
|
qWarning() << "O2Requestor::onUploadProgress: No pending request";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Restart timeout because request in progress
|
||||||
|
Reply *o2Reply = timedReplies_.find(reply_);
|
||||||
|
if(o2Reply)
|
||||||
|
o2Reply->start();
|
||||||
|
emit uploadProgress(id_, uploaded, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Requestor::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) {
|
||||||
|
static int currentId;
|
||||||
|
|
||||||
|
if (status_ != Idle) {
|
||||||
|
qWarning() << "O2Requestor::setup: Another request pending";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
request_ = req;
|
||||||
|
operation_ = operation;
|
||||||
|
id_ = currentId++;
|
||||||
|
url_ = req.url();
|
||||||
|
|
||||||
|
QUrl url = url_;
|
||||||
|
if (addAccessTokenInQuery_) {
|
||||||
|
QUrlQuery query(url);
|
||||||
|
query.addQueryItem(OAUTH2_ACCESS_TOKEN, authenticator_->token());
|
||||||
|
url.setQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
request_.setUrl(url);
|
||||||
|
|
||||||
|
// If the service require the access token to be sent as a Authentication HTTP header, we add the access token.
|
||||||
|
if (!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) {
|
||||||
|
request_.setRawHeader(HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verb.isEmpty()) {
|
||||||
|
request_.setRawHeader(HTTP_HTTP_HEADER, verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
status_ = Requesting;
|
||||||
|
error_ = QNetworkReply::NoError;
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::finish() {
|
||||||
|
QByteArray data;
|
||||||
|
if (status_ == Idle) {
|
||||||
|
qWarning() << "O2Requestor::finish: No pending request";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = reply_->readAll();
|
||||||
|
status_ = Idle;
|
||||||
|
timedReplies_.remove(reply_);
|
||||||
|
reply_->disconnect(this);
|
||||||
|
reply_->deleteLater();
|
||||||
|
QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs();
|
||||||
|
emit finished(id_, error_, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Requestor::retry() {
|
||||||
|
if (status_ != Requesting) {
|
||||||
|
qWarning() << "O2Requestor::retry: No pending request";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timedReplies_.remove(reply_);
|
||||||
|
reply_->disconnect(this);
|
||||||
|
reply_->deleteLater();
|
||||||
|
QUrl url = url_;
|
||||||
|
if (addAccessTokenInQuery_) {
|
||||||
|
QUrlQuery query(url);
|
||||||
|
query.addQueryItem(OAUTH2_ACCESS_TOKEN, authenticator_->token());
|
||||||
|
url.setQuery(query);
|
||||||
|
}
|
||||||
|
request_.setUrl(url);
|
||||||
|
|
||||||
|
// If the service require the access token to be sent as a Authentication HTTP header,
|
||||||
|
// we update the access token when retrying.
|
||||||
|
if(!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) {
|
||||||
|
request_.setRawHeader(HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1());
|
||||||
|
}
|
||||||
|
|
||||||
|
status_ = ReRequesting;
|
||||||
|
switch (operation_) {
|
||||||
|
case QNetworkAccessManager::GetOperation:
|
||||||
|
reply_ = manager_->get(request_);
|
||||||
|
break;
|
||||||
|
case QNetworkAccessManager::PostOperation:
|
||||||
|
reply_ = rawData_ ? manager_->post(request_, data_) : manager_->post(request_, multipartData_);
|
||||||
|
break;
|
||||||
|
case QNetworkAccessManager::CustomOperation:
|
||||||
|
{
|
||||||
|
QBuffer * buffer = new QBuffer;
|
||||||
|
buffer->setData(data_);
|
||||||
|
reply_ = manager_->sendCustomRequest(request_, request_.rawHeader(HTTP_HTTP_HEADER), buffer);
|
||||||
|
buffer->setParent(reply_);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QNetworkAccessManager::PutOperation:
|
||||||
|
reply_ = rawData_ ? manager_->post(request_, data_) : manager_->put(request_, multipartData_);
|
||||||
|
break;
|
||||||
|
case QNetworkAccessManager::HeadOperation:
|
||||||
|
reply_ = manager_->head(request_);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(!"Unspecified operation for request");
|
||||||
|
reply_ = manager_->get(request_);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
timedReplies_.add(reply_);
|
||||||
|
connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
|
||||||
|
connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user