diff --git a/src/gate.rs b/src/gate.rs new file mode 100644 index 0000000..3a6795b --- /dev/null +++ b/src/gate.rs @@ -0,0 +1,108 @@ +//! Проверка доступа через кабинет (патч Remote Desk к rustdesk-server 1.1.16). +//! +//! Перед тем как передать запрос на подключение компьютеру, hbbs спрашивает кабинет: +//! можно ли владельцу ключа входа `token` подключиться к компьютеру `target_id`. +//! Кабинет отвечает `{"allow": bool, "permissions": u64, "reason": str}`. +//! `permissions` — битовая маска ControlPermissions (по 2 бита на право: 1 — запрещено, 2 — разрешено), +//! компьютер применяет её сам (RustDesk 1.5.0, src/server/connection.rs). +//! +//! Проверка выключена, пока не задан GATE_URL. Если кабинет не ответил — подключения нет. + +use hbb_common::{log, tokio}; +use once_cell::sync::Lazy; + +pub enum Decision { + /// Разрешено. `None` — проверка выключена, права не передаём. + Allow(Option), + Deny(String), +} + +struct Cfg { + url: String, + token: String, + timeout_secs: u64, +} + +static CFG: Lazy> = Lazy::new(|| { + let url = std::env::var("GATE_URL").unwrap_or_default(); + if url.trim().is_empty() { + log::warn!("gate: GATE_URL не задан, проверка доступа выключена"); + return None; + } + let token = std::env::var("GATE_TOKEN").unwrap_or_default(); + let timeout_secs = std::env::var("GATE_TIMEOUT_SECS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(3); + log::info!("gate: проверка доступа включена, {}", url); + Some(Cfg { url, token, timeout_secs }) +}); + +pub const MSG_LOGIN: &str = "Войдите в аккаунт сервиса в приложении: Настройки → Аккаунт"; +pub const MSG_UNAVAILABLE: &str = "Проверка доступа сейчас недоступна, попробуйте через минуту"; + +pub fn enabled() -> bool { + CFG.is_some() +} + +pub async fn authorize(token: &str, target_id: &str, from_ip: &str, kind: &str) -> Decision { + let cfg = match CFG.as_ref() { + None => return Decision::Allow(None), + Some(c) => c, + }; + if token.is_empty() { + return Decision::Deny(MSG_LOGIN.to_owned()); + } + let body = serde_json::json!({ + "token": token, + "target_id": target_id, + "from_ip": from_ip, + "kind": kind, + }) + .to_string(); + let url = cfg.url.clone(); + let auth = format!("Bearer {}", cfg.token); + let timeout = cfg.timeout_secs; + let res = tokio::task::spawn_blocking(move || { + minreq::post(url) + .with_header("Authorization", auth) + .with_header("Content-Type", "application/json") + .with_body(body) + .with_timeout(timeout) + .send() + }) + .await; + let resp = match res { + Ok(Ok(r)) if r.status_code == 200 => r, + Ok(Ok(r)) => { + log::error!("gate: кабинет ответил {}", r.status_code); + return Decision::Deny(MSG_UNAVAILABLE.to_owned()); + } + Ok(Err(e)) => { + log::error!("gate: нет ответа от кабинета: {}", e); + return Decision::Deny(MSG_UNAVAILABLE.to_owned()); + } + Err(e) => { + log::error!("gate: ошибка задачи: {}", e); + return Decision::Deny(MSG_UNAVAILABLE.to_owned()); + } + }; + let v: serde_json::Value = match resp.as_str().ok().and_then(|s| serde_json::from_str(s).ok()) { + Some(v) => v, + None => return Decision::Deny(MSG_UNAVAILABLE.to_owned()), + }; + if v.get("allow").and_then(|x| x.as_bool()) == Some(true) { + let p = v.get("permissions").and_then(|x| x.as_u64()).unwrap_or(0); + log::info!("gate: {} к {} разрешено ({})", from_ip, target_id, kind); + Decision::Allow(Some(p)) + } else { + let reason = v + .get("reason") + .and_then(|x| x.as_str()) + .filter(|s| !s.is_empty()) + .unwrap_or("Нет доступа к этому компьютеру") + .to_owned(); + log::info!("gate: {} к {} отказ ({})", from_ip, target_id, kind); + Decision::Deny(reason) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8da29a2..ea35ff8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod gate; mod rendezvous_server; pub use rendezvous_server::*; pub mod common; diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index dff45eb..3ab30a8 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -495,6 +495,20 @@ impl RendezvousServer { if let Some(sink) = sink.take() { self.tcp_punch.lock().await.insert(try_into_v4(addr), sink); } + // Remote Desk: ключ сервера и проверка доступа, как у PunchHoleRequest. + if !key.is_empty() && rf.licence_key != key { + log::warn!("relay: неверный ключ от {} для {}", addr, rf.id); + return true; + } + match crate::gate::authorize(&rf.token, &rf.id, &try_into_v4(addr).ip().to_string(), "relay").await { + crate::gate::Decision::Allow(p) => { + rf.control_permissions = MessageField::from_option(p.map(|p| ControlPermissions { + permissions: p, + ..Default::default() + })); + } + crate::gate::Decision::Deny(_) => return true, + } if let Some(peer) = self.pm.get_in_memory(&rf.id).await { let mut msg_out = RendezvousMessage::new(); rf.socket_addr = AddrMangle::encode(addr).into(); @@ -688,6 +702,22 @@ impl RendezvousServer { }); return Ok((msg_out, None)); } + // Remote Desk: проверка доступа через кабинет до любых сведений о компьютере. + let gate_perms = match crate::gate::authorize(&ph.token, &ph.id, &try_into_v4(addr).ip().to_string(), "punch").await { + crate::gate::Decision::Allow(p) => p, + crate::gate::Decision::Deny(reason) => { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_punch_hole_response(PunchHoleResponse { + other_failure: reason, + ..Default::default() + }); + return Ok((msg_out, None)); + } + }; + let control_permissions = MessageField::from_option(gate_perms.map(|p| ControlPermissions { + permissions: p, + ..Default::default() + })); let id = ph.id; // punch hole request from A, relay to B, // check if in same intranet first, @@ -754,6 +784,7 @@ impl RendezvousServer { msg_out.set_fetch_local_addr(FetchLocalAddr { socket_addr, relay_server, + control_permissions: control_permissions.clone(), ..Default::default() }); } else { @@ -767,6 +798,7 @@ impl RendezvousServer { socket_addr, nat_type: ph.nat_type, relay_server, + control_permissions: control_permissions.clone(), ..Default::default() }); }