diff --git a/src/core_main.rs b/src/core_main.rs index 67a83a37e..a0ca5eb95 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -627,6 +627,98 @@ pub fn core_main() -> Option> { println!("Installation and administrative privileges required!"); } return None; + } else if args[0] == "--deploy" { + if config::Config::no_register_device() { + println!("Cannot deploy an unregistrable device!"); + } else if crate::platform::is_installed() && is_root() { + let max = args.len() - 1; + let pos = args.iter().position(|x| x == "--token").unwrap_or(max); + if pos >= max { + println!("--token is required!"); + return None; + } + let token = args[pos + 1].to_owned(); + let get_value = |c: &str| { + let pos = args.iter().position(|x| x == c).unwrap_or(max); + if pos < max { + Some(args[pos + 1].to_owned()) + } else { + None + } + }; + let new_id = get_value("--id"); + let local_id = crate::ipc::get_id(); + let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone()); + let uuid = crate::encode64(hbb_common::get_uuid()); + let pk = crate::encode64( + hbb_common::config::Config::get_key_pair().1, + ); + let body = serde_json::json!({ + "id": id_to_deploy, + "uuid": uuid, + "pk": pk, + }); + let header = "Authorization: Bearer ".to_owned() + &token; + let url = crate::ui_interface::get_api_server() + "/api/devices/deploy"; + match crate::post_request_sync(url, body.to_string(), &header) { + Err(err) => { + println!("Request failed: {}", err); + std::process::exit(1); + } + Ok(text) => { + let parsed: serde_json::Value = + serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); + let result = parsed["result"].as_str().unwrap_or(""); + match result { + "OK" => { + if let Some(ref new_id) = new_id { + if *new_id != local_id { + if let Err(err) = + crate::ipc::set_config("id", new_id.clone()) + { + println!( + "Failed to persist deployed id locally: {}", + err + ); + std::process::exit(1); + } + } + } + if let Err(err) = crate::ipc::notify_deployed() { + log::warn!("Failed to notify deployed state: {}", err); + } + println!("Device deployed."); + } + "NOT_ENABLED" => { + println!("Server does not require deployment."); + std::process::exit(3); + } + "INVALID_INPUT" => { + println!("Invalid input."); + std::process::exit(5); + } + "ID_TAKEN" => { + println!( + "Id `{}` is already used by another machine on the server.", + id_to_deploy + ); + std::process::exit(6); + } + _ => { + if text.is_empty() { + println!("Unknown response."); + } else { + println!("{}", text); + } + std::process::exit(1); + } + } + } + } + } else { + println!("Installation and administrative privileges required!"); + } + return None; } else if args[0] == "--check-hwcodec-config" { #[cfg(feature = "hwcodec")] crate::ipc::hwcodec_process(); diff --git a/src/ipc.rs b/src/ipc.rs index 0258a2816..0cd30634a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -312,6 +312,7 @@ pub enum Data { ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, + Deployed, #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -929,6 +930,10 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } + Data::Deployed => { + crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::SwitchSidesRequest(id) => { @@ -1737,6 +1742,13 @@ pub async fn test_rendezvous_server() -> ResultType<()> { Ok(()) } +#[tokio::main(flavor = "current_thread")] +pub async fn notify_deployed() -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Deployed).await?; + Ok(()) +} + #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { connect(1_000, "_url") diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 3ef280a2a..89d7fa01e 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -41,6 +41,30 @@ lazy_static::lazy_static! { static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); +pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false); +// register_pk retry interval (ms) when device is awaiting deployment +const DEPLOY_RETRY_INTERVAL: i64 = 30_000; +lazy_static::lazy_static! { + static ref LAST_NOT_DEPLOYED_REGISTER: Mutex> = Mutex::new(None); +} + +// Single source of truth for the "awaiting deployment" backoff. The server has +// already told us this device is not in its db; until the operator runs +// `rustdesk --deploy --token ` there is no point re-running the +// register path more often than DEPLOY_RETRY_INTERVAL. Gating in the timer +// loops (rather than only inside register_pk) also avoids the +// last_register_sent / fails / latency / UDP-rebind churn the loop would +// otherwise spin on while no response ever comes back. +async fn deploy_register_throttled() -> bool { + if !NEEDS_DEPLOY.load(Ordering::SeqCst) { + return false; + } + LAST_NOT_DEPLOYED_REGISTER + .lock() + .await + .map(|t| (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL) + .unwrap_or(false) +} #[derive(Clone)] pub struct RendezvousMediator { @@ -226,6 +250,14 @@ impl RendezvousMediator { if SHOULD_EXIT.load(Ordering::SeqCst) { break; } + // The server already told us this device is not deployed. Skip + // the whole register / fails / latency / UDP-rebind path until + // DEPLOY_RETRY_INTERVAL elapses, otherwise the loop spins every + // few seconds (log spam + misapplied network-recovery rebind) + // until the operator runs `rustdesk --deploy`. + if deploy_register_throttled().await { + continue; + } let now = Some(Instant::now()); let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true); let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false); @@ -289,10 +321,22 @@ impl RendezvousMediator { Config::set_key_confirmed(true); Config::set_host_key_confirmed(&self.host_prefix, true); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); + NEEDS_DEPLOY.store(false, Ordering::SeqCst); } Ok(register_pk_response::Result::UUID_MISMATCH) => { self.handle_uuid_mismatch(sink).await?; } + Ok(register_pk_response::Result::NOT_DEPLOYED) => { + if !NEEDS_DEPLOY.load(Ordering::SeqCst) { + log::warn!("Server requires deployment. Run `rustdesk --deploy --token ` on this device."); + } + NEEDS_DEPLOY.store(true, Ordering::SeqCst); + // Clear key_confirmed so the UI reflects the truth: this device is + // not currently registered. Covers the case where an online device + // was deleted by an admin while running. + Config::set_key_confirmed(false); + Config::set_host_key_confirmed(&self.host_prefix, false); + } _ => { log::error!("unknown RegisterPkResponse"); } @@ -678,6 +722,21 @@ impl RendezvousMediator { } async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> { + // Throttle register_pk when the device is awaiting deployment: server + // already told us we're not in its db; sending more often than every + // DEPLOY_RETRY_INTERVAL ms is wasted traffic until the operator runs + // `rustdesk --deploy --token `. + if NEEDS_DEPLOY.load(Ordering::SeqCst) { + let mut last = LAST_NOT_DEPLOYED_REGISTER.lock().await; + if let Some(t) = *last { + if (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL { + return Ok(()); + } + } + *last = Some(Instant::now()); + } else { + *LAST_NOT_DEPLOYED_REGISTER.lock().await = None; + } let mut msg_out = Message::new(); let pk = Config::get_key_pair().1; let uuid = hbb_common::get_uuid();