mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-22 19:50:44 +00:00
Allow use offline when logged in to Orca Cloud (#14235)
* Store user session information along with refresh token, to allow offline use once user is logged in * Don't bother with avatar because we won't see it when offline anyway * Fix offline Sync Presets freezing the UI on repeat clicks Ignore restart_sync_user_preset() while a manual sync's progress dialog is on screen, so a second app-modal dialog can't stack on the first. Offline the dialog blocks on a long, uncancellable HTTP timeout; on macOS the global menu stays live while the window is disabled, so a second click otherwise wedges the app (force-quit only). * Skip redundant user-secret re-write on startup set_user_session() always re-encrypts and writes the secret to disk; on the startup restore path that just rewrites the bytes it was loaded from. Add a persist flag so the restore path skips it. Also drop an unused catch binding and a stray blank line. --------- Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
@@ -480,7 +480,7 @@ int OrcaCloudServiceAgent::set_config_dir(std::string cfg_dir)
|
||||
config_dir = cfg_dir;
|
||||
wxFileName fallback(wxString::FromUTF8(cfg_dir.c_str()), "orca_refresh_token.sec");
|
||||
fallback.Normalize();
|
||||
refresh_fallback_path = fallback.GetFullPath().ToStdString();
|
||||
secret_fallback_path = fallback.GetFullPath().ToStdString();
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -498,14 +498,71 @@ int OrcaCloudServiceAgent::set_country_code(std::string code)
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
/// Decode a saved user session or a refresh token.
|
||||
///
|
||||
/// Returns `false` if invalid input, and a re-authentication is required.
|
||||
///
|
||||
/// If returns `true`, `out_refresh_token` will contain the user refresh token, and `out_session` can be one of two scenarios:
|
||||
/// - if `out_session.logged_in` is `true`, then `out_session.refresh_token` and `out_session.user_id` are guaranteed to be present,
|
||||
/// and a refresh is not necessarily required until you need to make any network call
|
||||
/// - otherwise if `out_session.logged_in` is `false`, you should do a refresh immediately to get the user information before proceed,
|
||||
/// otherwise user will be logged out
|
||||
static bool parse_stored_secret(const std::string& secret, std::string& out_refresh_token, OrcaCloudServiceAgent::SessionInfo& out_session)
|
||||
{
|
||||
out_refresh_token.clear();
|
||||
out_session = OrcaCloudServiceAgent::SessionInfo{};
|
||||
|
||||
try {
|
||||
// Valid secret should be a json object, otherwise it's a plain refresh token
|
||||
const json secret_json = json::parse(secret, nullptr, false);
|
||||
if (secret_json.type() != json::value_t::object) {
|
||||
out_refresh_token = secret;
|
||||
return true;
|
||||
}
|
||||
|
||||
OrcaCloudServiceAgent::SessionInfo user_session{};
|
||||
user_session.refresh_token = get_json_string_field(secret_json, "refresh_token");
|
||||
user_session.user_id = get_json_string_field(secret_json, "user_id");
|
||||
user_session.user_name = get_json_string_field(secret_json, "username");
|
||||
user_session.user_nickname = get_json_string_field(secret_json, "nickname");
|
||||
user_session.logged_in = true;
|
||||
// User session, must at least contains refresh token and user id
|
||||
if (user_session.refresh_token.empty() || user_session.user_id.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: secret does not contain valid user session, force re-authentication";
|
||||
return false;
|
||||
}
|
||||
|
||||
out_refresh_token = user_session.refresh_token;
|
||||
out_session = std::move(user_session);
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: parse_stored_secret exception, force re-authentication";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::start()
|
||||
{
|
||||
regenerate_pkce();
|
||||
|
||||
// Attempt silent sign-in from stored refresh token
|
||||
std::string stored_refresh;
|
||||
if (load_refresh_token(stored_refresh) && !stored_refresh.empty()) {
|
||||
refresh_now(stored_refresh, "refresh token", false);
|
||||
std::string stored_secret;
|
||||
if (load_user_secret(stored_secret) && !stored_secret.empty()) {
|
||||
// Backward compatibility: if secret it a json, then read it as use session,
|
||||
// which allows us to refresh it in a background thread to speed up the app startup;
|
||||
// otherwise it's a plain refresh token, then we force a sync refresh
|
||||
std::string refresh_token;
|
||||
SessionInfo stored_session;
|
||||
if (parse_stored_secret(stored_secret, refresh_token, stored_session)) {
|
||||
if (stored_session.logged_in) {
|
||||
// We have a previously saved user session, use it. Skip re-persisting: the secret was
|
||||
// just loaded from disk, so writing the identical bytes back is wasted startup I/O.
|
||||
set_user_session(stored_session.access_token, stored_session.user_id, stored_session.user_name,
|
||||
stored_session.user_nickname, stored_session.user_avatar, stored_session.refresh_token,
|
||||
/*persist=*/false);
|
||||
}
|
||||
refresh_now(refresh_token, "refresh token", stored_session.logged_in);
|
||||
}
|
||||
}
|
||||
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
@@ -1388,10 +1445,10 @@ void OrcaCloudServiceAgent::update_redirect_uri()
|
||||
// Auth - Token Persistence
|
||||
// ============================================================================
|
||||
|
||||
void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token)
|
||||
void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
|
||||
{
|
||||
if (token.empty()) {
|
||||
clear_refresh_token();
|
||||
if (secret.empty()) {
|
||||
clear_user_secret();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1401,13 +1458,13 @@ void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token)
|
||||
// Use encrypted file only
|
||||
auto key = sha256_bytes(get_encryption_key());
|
||||
if (key.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot derive key for refresh-token file storage";
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot derive key for user secret file storage";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string payload;
|
||||
if (!aes256gcm_encrypt(token, key, payload)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to encrypt refresh token for file storage";
|
||||
if (!aes256gcm_encrypt(secret, key, payload)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to encrypt user secret for file storage";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1417,38 +1474,38 @@ void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token)
|
||||
}
|
||||
|
||||
compute_fallback_path();
|
||||
if (refresh_fallback_path.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh-token storage path available; skipping file persistence";
|
||||
if (secret_fallback_path.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no user secret storage path available; skipping file persistence";
|
||||
return;
|
||||
}
|
||||
wxFileName path(wxString::FromUTF8(refresh_fallback_path.c_str()));
|
||||
wxFileName path(wxString::FromUTF8(secret_fallback_path.c_str()));
|
||||
path.Normalize();
|
||||
if (!wxFileName::DirExists(path.GetPath())) {
|
||||
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
||||
}
|
||||
|
||||
const std::string tmp_path = refresh_fallback_path + ".tmp";
|
||||
const std::string tmp_path = secret_fallback_path + ".tmp";
|
||||
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
|
||||
if (ofs.good()) {
|
||||
ofs << signed_payload;
|
||||
ofs.flush();
|
||||
ofs.close();
|
||||
|
||||
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(refresh_fallback_path.c_str()), true)) {
|
||||
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(secret_fallback_path.c_str()), true)) {
|
||||
stored = true;
|
||||
} else {
|
||||
wxRemoveFile(wxString::FromUTF8(tmp_path.c_str()));
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace refresh-token file";
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace user secret file";
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open refresh-token file for write - " << refresh_fallback_path;
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open user secret file for write - " << secret_fallback_path;
|
||||
}
|
||||
} else {
|
||||
// Use wxSecretStore only
|
||||
wxSecretStore store = wxSecretStore::GetDefault();
|
||||
if (store.IsOk()) {
|
||||
wxSecretValue secret(wxString::FromUTF8(token.c_str()));
|
||||
if (store.Save(SECRET_STORE_SERVICE, SECRET_STORE_USER, secret)) {
|
||||
wxSecretValue secret_value(wxString::FromUTF8(secret.c_str()));
|
||||
if (store.Save(SECRET_STORE_SERVICE, SECRET_STORE_USER, secret_value)) {
|
||||
stored = true;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain save failed";
|
||||
@@ -1461,15 +1518,15 @@ void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token)
|
||||
(void) stored;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token)
|
||||
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
|
||||
{
|
||||
out_token.clear();
|
||||
out_secret.clear();
|
||||
|
||||
if (m_use_encrypted_token_file) {
|
||||
// Load from encrypted file only
|
||||
compute_fallback_path();
|
||||
if (wxFileExists(wxString::FromUTF8(refresh_fallback_path.c_str()))) {
|
||||
std::ifstream ifs(refresh_fallback_path, std::ios::binary);
|
||||
if (wxFileExists(wxString::FromUTF8(secret_fallback_path.c_str()))) {
|
||||
std::ifstream ifs(secret_fallback_path, std::ios::binary);
|
||||
std::string payload((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
|
||||
auto key = sha256_bytes(get_encryption_key());
|
||||
std::string plain;
|
||||
@@ -1492,16 +1549,16 @@ bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token)
|
||||
std::transform(computed_hmac.begin(), computed_hmac.end(), computed_hmac.begin(), ::tolower);
|
||||
if (computed_hmac.empty() || computed_hmac != lower_stored) {
|
||||
integrity_ok = false;
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: refresh token integrity check failed (HMAC mismatch)";
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: user secret integrity check failed (HMAC mismatch)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) {
|
||||
out_token = plain;
|
||||
out_secret = plain;
|
||||
// Upgrade legacy payloads to signed format
|
||||
if (payload.rfind("v2:", 0) != 0) {
|
||||
persist_refresh_token(out_token);
|
||||
persist_user_secret(out_secret);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1513,8 +1570,8 @@ bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token)
|
||||
wxString username;
|
||||
wxSecretValue secret;
|
||||
if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) {
|
||||
out_token.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
|
||||
if (!out_token.empty()) {
|
||||
out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
|
||||
if (!out_secret.empty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1524,7 +1581,7 @@ bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token)
|
||||
return false;
|
||||
}
|
||||
|
||||
void OrcaCloudServiceAgent::clear_refresh_token()
|
||||
void OrcaCloudServiceAgent::clear_user_secret()
|
||||
{
|
||||
wxSecretStore store = wxSecretStore::GetDefault();
|
||||
if (store.IsOk()) {
|
||||
@@ -1532,8 +1589,8 @@ void OrcaCloudServiceAgent::clear_refresh_token()
|
||||
}
|
||||
|
||||
compute_fallback_path();
|
||||
if (!refresh_fallback_path.empty() && wxFileExists(wxString::FromUTF8(refresh_fallback_path.c_str()))) {
|
||||
wxRemoveFile(wxString::FromUTF8(refresh_fallback_path.c_str()));
|
||||
if (!secret_fallback_path.empty() && wxFileExists(wxString::FromUTF8(secret_fallback_path.c_str()))) {
|
||||
wxRemoveFile(wxString::FromUTF8(secret_fallback_path.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1615,7 +1672,11 @@ RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& rea
|
||||
{
|
||||
std::string refresh_token = get_refresh_token();
|
||||
if (refresh_token.empty()) {
|
||||
load_refresh_token(refresh_token);
|
||||
std::string user_secret;
|
||||
if (load_user_secret(user_secret) && !user_secret.empty()) {
|
||||
SessionInfo stored_session;
|
||||
parse_stored_secret(user_secret, refresh_token, stored_session);
|
||||
}
|
||||
}
|
||||
if (refresh_token.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")";
|
||||
@@ -1695,7 +1756,8 @@ bool OrcaCloudServiceAgent::set_user_session(const std::string& token,
|
||||
const std::string& username,
|
||||
const std::string& nickname,
|
||||
const std::string& avatar,
|
||||
const std::string& refresh_token)
|
||||
const std::string& refresh_token,
|
||||
bool persist)
|
||||
{
|
||||
std::chrono::system_clock::time_point exp_tp{};
|
||||
decode_jwt_expiry(token, exp_tp);
|
||||
@@ -1712,8 +1774,17 @@ bool OrcaCloudServiceAgent::set_user_session(const std::string& token,
|
||||
session.logged_in = true;
|
||||
}
|
||||
|
||||
if (!refresh_token.empty()) {
|
||||
persist_refresh_token(refresh_token);
|
||||
if (persist) {
|
||||
// Store user session on disk to not block use from using
|
||||
// an already logged in account if internet is not available.
|
||||
// Don't store access token though, we should always refresh it
|
||||
// once user is back online.
|
||||
json sec = json::object();
|
||||
sec["refresh_token"] = refresh_token;
|
||||
sec["user_id"] = user_id;
|
||||
sec["username"] = username;
|
||||
sec["nickname"] = nickname;
|
||||
persist_user_secret(sec.dump());
|
||||
}
|
||||
|
||||
// Set per-user sync state path
|
||||
@@ -1789,7 +1860,7 @@ void OrcaCloudServiceAgent::clear_session()
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
session = SessionInfo{};
|
||||
}
|
||||
clear_refresh_token();
|
||||
clear_user_secret();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -2224,7 +2295,7 @@ bool OrcaCloudServiceAgent::http_post_auth(const std::string& path, const std::s
|
||||
|
||||
void OrcaCloudServiceAgent::compute_fallback_path()
|
||||
{
|
||||
if (!refresh_fallback_path.empty())
|
||||
if (!secret_fallback_path.empty())
|
||||
return;
|
||||
// wxStandardPaths::GetUserDataDir() resolves the app data directory via
|
||||
// wxAppConsoleBase::GetAppName(), which dereferences wxTheApp. In headless
|
||||
@@ -2235,7 +2306,7 @@ void OrcaCloudServiceAgent::compute_fallback_path()
|
||||
return;
|
||||
wxFileName fallback(wxStandardPaths::Get().GetUserDataDir(), "orca_refresh_token.sec");
|
||||
fallback.Normalize();
|
||||
refresh_fallback_path = fallback.GetFullPath().ToStdString();
|
||||
secret_fallback_path = fallback.GetFullPath().ToStdString();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user