From 6a7ac533358723e5faa1b26df35a1bbdd22abaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ba=C5=99tip=C3=A1n?= Date: Fri, 12 Jul 2024 18:07:07 +0200 Subject: [PATCH 1/7] Connect Login Refresh: UserAccountComm: refresh token on wake; WebViewDialog: Connect login retry with exp backoff implemented, added a bit more logging --- src/slic3r/GUI/UserAccountCommunication.cpp | 16 ++++- src/slic3r/GUI/UserAccountCommunication.hpp | 3 +- src/slic3r/GUI/UserAccountSession.cpp | 2 + src/slic3r/GUI/WebViewDialog.cpp | 75 ++++++++++++++++++--- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 27a8dd2957..78227501aa 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -512,6 +512,14 @@ void UserAccountCommunication::on_activate_window(bool active) std::lock_guard lck(m_thread_stop_mutex); m_window_is_active = active; } + auto now = std::time(nullptr); + BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active; + if (active && m_next_token_refresh_at - now < 60) { + BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation"; + enqueue_refresh(); + m_token_timer->Stop(); + } + } void UserAccountCommunication::wakeup_session_thread() @@ -527,12 +535,16 @@ void UserAccountCommunication::set_refresh_time(int seconds) { assert(m_token_timer); m_token_timer->Stop(); - int miliseconds = std::max(seconds * 1000 - 66666, 60000); - m_token_timer->StartOnce(miliseconds); + const auto prior_expiration_secs = 5 * 60; + int milliseconds = std::max((seconds - prior_expiration_secs) * 1000, 60000); + m_next_token_refresh_at = std::time(nullptr) + milliseconds / 1000; + m_token_timer->StartOnce(milliseconds); } + void UserAccountCommunication::on_token_timer(wxTimerEvent& evt) { + BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication: Token refresh timer fired"; enqueue_refresh(); } void UserAccountCommunication::on_polling_timer(wxTimerEvent& evt) diff --git a/src/slic3r/GUI/UserAccountCommunication.hpp b/src/slic3r/GUI/UserAccountCommunication.hpp index f4ec0dbf99..48d9a62e39 100644 --- a/src/slic3r/GUI/UserAccountCommunication.hpp +++ b/src/slic3r/GUI/UserAccountCommunication.hpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include namespace Slic3r { namespace GUI { @@ -97,6 +97,7 @@ private: wxTimer* m_token_timer; wxEvtHandler* m_timer_evt_handler; + std::time_t m_next_token_refresh_at; void wakeup_session_thread(); void init_session_thread(); diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index db81c2984e..860e90cc5d 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -123,6 +123,7 @@ void UserAccountSession::init_with_code(const std::string& code, const std::stri void UserAccountSession::token_success_callback(const std::string& body) { + BOOST_LOG_TRIVIAL(debug) << "Access token refreshed"; // Data we need std::string access_token, refresh_token, shared_session_key; int expires_in = 300; @@ -176,6 +177,7 @@ void UserAccountSession::token_success_callback(const std::string& body) void UserAccountSession::code_exchange_fail_callback(const std::string& body) { + BOOST_LOG_TRIVIAL(debug) << "Access token refresh failed, body: " << body; clear(); cancel_queue(); // Unlike refresh_fail_callback, no event was triggered so far, do it. (USER_ACCOUNT_ACTION_CODE_FOR_TOKEN does not send events) diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index eac2873152..0411a06b84 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -636,6 +636,30 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) window.__access_token_version = 0; )", #else +// R"( +// console.log('Preparing login'); +// function errorHandler(err) { +// const msg = { +// action: 'ERROR', +// error: JSON.stringify(err), +// critical: false +// }; +// console.error('Login error occurred', msg); +// window._prusaSlicer.postMessage(msg); +// }; +// window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) +// .then(function (resp) { +// console.log('Login resp', resp); +// resp.text() +// .then(function (json) { console.log('Login resp body', json); return json; }) +// .then(function (body) { +// if (resp.status >= 400) errorHandler({status: resp.status, body}); +// }); +// }) +// .catch(function (err){ +// errorHandler({message: err.message, stack: err.stack}); +// }); +// )", R"( console.log('Preparing login'); function errorHandler(err) { @@ -647,18 +671,46 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) console.error('Login error occurred', msg); window._prusaSlicer.postMessage(msg); }; - window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) - .then(function (resp) { - console.log('Login resp', resp); - resp.text() - .then(function (json) { console.log('Login resp body', json); return json; }) - .then(function (body) { - if (resp.status >= 400) errorHandler({status: resp.status, body}); - }); - }) - .catch(function (err){ - errorHandler({message: err.message, stack: err.stack}); + + function delay(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); }); + } + + (async () => { + let retry = false; + let backoff = 1000; + const maxBackoff = 64000; + do { + + let error = false; + + try { + console.log('Slicer Login request'); + let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}); + let body = await resp.text(); + console.log('Slicer Login resp', resp.status, body); + if (resp.status >= 500) { + retry = true; + } else { + retry = false; + if (resp.status >= 400) + errorHandler({status: resp.status, body}); + } + } catch (e) { + console.error('Slicer Login failed', e.toString()); + retry = true; + } + + if (retry) { + await delay(backoff + 1000 * Math.random()); + if (backoff < maxBackoff) { + backoff *= 2; + } + } + } while (retry); + })(); )", #endif access_token @@ -691,6 +743,7 @@ void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) } void ConnectWebViewPanel::on_navigation_request(wxWebViewEvent &evt) { + BOOST_LOG_TRIVIAL(debug) << "Navigation requested to: " << into_u8(evt.GetURL()); if (evt.GetURL() == m_default_url) { m_reached_default_url = true; return; From ac859d711c06d133086c03bec7b3fc37dc5bbd76 Mon Sep 17 00:00:00 2001 From: Jan Bartipan Date: Mon, 22 Jul 2024 18:28:23 +0200 Subject: [PATCH 2/7] Connect token refresh on wake up from sleep: also resend config to make sure the token is loaded into frontend --- src/slic3r/GUI/WebViewDialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 0411a06b84..bace551d2b 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -734,6 +734,7 @@ void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) //m_browser->AddUserScript(javascript, wxWEBVIEW_INJECT_AT_DOCUMENT_END); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->RunScriptAsync(javascript); + resend_config(); } void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) From 54baccd6fc93ee9077eea6fb43e3dac53735d18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ba=C5=99tip=C3=A1n?= Date: Mon, 19 Aug 2024 10:02:03 +0200 Subject: [PATCH 3/7] Http: added support for retry with exponentail backoff, UserAccountCommunication::on_active triggered from app instrad of panel to correctly handle situation when multiple app windows opened, ConnectWebView: refactored the login script to prevent using old token --- src/slic3r/GUI/GUI_App.cpp | 4 ++ src/slic3r/GUI/Plater.cpp | 1 - src/slic3r/GUI/UserAccount.hpp | 2 +- src/slic3r/GUI/UserAccountCommunication.cpp | 12 ++-- src/slic3r/GUI/UserAccountCommunication.hpp | 4 +- src/slic3r/GUI/UserAccountSession.cpp | 24 ++++--- src/slic3r/GUI/UserAccountSession.hpp | 5 +- src/slic3r/GUI/WebViewDialog.cpp | 55 +++++++--------- src/slic3r/Utils/Http.cpp | 69 ++++++++++++++++++--- src/slic3r/Utils/Http.hpp | 16 ++++- 10 files changed, 127 insertions(+), 65 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 815dc42069..d107a83345 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1448,6 +1448,10 @@ bool GUI_App::on_init_inner() this->check_updates(false); }); + Bind(wxEVT_ACTIVATE_APP, [this](const wxActivateEvent &evt) { + plater_->get_user_account()->on_activate_app(evt.GetActive()); + }); + } else { #ifdef __WXMSW__ diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3ac107988e..3568b153dd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6473,7 +6473,6 @@ void Plater::force_print_bed_update() void Plater::on_activate(bool active) { - this->p->user_account->on_activate_window(active); if (active) { this->p->show_delayed_error_message(); } diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index 22787f9d7c..967581f3b0 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -60,7 +60,7 @@ public: bool on_connect_printers_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); bool on_connect_uiid_map_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); - void on_activate_window(bool active) { m_communication->on_activate_window(active); } + void on_activate_app(bool active) { m_communication->on_activate_app(active); } std::string get_username() const { return m_username; } std::string get_access_token(); diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 78227501aa..c5605bbf0a 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -475,6 +475,11 @@ void UserAccountCommunication::enqueue_refresh() BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; return; } + if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) + { + BOOST_LOG_TRIVIAL(debug) << "User Account: Token refresh already enqueued, skipping..."; + return; + } m_session->enqueue_refresh({}); } wakeup_session_thread(); @@ -506,7 +511,7 @@ void UserAccountCommunication::init_session_thread() }); } -void UserAccountCommunication::on_activate_window(bool active) +void UserAccountCommunication::on_activate_app(bool active) { { std::lock_guard lck(m_thread_stop_mutex); @@ -514,12 +519,11 @@ void UserAccountCommunication::on_activate_window(bool active) } auto now = std::time(nullptr); BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active; - if (active && m_next_token_refresh_at - now < 60) { + if (active && m_next_token_refresh_at > 0 && m_next_token_refresh_at - now < 60) { BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation"; - enqueue_refresh(); m_token_timer->Stop(); + enqueue_refresh(); } - } void UserAccountCommunication::wakeup_session_thread() diff --git a/src/slic3r/GUI/UserAccountCommunication.hpp b/src/slic3r/GUI/UserAccountCommunication.hpp index 48d9a62e39..c0859f4ca0 100644 --- a/src/slic3r/GUI/UserAccountCommunication.hpp +++ b/src/slic3r/GUI/UserAccountCommunication.hpp @@ -60,7 +60,7 @@ public: // Exchanges code for tokens and shared_session_key void on_login_code_recieved(const std::string& url_message); - void on_activate_window(bool active); + void on_activate_app(bool active); void set_username(const std::string& username); void set_remember_session(bool b); @@ -97,7 +97,7 @@ private: wxTimer* m_token_timer; wxEvtHandler* m_timer_evt_handler; - std::time_t m_next_token_refresh_at; + std::time_t m_next_token_refresh_at{0}; void wakeup_session_thread(); void init_session_thread(); diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index 860e90cc5d..19e92d95e4 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -46,7 +46,7 @@ void UserActionPost::perform(/*UNUSED*/ wxEvtHandler* evt_handler, /*UNUSED*/ co if (success_callback) success_callback(body); }); - http.perform_sync(); + http.perform_sync(HttpRetryOpt::default_retry()); } void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const @@ -69,9 +69,17 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin wxQueueEvent(evt_handler, new UserAccountSuccessEvent(succ_evt_type, body)); }); - http.perform_sync(); + http.perform_sync(HttpRetryOpt::default_retry()); } +bool UserAccountSession::is_enqueued(UserAccountActionID action_id) const { + return std::any_of( + std::begin(m_priority_action_queue), std::end(m_priority_action_queue), + [action_id](const ActionQueueData& item) { return item.action_id == action_id; } + ); +} + + void UserAccountSession::process_action_queue() { if (!m_proccessing_enabled) @@ -84,7 +92,7 @@ void UserAccountSession::process_action_queue() while (!m_priority_action_queue.empty()) { m_actions[m_priority_action_queue.front().action_id]->perform(p_evt_handler, m_access_token, m_priority_action_queue.front().success_callback, m_priority_action_queue.front().fail_callback, m_priority_action_queue.front().input); if (!m_priority_action_queue.empty()) - m_priority_action_queue.pop(); + m_priority_action_queue.pop_front(); } // regular queue has to wait until priority fills tokens if (!this->is_initialized()) @@ -115,7 +123,7 @@ void UserAccountSession::init_with_code(const std::string& code, const std::stri m_proccessing_enabled = true; // fail fn might be cancel_queue here - m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::code_exchange_fail_callback, this, std::placeholders::_1) , post_fields }); @@ -188,7 +196,7 @@ void UserAccountSession::enqueue_test_with_refresh() { // on test fail - try refresh m_proccessing_enabled = true; - m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); } @@ -199,7 +207,7 @@ void UserAccountSession::enqueue_refresh(const std::string& body) "&client_id=" + client_id() + "&refresh_token=" + m_refresh_token; - m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::refresh_fail_callback, this, std::placeholders::_1) , post_fields }); @@ -218,9 +226,7 @@ void UserAccountSession::refresh_fail_callback(const std::string& body) void UserAccountSession::cancel_queue() { - while (!m_priority_action_queue.empty()) { - m_priority_action_queue.pop(); - } + m_priority_action_queue.clear(); while (!m_action_queue.empty()) { m_action_queue.pop(); } diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index d540319ab3..333894cded 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -150,7 +150,8 @@ public: void enqueue_refresh(const std::string& body); void process_action_queue(); - bool is_initialized() { return !m_access_token.empty() || !m_refresh_token.empty(); } + bool is_initialized() const { return !m_access_token.empty() || !m_refresh_token.empty(); } + bool is_enqueued(UserAccountActionID action_id) const; std::string get_access_token() const { return m_access_token; } std::string get_refresh_token() const { return m_refresh_token; } std::string get_shared_session_key() const { return m_shared_session_key; } @@ -179,7 +180,7 @@ private: long long m_next_token_timeout; std::queue m_action_queue; - std::queue m_priority_action_queue; + std::deque m_priority_action_queue; std::map> m_actions; wxEvtHandler* p_evt_handler; diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index bace551d2b..fc53db1b3b 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -636,33 +636,9 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) window.__access_token_version = 0; )", #else -// R"( -// console.log('Preparing login'); -// function errorHandler(err) { -// const msg = { -// action: 'ERROR', -// error: JSON.stringify(err), -// critical: false -// }; -// console.error('Login error occurred', msg); -// window._prusaSlicer.postMessage(msg); -// }; -// window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) -// .then(function (resp) { -// console.log('Login resp', resp); -// resp.text() -// .then(function (json) { console.log('Login resp body', json); return json; }) -// .then(function (body) { -// if (resp.status >= 400) errorHandler({status: resp.status, body}); -// }); -// }) -// .catch(function (err){ -// errorHandler({message: err.message, stack: err.stack}); -// }); -// )", + refresh ? "console.log('Refreshing login'); _prusaSlicer_initLogin('%s');" : R"( - console.log('Preparing login'); - function errorHandler(err) { + function _prusaSlicer_errorHandler(err) { const msg = { action: 'ERROR', error: JSON.stringify(err), @@ -672,13 +648,21 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) window._prusaSlicer.postMessage(msg); }; - function delay(ms) { + function _prusaSlicer_delay(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms); }); } - (async () => { + async function _prusaSlicer_initLogin(token) { + const parts = token.split('.'); + const claims = JSON.parse(atob(parts[1])); + const now = new Date().getTime() / 1000; + if (claims.exp <= now) { + console.log('Skipping initLogin as token is expired'); + return; + } + let retry = false; let backoff = 1000; const maxBackoff = 64000; @@ -688,15 +672,15 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) try { console.log('Slicer Login request'); - let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}); + let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); let body = await resp.text(); console.log('Slicer Login resp', resp.status, body); - if (resp.status >= 500) { + if (resp.status >= 500 || resp.status == 408) { retry = true; } else { retry = false; if (resp.status >= 400) - errorHandler({status: resp.status, body}); + _prusaSlicer_errorHandler({status: resp.status, body}); } } catch (e) { console.error('Slicer Login failed', e.toString()); @@ -704,13 +688,18 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) } if (retry) { - await delay(backoff + 1000 * Math.random()); + await _prusaSlicer_delay(backoff + 1000 * Math.random()); if (backoff < maxBackoff) { backoff *= 2; } } } while (retry); - })(); + } + if (window._prusaSlicer_initialLoad === undefined) { + console.log('Initial login'); + _prusaSlicer_initLogin('%s'); + window._prusaSlicer_initialLoad = true; + } )", #endif access_token diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 2d648b9494..21de6b4ec1 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include // IWYU pragma: keep #include #include @@ -154,7 +155,7 @@ struct Http::priv std::string curl_error(CURLcode curlcode); std::string body_size_error(); - void http_perform(); + void http_perform(const HttpRetryOpt& retry_opts = HttpRetryOpt::no_retry()); }; Http::priv::priv(const std::string &url) @@ -340,8 +341,20 @@ std::string Http::priv::body_size_error() return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str(); } -void Http::priv::http_perform() +bool is_transient_error(CURLcode res, long http_status) { + if (res == CURLE_OK || res == CURLE_HTTP_RETURNED_ERROR) + return http_status == 408 || http_status >= 500; + return res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST || + res == CURLE_OPERATION_TIMEDOUT; +} + +void Http::priv::http_perform(const HttpRetryOpt& retry_opts) +{ + using namespace std::chrono_literals; + static thread_local std::mt19937 generator; + std::uniform_int_distribution randomized_delay(retry_opts.initial_delay.count(), (retry_opts.initial_delay.count() * 3) / 2); + ::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); ::curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writecb); @@ -375,7 +388,28 @@ void Http::priv::http_perform() ::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size()); } - CURLcode res = ::curl_easy_perform(curl); + bool retry; + CURLcode res; + long http_status = 0; + std::chrono::milliseconds delay = std::chrono::milliseconds(randomized_delay(generator)); + size_t num_retries = 0; + do { + res = ::curl_easy_perform(curl); + + if (res == CURLE_OK) + ::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); + retry = delay >= 0ms && is_transient_error(res, http_status); + if (retry && retry_opts.max_retries > 0 && num_retries >= retry_opts.max_retries) + retry = false; + if (retry) { + num_retries++; + BOOST_LOG_TRIVIAL(error) + << "HTTP Transient error (code=" << res << ", http_status=" << http_status + << "), retrying in " << delay.count() / 1000.0f << " s"; + std::this_thread::sleep_for(delay); + delay = std::min(delay * 2, retry_opts.max_delay); + } + } while (retry); putFile.reset(); @@ -397,8 +431,6 @@ void Http::priv::http_perform() if (errorfn) { errorfn(std::move(buffer), curl_error(res), 0); } }; } else { - long http_status = 0; - ::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); if (http_status >= 400) { if (errorfn) { errorfn(std::move(buffer), std::string(), http_status); } @@ -420,6 +452,23 @@ Http::Http(const std::string &url) : p(new priv(url)) {} // Public +const HttpRetryOpt& HttpRetryOpt::default_retry() +{ + using namespace std::chrono_literals; + static HttpRetryOpt val = {500ms, 64s, 0}; + return val; +} + +const HttpRetryOpt& HttpRetryOpt::no_retry() +{ + using namespace std::chrono_literals; + static HttpRetryOpt val = {0ms}; + return val; +} + + + + Http::Http(Http &&other) : p(std::move(other.p)) {} Http::~Http() @@ -609,13 +658,13 @@ Http& Http::cookie_jar(const std::string& file_path) return *this; } -Http::Ptr Http::perform() +Http::Ptr Http::perform(const HttpRetryOpt& retry_opts) { auto self = std::make_shared(std::move(*this)); if (self->p) { - auto io_thread = std::thread([self](){ - self->p->http_perform(); + auto io_thread = std::thread([self, &retry_opts](){ + self->p->http_perform(retry_opts); }); self->p->io_thread = std::move(io_thread); } @@ -623,9 +672,9 @@ Http::Ptr Http::perform() return self; } -void Http::perform_sync() +void Http::perform_sync(const HttpRetryOpt& retry_opts) { - if (p) { p->http_perform(); } + if (p) { p->http_perform(retry_opts); } } void Http::cancel() diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 8e00b123fc..6d21570c12 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -11,10 +11,20 @@ #include #include #include - +#include namespace Slic3r { +struct HttpRetryOpt +{ + std::chrono::milliseconds initial_delay; + std::chrono::milliseconds max_delay; + size_t max_retries{0}; + + static const HttpRetryOpt& no_retry(); + static const HttpRetryOpt& default_retry(); +}; + /// Represetns a Http request class Http : public std::enable_shared_from_this { @@ -133,9 +143,9 @@ public: Http& set_referer(const std::string& referer); // Starts performing the request in a background thread - Ptr perform(); + Ptr perform(const HttpRetryOpt& retry_opts = HttpRetryOpt::no_retry()); // Starts performing the request on the current thread - void perform_sync(); + void perform_sync(const HttpRetryOpt &retry_opts = HttpRetryOpt::no_retry()); // Cancels a request in progress void cancel(); From 74ab2d24b7b5d006bc52a43976cfaa95a5e6f853 Mon Sep 17 00:00:00 2001 From: Jan Bartipan Date: Tue, 20 Aug 2024 11:32:22 +0200 Subject: [PATCH 4/7] ConnectWebViewPanel::get_login_script for refresh wrapped in check if _prusaSlicer_initLogin function (defined by AddUserScript) is already defined (i.e. handle gracefully situation when token refresh happen just before the UserScript gets executed) --- src/slic3r/GUI/WebViewDialog.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index fc53db1b3b..0e3f129402 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -636,7 +636,17 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) window.__access_token_version = 0; )", #else - refresh ? "console.log('Refreshing login'); _prusaSlicer_initLogin('%s');" : + refresh + ? + R"( + if (window._prusaSlicer_initLogin !== undefined) { + console.log('Refreshing login'); + _prusaSlicer_initLogin('%s'); + } else { + console.log('Refreshing login skipped as no _prusaSlicer_initLogin defined (yet?)'); + } + )" + : R"( function _prusaSlicer_errorHandler(err) { const msg = { From f9164da380ffe46d8de1aa8a171eadc4e0dd0c3c Mon Sep 17 00:00:00 2001 From: Jan Bartipan Date: Wed, 21 Aug 2024 14:03:39 +0200 Subject: [PATCH 5/7] ConnectWebViewPanel: fixed segfault on quit (accessing already deleted plater in on_activate handler), WebView scripts can now use _prusaSlicer.postMessage({action: 'LOG', ...}) to write logs into OS console --- src/slic3r/GUI/GUI_App.cpp | 5 ++++- src/slic3r/GUI/WebViewDialog.cpp | 22 ++++++++++++++++++---- src/slic3r/GUI/WebViewDialog.hpp | 1 + 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index d107a83345..c7eb2d7d9b 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1449,7 +1449,10 @@ bool GUI_App::on_init_inner() }); Bind(wxEVT_ACTIVATE_APP, [this](const wxActivateEvent &evt) { - plater_->get_user_account()->on_activate_app(evt.GetActive()); + if (plater_) { + if (auto user_account = plater_->get_user_account()) + user_account->on_activate_app(evt.GetActive()); + } }); } diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 0e3f129402..3b9096ac27 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -499,6 +499,7 @@ ConnectRequestHandler::ConnectRequestHandler() m_actions["PRINT"] = std::bind(&ConnectRequestHandler::on_connect_action_print, this, std::placeholders::_1); m_actions["REQUEST_OPEN_IN_BROWSER"] = std::bind(&ConnectRequestHandler::on_connect_action_request_open_in_browser, this, std::placeholders::_1); m_actions["ERROR"] = std::bind(&ConnectRequestHandler::on_connect_action_error, this, std::placeholders::_1); + m_actions["LOG"] = std::bind(&ConnectRequestHandler::on_connect_action_log, this, std::placeholders::_1); } ConnectRequestHandler::~ConnectRequestHandler() { @@ -547,6 +548,11 @@ void ConnectRequestHandler::resend_config() on_connect_action_request_config({}); } +void ConnectRequestHandler::on_connect_action_log(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(info) << "WebKit log: " << message_data; +} + void ConnectRequestHandler::on_connect_action_request_config(const std::string& message_data) { /* @@ -641,17 +647,24 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) R"( if (window._prusaSlicer_initLogin !== undefined) { console.log('Refreshing login'); + _prusaSlicer.postMessage({action: 'LOG', message: 'Refreshing login'}); _prusaSlicer_initLogin('%s'); } else { console.log('Refreshing login skipped as no _prusaSlicer_initLogin defined (yet?)'); + if (window._prusaSlicer === undefined) { + console.log('Message handler _prusaSlicer not defined yet'); + } else { + _prusaSlicer.postMessage({action: 'LOG', message: 'Refreshing login skipped as no _prusaSlicer_initLogin defined (yet?)'}); + } } )" : R"( + function _prusaSlicer_log(msg) { console.log(msg); _prusaSlicer.postMessage({action: 'LOG', message: msg}); } function _prusaSlicer_errorHandler(err) { const msg = { action: 'ERROR', - error: JSON.stringify(err), + error: typeof(err) === 'string' ? err : JSON.stringify(err), critical: false }; console.error('Login error occurred', msg); @@ -669,7 +682,7 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) const claims = JSON.parse(atob(parts[1])); const now = new Date().getTime() / 1000; if (claims.exp <= now) { - console.log('Skipping initLogin as token is expired'); + _prusaSlicer_log('Skipping initLogin as token is expired'); return; } @@ -681,10 +694,10 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) let error = false; try { - console.log('Slicer Login request'); + _prusaSlicer_log('Slicer Login request'); let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); let body = await resp.text(); - console.log('Slicer Login resp', resp.status, body); + _prusaSlicer_log('Slicer Login resp ' + resp.status + ' body: ' + body); if (resp.status >= 500 || resp.status == 408) { retry = true; } else { @@ -693,6 +706,7 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) _prusaSlicer_errorHandler({status: resp.status, body}); } } catch (e) { + _prusaSlicer_log('Slicer Login failed: ' + e.toString()); console.error('Slicer Login failed', e.toString()); retry = true; } diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index 92619b0a50..3cec6cf48e 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -184,6 +184,7 @@ public: void resend_config(); protected: // action callbacs stored in m_actions + virtual void on_connect_action_log(const std::string& message_data); virtual void on_connect_action_error(const std::string& message_data); virtual void on_connect_action_request_config(const std::string& message_data); virtual void on_connect_action_request_open_in_browser(const std::string& message_data); From 3d130ceb768e1bec265083a1d826d423299d4b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ba=C5=99tip=C3=A1n?= Date: Thu, 22 Aug 2024 13:39:12 +0200 Subject: [PATCH 6/7] Connect login: updating also user script when refreshed access token arives, this should fix passing old token on page reload --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/UserAccountCommunication.cpp | 3 +- src/slic3r/GUI/UserAccountSession.cpp | 12 ++++- src/slic3r/GUI/WebViewDialog.cpp | 30 +++++++---- src/slic3r/Utils/Jwt.cpp | 58 +++++++++++++++++++++ src/slic3r/Utils/Jwt.hpp | 10 ++++ 6 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/slic3r/Utils/Jwt.cpp create mode 100644 src/slic3r/Utils/Jwt.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index e09515f08c..78369233d1 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -332,6 +332,8 @@ set(SLIC3R_GUI_SOURCES Utils/Http.hpp Utils/FixModelByWin10.cpp Utils/FixModelByWin10.hpp + Utils/Jwt.cpp + Utils/Jwt.hpp Utils/Moonraker.cpp Utils/Moonraker.hpp Utils/OctoPrint.cpp diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index c5605bbf0a..c46e7d5953 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -475,8 +475,7 @@ void UserAccountCommunication::enqueue_refresh() BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; return; } - if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) - { + if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) { BOOST_LOG_TRIVIAL(debug) << "User Account: Token refresh already enqueued, skipping..."; return; } diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index 19e92d95e4..06ece32168 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -2,6 +2,7 @@ #include "GUI_App.hpp" #include "format.hpp" #include "../Utils/Http.hpp" +#include "../Utils/Jwt.hpp" #include "I18N.hpp" #include @@ -53,8 +54,17 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin { std::string url = m_url + input; auto http = Http::get(std::move(url)); - if (!access_token.empty()) + if (!access_token.empty()) { http.header("Authorization", "Bearer " + access_token); +#ifndef _NDEBUG + // In debug mode, also verify the token expiration + // This is here to help with "dev" accounts with shorten (sort of faked) expiration time + // The /api/v1/me will accept these tokens even if these are fake-marked as expired + if (!Utils::verify_exp(access_token)) { + fail_callback("Token Expired"); + } +#endif + } http.on_error([evt_handler, fail_callback, action_name = &m_action_name, fail_evt_type = m_fail_evt_type](std::string body, std::string error, unsigned status) { if (fail_callback) fail_callback(body); diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 3b9096ac27..f1d4825857 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -540,7 +540,7 @@ void ConnectRequestHandler::handle_message(const std::string& message) void ConnectRequestHandler::on_connect_action_error(const std::string &message_data) { - BOOST_LOG_TRIVIAL(error) << "WebKit runtime error: " << message_data; + BOOST_LOG_TRIVIAL(error) << "WebView runtime error: " << message_data; } void ConnectRequestHandler::resend_config() @@ -550,7 +550,7 @@ void ConnectRequestHandler::resend_config() void ConnectRequestHandler::on_connect_action_log(const std::string& message_data) { - BOOST_LOG_TRIVIAL(info) << "WebKit log: " << message_data; + BOOST_LOG_TRIVIAL(info) << "WebView log: " << message_data; } void ConnectRequestHandler::on_connect_action_request_config(const std::string& message_data) @@ -647,8 +647,9 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) R"( if (window._prusaSlicer_initLogin !== undefined) { console.log('Refreshing login'); - _prusaSlicer.postMessage({action: 'LOG', message: 'Refreshing login'}); - _prusaSlicer_initLogin('%s'); + if (window._prusaSlicer !== undefined) + _prusaSlicer.postMessage({action: 'LOG', message: 'Refreshing login'}); + _prusaSlicer_initLogin('%s', 'refresh'); } else { console.log('Refreshing login skipped as no _prusaSlicer_initLogin defined (yet?)'); if (window._prusaSlicer === undefined) { @@ -660,7 +661,11 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) )" : R"( - function _prusaSlicer_log(msg) { console.log(msg); _prusaSlicer.postMessage({action: 'LOG', message: msg}); } + function _prusaSlicer_log(msg) { + console.log(msg); + if (window._prusaSlicer !== undefined) + _prusaSlicer.postMessage({action: 'LOG', message: msg}); + } function _prusaSlicer_errorHandler(err) { const msg = { action: 'ERROR', @@ -677,7 +682,7 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) }); } - async function _prusaSlicer_initLogin(token) { + async function _prusaSlicer_initLogin(token, reason) { const parts = token.split('.'); const claims = JSON.parse(atob(parts[1])); const now = new Date().getTime() / 1000; @@ -694,10 +699,10 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) let error = false; try { - _prusaSlicer_log('Slicer Login request'); + _prusaSlicer_log('Slicer Login request (' + reason + ') ' + token.substring(token.length - 8)); let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); let body = await resp.text(); - _prusaSlicer_log('Slicer Login resp ' + resp.status + ' body: ' + body); + _prusaSlicer_log('Slicer Login resp ' + resp.status + ' (' +reason + ' ' + token.substring(token.length - 8) + ') body: ' + body); if (resp.status >= 500 || resp.status == 408) { retry = true; } else { @@ -721,7 +726,7 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) } if (window._prusaSlicer_initialLoad === undefined) { console.log('Initial login'); - _prusaSlicer_initLogin('%s'); + _prusaSlicer_initLogin('%s', 'init-load'); window._prusaSlicer_initialLoad = true; } )", @@ -743,7 +748,12 @@ void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) e.Skip(); auto access_token = wxGetApp().plater()->get_user_account()->get_access_token(); assert(!access_token.empty()); - wxString javascript = get_login_script(true); + wxString javascript = get_login_script(false); + + m_browser->RemoveAllUserScripts(); + m_browser->AddUserScript(javascript); + + javascript = get_login_script(true); //m_browser->AddUserScript(javascript, wxWEBVIEW_INJECT_AT_DOCUMENT_END); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->RunScriptAsync(javascript); diff --git a/src/slic3r/Utils/Jwt.cpp b/src/slic3r/Utils/Jwt.cpp new file mode 100644 index 0000000000..4584471e04 --- /dev/null +++ b/src/slic3r/Utils/Jwt.cpp @@ -0,0 +1,58 @@ +#include "Jwt.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace Slic3r::Utils { + +bool verify_exp(const std::string& token) +{ + size_t payload_start = token.find('.'); + if (payload_start == std::string::npos) + return false; + payload_start += 1; // payload starts after dot + + const size_t payload_end = token.find('.', payload_start); + if (payload_end == std::string::npos) + return false; + + size_t encoded_length = payload_end - payload_start; + size_t decoded_length = boost::beast::detail::base64::decoded_size(encoded_length); + + auto json_b64 = token.substr(payload_start, encoded_length); + std::replace(json_b64.begin(), json_b64.end(), '-', '+'); + std::replace(json_b64.begin(), json_b64.end(), '_', '/'); + + size_t padding = encoded_length % 4; + encoded_length += padding; + while (padding--) json_b64 += '='; + + + std::string json; + json.resize(decoded_length + 2); + size_t read_bytes, written_bytes; + std::tie(written_bytes, read_bytes) = boost::beast::detail::base64::decode(json.data(), json_b64.data(), json_b64.length()); + json.resize(written_bytes); + if (written_bytes == 0) + return false; + + namespace pt = boost::property_tree; + + pt::ptree payload; + std::istringstream iss(json); + pt::json_parser::read_json(iss, payload); + + auto exp_opt = payload.get_optional("exp"); + if (!exp_opt) + return false; + + auto now = time(nullptr); + return exp_opt.get() > now; +} + +} diff --git a/src/slic3r/Utils/Jwt.hpp b/src/slic3r/Utils/Jwt.hpp new file mode 100644 index 0000000000..b957e8df8d --- /dev/null +++ b/src/slic3r/Utils/Jwt.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace Slic3r::Utils { + +bool verify_exp(const std::string& token); + + +} From d5ed649ab164d2b1d4b7a8ba4716ea0f96ef8b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ba=C5=99tip=C3=A1n?= Date: Tue, 27 Aug 2024 09:54:38 +0200 Subject: [PATCH 7/7] Connect login: fix debug build crash on exit --- src/slic3r/GUI/UserAccountSession.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index 06ece32168..23fe45cebf 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -60,7 +60,7 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin // In debug mode, also verify the token expiration // This is here to help with "dev" accounts with shorten (sort of faked) expiration time // The /api/v1/me will accept these tokens even if these are fake-marked as expired - if (!Utils::verify_exp(access_token)) { + if (!Utils::verify_exp(access_token) && fail_callback) { fail_callback("Token Expired"); } #endif