text: /* Copyright 2013-2021 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 "Yggdrasil.h"
  #include "AccountData.h"
  
  #include <QObject>
  #include <QString>
  #include <QJsonObject>
  #include <QJsonDocument>
  #include <QNetworkReply>
  #include <QByteArray>
  
  #include <QDebug>
  
  #include "Application.h"
  
  Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
      : AccountTask(data, parent)
  {
      changeState(AccountTaskState::STATE_CREATED);
  }
  
  void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
      changeState(AccountTaskState::STATE_WORKING);
  
      QNetworkRequest netRequest(endpoint);
      netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
      m_netReply = APPLICATION->network()->post(netRequest, content);
      connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
      connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
      connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
      connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
      timeout_keeper.setSingleShot(true);
      timeout_keeper.start(timeout_max);
      counter.setSingleShot(false);
      counter.start(time_step);
      progress(0, timeout_max);
      connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout);
      connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
  }
  
  void Yggdrasil::executeTask() {
  }
  
  void Yggdrasil::login(QString password) {
      start();
      /*
       * {
       *   "agent": {                              // optional
       *   "name": "Minecraft",                    // So far this is the only encountered value
       *   "version": 1                            // This number might be increased
       *                                           // by the vanilla client in the future
       *   },
       *   "username": "mojang account name",      // Can be an email address or player name for
       *                                           // unmigrated accounts
       *   "password": "mojang account password",
       *   "clientToken": "client identifier",     // optional
       *   "requestUser": true/false               // request the user structure
       * }
       */
      QJsonObject req;
  
      {
          QJsonObject agent;
          // C++ makes string literals void* for some stupid reason, so we have to tell it
          // QString... Thanks Obama.
          agent.insert("name", QString("Minecraft"));
          agent.insert("version", 1);
          req.insert("agent", agent);
      }
  
      req.insert("username", m_data->userName());
      req.insert("password", password);
      req.insert("requestUser", false);
  
      // we literally don't care about the client token since its usage is locked
      req.insert("clientToken", QUuid::createUuid().toString().remove(QRegExp("[{-}]")));
  
      QJsonDocument doc(req);
  
      QUrl reqUrl("https://authserver.mojang.com/authenticate");
      QNetworkRequest netRequest(reqUrl);
      QByteArray requestData = doc.toJson();
  
      sendRequest(reqUrl, requestData);
  }
  
  
  
  void Yggdrasil::refreshTimers(qint64, qint64) {
      timeout_keeper.stop();
      timeout_keeper.start(timeout_max);
      progress(count = 0, timeout_max);
  }
  
  void Yggdrasil::heartbeat() {
      count += time_step;
      progress(count, timeout_max);
  }
  
  bool Yggdrasil::abort() {
      progress(timeout_max, timeout_max);
      // TODO: actually use this in a meaningful way
      m_aborted = Yggdrasil::BY_USER;
      m_netReply->abort();
      return true;
  }
  
  void Yggdrasil::abortByTimeout() {
      progress(timeout_max, timeout_max);
      // TODO: actually use this in a meaningful way
      m_aborted = Yggdrasil::BY_TIMEOUT;
      m_netReply->abort();
  }
  
  void Yggdrasil::sslErrors(QList<QSslError> errors) {
      int i = 1;
      for (auto error : errors) {
          qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
          auto cert = error.certificate();
          qCritical() << "Certificate in question:\n" << cert.toText();
          i++;
      }
  }
  
  void Yggdrasil::processResponse(QJsonObject responseData) {
      // Read the response data. We need to get the client token, access token, and the selected
      // profile.
      qDebug() << "Processing authentication response.";
  
      // qDebug() << responseData;
      // If we already have a client token, make sure the one the server gave us matches our
      // existing one.
      QString clientToken = responseData.value("clientToken").toString("");
      if (clientToken.isEmpty()) {
          // Fail if the server gave us an empty client token
          changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
          return;
      }
  
      // Now, we set the access token.
      qDebug() << "Getting access token.";
      QString accessToken = responseData.value("accessToken").toString("");
      if (accessToken.isEmpty()) {
          // Fail if the server didn't give us an access token.
          changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
          return;
      }
      // Set the access token and account name.
      m_data->token = accessToken;
  
      // Get UUID here since we need it for later
      auto profile = responseData.value("selectedProfile");
      if (!profile.isObject()) {
          changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile."));
          return;
      }
  
      auto profileObj = profile.toObject();
      for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
          if (i.key() == "name" && i.value().isString()) {
              m_data->minecraftProfile.name = i->toString();
          }
          else if (i.key() == "id" && i.value().isString()) {
              m_data->minecraftProfile.id = i->toString();
          }
      }
  
      if (m_data->minecraftProfile.id.isEmpty()) {
          changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
          return;
      }
  
      // We've made it through the minefield of possible errors. Return true to indicate that
      // we've succeeded.
      qDebug() << "Finished reading authentication response.";
      changeState(AccountTaskState::STATE_SUCCEEDED);
  }
  
  void Yggdrasil::processReply() {
      changeState(AccountTaskState::STATE_WORKING);
  
      switch (m_netReply->error())
      {
      case QNetworkReply::NoError:
          break;
      case QNetworkReply::TimeoutError:
          changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
          return;
      case QNetworkReply::OperationCanceledError:
          changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
          return;
      case QNetworkReply::SslHandshakeFailedError:
          changeState(
              AccountTaskState::STATE_FAILED_SOFT,
              tr(
                  "<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
                  "<ul>"
                  "<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>"
                  "<li>Some device on your network is interfering with SSL traffic. In that case, "
                  "you have bigger worries than Minecraft not starting.</li>"
                  "<li>Possibly something else. Check the log file for details</li>"
                  "</ul>"
              )
          );
          return;
      // used for invalid credentials and similar errors. Fall through.
      case QNetworkReply::ContentAccessDenied:
      case QNetworkReply::ContentOperationNotPermittedError:
          break;
      case QNetworkReply::ContentGoneError: {
          changeState(
              AccountTaskState::STATE_FAILED_GONE,
              tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
          );
      }
      default:
          changeState(
              AccountTaskState::STATE_FAILED_SOFT,
              tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error())
          );
          return;
      }
  
      // Try to parse the response regardless of the response code.
      // Sometimes the auth server will give more information and an error code.
      QJsonParseError jsonError;
      QByteArray replyData = m_netReply->readAll();
      QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
      // Check the response code.
      int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
  
      if (responseCode == 200) {
          // If the response code was 200, then there shouldn't be an error. Make sure
          // anyways.
          // Also, sometimes an empty reply indicates success. If there was no data received,
          // pass an empty json object to the processResponse function.
          if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) {
              processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
              return;
          }
          else {
              changeState(
                  AccountTaskState::STATE_FAILED_SOFT,
                  tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)
              );
              qCritical() << replyData;
          }
          return;
      }
  
      // If the response code was not 200, then Yggdrasil may have given us information
      // about the error.
      // If we can parse the response, then get information from it. Otherwise just say
      // there was an unknown error.
      if (jsonError.error == QJsonParseError::NoError) {
          // We were able to parse the server's response. Woo!
          // Call processError. If a subclass has overridden it then they'll handle their
          // stuff there.
          qDebug() << "The request failed, but the server gave us an error message. Processing error.";
          processError(doc.object());
      }
      else {
          // The server didn't say anything regarding the error. Give the user an unknown
          // error.
          qDebug() << "The request failed and the server gave no error message. Unknown error.";
          changeState(
              AccountTaskState::STATE_FAILED_SOFT,
              tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString())
          );
      }
  }
  
  void Yggdrasil::processError(QJsonObject responseData) {
      QJsonValue errorVal = responseData.value("error");
      QJsonValue errorMessageValue = responseData.value("errorMessage");
      QJsonValue causeVal = responseData.value("cause");
  
      if (errorVal.isString() && errorMessageValue.isString()) {
          m_error = std::shared_ptr<Error>(
              new Error {
                  errorVal.toString(""),
                  errorMessageValue.toString(""),
                  causeVal.toString("")
              }
          );
          changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
      }
      else {
          // Error is not in standard format. Don't set m_error and return unknown error.
          qDebug() << responseData;
          changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
      }
  }
  