NOISSUE bulk addition of code from Katabasis

This commit is contained in:
Petr Mrázek 2021-07-22 20:15:20 +02:00
parent 2568752af5
commit dd13368085
31 changed files with 3687 additions and 18 deletions

View File

@ -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/optional-bare)
add_subdirectory(libraries/tomlc99) # toml parser
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
############################### Built Artifacts ###############################

View File

@ -254,25 +254,51 @@
# tomlc99
MIT License
MIT License
Copyright (c) 2017 CK Tan
https://github.com/cktan/tomlc99
Copyright (c) 2017 CK Tan
https://github.com/cktan/tomlc99
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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
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.

View File

@ -0,0 +1,9 @@
#include "BuildConfig.h"
#include <QObject>
const Config BuildConfig;
Config::Config()
{
CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
}

View File

@ -0,0 +1,11 @@
#pragma once
#include <QString>
class Config
{
public:
Config();
QString CLIENT_ID;
};
extern const Config BuildConfig;

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

View 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);
}

View 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;
};

View 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"

View 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();
}

View 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;
};

View 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
View File

@ -0,0 +1,2 @@
build/
*.kdev4

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

View 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.

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

View 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

View 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;
};
}

View 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";
}

View 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> &parameters);
/// Set refresh token.
void setRefreshToken(const QString &v);
/// Set token expiration time.
void setExpires(QDateTime v);
/// Start polling authorization server
void startPollServer(const QVariantMap &params);
/// 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;
};
}

View 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;
};
}

View 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_;
};
}

View 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_;
};
}

View 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;
};
}

View 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_;
};
}

View 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();
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include <QVariantMap>
class QByteArray;
namespace Katabasis {
/// Parse JSON data into a QVariantMap
QVariantMap parseJsonResponse(const QByteArray &data);
}

View 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> &parameters) {
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> &parameters) {
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 &params)
{
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);
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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)));
}
}