From 984c36e571aae45cdd55da2fb689538198aadd3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 23 Sep 2013 00:23:50 +0200 Subject: Implement basic yggdrasil auth. No fancy login token saving involved. --- logic/net/LoginTask.cpp | 269 ++++++++++++++++++++++++++++++++++++++++++++++++ logic/net/LoginTask.h | 62 +++++++++++ 2 files changed, 331 insertions(+) create mode 100644 logic/net/LoginTask.cpp create mode 100644 logic/net/LoginTask.h (limited to 'logic/net') diff --git a/logic/net/LoginTask.cpp b/logic/net/LoginTask.cpp new file mode 100644 index 00000000..2a45400e --- /dev/null +++ b/logic/net/LoginTask.cpp @@ -0,0 +1,269 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LoginTask.h" +#include "MultiMC.h" +#include + +#include + +#include +#include + +#include +#include +#include +#include + +LoginTask::LoginTask(const UserInfo &uInfo, QObject *parent) : Task(parent), uInfo(uInfo) +{ +} + +void LoginTask::executeTask() +{ + yggdrasilLogin(); +} + +void LoginTask::legacyLogin() +{ + setStatus(tr("Logging in...")); + auto worker = MMC->qnam(); + connect(worker.data(), SIGNAL(finished(QNetworkReply *)), this, + SLOT(processLegacyReply(QNetworkReply *))); + + QUrl loginURL("https://login.minecraft.net/"); + QNetworkRequest netRequest(loginURL); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, + "application/x-www-form-urlencoded"); + + QUrlQuery params; + params.addQueryItem("user", uInfo.username); + params.addQueryItem("password", uInfo.password); + params.addQueryItem("version", "13"); + + netReply = worker->post(netRequest, params.query(QUrl::EncodeSpaces).toUtf8()); +} + +void LoginTask::processLegacyReply(QNetworkReply *reply) +{ + if (netReply != reply) + return; + // Check for errors. + switch (reply->error()) + { + case QNetworkReply::NoError: + { + // Check the response code. + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) + { + parseLegacyReply(reply->readAll()); + } + else if (responseCode == 503) + { + emitFailed(tr("The login servers are currently unavailable. Check " + "http://help.mojang.com/ for more info.")); + } + else + { + emitFailed(tr("Login failed: Unknown HTTP error %1 occurred.") + .arg(QString::number(responseCode))); + } + break; + } + + case QNetworkReply::OperationCanceledError: + emitFailed(tr("Login canceled.")); + break; + + default: + emitFailed(tr("Login failed: %1").arg(reply->errorString())); + break; + } +} + +void LoginTask::parseLegacyReply(QByteArray data) +{ + QString responseStr = QString::fromUtf8(data); + + QStringList strings = responseStr.split(":"); + if (strings.count() >= 4) + { + // strings[1] is the download ticket. It isn't used anymore. + QString username = strings[2]; + QString sessionID = strings[3]; + /* + struct LoginResponse + { + QString username; + QString session_id; + QString player_name; + QString player_id; + QString client_id; + }; + */ + result = {username, sessionID, username, QString()}; + emitSucceeded(); + } + else + { + if (responseStr.toLower() == "bad login") + emitFailed(tr("Invalid username or password.")); + else if (responseStr.toLower() == "old version") + emitFailed(tr("Launcher outdated, please update.")); + else + emitFailed(tr("Login failed: %1").arg(responseStr)); + } +} + + +void LoginTask::yggdrasilLogin() +{ + setStatus(tr("Logging in...")); + auto worker = MMC->qnam(); + connect(worker.data(), SIGNAL(finished(QNetworkReply *)), this, + SLOT(processYggdrasilReply(QNetworkReply *))); + + /* + { + // agent def. version might be incremented at some point + "agent":{"name":"Minecraft","version":1}, + "username": "mojang account name", + "password": "mojang account password", + // client token is optional. but we supply one anyway + "clientToken": "client identifier" + } + */ + + QUrl loginURL("https://authserver.mojang.com/authenticate"); + QNetworkRequest netRequest(loginURL); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + auto settings = MMC->settings(); + QString clientToken = settings->get("YggdrasilClientToken").toString(); + // escape the {} + clientToken.remove('{'); + clientToken.remove('}'); + // create the request + QString requestConstent; + requestConstent += "{"; + requestConstent += " \"agent\":{\"name\":\"Minecraft\",\"version\":1},\n"; + requestConstent += " \"username\":\"" + uInfo.username + "\",\n"; + requestConstent += " \"password\":\"" + uInfo.password + "\",\n"; + requestConstent += " \"clientToken\":\"" + clientToken + "\"\n"; + requestConstent += "}"; + netReply = worker->post(netRequest, requestConstent.toUtf8()); +} + +void LoginTask::processYggdrasilReply(QNetworkReply *reply) +{ + if (netReply != reply) + return; + // Check for errors. + switch (reply->error()) + { + case QNetworkReply::NoError: + { + // Check the response code. + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) + { + parseYggdrasilReply(reply->readAll()); + } + else if (responseCode == 503) + { + emitFailed(tr("The login servers are currently unavailable. Check " + "http://help.mojang.com/ for more info.")); + } + else + { + emitFailed(tr("Login failed: Unknown HTTP error %1 occurred.") + .arg(QString::number(responseCode))); + } + break; + } + + case QNetworkReply::OperationCanceledError: + emitFailed(tr("Login canceled.")); + break; + + default: + emitFailed(tr("Login failed: %1").arg(reply->errorString())); + break; + } +} + +/* +{ + "accessToken": "random access token", // hexadecimal + "clientToken": "client identifier", // identical to the one received + "availableProfiles": [ // only present if the agent field was received + { + "id": "profile identifier", // hexadecimal + "name": "player name" + } + ], + "selectedProfile": { // only present if the agent field was received + "id": "profile identifier", + "name": "player name" + } +} +*/ +void LoginTask::parseYggdrasilReply(QByteArray data) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed(tr("Login failed: %1").arg(jsonError.errorString())); + return; + } + + if (!jsonDoc.isObject()) + { + emitFailed(tr("Login failed: BAD FORMAT #1")); + return; + } + + QJsonObject root = jsonDoc.object(); + + QString accessToken = root.value("accessToken").toString(); + QString clientToken = root.value("clientToken").toString(); + QString playerID; + QString playerName; + auto selectedProfile = root.value("selectedProfile"); + if(selectedProfile.isObject()) + { + auto selectedProfileO = selectedProfile.toObject(); + playerID = selectedProfileO.value("id").toString(); + playerName = selectedProfileO.value("name").toString(); + } + QString sessionID = "token:" + accessToken + ":" + playerID; + /* + struct LoginResponse + { + QString username; + QString session_id; + QString player_name; + QString player_id; + QString client_id; + }; + */ + + result = {uInfo.username, sessionID, playerName, playerID}; + emitSucceeded(); +} diff --git a/logic/net/LoginTask.h b/logic/net/LoginTask.h new file mode 100644 index 00000000..ba87142d --- /dev/null +++ b/logic/net/LoginTask.h @@ -0,0 +1,62 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "logic/tasks/Task.h" +#include + +struct UserInfo +{ + QString username; + QString password; +}; + +struct LoginResponse +{ + QString username; + QString session_id; + QString player_name; + QString player_id; +}; + +class QNetworkReply; + +class LoginTask : public Task +{ + Q_OBJECT +public: + explicit LoginTask(const UserInfo &uInfo, QObject *parent = 0); + LoginResponse getResult() + { + return result; + } + +protected slots: + void legacyLogin(); + void processLegacyReply(QNetworkReply *reply); + void parseLegacyReply(QByteArray data); + + void yggdrasilLogin(); + void processYggdrasilReply(QNetworkReply *reply); + void parseYggdrasilReply(QByteArray data); + +protected: + void executeTask(); + + LoginResponse result; + QNetworkReply *netReply; + UserInfo uInfo; +}; -- cgit v1.2.3