easy access: move grant exchange from rendezvous to API server

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages 2026-05-02 17:20:54 +08:00
parent 8117f01598
commit 04554d9129
7 changed files with 281 additions and 204 deletions

View file

@ -61,7 +61,7 @@ Future<List<Map<String, dynamic>>> fetchEasyAccessManagers() async {
}
final response = await http.post(
Uri.parse('$url/api/devices/easy-access-managers'),
Uri.parse('$url/api/easy_access/managers'),
headers: {'Content-Type': 'application/json'},
body: authBody,
);

View file

@ -207,7 +207,6 @@ impl Client {
debug_assert!(peer == interface.get_id());
interface.update_direct(None);
interface.update_received(false);
interface.clear_easy_access_challenge();
match Self::_start(peer, key, token, conn_type, interface.clone()).await {
Err(err) => {
let err_str = err.to_string();
@ -344,9 +343,7 @@ impl Client {
contained,
);
if udp.0.is_none() {
let conn = fut.await?;
interface.get_lch().write().unwrap().controller_config = conn.3;
return Ok((conn.0, conn.1, conn.2));
return fut.await;
}
let mut connect_futures = Vec::new();
connect_futures.push(fut.boxed());
@ -355,7 +352,7 @@ impl Client {
key.to_owned(),
token.to_owned(),
conn_type,
interface.clone(),
interface,
(None, None),
None,
rendezvous_server,
@ -364,10 +361,7 @@ impl Client {
);
connect_futures.push(fut.boxed());
match select_ok(connect_futures).await {
Ok(conn) => {
interface.get_lch().write().unwrap().controller_config = conn.0 .3;
Ok((conn.0 .0, conn.0 .1, conn.0 .2))
}
Ok(conn) => Ok((conn.0 .0, conn.0 .1, conn.0 .2)),
Err(e) => Err(e),
}
}
@ -393,7 +387,6 @@ impl Client {
),
(i32, String),
bool,
Option<ControllerConfig>,
)> {
let mut start = Instant::now();
let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await;
@ -464,7 +457,6 @@ impl Client {
};
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0);
let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
let mut controller_config = None;
msg_out.set_punch_hole_request(PunchHoleRequest {
id: peer.to_owned(),
token: token.to_owned(),
@ -518,7 +510,6 @@ impl Client {
relay_server = ph.relay_server;
peer_addr = AddrMangle::decode(&ph.socket_addr);
feedback = ph.feedback;
controller_config = ph.controller_config.into_option();
let s = udp.0.take();
if ph.is_udp && s.is_some() {
if let Some(s) = s {
@ -558,7 +549,6 @@ impl Client {
}
}
signed_id_pk = rr.pk().into();
controller_config = rr.controller_config.into_option();
let fut = Self::create_relay(
&peer,
rr.uuid,
@ -589,7 +579,6 @@ impl Client {
(conn, typ == "IPv6", pk, kcp, typ),
(feedback, rendezvous_server),
false,
controller_config,
));
}
_ => {
@ -633,12 +622,10 @@ impl Client {
udp.0,
ipv6.0,
punch_type,
&mut controller_config,
)
.await?,
(feedback, rendezvous_server),
true,
controller_config,
))
}
@ -661,7 +648,6 @@ impl Client {
udp_socket_nat: Option<Arc<UdpSocket>>,
udp_socket_v6: Option<Arc<UdpSocket>>,
punch_type: &str,
controller_config: &mut Option<ControllerConfig>,
) -> ResultType<(
Stream,
bool,
@ -736,7 +722,6 @@ impl Client {
key,
token,
conn_type,
controller_config,
)
.await;
if let Err(e) = conn {
@ -857,7 +842,6 @@ impl Client {
key: &str,
token: &str,
conn_type: ConnType,
controller_config: &mut Option<ControllerConfig>,
) -> ResultType<Stream> {
let mut succeed = false;
let mut uuid = "".to_owned();
@ -902,7 +886,6 @@ impl Client {
if !rs.refuse_reason.is_empty() {
bail!(rs.refuse_reason);
}
*controller_config = rs.controller_config.into_option();
succeed = true;
break;
}
@ -1778,7 +1761,7 @@ pub struct LoginConfigHandler {
pub enable_trusted_devices: bool,
pub record_state: bool,
pub record_permission: bool,
pub controller_config: Option<ControllerConfig>,
easy_access_grant_id: Option<Vec<u8>>,
}
impl Deref for LoginConfigHandler {
@ -1893,6 +1876,7 @@ impl LoginConfigHandler {
self.shared_password = shared_password;
self.record_state = false;
self.record_permission = true;
self.easy_access_grant_id = None;
// `std::env::remove_var("IS_TERMINAL_ADMIN");` is called in `session_add_sync()` - `flutter_ffi.rs`.
let is_terminal_admin = conn_type == ConnType::TERMINAL
@ -2673,16 +2657,15 @@ impl LoginConfigHandler {
};
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
if avatar.is_empty() {
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
"user_info",
))
.ok()
.and_then(|x| {
x.get("avatar")
.and_then(|x| x.as_str())
.map(|x| x.trim().to_owned())
})
.unwrap_or_default();
avatar =
serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option("user_info"))
.ok()
.and_then(|x| {
x.get("avatar")
.and_then(|x| x.as_str())
.map(|x| x.trim().to_owned())
})
.unwrap_or_default();
}
avatar = resolve_avatar_url(avatar);
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
@ -2728,11 +2711,7 @@ impl LoginConfigHandler {
} else {
Bytes::new()
};
let easy_access_challenge: Bytes = self
.controller_config
.as_ref()
.map(|config| config.easy_access_challenge.clone())
.unwrap_or_default();
let easy_access_grant_id = self.easy_access_grant_id.clone().unwrap_or_default().into();
let mut lr = LoginRequest {
username: pure_id,
password: password.into(),
@ -2750,7 +2729,7 @@ impl LoginConfigHandler {
.into(),
hwid,
avatar,
easy_access_challenge,
easy_access_grant_id,
..Default::default()
};
match self.conn_type {
@ -2778,6 +2757,99 @@ impl LoginConfigHandler {
msg_out
}
async fn request_easy_access_grant(
lc: Arc<RwLock<LoginConfigHandler>>,
hash: &Hash,
secure: bool,
) {
let (peer, challenge, other_server) = {
let mut lc = lc.write().unwrap();
lc.easy_access_grant_id = None;
(
lc.id.clone(),
hash.easy_access_challenge.to_vec(),
lc.other_server.is_some(),
)
};
let grant_id =
Self::fetch_easy_access_grant_id(&peer, challenge, other_server, secure).await;
lc.write().unwrap().easy_access_grant_id = grant_id;
}
async fn fetch_easy_access_grant_id(
peer: &str,
challenge: Vec<u8>,
other_server: bool,
secure: bool,
) -> Option<Vec<u8>> {
if other_server || !secure || challenge.is_empty() {
return None;
}
if hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer) {
return None;
}
let access_token = LocalConfig::get_option("access_token");
if access_token.is_empty() {
return None;
}
let api_server = crate::get_api_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
);
if api_server.is_empty() || crate::is_public(&api_server) {
return None;
}
#[derive(Serialize)]
struct EasyAccessGrantRequest<'a> {
id: &'a str,
challenge: &'a str,
}
#[derive(Deserialize)]
struct EasyAccessGrantResponse {
#[serde(default)]
grant_id: String,
}
let challenge_base64 = crate::encode64(&challenge);
let body = match serde_json::to_string(&EasyAccessGrantRequest {
id: peer,
challenge: &challenge_base64,
}) {
Ok(body) => body,
Err(err) => {
log::warn!("Easy access grant request serialize failed: {}", err);
return None;
}
};
let url = format!("{}/api/easy_access/grant", api_server);
let header = format!("Authorization: Bearer {}", access_token);
let response = match crate::post_request(url, body, &header).await {
Ok(response) => response,
Err(err) => {
log::debug!("Easy access grant request failed: {}", err);
return None;
}
};
let response: EasyAccessGrantResponse = match serde_json::from_str(&response) {
Ok(response) => response,
Err(err) => {
log::warn!("Easy access grant response parse failed: {}", err);
return None;
}
};
let grant_id = match crate::decode64(&response.grant_id) {
Ok(grant_id) if !grant_id.is_empty() => grant_id,
Ok(_) => return None,
Err(err) => {
log::warn!("Easy access grant id invalid: {}", err);
return None;
}
};
Some(grant_id)
}
pub fn update_supported_decodings(&self) -> Message {
let decoding = scrap::codec::Decoder::supported_decodings(
Some(&self.id),
@ -3473,6 +3545,7 @@ pub async fn handle_hash(
peer: &mut Stream,
) {
lc.write().unwrap().hash = hash.clone();
LoginConfigHandler::request_easy_access_grant(lc.clone(), &hash, peer.is_secured()).await;
// Take care of password application order
// switch_uuid
@ -3739,12 +3812,6 @@ pub trait Interface: Send + Clone + 'static + Sized {
self.get_lch().write().unwrap().received = received;
}
fn clear_easy_access_challenge(&self) {
if let Some(config) = self.get_lch().write().unwrap().controller_config.as_mut() {
config.easy_access_challenge = Default::default();
}
}
fn on_establish_connection_error(&self, err: String) {
let title = "Connection Error";
let text = err.to_string();

View file

@ -2603,7 +2603,7 @@ pub fn str2color(s: &str, alpha: u8) -> u32 {
/// Returns: Some(true) = enabled, Some(false) = disabled, None = not set or invalid
pub fn get_control_permission(
permissions: u64,
permission: hbb_common::rendezvous_proto::controlled_config::Permission,
permission: hbb_common::rendezvous_proto::control_permissions::Permission,
) -> Option<bool> {
use hbb_common::protobuf::Enum;
let index = permission.value();

View file

@ -1084,7 +1084,7 @@ async fn handle(data: Data, stream: &mut Connection) {
}
},
Data::ControlPermissionsRemoteModify(_) => {
use hbb_common::rendezvous_proto::controlled_config::Permission;
use hbb_common::rendezvous_proto::control_permissions::Permission;
let state =
crate::server::get_control_permission_state(Permission::remote_modify, true);
allow_err!(
@ -1095,7 +1095,7 @@ async fn handle(data: Data, stream: &mut Connection) {
}
#[cfg(target_os = "windows")]
Data::FileTransferEnabledState(_) => {
use hbb_common::rendezvous_proto::controlled_config::Permission;
use hbb_common::rendezvous_proto::control_permissions::Permission;
let state = crate::server::get_control_permission_state(Permission::file, false);
let enabled = state.unwrap_or_else(|| {
crate::server::Connection::is_permission_enabled_locally(

View file

@ -472,8 +472,7 @@ impl RendezvousMediator {
rr.secure,
false,
Default::default(),
rr.controlled_config.clone().into_option(),
rr.request_id,
rr.control_permissions.clone().into_option(),
)
.await
}
@ -487,8 +486,7 @@ impl RendezvousMediator {
secure: bool,
initiate: bool,
socket_addr_v6: bytes::Bytes,
controlled_config: Option<ControlledConfig>,
request_id: bytes::Bytes,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let peer_addr = AddrMangle::decode(&socket_addr);
log::info!(
@ -506,7 +504,6 @@ impl RendezvousMediator {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
request_id,
..Default::default()
};
if initiate {
@ -523,7 +520,7 @@ impl RendezvousMediator {
peer_addr,
secure,
is_ipv4(&self.addr),
controlled_config,
control_permissions,
)
.await;
Ok(())
@ -546,7 +543,7 @@ impl RendezvousMediator {
peer_addr_v6,
addr,
server.clone(),
fla.controlled_config.clone().into_option(),
fla.control_permissions.clone().into_option(),
)
.await;
}
@ -574,8 +571,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6,
fla.controlled_config.into_option(),
fla.request_id,
fla.control_permissions.into_option(),
)
.await
}
@ -602,7 +598,6 @@ impl RendezvousMediator {
relay_server,
version: crate::VERSION.to_owned(),
socket_addr_v6,
request_id: fla.request_id,
..Default::default()
});
let bytes = msg_out.write_to_bytes()?;
@ -612,7 +607,7 @@ impl RendezvousMediator {
socket,
peer_addr,
true,
fla.controlled_config.into_option(),
fla.control_permissions.into_option(),
)
.await;
Ok(())
@ -629,13 +624,13 @@ impl RendezvousMediator {
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let mut socket_addr_v6 = Default::default();
let controlled_config = ph.controlled_config.into_option();
let control_permissions = ph.control_permissions.into_option();
if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6(
peer_addr_v6,
peer_addr,
server.clone(),
controlled_config.clone(),
control_permissions.clone(),
)
.await;
}
@ -656,8 +651,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6.clone(),
controlled_config,
ph.request_id,
control_permissions,
)
.await;
}
@ -670,12 +664,11 @@ impl RendezvousMediator {
nat_type: nat_type.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
request_id: ph.request_id,
..Default::default()
};
if ph.udp_port > 0 {
peer_addr.set_port(ph.udp_port as u16);
self.punch_udp_hole(peer_addr, server, msg_punch, controlled_config)
self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions)
.await?;
return Ok(());
}
@ -692,7 +685,8 @@ impl RendezvousMediator {
msg_out.set_punch_hole_sent(msg_punch);
let bytes = msg_out.write_to_bytes()?;
socket.send_raw(bytes).await?;
crate::accept_connection(server.clone(), socket, peer_addr, true, controlled_config).await;
crate::accept_connection(server.clone(), socket, peer_addr, true, control_permissions)
.await;
Ok(())
}
@ -701,7 +695,7 @@ impl RendezvousMediator {
peer_addr: SocketAddr,
server: ServerPtr,
msg_punch: PunchHoleSent,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let mut msg_out = Message::new();
msg_out.set_punch_hole_sent(msg_punch);
@ -721,7 +715,7 @@ impl RendezvousMediator {
peer_addr,
peer_addr,
server,
controlled_config,
control_permissions,
)
.await?;
Ok(())
@ -880,7 +874,7 @@ async fn direct_server(server: ServerPtr) {
hbb_common::Stream::from(stream, local_addr),
addr,
false,
None, // Direct connections don't have controlled_config
None, // Direct connections don't have control_permissions
)
.await
);
@ -912,7 +906,7 @@ async fn start_ipv6(
peer_addr_v6: SocketAddr,
peer_addr_v4: SocketAddr,
server: ServerPtr,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) -> bytes::Bytes {
crate::test_ipv6().await;
if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await {
@ -924,7 +918,7 @@ async fn start_ipv6(
peer_addr_v6,
peer_addr_v4,
server,
controlled_config,
control_permissions
)
.await
);
@ -939,7 +933,7 @@ async fn udp_nat_listen(
peer_addr: SocketAddr,
peer_addr_v4: SocketAddr,
server: ServerPtr,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let tm = Instant::now();
let socket_cloned = socket.clone();
@ -957,7 +951,7 @@ async fn udp_nat_listen(
stream.1,
peer_addr_v4,
true,
controlled_config,
control_permissions,
)
.await?;
Ok(())

View file

@ -163,7 +163,7 @@ async fn accept_connection_(
server: ServerPtr,
socket: Stream,
secure: bool,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let local_addr = socket.local_addr();
drop(socket);
@ -180,7 +180,7 @@ async fn accept_connection_(
Stream::from(stream, stream_addr),
addr,
secure,
controlled_config,
control_permissions,
)
.await?;
}
@ -192,7 +192,7 @@ pub async fn create_tcp_connection(
stream: Stream,
addr: SocketAddr,
secure: bool,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let mut stream = stream;
let id = server.write().unwrap().get_new_id();
@ -265,7 +265,7 @@ pub async fn create_tcp_connection(
stream,
id,
Arc::downgrade(&server),
controlled_config,
control_permissions,
)
.await;
Ok(())
@ -276,9 +276,9 @@ pub async fn accept_connection(
socket: Stream,
peer_addr: SocketAddr,
secure: bool,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) {
if let Err(err) = accept_connection_(server, socket, secure, controlled_config).await {
if let Err(err) = accept_connection_(server, socket, secure, control_permissions).await {
log::warn!("Failed to accept connection from {}: {}", peer_addr, err);
}
}
@ -290,7 +290,7 @@ pub async fn create_relay_connection(
peer_addr: SocketAddr,
secure: bool,
ipv4: bool,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) {
if let Err(err) = create_relay_connection_(
server,
@ -299,7 +299,7 @@ pub async fn create_relay_connection(
peer_addr,
secure,
ipv4,
controlled_config,
control_permissions,
)
.await
{
@ -319,7 +319,7 @@ async fn create_relay_connection_(
peer_addr: SocketAddr,
secure: bool,
ipv4: bool,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let mut stream = socket_client::connect_tcp(
socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4),
@ -334,7 +334,7 @@ async fn create_relay_connection_(
..Default::default()
});
stream.send(&msg_out).await?;
create_tcp_connection(server, stream, peer_addr, secure, controlled_config).await?;
create_tcp_connection(server, stream, peer_addr, secure, control_permissions).await?;
Ok(())
}

View file

@ -35,10 +35,10 @@ use hbb_common::{
fs::{self, can_enable_overwrite_detection, JobType},
futures::{SinkExt, StreamExt},
get_time, get_version_number,
message_proto::EasyAccessManagerApproval,
message_proto::{option_message::BoolOption, permission_info::Permission},
password_security::{self as password, ApproveMode},
rand,
rendezvous_proto::EasyAccessManagerApproval,
sha2::{Digest, Sha256},
sleep,
sodiumoxide::crypto::{
@ -82,7 +82,7 @@ lazy_static::lazy_static! {
static ref SESSIONS: Arc::<Mutex<HashMap<SessionKey, Session>>> = Default::default();
static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default();
pub static ref AUTHED_CONNS: Arc::<Mutex<Vec<AuthedConn>>> = Default::default();
pub static ref CONTROLLED_CONFIG_ARRAY: Arc::<Mutex<Vec<(i32, ControlledConfig)>>> = Default::default();
pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::<Mutex<Vec<(i32, ControlPermissions)>>> = Default::default();
static ref WAKELOCK_SENDER: Arc::<Mutex<std::sync::mpsc::Sender<(usize, usize)>>> = Arc::new(Mutex::new(start_wakelock_thread()));
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
}
@ -287,7 +287,7 @@ pub struct Connection {
recording: bool,
block_input: bool,
privacy_mode: bool,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
last_test_delay: Option<Instant>,
network_delay: u32,
lock_after_session_end: bool,
@ -404,6 +404,7 @@ const MILLI1: Duration = Duration::from_millis(1);
const SEND_TIMEOUT_VIDEO: u64 = 12_000;
const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10;
const SESSION_TIMEOUT: Duration = Duration::from_secs(30);
const EASY_ACCESS_CHALLENGE_LEN: usize = 32;
impl Connection {
pub async fn start(
@ -411,19 +412,25 @@ impl Connection {
stream: super::Stream,
id: i32,
server: super::ServerPtrWeak,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
) {
// Android does not support control_permissions yet.
// Android is not supported yet, so we always set control_permissions to None.
#[cfg(target_os = "android")]
let controlled_config = controlled_config.map(|mut c| {
c.control_permissions = 0;
c
});
let control_permissions = None;
let _raii_id = raii::ConnectionID::new(id);
let _raii_controlled_config_id = raii::ControlledConfigID::new(id, &controlled_config);
let _raii_control_permissions_id =
raii::ControlPermissionsID::new(id, &control_permissions);
let easy_access_challenge = if hbb_common::config::is_allow_easy_access() {
rand::random::<[u8; EASY_ACCESS_CHALLENGE_LEN]>()
.to_vec()
.into()
} else {
Default::default()
};
let hash = Hash {
salt: Config::get_salt(),
challenge: Config::get_auto_password(6),
easy_access_challenge,
..Default::default()
};
let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::<ipc::Data>();
@ -471,16 +478,16 @@ impl Connection {
port_forward_address: "".to_owned(),
tx_to_cm,
authorized: false,
keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &controlled_config),
clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &controlled_config),
audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &controlled_config),
keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &control_permissions),
clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &control_permissions),
audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &control_permissions),
// to-do: make sure is the option correct here
file: Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &controlled_config),
restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &controlled_config),
recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &controlled_config),
block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &controlled_config),
privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &controlled_config),
controlled_config,
file: Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &control_permissions),
restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions),
recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions),
block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions),
privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions),
control_permissions,
last_test_delay: None,
network_delay: 0,
lock_after_session_end: false,
@ -1004,7 +1011,7 @@ impl Connection {
match data {
#[cfg(all(target_os = "windows", feature = "flutter"))]
ipc::Data::PrinterData(data) => {
if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.controlled_config) {
if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.control_permissions) {
conn.send_printer_request(data).await;
} else {
conn.send_remote_printing_disallowed().await;
@ -1547,9 +1554,6 @@ impl Connection {
return false;
}
self.authorized = true;
if let Some(c) = self.controlled_config.as_mut() {
c.easy_access_grant = Default::default();
}
let (conn_type, auth_conn_type) = if self.file_transfer.is_some() {
(1, AuthConnType::FileTransfer)
} else if self.port_forward_socket.is_some() {
@ -2260,10 +2264,10 @@ impl Connection {
fn permission(
enable_prefix_option: &str,
controlled_config: &Option<ControlledConfig>,
control_permissions: &Option<ControlPermissions>,
) -> bool {
use hbb_common::rendezvous_proto::controlled_config::Permission;
if let Some(controlled_config) = controlled_config {
use hbb_common::rendezvous_proto::control_permissions::Permission;
if let Some(control_permissions) = control_permissions {
let permission = match enable_prefix_option {
keys::OPTION_ENABLE_KEYBOARD => Some(Permission::keyboard),
keys::OPTION_ENABLE_REMOTE_PRINTER => Some(Permission::remote_printer),
@ -2281,7 +2285,7 @@ impl Connection {
};
if let Some(permission) = permission {
if let Some(enabled) =
crate::get_control_permission(controlled_config.control_permissions, permission)
crate::get_control_permission(control_permissions.permissions, permission)
{
return enabled;
}
@ -2324,8 +2328,7 @@ impl Connection {
device_nonce: &[u8],
approved: bool,
) -> Vec<u8> {
const EASY_ACCESS_CONSUME_DECISION_DOMAIN: &[u8] =
b"easy-access-consume-decision/v1";
const EASY_ACCESS_CONSUME_DECISION_DOMAIN: &[u8] = b"easy-access-consume-decision/v1";
let mut bytes = Vec::new();
bytes.extend_from_slice(EASY_ACCESS_CONSUME_DECISION_DOMAIN);
@ -2356,6 +2359,7 @@ impl Connection {
device_nonce: String,
approved: bool,
signature: String,
grant: String,
}
async fn consume_easy_access_grant(
@ -2363,26 +2367,26 @@ impl Connection {
target_uuid: &[u8],
target_sk: &[u8],
server_pk: &sign::PublicKey,
) -> bool {
) -> Option<EasyAccessGrant> {
let api_server = crate::get_api_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
);
if api_server.is_empty() {
log::warn!("Easy access consume skipped: api server missing");
return false;
if api_server.is_empty() || crate::is_public(&api_server) {
log::warn!("Easy access consume skipped: api server missing or public");
return None;
}
let Some(device_sign_sk) = sign::SecretKey::from_slice(target_sk) else {
log::warn!("Easy access consume failed: target private key invalid");
return false;
return None;
};
let Ok(device_box_sk) = ed25519::to_curve25519_sk(&device_sign_sk) else {
log::warn!("Easy access consume failed: target box private key invalid");
return false;
return None;
};
let Ok(server_box_pk) = ed25519::to_curve25519_pk(server_pk) else {
log::warn!("Easy access consume failed: server box public key invalid");
return false;
return None;
};
let device_nonce: [u8; 32] = rand::random();
let plaintext = match serde_json::to_vec(&EasyAccessGrantConsumeDeviceAuthPayload {
@ -2393,7 +2397,7 @@ impl Connection {
Ok(bytes) => bytes,
Err(err) => {
log::warn!("Easy access consume payload serialize failed: {}", err);
return false;
return None;
}
};
let nonce = box_::gen_nonce();
@ -2408,45 +2412,45 @@ impl Connection {
Ok(body) => body,
Err(err) => {
log::warn!("Easy access consume request serialize failed: {}", err);
return false;
return None;
}
};
let url = format!("{}/api/devices/consume-easy-access-grant", api_server);
let url = format!("{}/api/easy_access/consume", api_server);
let response = match crate::post_request(url, body, "").await {
Ok(response) => response,
Err(err) => {
log::warn!("Easy access consume request failed: {}", err);
return false;
return None;
}
};
let response: EasyAccessGrantConsumeResponse = match serde_json::from_str(&response) {
Ok(response) => response,
Err(err) => {
log::warn!("Easy access consume response parse failed: {}", err);
return false;
return None;
}
};
let response_grant_id = match crate::decode64(&response.grant_id) {
Ok(grant_id) => grant_id,
Err(err) => {
log::warn!("Easy access consume response grant_id invalid: {}", err);
return false;
return None;
}
};
let response_device_nonce = match crate::decode64(&response.device_nonce) {
Ok(device_nonce) => device_nonce,
Err(err) => {
log::warn!("Easy access consume response device_nonce invalid: {}", err);
return false;
return None;
}
};
if response_grant_id.as_slice() != grant_id {
log::warn!("Easy access consume response grant_id mismatch");
return false;
return None;
}
if response_device_nonce.as_slice() != device_nonce.as_slice() {
log::warn!("Easy access consume response device_nonce mismatch");
return false;
return None;
}
let signature = match crate::decode64(&response.signature)
.ok()
@ -2455,7 +2459,7 @@ impl Connection {
Some(signature) => signature,
None => {
log::warn!("Easy access consume response signature invalid");
return false;
return None;
}
};
if !sign::verify_detached(
@ -2468,39 +2472,43 @@ impl Connection {
server_pk,
) {
log::warn!("Easy access consume response signature verify failed");
return false;
return None;
}
if !response.approved {
log::warn!("Easy access consume denied by server");
return false;
return None;
}
match crate::decode64(&response.grant) {
Ok(grant) if !grant.is_empty() => match EasyAccessGrant::parse_from_bytes(&grant) {
Ok(grant) => Some(grant),
Err(err) => {
log::warn!("Easy access grant parse failed: {}", err);
None
}
},
Ok(_) => None,
Err(err) => {
log::warn!("Easy access grant payload invalid: {}", err);
None
}
}
true
}
if !hbb_common::config::is_allow_easy_access() {
return false;
}
let Some(ticket) = self
.controlled_config
.as_ref()
.and_then(|c| c.easy_access_grant.as_ref().cloned())
else {
if !self.stream.is_secured() {
log::warn!("Easy access requires a secured peer connection");
return false;
}
let grant_id = self.lr.easy_access_grant_id.clone();
if grant_id.is_empty() {
return false;
};
if ticket.version != EASY_ACCESS_GRANT_VERSION {
log::warn!("Easy access grant version invalid: {}", ticket.version);
return false;
}
if ticket.server_approval_signature.is_empty() {
log::warn!("Easy access server approval signature missing");
return false;
}
if ticket.device_bound_proof.is_empty() {
log::warn!("Easy access device-bound proof missing");
return false;
}
let lr_challenge_bytes = self.lr.easy_access_challenge.clone();
if lr_challenge_bytes.is_empty() {
let target_challenge = self.hash.easy_access_challenge.clone();
if target_challenge.is_empty() {
log::warn!("Easy access target challenge missing");
return false;
}
let target_uuid = hbb_common::get_uuid();
@ -2525,6 +2533,24 @@ impl Connection {
return false;
}
};
let Some(ticket) =
consume_easy_access_grant(&grant_id, target_uuid.as_slice(), &target_sk, &server_pk)
.await
else {
return false;
};
if ticket.version != EASY_ACCESS_GRANT_VERSION {
log::warn!("Easy access grant version invalid: {}", ticket.version);
return false;
}
if ticket.server_approval_signature.is_empty() {
log::warn!("Easy access server approval signature missing");
return false;
}
if ticket.device_bound_proof.is_empty() {
log::warn!("Easy access device-bound proof missing");
return false;
}
let manager_approval_bytes = match open_easy_access_device_bound_proof(
&ticket.device_bound_proof,
&ticket.server_approval_signature,
@ -2565,15 +2591,19 @@ impl Connection {
log::warn!("Easy access target binding missing");
return false;
};
if manager_approval.manager_signing_pk.is_empty() {
log::warn!("Easy access manager signing public key missing");
if manager_approval.manager_pk.is_empty() {
log::warn!("Easy access manager public key missing");
return false;
}
if manager_approval.manager_approval_signature.is_empty() {
log::warn!("Easy access manager approval signature missing");
return false;
}
if target_binding.challenge.as_ref() != lr_challenge_bytes.as_ref() {
if manager_approval.manager_id.is_empty() {
log::warn!("Easy access manager id missing");
return false;
}
if target_binding.challenge.as_ref() != target_challenge.as_ref() {
log::warn!("Easy access challenge mismatch");
return false;
}
@ -2589,14 +2619,13 @@ impl Connection {
log::warn!("Easy access grant id missing");
return false;
}
let manager_signing_pk =
match sign::PublicKey::from_slice(&manager_approval.manager_signing_pk) {
Some(pk) => pk,
None => {
log::warn!("Easy access manager signing public key invalid");
return false;
}
};
let manager_pk = match sign::PublicKey::from_slice(&manager_approval.manager_pk) {
Some(pk) => pk,
None => {
log::warn!("Easy access manager public key invalid");
return false;
}
};
let manager_approval_signature =
match sign::Signature::from_bytes(&manager_approval.manager_approval_signature) {
Ok(sig) => sig,
@ -2615,19 +2644,13 @@ impl Connection {
if !sign::verify_detached(
&manager_approval_signature,
&target_binding_bytes,
&manager_signing_pk,
&manager_pk,
) {
log::warn!("Easy access manager approval signature verify failed");
return false;
}
if !consume_easy_access_grant(
target_binding.grant_id.as_ref(),
target_uuid.as_slice(),
&target_sk,
&server_pk,
)
.await
{
if target_binding.grant_id.as_ref() != grant_id.as_ref() {
log::warn!("Easy access grant id mismatch");
return false;
}
log::info!("Easy access grant verified");
@ -2729,8 +2752,10 @@ impl Connection {
}
match lr.union {
Some(login_request::Union::FileTransfer(ft)) => {
if !Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &self.controlled_config)
{
if !Self::permission(
keys::OPTION_ENABLE_FILE_TRANSFER,
&self.control_permissions,
) {
self.send_login_error("No permission of file transfer")
.await;
sleep(1.).await;
@ -2739,7 +2764,7 @@ impl Connection {
self.file_transfer = Some((ft.dir, ft.show_hidden));
}
Some(login_request::Union::ViewCamera(_vc)) => {
if !Self::permission(keys::OPTION_ENABLE_CAMERA, &self.controlled_config) {
if !Self::permission(keys::OPTION_ENABLE_CAMERA, &self.control_permissions) {
self.send_login_error("No permission of viewing camera")
.await;
sleep(1.).await;
@ -2748,7 +2773,7 @@ impl Connection {
self.view_camera = true;
}
Some(login_request::Union::Terminal(terminal)) => {
if !Self::permission(keys::OPTION_ENABLE_TERMINAL, &self.controlled_config) {
if !Self::permission(keys::OPTION_ENABLE_TERMINAL, &self.control_permissions) {
self.send_login_error("No permission of terminal").await;
sleep(1.).await;
return false;
@ -2769,7 +2794,7 @@ impl Connection {
self.terminal_service_id = terminal.service_id;
}
Some(login_request::Union::PortForward(mut pf)) => {
if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.controlled_config) {
if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) {
self.send_login_error("No permission of IP tunneling").await;
sleep(1.).await;
return false;
@ -2873,22 +2898,13 @@ impl Connection {
&& is_logon();
if self.verify_easy_access().await {
// Consume the token so it cannot be reused
if let Some(c) = self.controlled_config.as_mut() {
c.easy_access_signature = Default::default();
c.manager_id = Default::default();
}
// Easy access: token verified, skip password validation and click accept
if err_msg.is_empty() {
#[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await;
if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized);
} else {
self.send_login_error(err_msg).await;
#[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await;
if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized);
} else if (password::approve_mode() == ApproveMode::Click
&& !allow_logon_screen_password)
|| password::approve_mode() == ApproveMode::Both && !password::has_valid_password()
@ -6197,17 +6213,17 @@ impl Retina {
}
}
/// Get control permission state from CONNECTION_CONFIG_ARRAY.
/// Get control permission state from CONTROL_PERMISSIONS_ARRAY.
/// Returns: Some(false) if any disable, Some(true) if any enable (and no disable), None if not set.
pub fn get_control_permission_state(
permission: hbb_common::rendezvous_proto::controlled_config::Permission,
permission: hbb_common::rendezvous_proto::control_permissions::Permission,
disable_if_has_disabled: bool,
) -> Option<bool> {
let controlled_configs = CONTROLLED_CONFIG_ARRAY.lock().unwrap();
let control_permissions = CONTROL_PERMISSIONS_ARRAY.lock().unwrap();
let mut has_enable = false;
let mut has_disable = false;
for (_, cc) in controlled_configs.iter() {
match crate::get_control_permission(cc.control_permissions, permission) {
for (_, cp) in control_permissions.iter() {
match crate::get_control_permission(cp.permissions, permission) {
Some(false) => has_disable = true,
Some(true) => has_enable = true,
None => {}
@ -6447,30 +6463,30 @@ mod raii {
}
}
pub struct ControlledConfigID {
pub struct ControlPermissionsID {
id: i32,
controlled_config: Option<ControlledConfig>,
control_permissions: Option<ControlPermissions>,
}
impl Drop for ControlledConfigID {
impl Drop for ControlPermissionsID {
fn drop(&mut self) {
if self.controlled_config.is_some() {
let mut lock = CONTROLLED_CONFIG_ARRAY.lock().unwrap();
if self.control_permissions.is_some() {
let mut lock = CONTROL_PERMISSIONS_ARRAY.lock().unwrap();
lock.retain(|(conn_id, _)| *conn_id != self.id);
}
}
}
impl ControlledConfigID {
pub fn new(id: i32, controlled_config: &Option<ControlledConfig>) -> Self {
if let Some(s) = controlled_config {
CONTROLLED_CONFIG_ARRAY
impl ControlPermissionsID {
pub fn new(id: i32, control_permissions: &Option<ControlPermissions>) -> Self {
if let Some(s) = control_permissions {
CONTROL_PERMISSIONS_ARRAY
.lock()
.unwrap()
.push((id, s.clone()));
}
Self {
id,
controlled_config: controlled_config.clone(),
control_permissions: control_permissions.clone(),
}
}
}