From 850ac19167de0f41c39ab45530ff19f159a6c966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ba=C5=99tip=C3=A1n?= Date: Thu, 6 Jun 2024 13:36:20 +0200 Subject: [PATCH] WebView and Connect integration: implemented login ceremony via /slicer/login request (and alternative disabled one with fetch function overriding); all stored JWT tokens merged into single secret store record to reduce number of login dialogs appearances when new app is run for first time on Mac. --- src/libslic3r/Config.hpp | 2 +- src/slic3r/GUI/PhysicalPrinterDialog.cpp | 8 +- src/slic3r/GUI/Plater.cpp | 2 + src/slic3r/GUI/UserAccountCommunication.cpp | 34 ++++++--- src/slic3r/GUI/UserAccountCommunication.hpp | 3 +- src/slic3r/GUI/UserAccountSession.hpp | 18 ++--- src/slic3r/GUI/WebViewDialog.cpp | 84 ++++++++++++++++++++- src/slic3r/GUI/WebViewDialog.hpp | 13 +++- src/slic3r/Utils/PrusaConnect.cpp | 2 +- src/slic3r/Utils/PrusaConnect.hpp | 2 +- 10 files changed, 136 insertions(+), 32 deletions(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 7f922481e2..395693db89 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -342,7 +342,7 @@ template struct NilValueTempl, void>> { using NilType = T; - static constexpr auto value = static_cast(std::numeric_limits>::max()); + static constexpr auto value = std::numeric_limits>::max(); }; template struct NilValueTempl, void>> { diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 8d599efd8c..b8d7dc36eb 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -653,7 +653,7 @@ void PhysicalPrinterDialog::update(bool printer_change) text_ctrl* printhost_win = printhost_field ? dynamic_cast(printhost_field->getWindow()) : nullptr; if (!m_opened_as_connect && printhost_win && m_last_host_type != htPrusaConnect){ m_stored_host = printhost_win->GetValue(); - printhost_win->SetValue(L"https://connect.prusa3d.com"); + printhost_win->SetValue(L"https://dev.connect.prusa3d.com"); } } else { m_printhost_browse_btn->Show(); @@ -888,10 +888,10 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event) text_ctrl* printhost_win = printhost_field ? dynamic_cast(printhost_field->getWindow()) : nullptr; const auto opt = m_config->option>("host_type"); if (opt->value == htPrusaConnect) { - if (printhost_win && printhost_win->GetValue() != L"https://connect.prusa3d.com"){ - InfoDialog msg(this, _L("Warning"), _L("URL of PrusaConnect is different from https://connect.prusa3d.com. Do you want to continue?"), true, wxYES_NO); + if (printhost_win && printhost_win->GetValue() != L"https://dev.connect.prusa3d.com"){ + InfoDialog msg(this, _L("Warning"), _L("URL of PrusaConnect is different from https://dev.connect.prusa3d.com. Do you want to continue?"), true, wxYES_NO); if(msg.ShowModal() != wxID_YES){ - printhost_win->SetValue(L"https://connect.prusa3d.com"); + printhost_win->SetValue(L"https://dev.connect.prusa3d.com"); return; } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 840e34191f..d32b9a6c7f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -889,6 +889,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); this->q->Bind(EVT_UA_ID_USER_SUCCESS, [this](UserAccountSuccessEvent& evt) { + // There are multiple handlers and we want to notify all + evt.Skip(); std::string username; if (user_account->on_user_id_success(evt.data, username)) { // login notification diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 9d34028fa5..9e2f11f1d3 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -143,12 +143,22 @@ UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, Ap std::string access_token, refresh_token, shared_session_key, next_timeout; if (is_secret_store_ok()) { - std::string key0, key1, key2; - load_secret("access_token", key0, access_token); - load_secret("refresh_token", key1, refresh_token); - load_secret("access_token_timeout", key2, next_timeout); - assert(key0 == key1); + std::string key0, key1, key2, tokens; + if (load_secret("tokens", key0, tokens)) { + std::vector token_list; + boost::split(token_list, tokens, boost::is_any_of("|"), boost::token_compress_off); + assert(token_list.empty() || token_list.size() == 3); + access_token = token_list.size() > 0 ? token_list[0] : std::string(); + refresh_token = token_list.size() > 1 ? token_list[1] : std::string(); + next_timeout = token_list.size() > 2 ? token_list[2] : std::string(); + } else { + load_secret("access_token", key0, access_token); + load_secret("refresh_token", key1, refresh_token); + load_secret("access_token_timeout", key2, next_timeout); + assert(key0 == key1); + } shared_session_key = key0; + } else { access_token = m_app_config->get("access_token"); refresh_token = m_app_config->get("refresh_token"); @@ -173,7 +183,7 @@ UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, Ap UserAccountCommunication::~UserAccountCommunication() { if (m_thread.joinable()) { - Stop the worker thread, if running. + // Stop the worker thread, if running. { // Notify the worker thread to cancel wait on detection polling. std::lock_guard lck(m_thread_stop_mutex); @@ -191,9 +201,13 @@ void UserAccountCommunication::set_username(const std::string& username) { std::lock_guard 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()); - save_secret("access_token_timeout", m_session->get_shared_session_key(), m_remember_session ? GUI::format("%1%",m_session->get_next_token_timeout()) : "0"); + std::string tokens; + if (m_remember_session) { + tokens = m_session->get_access_token() + + "|" + m_session->get_refresh_token() + + "|" + std::to_string(m_session->get_next_token_timeout()); + } + save_secret("tokens", m_session->get_shared_session_key(), tokens); } else { m_app_config->set("access_token", m_remember_session ? m_session->get_access_token() : std::string()); @@ -245,7 +259,7 @@ void UserAccountCommunication::on_uuid_map_success() void UserAccountCommunication::login_redirect() { - const std::string AUTH_HOST = "https://account.prusa3d.com"; + const std::string AUTH_HOST = "https://test-account.prusa3d.com"; const std::string CLIENT_ID = client_id(); const std::string REDIRECT_URI = "prusaslicer://login"; CodeChalengeGenerator ccg; diff --git a/src/slic3r/GUI/UserAccountCommunication.hpp b/src/slic3r/GUI/UserAccountCommunication.hpp index 0d8b2b09d7..8d69a41d55 100644 --- a/src/slic3r/GUI/UserAccountCommunication.hpp +++ b/src/slic3r/GUI/UserAccountCommunication.hpp @@ -94,8 +94,7 @@ private: void wakeup_session_thread(); void init_session_thread(); void login_redirect(); - std::string client_id() const { return "oamhmhZez7opFosnwzElIgE2oGgI2iJORSkw587O"; } - + std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; } }; diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index a48f84d0a7..93324982f8 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -111,15 +111,15 @@ public: // do not forget to add delete to destructor m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY] = std::make_unique(); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://account.prusa3d.com/o/token/"); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://account.prusa3d.com/o/token/"); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID] = std::make_unique("USER_ID", "https://account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_RESET); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN] = std::make_unique("TEST_ACCESS_TOKEN", "https://account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_FAIL); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", "https://account.prusa3d.com/api/v1/me/", wxEVT_NULL, EVT_UA_RESET); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS] = std::make_unique("CONNECT_STATUS", "https://connect.prusa3d.com/slicer/status", EVT_UA_PRUSACONNECT_STATUS_SUCCESS, EVT_UA_FAIL); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS] = std::make_unique("CONNECT_PRINTER_MODELS", "https://connect.prusa3d.com/slicer/printer_list", EVT_UA_PRUSACONNECT_PRINTER_MODELS_SUCCESS, EVT_UA_FAIL); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN] = std::make_unique("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/"); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID] = std::make_unique("USER_ID", "https://test-account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_RESET); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN] = std::make_unique("TEST_ACCESS_TOKEN", "https://test-account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_FAIL); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION] = std::make_unique("TEST_CONNECTION", "https://test-account.prusa3d.com/api/v1/me/", wxEVT_NULL, EVT_UA_RESET); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS] = std::make_unique("CONNECT_STATUS", "https://dev.connect.prusa3d.com/slicer/status", EVT_UA_PRUSACONNECT_STATUS_SUCCESS, EVT_UA_FAIL); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS] = std::make_unique("CONNECT_PRINTER_MODELS", "https://dev.connect.prusa3d.com/slicer/printer_list", EVT_UA_PRUSACONNECT_PRINTER_MODELS_SUCCESS, EVT_UA_FAIL); m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR] = std::make_unique("AVATAR", "https://media.printables.com/media/", EVT_UA_AVATAR_SUCCESS, EVT_UA_FAIL); - m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID] = std::make_unique("USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID", "https://connect.prusa3d.com/app/printers/", EVT_UA_PRUSACONNECT_PRINTER_DATA_SUCCESS, EVT_UA_FAIL); + m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID] = std::make_unique("USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID", "https://dev.connect.prusa3d.com/app/printers/", EVT_UA_PRUSACONNECT_PRINTER_DATA_SUCCESS, EVT_UA_FAIL); } ~UserAccountSession() { @@ -162,7 +162,7 @@ private: void cancel_queue(); void code_exchange_fail_callback(const std::string& body); void token_success_callback(const std::string& body); - std::string client_id() const { return "oamhmhZez7opFosnwzElIgE2oGgI2iJORSkw587O"; } + std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; } // false prevents action queu to be processed - no communication is done // sets to true by init_with_code or enqueue_action call diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 3804ec4ea3..6b2b0e47db 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -15,6 +15,11 @@ #include #include +// if set to 1 the fetch() JS function gets override to include JWT in authorization header +// if set to 0, the /slicer/login is invoked from WebKit (passing JWT token only to this request) +// to set authorization cookie for all WebKit requests to Connect +#define AUTH_VIA_FETCH_OVERRIDE 0 + namespace pt = boost::property_tree; @@ -542,8 +547,83 @@ void ConnectRequestHandler::on_connect_action_request_config() } ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) - : WebViewPanel(parent, L"https://connect.prusa3d.com/", { "_prusaSlicer" }, "connect_loading") + : WebViewPanel(parent, L"https://dev.connect.prusa3d.com/", { "_prusaSlicer" }, "connect_loading") { + //m_browser->RegisterHandler(wxSharedPtr(new WebViewHandler("https"))); + + Plater* plater = wxGetApp().plater(); + m_browser->AddUserScript(wxString::Format( + +#if AUTH_VIA_FETCH_OVERRIDE + /* + * Notes: + * - The fetch() function has two distinct prototypes (i.e. input args): + * 1. fetch(url: string, options: object | undefined) + * 2. fetch(req: Request, options: object | undefined) + * - For some reason I can't explain the headers can be extended only via Request object + * i.e. the fetch prototype (2). So we need to convert (1) call into (2) before + * + */ + R"( + if (window.__fetch === undefined) { + window.__fetch = fetch; + window.fetch = function(req, opts = {}) { + if (typeof req === 'string') { + req = new Request(req, opts); + opts = {}; + } + if (window.__access_token && (req.url[0] == '/' || req.url.indexOf('prusa3d.com') > 0)) { + req.headers.set('Authorization', 'Bearer ' + window.__access_token); + console.log('Header updated: ', req.headers.get('Authorization')); + console.log('AT Version: ', __access_token_version); + } + //console.log('Injected fetch used', req, opts); + return __fetch(req, opts); + }; + } + window.__access_token = '%s'; + window.__access_token_version = 0; + )", +#else + R"( + console.log('Preparing login'); + window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) + .then((resp) => { + console.log('Login resp', resp); + resp.text().then((json) => console.log('Login resp body', json)); + }); + )", +#endif + plater->get_user_account()->get_access_token() + )); + plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); +} + +ConnectWebViewPanel::~ConnectWebViewPanel() +{ + m_browser->Unbind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); +} + +void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) +{ + e.Skip(); + wxString javascript = wxString::Format( +#if AUTH_VIA_FETCH_OVERRIDE + "window.__access_token = '%s';window.__access_token_version = (window.__access_token_version || 0) + 1;console.log('Updated Auth token', window.__access_token);", +#else + R"( + console.log('Preparing login'); + window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) + .then((resp) => { + console.log('Login resp', resp); + resp.text().then((json) => console.log('Login resp body', json)); + }); + )", +#endif + wxGetApp().plater()->get_user_account()->get_access_token() + ); + //m_browser->AddUserScript(javascript, wxWEBVIEW_INJECT_AT_DOCUMENT_END); + m_browser->RunScript(javascript); } void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) @@ -1040,7 +1120,7 @@ void WebViewDialog::EndModal(int retCode) PrinterPickWebViewDialog::PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val) : WebViewDialog(parent - , L"https://connect.prusa3d.com/slicer-select-printer" + , L"https://dev.connect.prusa3d.com/slicer-select-printer" , _L("Choose a printer") , wxSize(std::max(parent->GetClientSize().x / 2, 100 * wxGetApp().em_unit()), std::max(parent->GetClientSize().y / 2, 50 * wxGetApp().em_unit())) ,{"_prusaSlicer"} diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index 6b95ff8975..a40e634369 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -1,18 +1,24 @@ #ifndef slic3r_WebViewDialog_hpp_ #define slic3r_WebViewDialog_hpp_ +#define DEBUG_URL_PANEL + #include #include #include +#include "UserAccountSession.hpp" + +#ifdef DEBUG_URL_PANEL +#include +#endif + class wxWebView; class wxWebViewEvent; namespace Slic3r { namespace GUI { -//#define DEBUG_URL_PANEL - class WebViewPanel : public wxPanel { public: @@ -183,6 +189,7 @@ class ConnectWebViewPanel : public WebViewPanel, public ConnectRequestHandler { public: ConnectWebViewPanel(wxWindow* parent); + ~ConnectWebViewPanel() override; void on_script_message(wxWebViewEvent& evt) override; void logout(); void sys_color_changed() override; @@ -191,6 +198,8 @@ protected: void on_connect_action_print() override; void on_connect_action_webapp_ready() override {} void run_script_bridge(const wxString& script) override {run_script(script); } +private: + void on_user_token(UserAccountSuccessEvent& e); }; class PrinterWebViewPanel : public WebViewPanel diff --git a/src/slic3r/Utils/PrusaConnect.cpp b/src/slic3r/Utils/PrusaConnect.cpp index 19da1cb832..573bf03c55 100644 --- a/src/slic3r/Utils/PrusaConnect.cpp +++ b/src/slic3r/Utils/PrusaConnect.cpp @@ -60,7 +60,7 @@ bool PrusaConnectNew::test(wxString& curl_msg) const { // Test is not used by upload and gets list of files on a device. const std::string name = get_name(); - std::string url = GUI::format("https://connect.prusa3d.com/app/teams/%1%/files?printer_uuid=%2%", m_team_id, m_uuid); + std::string url = GUI::format("https://dev.connect.prusa3d.com/app/teams/%1%/files?printer_uuid=%2%", m_team_id, m_uuid); const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); BOOST_LOG_TRIVIAL(info) << GUI::format("%1%: Get files/raw at: %2%", name, url); bool res = true; diff --git a/src/slic3r/Utils/PrusaConnect.hpp b/src/slic3r/Utils/PrusaConnect.hpp index d60d28bf5e..c569f0d566 100644 --- a/src/slic3r/Utils/PrusaConnect.hpp +++ b/src/slic3r/Utils/PrusaConnect.hpp @@ -32,7 +32,7 @@ public: bool has_auto_discovery() const override { return true; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; } - std::string get_host() const override { return "https://connect.prusa3d.com"; } + std::string get_host() const override { return "https://dev.connect.prusa3d.com"; } bool get_storage(wxArrayString& storage_path, wxArrayString& storage_name) const override; //const std::string& get_apikey() const { return m_apikey; } //const std::string& get_cafile() const { return m_cafile; }