mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-27 11:41:51 +02:00
refactor: cleanup cloud sync
This commit is contained in:
+132
-6
@@ -264,6 +264,26 @@ pub struct RunRemoteRequest {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SetCloudSyncRequest {
|
||||||
|
/// `Disabled`, `Regular`, or `Encrypted`.
|
||||||
|
///
|
||||||
|
/// `Encrypted` derives its key from a passphrase that never leaves this
|
||||||
|
/// machine, so a profile in that mode can be synced but NOT run remotely —
|
||||||
|
/// a remote host would download ciphertext it cannot decrypt.
|
||||||
|
pub mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SetCloudSyncResponse {
|
||||||
|
pub profile_id: String,
|
||||||
|
pub mode: String,
|
||||||
|
/// Whether the profile can now be launched on a remote host.
|
||||||
|
pub remote_launchable: bool,
|
||||||
|
/// Why not, when `remote_launchable` is false.
|
||||||
|
pub remote_blocked_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct RunRemoteResponse {
|
pub struct RunRemoteResponse {
|
||||||
pub profile_id: String,
|
pub profile_id: String,
|
||||||
@@ -573,6 +593,7 @@ impl ApiServer {
|
|||||||
.routes(routes!(get_profile, update_profile, delete_profile))
|
.routes(routes!(get_profile, update_profile, delete_profile))
|
||||||
.routes(routes!(run_profile))
|
.routes(routes!(run_profile))
|
||||||
.routes(routes!(run_profile_remote))
|
.routes(routes!(run_profile_remote))
|
||||||
|
.routes(routes!(set_profile_cloud_sync))
|
||||||
.routes(routes!(open_url_in_profile))
|
.routes(routes!(open_url_in_profile))
|
||||||
.routes(routes!(kill_profile))
|
.routes(routes!(kill_profile))
|
||||||
.routes(routes!(batch_run_profiles))
|
.routes(routes!(batch_run_profiles))
|
||||||
@@ -2220,6 +2241,88 @@ async fn run_profile_remote(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/profiles/{id}/cloud-sync",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Profile ID")
|
||||||
|
),
|
||||||
|
request_body = SetCloudSyncRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Cloud sync mode updated", body = SetCloudSyncResponse),
|
||||||
|
(status = 400, description = "Invalid mode, or the profile cannot be synced"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 402, description = "Active paid plan with cloud backup required"),
|
||||||
|
(status = 404, description = "Profile not found"),
|
||||||
|
(status = 409, description = "Profile is running — stop it before enabling sync"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("bearer_auth" = [])
|
||||||
|
),
|
||||||
|
tag = "profiles"
|
||||||
|
)]
|
||||||
|
async fn set_profile_cloud_sync(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(state): State<ApiServerState>,
|
||||||
|
Json(request): Json<SetCloudSyncRequest>,
|
||||||
|
) -> Result<Json<SetCloudSyncResponse>, (StatusCode, String)> {
|
||||||
|
// Remote launch requires cloud sync, and until now sync could only be turned
|
||||||
|
// on from the GUI — so an automation-only caller could never reach the state
|
||||||
|
// that makes /run-remote work.
|
||||||
|
let mode = match request.mode.as_str() {
|
||||||
|
"Disabled" | "Regular" | "Encrypted" => request.mode.clone(),
|
||||||
|
other => {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("invalid sync mode {other:?}; expected Disabled, Regular or Encrypted"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
crate::sync::set_profile_sync_mode(state.app_handle.clone(), id.clone(), mode.clone())
|
||||||
|
.await
|
||||||
|
.map_err(sync_mode_error_response)?;
|
||||||
|
|
||||||
|
let profile_manager = ProfileManager::instance();
|
||||||
|
let profiles = profile_manager
|
||||||
|
.list_profiles()
|
||||||
|
.map_err(manager_error_response)?;
|
||||||
|
let profile = profiles
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id.to_string() == id)
|
||||||
|
.ok_or((StatusCode::NOT_FOUND, "profile not found".to_string()))?;
|
||||||
|
|
||||||
|
// Reported rather than left for the caller to discover at launch time: the
|
||||||
|
// most common reason a caller enables sync is to run the profile remotely,
|
||||||
|
// and Encrypted mode silently makes that impossible.
|
||||||
|
let blocked = remote_launch_precondition(profile).err();
|
||||||
|
Ok(Json(SetCloudSyncResponse {
|
||||||
|
profile_id: profile.id.to_string(),
|
||||||
|
mode,
|
||||||
|
remote_launchable: blocked.is_none(),
|
||||||
|
remote_blocked_reason: blocked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a sync-mode failure onto the status the caller can act on.
|
||||||
|
///
|
||||||
|
/// `set_profile_sync_mode` reports a running profile as a JSON body rather than
|
||||||
|
/// a plain message, because enabling sync under a live browser would race the
|
||||||
|
/// browser's own writes.
|
||||||
|
fn sync_mode_error_response(err: String) -> (StatusCode, String) {
|
||||||
|
if err.contains("PROFILE_RUNNING") {
|
||||||
|
return (
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"profile is running; stop it before changing cloud sync".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if err.contains("cross-OS") || err.contains("ephemeral") {
|
||||||
|
return (StatusCode::BAD_REQUEST, err);
|
||||||
|
}
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, err)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a profile may be launched on a remote host.
|
/// Whether a profile may be launched on a remote host.
|
||||||
///
|
///
|
||||||
/// Extracted so the rule is unit-testable without a running app: it is the one
|
/// Extracted so the rule is unit-testable without a running app: it is the one
|
||||||
@@ -2235,6 +2338,16 @@ pub fn remote_launch_precondition(
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if profile.is_encrypted_sync() {
|
||||||
|
// The key is derived from a passphrase that never leaves this machine, so
|
||||||
|
// the host would download ciphertext, launch Chromium on it, and push the
|
||||||
|
// corruption back over the user's real profile.
|
||||||
|
return Err(
|
||||||
|
"profile uses end-to-end encrypted sync; a remote host cannot decrypt \
|
||||||
|
it. Switch the profile to Regular sync to run it remotely."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
if profile.resolved_os().is_none() {
|
if profile.resolved_os().is_none() {
|
||||||
return Err(
|
return Err(
|
||||||
"profile has no recorded operating system, so it cannot be scheduled \
|
"profile has no recorded operating system, so it cannot be scheduled \
|
||||||
@@ -2804,12 +2917,25 @@ mod tests {
|
|||||||
.expect_err("a non-synced profile must be refused");
|
.expect_err("a non-synced profile must be refused");
|
||||||
assert!(err.contains("cloud sync"), "unhelpful message: {err}");
|
assert!(err.contains("cloud sync"), "unhelpful message: {err}");
|
||||||
|
|
||||||
for mode in [SyncMode::Regular, SyncMode::Encrypted] {
|
assert!(
|
||||||
assert!(
|
remote_launch_precondition(&profile_with(SyncMode::Regular, Some("macos"))).is_ok(),
|
||||||
remote_launch_precondition(&profile_with(mode, Some("macos"))).is_ok(),
|
"a synced profile must be allowed"
|
||||||
"a synced profile must be allowed"
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
#[test]
|
||||||
|
fn remote_launch_refuses_an_end_to_end_encrypted_profile() {
|
||||||
|
// The key is derived from a passphrase that never leaves this machine, so
|
||||||
|
// a remote host downloads ciphertext, launches Chromium on it, and pushes
|
||||||
|
// the corruption back over the user's real profile. Refusing here also
|
||||||
|
// saves taking the profile lock and a slot on leased hardware for a
|
||||||
|
// session that cannot possibly work.
|
||||||
|
let err = remote_launch_precondition(&profile_with(SyncMode::Encrypted, Some("macos")))
|
||||||
|
.expect_err("an encrypted profile must be refused");
|
||||||
|
assert!(
|
||||||
|
err.contains("encrypted") && err.contains("Regular"),
|
||||||
|
"the message must say what to change: {err}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user