diff --git a/CMakeLists.txt b/CMakeLists.txt index c6389b8..da58a01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,13 @@ project (lgogdownloader LANGUAGES C CXX VERSION 3.4) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") set(LINK_LIBCRYPTO 0) +option(USE_QT_GUI "Build with Qt GUI login support" OFF) +if(USE_QT_GUI) + add_definitions(-DUSE_QT_GUI_LOGIN=1) + set(CMAKE_AUTOMOC ON) + set(CMAKE_AUTOUIC ON) +endif(USE_QT_GUI) + find_program(READELF readelf DOC "Location of the readelf program") find_program(GREP grep DOC "Location of the grep program") find_package(Boost @@ -51,6 +58,17 @@ file(GLOB SRC_FILES src/ziputil.cpp ) +if(USE_QT_GUI) + find_package(Qt5Widgets CONFIG REQUIRED) + find_package(Qt5WebEngineWidgets CONFIG REQUIRED) + + file(GLOB QT_GUI_SRC_FILES + src/gui_login.cpp + ) + list(APPEND SRC_FILES ${QT_GUI_SRC_FILES}) +endif(USE_QT_GUI) + + set(GIT_CHECKOUT FALSE) if(EXISTS ${PROJECT_SOURCE_DIR}/.git) if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow) @@ -127,6 +145,13 @@ if(LINK_LIBCRYPTO EQUAL 1) ) endif(LINK_LIBCRYPTO EQUAL 1) +if(USE_QT_GUI) + target_link_libraries(${PROJECT_NAME} + PRIVATE Qt5::Widgets + PRIVATE Qt5::WebEngineWidgets + ) +endif(USE_QT_GUI) + if(MSVC) # Force to always compile with W4 if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]") diff --git a/README.md b/README.md index 06582cd..07795fb 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This repository contains the code of unofficial [GOG](http://www.gog.com/) downl * [boost](http://www.boost.org/) (regex, date-time, system, filesystem, program-options, iostreams) * [libcrypto](https://www.openssl.org/) if libcurl is built with OpenSSL * [zlib](https://www.zlib.net/) +* [qtwebengine](https://www.qt.io/) if built with -DUSE_QT_GUI=ON ## Make dependencies * [cmake](https://cmake.org/) >= 3.0.0 @@ -26,7 +27,7 @@ This repository contains the code of unofficial [GOG](http://www.gog.com/) downl libjsoncpp-dev liboauth-dev librhash-dev libtinyxml2-dev libhtmlcxx-dev \ libboost-system-dev libboost-filesystem-dev libboost-program-options-dev \ libboost-date-time-dev libboost-iostreams-dev help2man cmake libssl-dev \ - pkg-config zlib1g-dev + pkg-config zlib1g-dev qtwebengine5-dev ## Build and install diff --git a/include/gui_login.h b/include/gui_login.h new file mode 100644 index 0000000..e3590da --- /dev/null +++ b/include/gui_login.h @@ -0,0 +1,41 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + +#ifndef GUI_LOGIN_H +#define GUI_LOGIN_H + +#include "config.h" +#include "util.h" +#include "globals.h" + +#include +#include +#include +#include + +class GuiLogin : public QObject +{ + Q_OBJECT + + public: + GuiLogin(); + virtual ~GuiLogin(); + + void Login(); + std::string getCode(); + std::vector getCookies(); + + private: + QWebEngineCookieStore *cookiestore; + std::vector cookies; + std::string auth_code; + + public slots: + void loadFinished(bool success); + void cookieAdded(const QNetworkCookie &cookie); +}; + +#endif // GUI_LOGIN_H diff --git a/src/gui_login.cpp b/src/gui_login.cpp new file mode 100644 index 0000000..55fda03 --- /dev/null +++ b/src/gui_login.cpp @@ -0,0 +1,134 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ + +#include "gui_login.h" + +#include +#include +#include +#include +#include +#include +#include + +GuiLogin::GuiLogin() +{ + // constructor +} + +GuiLogin::~GuiLogin() +{ + // destructor +} + +void GuiLogin::loadFinished(bool success) +{ + QWebEngineView *view = qobject_cast(sender()); + std::string url = view->page()->url().toString().toUtf8().constData(); + if (success && url.find("https://embed.gog.com/on_login_success") != std::string::npos) + { + std::string find_str = "code="; + auto pos = url.find(find_str); + if (pos != std::string::npos) + { + pos += find_str.length(); + std::string code; + code.assign(url.begin()+pos, url.end()); + if (!code.empty()) + { + this->auth_code = code; + QCoreApplication::exit(); + } + } + } +} + +void GuiLogin::cookieAdded(const QNetworkCookie& cookie) +{ + std::string raw_cookie = cookie.toRawForm().toStdString(); + if (!raw_cookie.empty()) + { + std::string set_cookie = "Set-Cookie: " + raw_cookie; + bool duplicate = false; + for (auto cookie : this->cookies) + { + if (set_cookie == cookie) + { + duplicate = true; + break; + } + } + if (!duplicate) + this->cookies.push_back(set_cookie); + } +} + +void GuiLogin::Login() +{ + QByteArray redirect_uri = QUrl::toPercentEncoding(QString::fromStdString(Globals::galaxyConf.getRedirectUri())); + std::string auth_url = "https://auth.gog.com/auth?client_id=" + Globals::galaxyConf.getClientId() + "&redirect_uri=" + redirect_uri.toStdString() + "&response_type=code"; + QUrl url = QString::fromStdString(auth_url); + + std::vector version_string( + Globals::globalConfig.sVersionString.c_str(), + Globals::globalConfig.sVersionString.c_str() + Globals::globalConfig.sVersionString.size() + 1 + ); + + int argc = 1; + char *argv[] = {&version_string[0]}; + QApplication app(argc, argv); + + QWidget window; + QVBoxLayout *layout = new QVBoxLayout; + QSize window_size(440, 540); + + window.setGeometry( + QStyle::alignedRect( + Qt::LeftToRight, + Qt::AlignCenter, + window_size, + qApp->desktop()->availableGeometry() + ) + ); + + QWebEngineView *webengine = new QWebEngineView(&window); + layout->addWidget(webengine); + QWebEngineProfile profile; + profile.setHttpUserAgent(QString::fromStdString(Globals::globalConfig.curlConf.sUserAgent)); + QWebEnginePage page(&profile); + cookiestore = profile.cookieStore(); + + QObject::connect( + webengine, SIGNAL(loadFinished(bool)), + this, SLOT(loadFinished(bool)) + ); + + QObject::connect( + this->cookiestore, SIGNAL(cookieAdded(const QNetworkCookie&)), + this, SLOT(cookieAdded(const QNetworkCookie&)) + ); + + webengine->resize(window.frameSize()); + webengine->setPage(&page); + webengine->setUrl(url); + + window.setLayout(layout); + window.show(); + + app.exec(); +} + +std::string GuiLogin::getCode() +{ + return this->auth_code; +} + +std::vector GuiLogin::getCookies() +{ + return this->cookies; +} + +#include "moc_gui_login.cpp" diff --git a/src/website.cpp b/src/website.cpp index 0885cda..c79759b 100644 --- a/src/website.cpp +++ b/src/website.cpp @@ -10,6 +10,10 @@ #include #include +#ifdef USE_QT_GUI_LOGIN + #include "gui_login.h" +#endif + Website::Website() { this->retries = 0; @@ -297,6 +301,7 @@ int Website::Login(const std::string& email, const std::string& password) std::string tagname_token; std::string auth_url = "https://auth.gog.com/auth?client_id=" + Globals::galaxyConf.getClientId() + "&redirect_uri=" + (std::string)curl_easy_escape(curlhandle, Globals::galaxyConf.getRedirectUri().c_str(), Globals::galaxyConf.getRedirectUri().size()) + "&response_type=code&layout=default&brand=gog"; std::string auth_code; + bool bRecaptcha = false; std::string login_form_html = this->getResponse(auth_url); #ifdef DEBUG @@ -305,110 +310,62 @@ int Website::Login(const std::string& email, const std::string& password) #endif if (login_form_html.find("google.com/recaptcha") != std::string::npos) { - std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl - << "Try to login later" << std::endl; - return res = 0; - } - - htmlcxx::HTML::ParserDom parser; - tree login_dom = parser.parseTree(login_form_html); - tree::iterator login_it = login_dom.begin(); - tree::iterator login_it_end = login_dom.end(); - for (; login_it != login_it_end; ++login_it) - { - if (login_it->tagName()=="input") - { - login_it->parseAttributes(); - if (login_it->attribute("id").second == "login__token") + bRecaptcha = true; + #ifndef USE_QT_GUI_LOGIN + std::cout << "Login form contains reCAPTCHA (https://www.google.com/recaptcha/)" << std::endl + << "Try to login later or compile LGOGDownloader with -DUSE_QT_GUI=ON" << std::endl; + return res = 0; + #else + GuiLogin gl; + gl.Login(); + + auto cookies = gl.getCookies(); + for (auto cookie : cookies) { - token = login_it->attribute("value").second; // login token - tagname_token = login_it->attribute("name").second; + curl_easy_setopt(curlhandle, CURLOPT_COOKIELIST, cookie.c_str()); } - } - } - - if (token.empty()) - { - std::cout << "Failed to get login token" << std::endl; - return res = 0; + auth_code = gl.getCode(); + #endif } - //Create postdata - escape characters in email/password to support special characters - postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size()) - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size()) - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "=" - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size()); - curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check"); - curl_easy_setopt(curlhandle, CURLOPT_POST, 1); - curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); - curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); - curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); - curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); - curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); - curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); - - // Don't follow to redirect location because we need to check it for two step authorization. - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); - CURLcode result = curl_easy_perform(curlhandle); - memory.str(std::string()); - - if (result != CURLE_OK) + if (bRecaptcha) { - // Expected to hit maximum amount of redirects so don't print error on it - if (result != CURLE_TOO_MANY_REDIRECTS) - std::cout << curl_easy_strerror(result) << std::endl; + // This should never be reached but do additional check here just in case + #ifndef USE_QT_GUI_LOGIN + return res = 0; + #endif } - - // Get redirect url - char *redirect_url; - curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); - - // Handle two step authorization - if (std::string(redirect_url).find("two_step") != std::string::npos) + else { - std::string security_code; - std::string tagname_two_step_send = "second_step_authentication[send]"; - std::string tagname_two_step_auth_letter_1 = "second_step_authentication[token][letter_1]"; - std::string tagname_two_step_auth_letter_2 = "second_step_authentication[token][letter_2]"; - std::string tagname_two_step_auth_letter_3 = "second_step_authentication[token][letter_3]"; - std::string tagname_two_step_auth_letter_4 = "second_step_authentication[token][letter_4]"; - std::string tagname_two_step_token; - std::string token_two_step; - std::string two_step_html = this->getResponse(redirect_url); - redirect_url = NULL; - - tree two_step_dom = parser.parseTree(two_step_html); - tree::iterator two_step_it = two_step_dom.begin(); - tree::iterator two_step_it_end = two_step_dom.end(); - for (; two_step_it != two_step_it_end; ++two_step_it) + htmlcxx::HTML::ParserDom parser; + tree login_dom = parser.parseTree(login_form_html); + tree::iterator login_it = login_dom.begin(); + tree::iterator login_it_end = login_dom.end(); + for (; login_it != login_it_end; ++login_it) { - if (two_step_it->tagName()=="input") + if (login_it->tagName()=="input") { - two_step_it->parseAttributes(); - if (two_step_it->attribute("id").second == "second_step_authentication__token") + login_it->parseAttributes(); + if (login_it->attribute("id").second == "login__token") { - token_two_step = two_step_it->attribute("value").second; // two step token - tagname_two_step_token = two_step_it->attribute("name").second; + token = login_it->attribute("value").second; // login token + tagname_token = login_it->attribute("name").second; } } } - std::cerr << "Security code: "; - std::getline(std::cin,security_code); - if (security_code.size() != 4) + if (token.empty()) { - std::cerr << "Security code must be 4 characters long" << std::endl; - exit(1); + std::cout << "Failed to get login token" << std::endl; + return res = 0; } - postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3] - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "=" - + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size()); - - curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step"); + //Create postdata - escape characters in email/password to support special characters + postdata = (std::string)curl_easy_escape(curlhandle, tagname_username.c_str(), tagname_username.size()) + "=" + (std::string)curl_easy_escape(curlhandle, email.c_str(), email.size()) + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_password.c_str(), tagname_password.size()) + "=" + (std::string)curl_easy_escape(curlhandle, password.c_str(), password.size()) + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_login.c_str(), tagname_login.size()) + "=" + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_token.c_str(), tagname_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token.c_str(), token.size()); + curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login_check"); curl_easy_setopt(curlhandle, CURLOPT_POST, 1); curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); @@ -417,47 +374,118 @@ int Website::Login(const std::string& email, const std::string& password) curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); - // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first. + // Don't follow to redirect location because we need to check it for two step authorization. curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); - result = curl_easy_perform(curlhandle); + CURLcode result = curl_easy_perform(curlhandle); memory.str(std::string()); + + if (result != CURLE_OK) + { + // Expected to hit maximum amount of redirects so don't print error on it + if (result != CURLE_TOO_MANY_REDIRECTS) + std::cout << curl_easy_strerror(result) << std::endl; + } + + // Get redirect url + char *redirect_url; curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); - } - if (!std::string(redirect_url).empty()) - { - long response_code; - do + // Handle two step authorization + if (std::string(redirect_url).find("two_step") != std::string::npos) { - curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); + std::string security_code; + std::string tagname_two_step_send = "second_step_authentication[send]"; + std::string tagname_two_step_auth_letter_1 = "second_step_authentication[token][letter_1]"; + std::string tagname_two_step_auth_letter_2 = "second_step_authentication[token][letter_2]"; + std::string tagname_two_step_auth_letter_3 = "second_step_authentication[token][letter_3]"; + std::string tagname_two_step_auth_letter_4 = "second_step_authentication[token][letter_4]"; + std::string tagname_two_step_token; + std::string token_two_step; + std::string two_step_html = this->getResponse(redirect_url); + redirect_url = NULL; + + tree two_step_dom = parser.parseTree(two_step_html); + tree::iterator two_step_it = two_step_dom.begin(); + tree::iterator two_step_it_end = two_step_dom.end(); + for (; two_step_it != two_step_it_end; ++two_step_it) + { + if (two_step_it->tagName()=="input") + { + two_step_it->parseAttributes(); + if (two_step_it->attribute("id").second == "second_step_authentication__token") + { + token_two_step = two_step_it->attribute("value").second; // two step token + tagname_two_step_token = two_step_it->attribute("name").second; + } + } + } + + std::cerr << "Security code: "; + std::getline(std::cin,security_code); + if (security_code.size() != 4) + { + std::cerr << "Security code must be 4 characters long" << std::endl; + exit(1); + } + + postdata = (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_1.c_str(), tagname_two_step_auth_letter_1.size()) + "=" + security_code[0] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_2.c_str(), tagname_two_step_auth_letter_2.size()) + "=" + security_code[1] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_3.c_str(), tagname_two_step_auth_letter_3.size()) + "=" + security_code[2] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_auth_letter_4.c_str(), tagname_two_step_auth_letter_4.size()) + "=" + security_code[3] + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_send.c_str(), tagname_two_step_send.size()) + "=" + + "&" + (std::string)curl_easy_escape(curlhandle, tagname_two_step_token.c_str(), tagname_two_step_token.size()) + "=" + (std::string)curl_easy_escape(curlhandle, token_two_step.c_str(), token_two_step.size()); + + curl_easy_setopt(curlhandle, CURLOPT_URL, "https://login.gog.com/login/two_step"); + curl_easy_setopt(curlhandle, CURLOPT_POST, 1); + curl_easy_setopt(curlhandle, CURLOPT_POSTFIELDS, postdata.c_str()); + curl_easy_setopt(curlhandle, CURLOPT_WRITEFUNCTION, Website::writeMemoryCallback); + curl_easy_setopt(curlhandle, CURLOPT_WRITEDATA, &memory); + curl_easy_setopt(curlhandle, CURLOPT_NOPROGRESS, 1); + curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, 0); + curl_easy_setopt(curlhandle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); + + // Don't follow to redirect location because it doesn't work properly. Must clean up the redirect url first. + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 0); result = curl_easy_perform(curlhandle); memory.str(std::string()); + curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); + } - result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); - if ((response_code / 100) == 3) - curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); - - std::string redir_url = std::string(redirect_url); - boost::regex re(".*code=(.*?)([\?&].*|$)", boost::regex_constants::icase); - boost::match_results what; - if (boost::regex_search(redir_url, what, re)) + if (!std::string(redirect_url).empty()) + { + long response_code; + do { - auth_code = what[1]; - if (!auth_code.empty()) - break; - } - } while (result == CURLE_OK && (response_code / 100) == 3); - } + curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); + result = curl_easy_perform(curlhandle); + memory.str(std::string()); + + result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code); + if ((response_code / 100) == 3) + curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url); + + std::string redir_url = std::string(redirect_url); + boost::regex re(".*code=(.*?)([\?&].*|$)", boost::regex_constants::icase); + boost::match_results what; + if (boost::regex_search(redir_url, what, re)) + { + auth_code = what[1]; + if (!auth_code.empty()) + break; + } + } while (result == CURLE_OK && (response_code / 100) == 3); + } - curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); - curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1); - curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1); - curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); - result = curl_easy_perform(curlhandle); + curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url); + curl_easy_setopt(curlhandle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curlhandle, CURLOPT_MAXREDIRS, -1); + curl_easy_setopt(curlhandle, CURLOPT_FOLLOWLOCATION, 1); + result = curl_easy_perform(curlhandle); - if (result != CURLE_OK) - { - std::cout << curl_easy_strerror(result) << std::endl; + if (result != CURLE_OK) + { + std::cout << curl_easy_strerror(result) << std::endl; + } } if (this->IsLoggedInComplex(email))