diff options
Diffstat (limited to 'logic/net')
-rw-r--r-- | logic/net/ByteArrayDownload.cpp | 65 | ||||
-rw-r--r-- | logic/net/ByteArrayDownload.h | 24 | ||||
-rw-r--r-- | logic/net/CacheDownload.cpp | 136 | ||||
-rw-r--r-- | logic/net/CacheDownload.h | 34 | ||||
-rw-r--r-- | logic/net/Download.h | 54 | ||||
-rw-r--r-- | logic/net/DownloadJob.cpp | 222 | ||||
-rw-r--r-- | logic/net/DownloadJob.h | 124 | ||||
-rw-r--r-- | logic/net/FileDownload.cpp | 112 | ||||
-rw-r--r-- | logic/net/FileDownload.h | 35 | ||||
-rw-r--r-- | logic/net/ForgeXzDownload.cpp | 279 | ||||
-rw-r--r-- | logic/net/ForgeXzDownload.h | 35 | ||||
-rw-r--r-- | logic/net/HttpMetaCache.cpp | 165 | ||||
-rw-r--r-- | logic/net/HttpMetaCache.h | 39 | ||||
-rw-r--r-- | logic/net/LoginTask.cpp | 285 | ||||
-rw-r--r-- | logic/net/LoginTask.h | 67 |
15 files changed, 1421 insertions, 255 deletions
diff --git a/logic/net/ByteArrayDownload.cpp b/logic/net/ByteArrayDownload.cpp new file mode 100644 index 00000000..ba771eef --- /dev/null +++ b/logic/net/ByteArrayDownload.cpp @@ -0,0 +1,65 @@ +#include "ByteArrayDownload.h" +#include "MultiMC.h" +#include <logger/QsLog.h> + +ByteArrayDownload::ByteArrayDownload(QUrl url) : Download() +{ + m_url = url; + m_status = Job_NotStarted; +} + +void ByteArrayDownload::start() +{ + QLOG_INFO() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + auto worker = MMC->qnam(); + QNetworkReply *rep = worker->get(request); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); +} + +void ByteArrayDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + emit progress(index_within_job, bytesReceived, bytesTotal); +} + +void ByteArrayDownload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + QLOG_ERROR() << "Error getting URL:" << m_url.toString().toLocal8Bit() + << "Network error: " << error; + m_status = Job_Failed; +} + +void ByteArrayDownload::downloadFinished() +{ + // if the download succeeded + if (m_status != Job_Failed) + { + // nothing went wrong... + m_status = Job_Finished; + m_data = m_reply->readAll(); + m_reply.reset(); + emit succeeded(index_within_job); + return; + } + // else the download failed + else + { + m_reply.reset(); + emit failed(index_within_job); + return; + } +} + +void ByteArrayDownload::downloadReadyRead() +{ + // ~_~ +} diff --git a/logic/net/ByteArrayDownload.h b/logic/net/ByteArrayDownload.h new file mode 100644 index 00000000..cfc6a8d0 --- /dev/null +++ b/logic/net/ByteArrayDownload.h @@ -0,0 +1,24 @@ +#pragma once +#include "Download.h" + +class ByteArrayDownload: public Download +{ + Q_OBJECT +public: + ByteArrayDownload(QUrl url); + +public: + /// if not saving to file, downloaded data is placed here + QByteArray m_data; + +public slots: + virtual void start(); + +protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadError(QNetworkReply::NetworkError error); + void downloadFinished(); + void downloadReadyRead(); +}; + +typedef std::shared_ptr<ByteArrayDownload> ByteArrayDownloadPtr; diff --git a/logic/net/CacheDownload.cpp b/logic/net/CacheDownload.cpp new file mode 100644 index 00000000..309eb345 --- /dev/null +++ b/logic/net/CacheDownload.cpp @@ -0,0 +1,136 @@ +#include "MultiMC.h" +#include "CacheDownload.h" +#include <pathutils.h> + +#include <QCryptographicHash> +#include <QFileInfo> +#include <QDateTime> +#include <logger/QsLog.h> + +CacheDownload::CacheDownload(QUrl url, MetaEntryPtr entry) + : Download(), md5sum(QCryptographicHash::Md5) +{ + m_url = url; + m_entry = entry; + m_target_path = entry->getFullPath(); + m_status = Job_NotStarted; + m_opened_for_saving = false; +} + +void CacheDownload::start() +{ + if (!m_entry->stale) + { + emit succeeded(index_within_job); + return; + } + m_output_file.setFileName(m_target_path); + // if there already is a file and md5 checking is in effect and it can be opened + if (!ensureFilePathExists(m_target_path)) + { + emit failed(index_within_job); + return; + } + QLOG_INFO() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + if(m_entry->remote_changed_timestamp.size()) + request.setRawHeader(QString("If-Modified-Since").toLatin1(), m_entry->remote_changed_timestamp.toLatin1()); + if(m_entry->etag.size()) + request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1()); + + request.setHeader(QNetworkRequest::UserAgentHeader,"MultiMC/5.0 (Cached)"); + + auto worker = MMC->qnam(); + QNetworkReply *rep = worker->get(request); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); +} + +void CacheDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + emit progress(index_within_job, bytesReceived, bytesTotal); +} + +void CacheDownload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + QLOG_ERROR() << "Failed" << m_url.toString() << "with reason" << error; + m_status = Job_Failed; +} +void CacheDownload::downloadFinished() +{ + // if the download succeeded + if (m_status != Job_Failed) + { + + // nothing went wrong... + m_status = Job_Finished; + if (m_opened_for_saving) + { + // save the data to the downloadable if we aren't saving to file + m_output_file.close(); + m_entry->md5sum = md5sum.result().toHex().constData(); + } + else + { + if (m_output_file.open(QIODevice::ReadOnly)) + { + m_entry->md5sum = + QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5) + .toHex() + .constData(); + m_output_file.close(); + } + } + QFileInfo output_file_info(m_target_path); + + m_entry->etag = m_reply->rawHeader("ETag").constData(); + if(m_reply->hasRawHeader("Last-Modified")) + { + m_entry->remote_changed_timestamp = m_reply->rawHeader("Last-Modified").constData(); + } + m_entry->local_changed_timestamp = + output_file_info.lastModified().toUTC().toMSecsSinceEpoch(); + m_entry->stale = false; + MMC->metacache()->updateEntry(m_entry); + + m_reply.reset(); + emit succeeded(index_within_job); + return; + } + // else the download failed + else + { + m_output_file.close(); + m_output_file.remove(); + m_reply.reset(); + emit failed(index_within_job); + return; + } +} + +void CacheDownload::downloadReadyRead() +{ + if (!m_opened_for_saving) + { + if (!m_output_file.open(QIODevice::WriteOnly)) + { + /* + * Can't open the file... the job failed + */ + m_reply->abort(); + emit failed(index_within_job); + return; + } + m_opened_for_saving = true; + } + QByteArray ba = m_reply->readAll(); + md5sum.addData(ba); + m_output_file.write(ba); +} diff --git a/logic/net/CacheDownload.h b/logic/net/CacheDownload.h new file mode 100644 index 00000000..295391b1 --- /dev/null +++ b/logic/net/CacheDownload.h @@ -0,0 +1,34 @@ +#pragma once + +#include "Download.h" +#include "HttpMetaCache.h" +#include <QFile> +#include <qcryptographichash.h> + +class CacheDownload : public Download +{ + Q_OBJECT +public: + MetaEntryPtr m_entry; + /// is the saving file already open? + bool m_opened_for_saving; + /// if saving to file, use the one specified in this string + QString m_target_path; + /// this is the output file, if any + QFile m_output_file; + /// the hash-as-you-download + QCryptographicHash md5sum; +public: + explicit CacheDownload(QUrl url, MetaEntryPtr entry); + +protected slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead(); + +public slots: + virtual void start(); +}; + +typedef std::shared_ptr<CacheDownload> CacheDownloadPtr; diff --git a/logic/net/Download.h b/logic/net/Download.h new file mode 100644 index 00000000..ca4bee9f --- /dev/null +++ b/logic/net/Download.h @@ -0,0 +1,54 @@ +#pragma once + +#include <QObject> +#include <QUrl> +#include <memory> +#include <QNetworkReply> + +enum JobStatus +{ + Job_NotStarted, + Job_InProgress, + Job_Finished, + Job_Failed +}; + +class Download : public QObject +{ + Q_OBJECT +protected: + explicit Download() : QObject(0) {}; + +public: + virtual ~Download() {}; + +public: + /// the network reply + std::shared_ptr<QNetworkReply> m_reply; + + /// source URL + QUrl m_url; + + /// The file's status + JobStatus m_status; + + /// index within the parent job + int index_within_job = 0; + +signals: + void started(int index); + void progress(int index, qint64 current, qint64 total); + void succeeded(int index); + void failed(int index); + +protected slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; + virtual void downloadError(QNetworkReply::NetworkError error) = 0; + virtual void downloadFinished() = 0; + virtual void downloadReadyRead() = 0; + +public slots: + virtual void start() = 0; +}; + +typedef std::shared_ptr<Download> DownloadPtr; diff --git a/logic/net/DownloadJob.cpp b/logic/net/DownloadJob.cpp index cad9ae72..fa3e655e 100644 --- a/logic/net/DownloadJob.cpp +++ b/logic/net/DownloadJob.cpp @@ -1,181 +1,135 @@ #include "DownloadJob.h" #include "pathutils.h" #include "MultiMC.h" +#include "FileDownload.h" +#include "ByteArrayDownload.h" +#include "CacheDownload.h" -Download::Download (QUrl url, QString target_path, QString expected_md5 ) - :Job() -{ - m_url = url; - m_target_path = target_path; - m_expected_md5 = expected_md5; - - m_check_md5 = m_expected_md5.size(); - m_save_to_file = m_target_path.size(); - m_status = Job_NotStarted; - m_opened_for_saving = false; -} - -void Download::start() -{ - if ( m_save_to_file ) - { - QString filename = m_target_path; - m_output_file.setFileName ( filename ); - // if there already is a file and md5 checking is in effect and it can be opened - if ( m_output_file.exists() && m_output_file.open ( QIODevice::ReadOnly ) ) - { - // check the md5 against the expected one - QString hash = QCryptographicHash::hash ( m_output_file.readAll(), QCryptographicHash::Md5 ).toHex().constData(); - m_output_file.close(); - // skip this file if they match - if ( m_check_md5 && hash == m_expected_md5 ) - { - qDebug() << "Skipping " << m_url.toString() << ": md5 match."; - emit succeeded(index_within_job); - return; - } - else - { - m_expected_md5 = hash; - } - } - if(!ensureFilePathExists(filename)) - { - emit failed(index_within_job); - return; - } - } - qDebug() << "Downloading " << m_url.toString(); - QNetworkRequest request ( m_url ); - request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1()); - - auto worker = MMC->qnam(); - QNetworkReply * rep = worker->get ( request ); - - m_reply = QSharedPointer<QNetworkReply> ( rep, &QObject::deleteLater ); - connect ( rep, SIGNAL ( downloadProgress ( qint64,qint64 ) ), SLOT ( downloadProgress ( qint64,qint64 ) ) ); - connect ( rep, SIGNAL ( finished() ), SLOT ( downloadFinished() ) ); - connect ( rep, SIGNAL ( error ( QNetworkReply::NetworkError ) ), SLOT ( downloadError ( QNetworkReply::NetworkError ) ) ); - connect ( rep, SIGNAL ( readyRead() ), SLOT ( downloadReadyRead() ) ); -} - -void Download::downloadProgress ( qint64 bytesReceived, qint64 bytesTotal ) -{ - emit progress (index_within_job, bytesReceived, bytesTotal ); -} +#include <logger/QsLog.h> -void Download::downloadError ( QNetworkReply::NetworkError error ) +ByteArrayDownloadPtr DownloadJob::addByteArrayDownload(QUrl url) { - // error happened during download. - // TODO: log the reason why - m_status = Job_Failed; + ByteArrayDownloadPtr ptr(new ByteArrayDownload(url)); + ptr->index_within_job = downloads.size(); + downloads.append(ptr); + parts_progress.append(part_info()); + total_progress++; + return ptr; } -void Download::downloadFinished() +FileDownloadPtr DownloadJob::addFileDownload(QUrl url, QString rel_target_path) { - // if the download succeeded - if ( m_status != Job_Failed ) - { - // nothing went wrong... - m_status = Job_Finished; - // save the data to the downloadable if we aren't saving to file - if ( !m_save_to_file ) - { - m_data = m_reply->readAll(); - } - else - { - m_output_file.close(); - } - - //TODO: check md5 here! - m_reply.clear(); - emit succeeded(index_within_job); - return; - } - // else the download failed - else - { - if ( m_save_to_file ) - { - m_output_file.close(); - m_output_file.remove(); - } - m_reply.clear(); - emit failed(index_within_job); - return; - } + FileDownloadPtr ptr(new FileDownload(url, rel_target_path)); + ptr->index_within_job = downloads.size(); + downloads.append(ptr); + parts_progress.append(part_info()); + total_progress++; + return ptr; } -void Download::downloadReadyRead() +CacheDownloadPtr DownloadJob::addCacheDownload(QUrl url, MetaEntryPtr entry) { - if( m_save_to_file ) - { - if(!m_opened_for_saving) - { - if ( !m_output_file.open ( QIODevice::WriteOnly ) ) - { - /* - * Can't open the file... the job failed - */ - m_reply->abort(); - emit failed(index_within_job); - return; - } - m_opened_for_saving = true; - } - m_output_file.write ( m_reply->readAll() ); - } + CacheDownloadPtr ptr(new CacheDownload(url, entry)); + ptr->index_within_job = downloads.size(); + downloads.append(ptr); + parts_progress.append(part_info()); + total_progress++; + return ptr; } -DownloadPtr DownloadJob::add ( QUrl url, QString rel_target_path, QString expected_md5 ) +ForgeXzDownloadPtr DownloadJob::addForgeXzDownload(QUrl url, MetaEntryPtr entry) { - DownloadPtr ptr (new Download(url, rel_target_path, expected_md5)); + ForgeXzDownloadPtr ptr(new ForgeXzDownload(url, entry)); ptr->index_within_job = downloads.size(); downloads.append(ptr); + parts_progress.append(part_info()); + total_progress++; return ptr; } -void DownloadJob::partSucceeded ( int index ) +void DownloadJob::partSucceeded(int index) { + // do progress. all slots are 1 in size at least + auto &slot = parts_progress[index]; + partProgress(index, slot.total_progress, slot.total_progress); + num_succeeded++; - if(num_failed + num_succeeded == downloads.size()) + QLOG_INFO() << m_job_name.toLocal8Bit() << "progress:" << num_succeeded << "/" + << downloads.size(); + if (num_failed + num_succeeded == downloads.size()) { - if(num_failed) + if (num_failed) { - qDebug() << "Download JOB failed: " << this; + QLOG_ERROR() << m_job_name.toLocal8Bit() << "failed."; emit failed(); } else { - qDebug() << "Download JOB succeeded: " << this; + QLOG_INFO() << m_job_name.toLocal8Bit() << "succeeded."; emit succeeded(); } } } -void DownloadJob::partFailed ( int index ) +void DownloadJob::partFailed(int index) { - num_failed++; - if(num_failed + num_succeeded == downloads.size()) + auto &slot = parts_progress[index]; + if (slot.failures == 3) { - qDebug() << "Download JOB failed: " << this; - emit failed(); + QLOG_ERROR() << "Part" << index << "failed 3 times (" << downloads[index]->m_url << ")"; + num_failed++; + if (num_failed + num_succeeded == downloads.size()) + { + QLOG_ERROR() << m_job_name.toLocal8Bit() << "failed."; + emit failed(); + } + } + else + { + QLOG_ERROR() << "Part" << index << "failed, restarting (" << downloads[index]->m_url + << ")"; + // restart the job + slot.failures++; + downloads[index]->start(); } } -void DownloadJob::partProgress ( int index, qint64 bytesReceived, qint64 bytesTotal ) +void DownloadJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) { - // PROGRESS? DENIED! -} + auto &slot = parts_progress[index]; + + current_progress -= slot.current_progress; + slot.current_progress = bytesReceived; + current_progress += slot.current_progress; + total_progress -= slot.total_progress; + slot.total_progress = bytesTotal; + total_progress += slot.total_progress; + emit progress(current_progress, total_progress); +} void DownloadJob::start() { - qDebug() << "Download JOB started: " << this; - for(auto iter: downloads) + QLOG_INFO() << m_job_name.toLocal8Bit() << " started."; + for (auto iter : downloads) { - connect(iter.data(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); + connect(iter.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); + connect(iter.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); + connect(iter.get(), SIGNAL(progress(int, qint64, qint64)), + SLOT(partProgress(int, qint64, qint64))); iter->start(); } } + +QStringList DownloadJob::getFailedFiles() +{ + QStringList failed; + for (auto download : downloads) + { + if (download->m_status == Job_Failed) + { + failed.push_back(download->m_url.toString()); + } + } + return failed; +} diff --git a/logic/net/DownloadJob.h b/logic/net/DownloadJob.h index adeef646..91b014ad 100644 --- a/logic/net/DownloadJob.h +++ b/logic/net/DownloadJob.h @@ -1,98 +1,31 @@ #pragma once #include <QtNetwork> +#include "Download.h" +#include "ByteArrayDownload.h" +#include "FileDownload.h" +#include "CacheDownload.h" +#include "HttpMetaCache.h" +#include "ForgeXzDownload.h" +#include "logic/tasks/ProgressProvider.h" class DownloadJob; -class Download; -typedef QSharedPointer<DownloadJob> DownloadJobPtr; -typedef QSharedPointer<Download> DownloadPtr; - -enum JobStatus -{ - Job_NotStarted, - Job_InProgress, - Job_Finished, - Job_Failed -}; - -class Job : public QObject -{ - Q_OBJECT -protected: - explicit Job(): QObject(0){}; -public: - virtual ~Job() {}; - -public slots: - virtual void start() = 0; -}; - -class Download: public Job -{ - friend class DownloadJob; - Q_OBJECT -protected: - Download(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString()); -public: - /// the network reply - QSharedPointer<QNetworkReply> m_reply; - /// source URL - QUrl m_url; - - /// if true, check the md5sum against a provided md5sum - /// also, if a file exists, perform an md5sum first and don't download only if they don't match - bool m_check_md5; - /// the expected md5 checksum - QString m_expected_md5; - - /// save to file? - bool m_save_to_file; - /// is the saving file already open? - bool m_opened_for_saving; - /// if saving to file, use the one specified in this string - QString m_target_path; - /// this is the output file, if any - QFile m_output_file; - /// if not saving to file, downloaded data is placed here - QByteArray m_data; - - int currentProgress = 0; - int totalProgress = 0; - - /// The file's status - JobStatus m_status; - - int index_within_job = 0; -signals: - void started(int index); - void progress(int index, qint64 current, qint64 total); - void succeeded(int index); - void failed(int index); -public slots: - virtual void start(); - -private slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);; - void downloadError(QNetworkReply::NetworkError error); - void downloadFinished(); - void downloadReadyRead(); -}; +typedef std::shared_ptr<DownloadJob> DownloadJobPtr; /** * A single file for the downloader/cache to process. */ -class DownloadJob : public Job +class DownloadJob : public ProgressProvider { Q_OBJECT public: - explicit DownloadJob() - :Job(){}; - explicit DownloadJob(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString()) - :Job() - { - add(url, rel_target_path, expected_md5); - }; + explicit DownloadJob(QString job_name) + :ProgressProvider(), m_job_name(job_name){}; + + ByteArrayDownloadPtr addByteArrayDownload(QUrl url); + FileDownloadPtr addFileDownload(QUrl url, QString rel_target_path); + CacheDownloadPtr addCacheDownload(QUrl url, MetaEntryPtr entry); + ForgeXzDownloadPtr addForgeXzDownload(QUrl url, MetaEntryPtr entry); - DownloadPtr add(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString()); DownloadPtr operator[](int index) { return downloads[index]; @@ -107,6 +40,20 @@ public: { return downloads.size(); } + virtual void getProgress(qint64& current, qint64& total) + { + current = current_progress; + total = total_progress; + }; + virtual QString getStatus() const + { + return m_job_name; + }; + virtual bool isRunning() const + { + return m_running; + }; + QStringList getFailedFiles(); signals: void started(); void progress(qint64 current, qint64 total); @@ -119,8 +66,19 @@ private slots: void partSucceeded(int index); void partFailed(int index); private: + struct part_info + { + qint64 current_progress = 0; + qint64 total_progress = 1; + int failures = 0; + }; + QString m_job_name; QList<DownloadPtr> downloads; + QList<part_info> parts_progress; + qint64 current_progress = 0; + qint64 total_progress = 0; int num_succeeded = 0; int num_failed = 0; + bool m_running = false; }; diff --git a/logic/net/FileDownload.cpp b/logic/net/FileDownload.cpp new file mode 100644 index 00000000..3f38b0fa --- /dev/null +++ b/logic/net/FileDownload.cpp @@ -0,0 +1,112 @@ +#include "MultiMC.h" +#include "FileDownload.h" +#include <pathutils.h> +#include <QCryptographicHash> +#include <logger/QsLog.h> + + +FileDownload::FileDownload ( QUrl url, QString target_path ) + :Download() +{ + m_url = url; + m_target_path = target_path; + m_check_md5 = false; + m_status = Job_NotStarted; + m_opened_for_saving = false; +} + +void FileDownload::start() +{ + QString filename = m_target_path; + m_output_file.setFileName ( filename ); + // if there already is a file and md5 checking is in effect and it can be opened + if ( m_output_file.exists() && m_output_file.open ( QIODevice::ReadOnly ) ) + { + // check the md5 against the expected one + QString hash = QCryptographicHash::hash ( m_output_file.readAll(), QCryptographicHash::Md5 ).toHex().constData(); + m_output_file.close(); + // skip this file if they match + if ( m_check_md5 && hash == m_expected_md5 ) + { + QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match."; + emit succeeded(index_within_job); + return; + } + else + { + m_expected_md5 = hash; + } + } + if(!ensureFilePathExists(filename)) + { + emit failed(index_within_job); + return; + } + + QLOG_INFO() << "Downloading " << m_url.toString(); + QNetworkRequest request ( m_url ); + request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1()); + request.setHeader(QNetworkRequest::UserAgentHeader,"MultiMC/5.0 (Uncached)"); + + auto worker = MMC->qnam(); + QNetworkReply * rep = worker->get ( request ); + + m_reply = std::shared_ptr<QNetworkReply> ( rep ); + connect ( rep, SIGNAL ( downloadProgress ( qint64,qint64 ) ), SLOT ( downloadProgress ( qint64,qint64 ) ) ); + connect ( rep, SIGNAL ( finished() ), SLOT ( downloadFinished() ) ); + connect ( rep, SIGNAL ( error ( QNetworkReply::NetworkError ) ), SLOT ( downloadError ( QNetworkReply::NetworkError ) ) ); + connect ( rep, SIGNAL ( readyRead() ), SLOT ( downloadReadyRead() ) ); +} + +void FileDownload::downloadProgress ( qint64 bytesReceived, qint64 bytesTotal ) +{ + emit progress (index_within_job, bytesReceived, bytesTotal ); +} + +void FileDownload::downloadError ( QNetworkReply::NetworkError error ) +{ + // error happened during download. + // TODO: log the reason why + m_status = Job_Failed; +} + +void FileDownload::downloadFinished() +{ + // if the download succeeded + if ( m_status != Job_Failed ) + { + // nothing went wrong... + m_status = Job_Finished; + m_output_file.close(); + + m_reply.reset(); + emit succeeded(index_within_job); + return; + } + // else the download failed + else + { + m_output_file.close(); + m_reply.reset(); + emit failed(index_within_job); + return; + } +} + +void FileDownload::downloadReadyRead() +{ + if(!m_opened_for_saving) + { + if ( !m_output_file.open ( QIODevice::WriteOnly ) ) + { + /* + * Can't open the file... the job failed + */ + m_reply->abort(); + emit failed(index_within_job); + return; + } + m_opened_for_saving = true; + } + m_output_file.write ( m_reply->readAll() ); +} diff --git a/logic/net/FileDownload.h b/logic/net/FileDownload.h new file mode 100644 index 00000000..9abb590d --- /dev/null +++ b/logic/net/FileDownload.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Download.h" +#include <QFile> + +class FileDownload : public Download +{ + Q_OBJECT +public: + /// if true, check the md5sum against a provided md5sum + /// also, if a file exists, perform an md5sum first and don't download only if they don't match + bool m_check_md5; + /// the expected md5 checksum + QString m_expected_md5; + /// is the saving file already open? + bool m_opened_for_saving; + /// if saving to file, use the one specified in this string + QString m_target_path; + /// this is the output file, if any + QFile m_output_file; + +public: + explicit FileDownload(QUrl url, QString target_path); + +protected slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead(); + +public slots: + virtual void start(); +}; + +typedef std::shared_ptr<FileDownload> FileDownloadPtr; diff --git a/logic/net/ForgeXzDownload.cpp b/logic/net/ForgeXzDownload.cpp new file mode 100644 index 00000000..0e5287d8 --- /dev/null +++ b/logic/net/ForgeXzDownload.cpp @@ -0,0 +1,279 @@ +#include "MultiMC.h" +#include "ForgeXzDownload.h" +#include <pathutils.h> + +#include <QCryptographicHash> +#include <QFileInfo> +#include <QDateTime> +#include <logger/QsLog.h> + +ForgeXzDownload::ForgeXzDownload(QUrl url, MetaEntryPtr entry) + : Download() +{ + QString urlstr = url.toString(); + urlstr.append(".pack.xz"); + m_url = QUrl(urlstr); + m_entry = entry; + m_target_path = entry->getFullPath(); + m_status = Job_NotStarted; + m_opened_for_saving = false; +} + +void ForgeXzDownload::start() +{ + if (!m_entry->stale) + { + emit succeeded(index_within_job); + return; + } + // can we actually create the real, final file? + if (!ensureFilePathExists(m_target_path)) + { + emit failed(index_within_job); + return; + } + QLOG_INFO() << "Downloading " << m_url.toString(); + QNetworkRequest request(m_url); + request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1()); + request.setHeader(QNetworkRequest::UserAgentHeader,"MultiMC/5.0 (Cached)"); + + auto worker = MMC->qnam(); + QNetworkReply *rep = worker->get(request); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); +} + +void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + emit progress(index_within_job, bytesReceived, bytesTotal); +} + +void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + // TODO: log the reason why + m_status = Job_Failed; +} + +void ForgeXzDownload::downloadFinished() +{ + // if the download succeeded + if (m_status != Job_Failed) + { + // nothing went wrong... + m_status = Job_Finished; + if (m_opened_for_saving) + { + // we actually downloaded something! process and isntall it + decompressAndInstall(); + return; + } + else + { + // something bad happened + m_pack200_xz_file.remove(); + m_reply.reset(); + emit failed(index_within_job); + return; + } + } + // else the download failed + else + { + m_pack200_xz_file.close(); + m_pack200_xz_file.remove(); + m_reply.reset(); + emit failed(index_within_job); + return; + } +} + +void ForgeXzDownload::downloadReadyRead() +{ + + if (!m_opened_for_saving) + { + if (!m_pack200_xz_file.open()) + { + /* + * Can't open the file... the job failed + */ + m_reply->abort(); + emit failed(index_within_job); + return; + } + m_opened_for_saving = true; + } + m_pack200_xz_file.write(m_reply->readAll()); +} + +#include "xz.h" +#include "unpack200.h" +#include <stdexcept> + +const size_t buffer_size = 8196; + +void ForgeXzDownload::decompressAndInstall() +{ + // rewind the downloaded temp file + m_pack200_xz_file.seek(0); + // de-xz'd file + QTemporaryFile pack200_file; + pack200_file.open(); + + bool xz_success = false; + // first, de-xz + { + uint8_t in[buffer_size]; + uint8_t out[buffer_size]; + struct xz_buf b; + struct xz_dec *s; + enum xz_ret ret; + xz_crc32_init(); + xz_crc64_init(); + s = xz_dec_init(XZ_DYNALLOC, 1 << 26); + if (s == nullptr) + { + xz_dec_end(s); + emit failed(index_within_job); + return; + } + b.in = in; + b.in_pos = 0; + b.in_size = 0; + b.out = out; + b.out_pos = 0; + b.out_size = buffer_size; + while (!xz_success) + { + if (b.in_pos == b.in_size) + { + b.in_size = m_pack200_xz_file.read((char*)in, sizeof(in)); + b.in_pos = 0; + } + + ret = xz_dec_run(s, &b); + + if (b.out_pos == sizeof(out)) + { + if (pack200_file.write((char*)out, b.out_pos) != b.out_pos) + { + // msg = "Write error\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + } + + b.out_pos = 0; + } + + if (ret == XZ_OK) + continue; + + if (ret == XZ_UNSUPPORTED_CHECK) + { + // unsupported check. this is OK, but we should log this + continue; + } + + if (pack200_file.write((char*)out, b.out_pos) != b.out_pos ) + { + // write error + pack200_file.close(); + xz_dec_end(s); + return; + } + + switch (ret) + { + case XZ_STREAM_END: + xz_dec_end(s); + xz_success = true; + break; + + case XZ_MEM_ERROR: + QLOG_ERROR() << "Memory allocation failed\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + + case XZ_MEMLIMIT_ERROR: + QLOG_ERROR() << "Memory usage limit reached\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + + case XZ_FORMAT_ERROR: + QLOG_ERROR() << "Not a .xz file\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + + case XZ_OPTIONS_ERROR: + QLOG_ERROR() << "Unsupported options in the .xz headers\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + + case XZ_DATA_ERROR: + case XZ_BUF_ERROR: + QLOG_ERROR() << "File is corrupt\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + + default: + QLOG_ERROR() << "Bug!\n"; + xz_dec_end(s); + emit failed(index_within_job); + return; + } + } + } + + // revert pack200 + pack200_file.close(); + QString pack_name = pack200_file.fileName(); + try + { + unpack_200(pack_name.toStdString(), m_target_path.toStdString()); + } + catch(std::runtime_error & err) + { + QLOG_ERROR() << "Error unpacking " << pack_name.toUtf8() << " : " << err.what(); + QFile f(m_target_path); + if(f.exists()) + f.remove(); + emit failed(index_within_job); + return; + } + + QFile jar_file(m_target_path); + + if (!jar_file.open(QIODevice::ReadOnly)) + { + jar_file.remove(); + emit failed(index_within_job); + return; + } + m_entry->md5sum = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5) + .toHex() + .constData(); + jar_file.close(); + + QFileInfo output_file_info(m_target_path); + m_entry->etag = m_reply->rawHeader("ETag").constData(); + m_entry->local_changed_timestamp = + output_file_info.lastModified().toUTC().toMSecsSinceEpoch(); + m_entry->stale = false; + MMC->metacache()->updateEntry(m_entry); + + m_reply.reset(); + emit succeeded(index_within_job); +} diff --git a/logic/net/ForgeXzDownload.h b/logic/net/ForgeXzDownload.h new file mode 100644 index 00000000..5d677947 --- /dev/null +++ b/logic/net/ForgeXzDownload.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Download.h" +#include "HttpMetaCache.h" +#include <QFile> +#include <QTemporaryFile> + +class ForgeXzDownload : public Download +{ + Q_OBJECT +public: + MetaEntryPtr m_entry; + /// is the saving file already open? + bool m_opened_for_saving; + /// if saving to file, use the one specified in this string + QString m_target_path; + /// this is the output file, if any + QTemporaryFile m_pack200_xz_file; + +public: + explicit ForgeXzDownload(QUrl url, MetaEntryPtr entry); + +protected slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead(); + +public slots: + virtual void start(); +private: + void decompressAndInstall(); +}; + +typedef std::shared_ptr<ForgeXzDownload> ForgeXzDownloadPtr; diff --git a/logic/net/HttpMetaCache.cpp b/logic/net/HttpMetaCache.cpp index 87741dc9..5ba5b98d 100644 --- a/logic/net/HttpMetaCache.cpp +++ b/logic/net/HttpMetaCache.cpp @@ -1,38 +1,130 @@ +#include "MultiMC.h" #include "HttpMetaCache.h" #include <pathutils.h> + +#include <QFileInfo> #include <QFile> -#include <qjsondocument.h> -#include <qjsonarray.h> -#include <qjsonobject.h> -#include <qfileinfo.h> -#include <qtemporaryfile.h> -#include <qsavefile.h> +#include <QTemporaryFile> +#include <QSaveFile> +#include <QDateTime> +#include <QCryptographicHash> + +#include <logger/QsLog.h> + +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> + +QString MetaEntry::getFullPath() +{ + return PathCombine(MMC->metacache()->getBasePath(base), path); +} + HttpMetaCache::HttpMetaCache(QString path) + :QObject() { m_index_file = path; + saveBatchingTimer.setSingleShot(true); + saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); + connect(&saveBatchingTimer,SIGNAL(timeout()),SLOT(SaveNow())); } + HttpMetaCache::~HttpMetaCache() { - Save(); + saveBatchingTimer.stop(); + SaveNow(); } -void HttpMetaCache::addEntry ( QString base, QString resource_path, QString etag ) +MetaEntryPtr HttpMetaCache::getEntry ( QString base, QString resource_path ) { // no base. no base path. can't store if(!m_entries.contains(base)) - return; - QString real_path = PathCombine(m_entries[base].base_path, resource_path); + { + // TODO: log problem + return MetaEntryPtr(); + } + EntryMap & map = m_entries[base]; + if(map.entry_list.contains(resource_path)) + { + return map.entry_list[resource_path]; + } + return MetaEntryPtr(); +} + +MetaEntryPtr HttpMetaCache::resolveEntry ( QString base, QString resource_path, QString expected_etag ) +{ + auto entry = getEntry(base, resource_path); + // it's not present? generate a default stale entry + if(!entry) + { + return staleEntry(base, resource_path); + } + + auto & selected_base = m_entries[base]; + QString real_path = PathCombine(selected_base.base_path, resource_path); QFileInfo finfo(real_path); - // just ignore it, it's garbage if it's not a proper file + // is the file really there? if not -> stale if(!finfo.isFile() || !finfo.isReadable()) { - // TODO: log problem - return; + // if the file doesn't exist, we disown the entry + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + + if(!expected_etag.isEmpty() && expected_etag != entry->etag) + { + // if the etag doesn't match expected, we disown the entry + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); } - Save(); + // if the file changed, check md5sum + qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); + if(file_last_changed != entry->local_changed_timestamp) + { + QFile input(real_path); + input.open(QIODevice::ReadOnly); + QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); + if(entry->md5sum != md5sum) + { + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + // md5sums matched... keep entry and save the new state to file + entry->local_changed_timestamp = file_last_changed; + SaveEventually(); + } + + // entry passed all the checks we cared about. + return entry; +} + +bool HttpMetaCache::updateEntry ( MetaEntryPtr stale_entry ) +{ + if(!m_entries.contains(stale_entry->base)) + { + QLOG_ERROR() << "Cannot add entry with unknown base: " << stale_entry->base.toLocal8Bit(); + return false; + } + if(stale_entry->stale) + { + QLOG_ERROR() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); + return false; + } + m_entries[stale_entry->base].entry_list[stale_entry->path] = stale_entry; + SaveEventually(); + return true; +} + +MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path) +{ + auto foo = new MetaEntry; + foo->base = base; + foo->path = resource_path; + foo->stale = true; + return MetaEntryPtr(foo); } void HttpMetaCache::addBase ( QString base, QString base_root ) @@ -46,6 +138,16 @@ void HttpMetaCache::addBase ( QString base, QString base_root ) m_entries[base] = foo; } +QString HttpMetaCache::getBasePath ( QString base ) +{ + if(m_entries.contains(base)) + { + return m_entries[base].base_path; + } + return QString(); +} + + void HttpMetaCache::Load() { QFile index(m_index_file); @@ -65,12 +167,12 @@ void HttpMetaCache::Load() // read the entry array auto entries_val =root.value("entries"); - if(!version_val.isArray()) + if(!entries_val.isArray()) return; - QJsonArray array = json.array(); + QJsonArray array = entries_val.toArray(); for(auto element: array) { - if(!element.isObject()); + if(!element.isObject()) return; auto element_obj = element.toObject(); QString base = element_obj.value("base").toString(); @@ -82,12 +184,22 @@ void HttpMetaCache::Load() QString path = foo->path = element_obj.value("path").toString(); foo->md5sum = element_obj.value("md5sum").toString(); foo->etag = element_obj.value("etag").toString(); - foo->last_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); + foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); + foo->remote_changed_timestamp = element_obj.value("remote_changed_timestamp").toString(); + // presumed innocent until closer examination + foo->stale = false; entrymap.entry_list[path] = MetaEntryPtr( foo ); } } -void HttpMetaCache::Save() +void HttpMetaCache::SaveEventually() +{ + // reset the save timer + saveBatchingTimer.stop(); + saveBatchingTimer.start(30000); +} + +void HttpMetaCache::SaveNow() { QSaveFile tfile(m_index_file); if(!tfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) @@ -104,7 +216,9 @@ void HttpMetaCache::Save() entryObj.insert("path", QJsonValue(entry->path)); entryObj.insert("md5sum", QJsonValue(entry->md5sum)); entryObj.insert("etag", QJsonValue(entry->etag)); - entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->last_changed_timestamp))); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); + if(!entry->remote_changed_timestamp.isEmpty()) + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); entriesArr.append(entryObj); } } @@ -118,14 +232,3 @@ void HttpMetaCache::Save() return; tfile.commit(); } - - -MetaEntryPtr HttpMetaCache::getEntryForResource ( QString base, QString resource_path ) -{ - if(!m_entries.contains(base)) - return MetaEntryPtr(); - auto & entrymap = m_entries[base]; - if(!entrymap.entry_list.contains(resource_path)) - return MetaEntryPtr(); - return entrymap.entry_list[resource_path]; -} diff --git a/logic/net/HttpMetaCache.h b/logic/net/HttpMetaCache.h index 161483ad..557d9298 100644 --- a/logic/net/HttpMetaCache.h +++ b/logic/net/HttpMetaCache.h @@ -2,6 +2,7 @@ #include <QString> #include <QSharedPointer> #include <QMap> +#include <qtimer.h> struct MetaEntry { @@ -9,23 +10,46 @@ struct MetaEntry QString path; QString md5sum; QString etag; - quint64 last_changed_timestamp = 0; + qint64 local_changed_timestamp = 0; + QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time + bool stale = true; + QString getFullPath(); }; -typedef QSharedPointer<MetaEntry> MetaEntryPtr; +typedef std::shared_ptr<MetaEntry> MetaEntryPtr; -class HttpMetaCache +class HttpMetaCache : public QObject { + Q_OBJECT public: // supply path to the cache index file HttpMetaCache(QString path); ~HttpMetaCache(); - MetaEntryPtr getEntryForResource(QString base, QString resource_path); - void addEntry(QString base, QString resource_path, QString etag); + + // get the entry solely from the cache + // you probably don't want this, unless you have some specific caching needs. + MetaEntryPtr getEntry(QString base, QString resource_path); + + // get the entry from cache and verify that it isn't stale (within reason) + MetaEntryPtr resolveEntry(QString base, QString resource_path, + QString expected_etag = QString()); + + // add a previously resolved stale entry + bool updateEntry(MetaEntryPtr stale_entry); + void addBase(QString base, QString base_root); -private: - void Save(); + + // (re)start a timer that calls SaveNow later. + void SaveEventually(); void Load(); + QString getBasePath(QString base); +public +slots: + void SaveNow(); + +private: + // create a new stale entry, given the parameters + MetaEntryPtr staleEntry(QString base, QString resource_path); struct EntryMap { QString base_path; @@ -33,4 +57,5 @@ private: }; QMap<QString, EntryMap> m_entries; QString m_index_file; + QTimer saveBatchingTimer; };
\ No newline at end of file diff --git a/logic/net/LoginTask.cpp b/logic/net/LoginTask.cpp new file mode 100644 index 00000000..4098783b --- /dev/null +++ b/logic/net/LoginTask.cpp @@ -0,0 +1,285 @@ +/* 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 <settingsobject.h> + +#include <QStringList> + +#include <QNetworkReply> +#include <QNetworkRequest> + +#include <QUrl> +#include <QUrlQuery> +#include <QJsonParseError> +#include <QJsonObject> + +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.get(), 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::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::processLegacyReply(QNetworkReply *reply) +{ + processReply(reply, &LoginTask::parseLegacyReply, &LoginTask::parseLegacyError); +} + +void LoginTask::processYggdrasilReply(QNetworkReply *reply) +{ + processReply(reply, &LoginTask::parseYggdrasilReply, &LoginTask::parseYggdrasilError); +} + +void LoginTask::processReply(QNetworkReply *reply, std::function<void (LoginTask*, QByteArray)> parser, std::function<QString (LoginTask*, QNetworkReply*)> errorHandler) +{ + if (netReply != reply) + return; + // Check for errors. + switch (reply->error()) + { + case QNetworkReply::NoError: + { + // Check the response code. + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + switch (responseCode) + { + case 200: + parser(this, reply->readAll()); + break; + + default: + emitFailed(tr("Login failed: Unknown HTTP code %1 encountered.").arg(responseCode)); + break; + } + + break; + } + + case QNetworkReply::OperationCanceledError: + emitFailed(tr("Login canceled.")); + break; + + default: + emitFailed(errorHandler(this, reply)); + break; + } +} + +QString LoginTask::parseLegacyError(QNetworkReply *reply) +{ + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + switch (responseCode) + { + case 403: + return tr("Invalid username or password."); + + case 503: + return tr("The login servers are currently unavailable. Check " + "http://help.mojang.com/ for more info."); + + default: + QLOG_DEBUG() << "Login failed with QNetworkReply code:" << reply->error(); + return tr("Login failed: %1").arg(reply->errorString()); + } +} + +QString LoginTask::parseYggdrasilError(QNetworkReply *reply) +{ + QByteArray data = reply->readAll(); + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + // If there are JSON errors fall back to using the legacy error handling using HTTP status codes + if (jsonError.error != QJsonParseError::NoError) + { + return parseLegacyError(reply); + } + + if (!jsonDoc.isObject()) + { + return parseLegacyError(reply); + } + + QJsonObject root = jsonDoc.object(); + + //QString error = root.value("error").toString(); + QString errorMessage = root.value("errorMessage").toString(); + + if(errorMessage.isEmpty()) + { + return parseLegacyError(reply); + } + + return tr("Login failed: ") + errorMessage; +} + +void LoginTask::yggdrasilLogin() +{ + setStatus(tr("Logging in...")); + auto worker = MMC->qnam(); + connect(worker.get(), 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()); +} + +/* +{ + "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, accessToken}; + emitSucceeded(); +} diff --git a/logic/net/LoginTask.h b/logic/net/LoginTask.h new file mode 100644 index 00000000..aa925999 --- /dev/null +++ b/logic/net/LoginTask.h @@ -0,0 +1,67 @@ +/* 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 <QSharedPointer> + +struct UserInfo +{ + QString username; + QString password; +}; + +struct LoginResponse +{ + QString username; + QString session_id; // session id is a combination of player id and the access token + QString player_name; + QString player_id; + QString access_token; +}; + +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); + QString parseLegacyError(QNetworkReply *reply); + + void yggdrasilLogin(); + void processYggdrasilReply(QNetworkReply *reply); + void parseYggdrasilReply(QByteArray data); + QString parseYggdrasilError(QNetworkReply *reply); + + void processReply(QNetworkReply *reply, std::function<void(LoginTask*, QByteArray)>, std::function<QString(LoginTask*, QNetworkReply*)>); + +protected: + void executeTask(); + + LoginResponse result; + QNetworkReply *netReply; + UserInfo uInfo; +}; |