Add GUI login

Use QtWebEngine for getting cookies and authorization code if login form contains recaptcha and downloader is compiled with -DUSE_QT_GUI=ON
This commit is contained in:
Sude 2019-03-01 10:05:16 +02:00
parent 01eed3ec9e
commit 1e8ebbfd94
5 changed files with 347 additions and 118 deletions

View File

@ -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]")

View File

@ -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

41
include/gui_login.h Normal file
View File

@ -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 <QObject>
#include <QWebEngineCookieStore>
#include <iostream>
#include <vector>
class GuiLogin : public QObject
{
Q_OBJECT
public:
GuiLogin();
virtual ~GuiLogin();
void Login();
std::string getCode();
std::vector<std::string> getCookies();
private:
QWebEngineCookieStore *cookiestore;
std::vector<std::string> cookies;
std::string auth_code;
public slots:
void loadFinished(bool success);
void cookieAdded(const QNetworkCookie &cookie);
};
#endif // GUI_LOGIN_H

134
src/gui_login.cpp Normal file
View File

@ -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 <QApplication>
#include <QtWidgets/QWidget>
#include <QtWebEngineWidgets/QWebEngineView>
#include <QStyle>
#include <QLayout>
#include <QDesktopWidget>
#include <QWebEngineProfile>
GuiLogin::GuiLogin()
{
// constructor
}
GuiLogin::~GuiLogin()
{
// destructor
}
void GuiLogin::loadFinished(bool success)
{
QWebEngineView *view = qobject_cast<QWebEngineView*>(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<char> 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<std::string> GuiLogin::getCookies()
{
return this->cookies;
}
#include "moc_gui_login.cpp"

View File

@ -10,6 +10,10 @@
#include <htmlcxx/html/ParserDom.h>
#include <boost/algorithm/string/case_conv.hpp>
#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;
}
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();
htmlcxx::HTML::ParserDom parser;
tree<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
tree<htmlcxx::HTML::Node>::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")
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());
}
}
auth_code = gl.getCode();
#endif
}
if (token.empty())
if (bRecaptcha)
{
std::cout << "Failed to get login token" << std::endl;
return res = 0;
// This should never be reached but do additional check here just in case
#ifndef USE_QT_GUI_LOGIN
return res = 0;
#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)
else
{
// 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);
// Handle two step authorization
if (std::string(redirect_url).find("two_step") != std::string::npos)
{
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<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html);
tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin();
tree<htmlcxx::HTML::Node>::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<htmlcxx::HTML::Node> login_dom = parser.parseTree(login_form_html);
tree<htmlcxx::HTML::Node>::iterator login_it = login_dom.begin();
tree<htmlcxx::HTML::Node>::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());
curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
}
if (!std::string(redirect_url).empty())
{
long response_code;
do
if (result != CURLE_OK)
{
curl_easy_setopt(curlhandle, CURLOPT_URL, redirect_url);
// 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);
// Handle two step authorization
if (std::string(redirect_url).find("two_step") != std::string::npos)
{
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<htmlcxx::HTML::Node> two_step_dom = parser.parseTree(two_step_html);
tree<htmlcxx::HTML::Node>::iterator two_step_it = two_step_dom.begin();
tree<htmlcxx::HTML::Node>::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<std::string::const_iterator> 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());
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);
result = curl_easy_getinfo(curlhandle, CURLINFO_RESPONSE_CODE, &response_code);
if ((response_code / 100) == 3)
curl_easy_getinfo(curlhandle, CURLINFO_REDIRECT_URL, &redirect_url);
if (result != CURLE_OK)
{
std::cout << curl_easy_strerror(result) << std::endl;
std::string redir_url = std::string(redirect_url);
boost::regex re(".*code=(.*?)([\?&].*|$)", boost::regex_constants::icase);
boost::match_results<std::string::const_iterator> 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);
if (result != CURLE_OK)
{
std::cout << curl_easy_strerror(result) << std::endl;
}
}
if (this->IsLoggedInComplex(email))