135 lines
4.9 KiB
Rust
135 lines
4.9 KiB
Rust
// Per-boot unattended-access password.
|
|
//
|
|
// On every service start (= every host reboot, since `--service` is the
|
|
// Windows service entry the SCM auto-starts on boot) hello-agent generates
|
|
// a random "permanent password" and reports it to the rustdesk-server
|
|
// admin API. A supporter reaching the device when no user is logged in
|
|
// can read the password from the admin UI and authenticate without the
|
|
// per-session approval popup ever firing.
|
|
//
|
|
// The password is:
|
|
// 1. Persisted locally via `Config::set_permanent_password` so the
|
|
// rustdesk auth path accepts it on the next LoginRequest.
|
|
// 2. POSTed to `<api-server>/api/unattended-password` with a retry
|
|
// loop. The first few attempts can legitimately fail with
|
|
// ID_NOT_FOUND because rendezvous registration runs in the
|
|
// `--server` child (which the supervisor hasn't even spawned yet
|
|
// when this fires), not in this `--service` process — we just back
|
|
// off and retry until the peer row exists server-side.
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use hbb_common::rand::{distributions::Alphanumeric, Rng};
|
|
use std::time::Duration;
|
|
|
|
const PASSWORD_LEN: usize = 12;
|
|
const MAX_RETRY_DELAY: Duration = Duration::from_secs(60);
|
|
const MAX_ATTEMPTS: u32 = 20;
|
|
|
|
/// Generate a fresh password, write it to local config, and kick off a
|
|
/// background reporter thread. Returns immediately; the reporter has its
|
|
/// own Tokio runtime so it doesn't tangle with the supervisor's poll loop.
|
|
pub fn rotate_and_report() {
|
|
let password = generate();
|
|
hbb_common::config::Config::set_permanent_password(&password);
|
|
log::info!(
|
|
"rotated unattended-access password (len={})",
|
|
password.len()
|
|
);
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
{
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
log::warn!("unattended-password reporter: build runtime: {e}");
|
|
return;
|
|
}
|
|
};
|
|
rt.block_on(report_with_retry(password));
|
|
});
|
|
}
|
|
|
|
fn generate() -> String {
|
|
hbb_common::rand::thread_rng()
|
|
.sample_iter(&Alphanumeric)
|
|
.take(PASSWORD_LEN)
|
|
.map(char::from)
|
|
.collect()
|
|
}
|
|
|
|
async fn report_with_retry(password: String) {
|
|
// Start at 2s and double up to MAX_RETRY_DELAY. The early ID_NOT_FOUND
|
|
// window typically clears within a minute (heartbeat sync registers
|
|
// the peer on its first iteration), so most boots land on the second
|
|
// or third attempt. After MAX_ATTEMPTS we give up — the password is
|
|
// already set locally, the only thing missing is its visibility in
|
|
// the admin UI, so silent forever-retry would just be log spam.
|
|
let mut delay = Duration::from_secs(2);
|
|
for attempt in 1..=MAX_ATTEMPTS {
|
|
match try_report(&password).await {
|
|
Ok(_) => {
|
|
log::info!(
|
|
"unattended-password: server acknowledged on attempt {attempt}"
|
|
);
|
|
return;
|
|
}
|
|
Err(e) => {
|
|
log::warn!(
|
|
"unattended-password: report attempt {attempt}/{MAX_ATTEMPTS} \
|
|
failed ({e:#}); retrying in {:?}",
|
|
delay
|
|
);
|
|
}
|
|
}
|
|
tokio::time::sleep(delay).await;
|
|
delay = (delay * 2).min(MAX_RETRY_DELAY);
|
|
}
|
|
log::error!(
|
|
"unattended-password: gave up after {MAX_ATTEMPTS} attempts — admin UI \
|
|
won't show the password until the next service start"
|
|
);
|
|
}
|
|
|
|
async fn try_report(password: &str) -> Result<()> {
|
|
let api = librustdesk::common::get_api_server(
|
|
hbb_common::config::Config::get_option("api-server"),
|
|
hbb_common::config::Config::get_option("custom-rendezvous-server"),
|
|
);
|
|
if api.is_empty() {
|
|
return Err(anyhow!("no api-server configured yet"));
|
|
}
|
|
|
|
let url = format!("{api}/api/unattended-password");
|
|
let id = hbb_common::config::Config::get_id();
|
|
let uuid = librustdesk::common::encode64(hbb_common::get_uuid());
|
|
let body = hbb_common::serde_json::json!({
|
|
"id": id,
|
|
"uuid": uuid,
|
|
"password": password,
|
|
})
|
|
.to_string();
|
|
|
|
// Same per-peer signature gate as heartbeat / sysinfo. Once this peer's
|
|
// `managed` flag has flipped to 1 server-side, unsigned posts here
|
|
// would be rejected — and we want unattended-password to keep landing
|
|
// through the same TOFU lifecycle as the other endpoints.
|
|
let headers = librustdesk::hbbs_http::sign::build_signed_headers(
|
|
"POST",
|
|
"/api/unattended-password",
|
|
body.as_bytes(),
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
let resp = librustdesk::common::post_request(url, body, &headers)
|
|
.await
|
|
.map_err(|e| anyhow!("post: {e}"))?;
|
|
let trimmed = resp.trim();
|
|
if trimmed == "OK" {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow!("unexpected response: {trimmed}"))
|
|
}
|
|
}
|