mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-08-01 04:02:02 +08:00

Define polling action that changes when uuid->printer_model map is filled. Also serves to turn off polling.
481 lines
16 KiB
C++
481 lines
16 KiB
C++
#include "UserAccountCommunication.hpp"
|
|
#include "GUI_App.hpp"
|
|
#include "GUI.hpp"
|
|
#include "format.hpp"
|
|
#include "../Utils/Http.hpp"
|
|
#include "slic3r/GUI/I18N.hpp"
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/beast/core/detail/base64.hpp>
|
|
#include <curl/curl.h>
|
|
#include <string>
|
|
|
|
#include <iostream>
|
|
#include <random>
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <regex>
|
|
#include <iomanip>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
|
|
#if wxUSE_SECRETSTORE
|
|
#include <wx/secretstore.h>
|
|
#endif
|
|
|
|
#ifdef WIN32
|
|
#include <wincrypt.h>
|
|
#endif // WIN32
|
|
|
|
#ifdef __APPLE__
|
|
#include <CommonCrypto/CommonDigest.h>
|
|
#endif
|
|
|
|
#ifdef __linux__
|
|
#include <openssl/evp.h>
|
|
#include <openssl/bio.h>
|
|
#include <openssl/buffer.h>
|
|
#endif // __linux__
|
|
|
|
|
|
|
|
namespace fs = boost::filesystem;
|
|
|
|
|
|
namespace Slic3r {
|
|
namespace GUI {
|
|
|
|
namespace {
|
|
|
|
std::string get_code_from_message(const std::string& url_message)
|
|
{
|
|
size_t pos = url_message.rfind("code=");
|
|
std::string out;
|
|
for (size_t i = pos + 5; i < url_message.size(); i++) {
|
|
const char& c = url_message[i];
|
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
|
|
out+= c;
|
|
else
|
|
break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool is_secret_store_ok()
|
|
{
|
|
#if wxUSE_SECRETSTORE
|
|
wxSecretStore store = wxSecretStore::GetDefault();
|
|
wxString errmsg;
|
|
if (!store.IsOk(&errmsg)) {
|
|
BOOST_LOG_TRIVIAL(warning) << "wxSecretStore is not supported: " << errmsg;
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
bool save_secret(const std::string& opt, const std::string& usr, const std::string& psswd)
|
|
{
|
|
#if wxUSE_SECRETSTORE
|
|
wxSecretStore store = wxSecretStore::GetDefault();
|
|
wxString errmsg;
|
|
if (!store.IsOk(&errmsg)) {
|
|
std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg);
|
|
BOOST_LOG_TRIVIAL(error) << msg;
|
|
//show_error(nullptr, msg);
|
|
return false;
|
|
}
|
|
const wxString service = GUI::format_wxstr(L"%1%/PrusaAccount/%2%", SLIC3R_APP_NAME, opt);
|
|
const wxString username = boost::nowide::widen(usr);
|
|
const wxSecretValue password(boost::nowide::widen(psswd));
|
|
if (!store.Save(service, username, password)) {
|
|
std::string msg(_u8L("Failed to save credentials to the system secret store."));
|
|
BOOST_LOG_TRIVIAL(error) << msg;
|
|
//show_error(nullptr, msg);
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot save password to the system store.";
|
|
return false;
|
|
#endif // wxUSE_SECRETSTORE
|
|
}
|
|
bool load_secret(const std::string& opt, std::string& usr, std::string& psswd)
|
|
{
|
|
#if wxUSE_SECRETSTORE
|
|
wxSecretStore store = wxSecretStore::GetDefault();
|
|
wxString errmsg;
|
|
if (!store.IsOk(&errmsg)) {
|
|
std::string msg = GUI::format("%1% (%2%).", _u8L("This system doesn't support storing passwords securely"), errmsg);
|
|
BOOST_LOG_TRIVIAL(error) << msg;
|
|
//show_error(nullptr, msg);
|
|
return false;
|
|
}
|
|
const wxString service = GUI::format_wxstr(L"%1%/PrusaAccount/%2%", SLIC3R_APP_NAME, opt);
|
|
wxString username;
|
|
wxSecretValue password;
|
|
if (!store.Load(service, username, password)) {
|
|
std::string msg(_u8L("Failed to load credentials from the system secret store."));
|
|
BOOST_LOG_TRIVIAL(error) << msg;
|
|
//show_error(nullptr, msg);
|
|
return false;
|
|
}
|
|
usr = into_u8(username);
|
|
psswd = into_u8(password.GetAsString());
|
|
return true;
|
|
#else
|
|
BOOST_LOG_TRIVIAL(error) << "wxUSE_SECRETSTORE not supported. Cannot load password from the system store.";
|
|
return false;
|
|
#endif // wxUSE_SECRETSTORE
|
|
}
|
|
}
|
|
|
|
UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, AppConfig* app_config)
|
|
: m_evt_handler(evt_handler)
|
|
, m_app_config(app_config)
|
|
{
|
|
std::string access_token, refresh_token, shared_session_key;
|
|
if (is_secret_store_ok()) {
|
|
std::string key0, key1;
|
|
load_secret("access_token", key0, access_token);
|
|
load_secret("refresh_token", key1, refresh_token);
|
|
assert(key0 == key1);
|
|
shared_session_key = key0;
|
|
} else {
|
|
access_token = m_app_config->get("access_token");
|
|
refresh_token = m_app_config->get("refresh_token");
|
|
shared_session_key = m_app_config->get("shared_session_key");
|
|
}
|
|
bool has_token = !access_token.empty() && !refresh_token.empty();
|
|
m_session = std::make_unique<UserAccountSession>(evt_handler, access_token, refresh_token, shared_session_key, m_app_config->get_bool("connect_polling"));
|
|
init_session_thread();
|
|
// perform login at the start, but only with tokens
|
|
if (has_token)
|
|
do_login();
|
|
}
|
|
|
|
UserAccountCommunication::~UserAccountCommunication()
|
|
{
|
|
if (m_thread.joinable()) {
|
|
// Stop the worker thread, if running.
|
|
{
|
|
// Notify the worker thread to cancel wait on detection polling.
|
|
std::lock_guard<std::mutex> lck(m_thread_stop_mutex);
|
|
m_thread_stop = true;
|
|
}
|
|
m_thread_stop_condition.notify_all();
|
|
// Wait for the worker thread to stop.
|
|
m_thread.join();
|
|
}
|
|
}
|
|
|
|
void UserAccountCommunication::set_username(const std::string& username)
|
|
{
|
|
m_username = username;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
if (is_secret_store_ok()) {
|
|
save_secret("access_token", m_session->get_shared_session_key(), m_remember_session ? m_session->get_access_token() : std::string());
|
|
save_secret("refresh_token", m_session->get_shared_session_key(), m_remember_session ? m_session->get_refresh_token() : std::string());
|
|
}
|
|
else {
|
|
m_app_config->set("access_token", m_remember_session ? m_session->get_access_token() : std::string());
|
|
m_app_config->set("refresh_token", m_remember_session ? m_session->get_refresh_token() : std::string());
|
|
m_app_config->set("shared_session_key", m_remember_session ? m_session->get_shared_session_key() : std::string());
|
|
}
|
|
}
|
|
}
|
|
|
|
void UserAccountCommunication::set_remember_session(bool b)
|
|
{
|
|
m_remember_session = b;
|
|
// tokens needs to be stored or deleted
|
|
set_username(m_username);
|
|
}
|
|
|
|
std::string UserAccountCommunication::get_access_token()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
return m_session->get_access_token();
|
|
}
|
|
}
|
|
|
|
std::string UserAccountCommunication::get_shared_session_key()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
return m_session->get_shared_session_key();
|
|
}
|
|
}
|
|
|
|
void UserAccountCommunication::set_polling_enabled(bool enabled)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
return m_session->set_polling_action(enabled ? UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS : UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY);
|
|
}
|
|
}
|
|
|
|
void UserAccountCommunication::on_uuid_map_success()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
return m_session->set_polling_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS);
|
|
}
|
|
}
|
|
|
|
void UserAccountCommunication::login_redirect()
|
|
{
|
|
const std::string AUTH_HOST = "https://account.prusa3d.com";
|
|
const std::string CLIENT_ID = client_id();
|
|
const std::string REDIRECT_URI = "prusaslicer://login";
|
|
CodeChalengeGenerator ccg;
|
|
m_code_verifier = ccg.generate_verifier();
|
|
std::string code_challenge = ccg.generate_chalenge(m_code_verifier);
|
|
BOOST_LOG_TRIVIAL(info) << "code verifier: " << m_code_verifier;
|
|
BOOST_LOG_TRIVIAL(info) << "code challenge: " << code_challenge;
|
|
|
|
wxString url = GUI::format_wxstr(L"%1%/o/authorize/?client_id=%2%&response_type=code&code_challenge=%3%&code_challenge_method=S256&scope=basic_info&redirect_uri=%4%&choose_account=1", AUTH_HOST, CLIENT_ID, code_challenge, REDIRECT_URI);
|
|
|
|
wxQueueEvent(m_evt_handler,new OpenPrusaAuthEvent(GUI::EVT_OPEN_PRUSAAUTH, std::move(url)));
|
|
}
|
|
|
|
bool UserAccountCommunication::is_logged()
|
|
{
|
|
return !m_username.empty();
|
|
}
|
|
void UserAccountCommunication::do_login()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
if (!m_session->is_initialized()) {
|
|
login_redirect();
|
|
} else {
|
|
m_session->enqueue_test_with_refresh();
|
|
}
|
|
}
|
|
wakeup_session_thread();
|
|
}
|
|
void UserAccountCommunication::do_logout()
|
|
{
|
|
do_clear();
|
|
wxQueueEvent(m_evt_handler, new UserAccountSuccessEvent(GUI::EVT_UA_LOGGEDOUT, {}));
|
|
}
|
|
|
|
void UserAccountCommunication::do_clear()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
m_session->clear();
|
|
}
|
|
set_username({});
|
|
}
|
|
|
|
void UserAccountCommunication::on_login_code_recieved(const std::string& url_message)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
const std::string code = get_code_from_message(url_message);
|
|
m_session->init_with_code(code, m_code_verifier);
|
|
}
|
|
wakeup_session_thread();
|
|
}
|
|
|
|
void UserAccountCommunication::enqueue_connect_printer_models_action()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
if (!m_session->is_initialized()) {
|
|
BOOST_LOG_TRIVIAL(error) << "Connect Printer Models connection failed - Not Logged in.";
|
|
return;
|
|
}
|
|
m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS, nullptr, nullptr, {});
|
|
}
|
|
wakeup_session_thread();
|
|
}
|
|
|
|
void UserAccountCommunication::enqueue_connect_status_action()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
if (!m_session->is_initialized()) {
|
|
BOOST_LOG_TRIVIAL(error) << "Connect Status endpoint connection failed - Not Logged in.";
|
|
return;
|
|
}
|
|
m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS, nullptr, nullptr, {});
|
|
}
|
|
wakeup_session_thread();
|
|
}
|
|
void UserAccountCommunication::enqueue_test_connection()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
if (!m_session->is_initialized()) {
|
|
BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in.";
|
|
return;
|
|
}
|
|
m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION, nullptr, nullptr, {});
|
|
}
|
|
wakeup_session_thread();
|
|
}
|
|
|
|
void UserAccountCommunication::enqueue_avatar_action(const std::string& url)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
if (!m_session->is_initialized()) {
|
|
BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in.";
|
|
return;
|
|
}
|
|
m_session->enqueue_action(UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR, nullptr, nullptr, url);
|
|
}
|
|
wakeup_session_thread();
|
|
}
|
|
void UserAccountCommunication::init_session_thread()
|
|
{
|
|
m_thread = std::thread([this]() {
|
|
for (;;) {
|
|
// Wait for 5 seconds or wakeup call
|
|
{
|
|
std::unique_lock<std::mutex> lck(m_thread_stop_mutex);
|
|
m_thread_stop_condition.wait_for(lck, std::chrono::seconds(5), [this] { return m_thread_stop || m_thread_wakeup; });
|
|
}
|
|
if (m_thread_stop)
|
|
// Stop the worker thread.
|
|
break;
|
|
m_thread_wakeup = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_session_mutex);
|
|
m_session->process_action_queue();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void UserAccountCommunication::wakeup_session_thread()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lck(m_thread_stop_mutex);
|
|
m_thread_wakeup = true;
|
|
}
|
|
m_thread_stop_condition.notify_all();
|
|
}
|
|
|
|
std::string CodeChalengeGenerator::generate_chalenge(const std::string& verifier)
|
|
{
|
|
std::string code_challenge;
|
|
try
|
|
{
|
|
code_challenge = sha256(verifier);
|
|
code_challenge = base64_encode(code_challenge);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
BOOST_LOG_TRIVIAL(error) << "Code Chalenge Generator failed: " << e.what();
|
|
}
|
|
assert(!code_challenge.empty());
|
|
return code_challenge;
|
|
|
|
}
|
|
std::string CodeChalengeGenerator::generate_verifier()
|
|
{
|
|
size_t length = 40;
|
|
std::string code_verifier = generate_code_verifier(length);
|
|
assert(code_verifier.size() == length);
|
|
return code_verifier;
|
|
}
|
|
std::string CodeChalengeGenerator::base64_encode(const std::string& input)
|
|
{
|
|
std::string output;
|
|
output.resize(boost::beast::detail::base64::encoded_size(input.size()));
|
|
boost::beast::detail::base64::encode(&output[0], input.data(), input.size());
|
|
// save encode - replace + and / with - and _
|
|
std::replace(output.begin(), output.end(), '+', '-');
|
|
std::replace(output.begin(), output.end(), '/', '_');
|
|
// remove last '=' sign
|
|
while (output.back() == '=')
|
|
output.pop_back();
|
|
return output;
|
|
}
|
|
std::string CodeChalengeGenerator::generate_code_verifier(size_t length)
|
|
{
|
|
const std::string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
std::random_device rd;
|
|
std::mt19937 gen(rd());
|
|
std::uniform_int_distribution<int> distribution(0, chars.size() - 1);
|
|
std::string code_verifier;
|
|
for (size_t i = 0; i < length; ++i) {
|
|
code_verifier += chars[distribution(gen)];
|
|
}
|
|
return code_verifier;
|
|
}
|
|
|
|
#ifdef WIN32
|
|
std::string CodeChalengeGenerator::sha256(const std::string& input)
|
|
{
|
|
HCRYPTPROV prov_handle = NULL;
|
|
HCRYPTHASH hash_handle = NULL;
|
|
DWORD hash_size = 0;
|
|
DWORD buffer_size = sizeof(DWORD);
|
|
std::string output;
|
|
|
|
if (!CryptAcquireContext(&prov_handle, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
|
|
throw std::exception("CryptAcquireContext failed.");
|
|
}
|
|
if (!CryptCreateHash(prov_handle, CALG_SHA_256, 0, 0, &hash_handle)) {
|
|
CryptReleaseContext(prov_handle, 0);
|
|
throw std::exception("CryptCreateHash failed.");
|
|
}
|
|
if (!CryptHashData(hash_handle, reinterpret_cast<const BYTE*>(input.c_str()), input.length(), 0)) {
|
|
CryptDestroyHash(hash_handle);
|
|
CryptReleaseContext(prov_handle, 0);
|
|
throw std::exception("CryptCreateHash failed.");
|
|
}
|
|
if (!CryptGetHashParam(hash_handle, HP_HASHSIZE, reinterpret_cast<BYTE*>(&hash_size), &buffer_size, 0)) {
|
|
CryptDestroyHash(hash_handle);
|
|
CryptReleaseContext(prov_handle, 0);
|
|
throw std::exception("CryptGetHashParam HP_HASHSIZE failed.");
|
|
}
|
|
output.resize(hash_size);
|
|
if (!CryptGetHashParam(hash_handle, HP_HASHVAL, reinterpret_cast<BYTE*>(&output[0]), &hash_size, 0)) {
|
|
CryptDestroyHash(hash_handle);
|
|
CryptReleaseContext(prov_handle, 0);
|
|
throw std::exception("CryptGetHashParam HP_HASHVAL failed.");
|
|
}
|
|
return output;
|
|
}
|
|
#elif __APPLE__
|
|
std::string CodeChalengeGenerator::sha256(const std::string& input) {
|
|
// Initialize the context
|
|
CC_SHA256_CTX sha256;
|
|
CC_SHA256_Init(&sha256);
|
|
|
|
// Update the context with the input data
|
|
CC_SHA256_Update(&sha256, input.c_str(), static_cast<CC_LONG>(input.length()));
|
|
|
|
// Finalize the hash and retrieve the result
|
|
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
CC_SHA256_Final(digest, &sha256);
|
|
|
|
return std::string(reinterpret_cast<char*>(digest), CC_SHA256_DIGEST_LENGTH);
|
|
}
|
|
#else
|
|
std::string CodeChalengeGenerator::sha256(const std::string& input) {
|
|
EVP_MD_CTX* mdctx;
|
|
const EVP_MD* md;
|
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
|
unsigned int digestLen;
|
|
|
|
md = EVP_sha256();
|
|
mdctx = EVP_MD_CTX_new();
|
|
EVP_DigestInit_ex(mdctx, md, NULL);
|
|
EVP_DigestUpdate(mdctx, input.c_str(), input.length());
|
|
EVP_DigestFinal_ex(mdctx, digest, &digestLen);
|
|
EVP_MD_CTX_free(mdctx);
|
|
|
|
return std::string(reinterpret_cast<char*>(digest), digestLen);
|
|
}
|
|
#endif // __linux__
|
|
}} // Slic3r::GUI
|