mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 16:31:03 +03:00
Compare commits
8 Commits
82aa28f129
...
9a1c8da143
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a1c8da143 | ||
|
|
978c901f49 | ||
|
|
d453a19601 | ||
|
|
50c4e435de | ||
|
|
d5c6d0f6b7 | ||
|
|
b6ff62c74b | ||
|
|
ba6de7990f | ||
|
|
a59ad333fc |
7
.github/workflows/flutter-build.yml
vendored
7
.github/workflows/flutter-build.yml
vendored
@@ -1537,7 +1537,6 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set Swap Space
|
||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
||||
with:
|
||||
swap-size-gb: 12
|
||||
@@ -2151,6 +2150,12 @@ jobs:
|
||||
echo "Modified vcpkg.json for armv7 build:"
|
||||
grep -A 2 -B 2 '"baseline"' vcpkg.json
|
||||
|
||||
- name: Set Swap Space
|
||||
if: matrix.job.arch == 'armv7'
|
||||
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
||||
with:
|
||||
swap-size-gb: 12
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
df -h
|
||||
|
||||
19
AGENTS.md
19
AGENTS.md
@@ -74,6 +74,25 @@
|
||||
* Accept a little duplication over a restructure. A new function that repeats a few lines of an existing one is a better diff than reshaping the original so both can share it.
|
||||
* Put new logic in self-contained functions in the module it belongs to (platform-specific logic in `src/platform/`, with `use` inside the function body to avoid churning shared import blocks). Call sites in shared files (`src/tray.rs`, `src/core_main.rs`, `src/server/connection.rs`, …) should be thin one-line hooks.
|
||||
|
||||
### Scope check before touching shared code
|
||||
|
||||
* Before changing a shared trait, a shared struct, or the signature of a widely used function, check whether the bug or feature is specific to one path. If it is, keep the change inside that path unless that is impossible, and say in the PR why it was.
|
||||
* If an unrelated caller needs `Default::default()`, `None`, or another placeholder solely to satisfy a signature you changed, the diff is too broad: stop and redesign.
|
||||
* The expected shape of a fix is a new function in the feature's own module, plus at most a new field or a thin hook in the shared code it needs. Feature-specific state belongs beside the feature's existing state, not in a new abstraction every caller has to learn.
|
||||
|
||||
### Mandatory regression-surface check
|
||||
|
||||
Before considering any implementation complete, perform a minimization pass over the final diff.
|
||||
|
||||
* Inspect every modified existing file and every modified existing code path. Each must be strictly necessary for the requested change. Revert changes that are merely cleanup, refactoring, consistency improvements, or fixes for pre-existing issues.
|
||||
* For new features, preserve the existing implementation path when the feature is disabled or unsupported whenever practical. `feature off` should run the old code, not a rewritten equivalent.
|
||||
* Do not route existing behavior through a new abstraction merely to share code with the new feature. Prefer a parallel new function or a small amount of duplication over changing a proven existing path.
|
||||
* Keep new implementation logic in new or feature-specific modules. Changes to shared/core files should normally be thin hooks, capability checks, or protocol plumbing.
|
||||
* Do not fix unrelated pre-existing bugs in the same PR. Put them in a separate change unless they directly block correctness or security of the requested work.
|
||||
* For submodule bumps, inspect the exact commit range and ensure unrelated changes are not being pulled into the parent PR.
|
||||
* Before finalizing, explicitly report the regression surface: list the existing files and existing runtime paths whose behavior changed, and explain why each change is unavoidable.
|
||||
* During review, treat an unnecessarily modified legacy path as a review finding even if tests pass and the rewritten behavior appears equivalent.
|
||||
|
||||
## Reviewing a PR
|
||||
|
||||
* Review only what the diff introduces. Verify ownership with `gh pr diff` before reporting a finding — if the offending lines are untouched context, it is a pre-existing problem, not this PR's.
|
||||
|
||||
@@ -87,7 +87,7 @@ if(VCPKG_HOST_IS_WINDOWS)
|
||||
vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16)
|
||||
set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe")
|
||||
vcpkg_add_to_path("${MSYS_ROOT}/usr/share/automake-1.16")
|
||||
string(APPEND OPTIONS " --pkg-config=${CURRENT_HOST_INSTALLED_DIR}/tools/pkgconf/pkgconf${VCPKG_HOST_EXECUTABLE_SUFFIX}")
|
||||
string(APPEND OPTIONS " --pkg-config=${CURRENT_HOST_INSTALLED_DIR}/tools/pkgconf/pkgconf${VCPKG_HOST_EXECUTABLE_SUFFIX} ")
|
||||
else()
|
||||
find_program(SHELL bash)
|
||||
endif()
|
||||
|
||||
@@ -1753,6 +1753,10 @@ pub struct LoginConfigHandler {
|
||||
pub remember: bool,
|
||||
config: PeerConfig,
|
||||
pub port_forward: (String, i32),
|
||||
/// Held by a port-forward mapping from filling `port_forward` and `hash`
|
||||
/// until its login is built from them; a window's mappings log in
|
||||
/// concurrently.
|
||||
pub(crate) port_forward_login_turn: Arc<hbb_common::tokio::sync::Mutex<()>>,
|
||||
pub version: i64,
|
||||
features: Option<Features>,
|
||||
pub session_id: u64, // used for local <-> server communication
|
||||
@@ -1792,6 +1796,10 @@ impl Deref for LoginConfigHandler {
|
||||
}
|
||||
|
||||
impl LoginConfigHandler {
|
||||
pub(crate) fn set_hash(&mut self, hash: Hash) {
|
||||
self.hash = hash;
|
||||
}
|
||||
|
||||
/// Initialize the login config handler.
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -659,9 +659,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("screenshot-merged-screen-not-supported-tip", "Sammanslagning av skärmdumpar från flera skärmar stöds för närvarande inte. Byt till en enda skärm och försök igen."),
|
||||
("screenshot-action-tip", "Välj hur du vill fortsätta med skärmdumpen."),
|
||||
("Save as", "Spara som"),
|
||||
("Export", ""),
|
||||
("Export Logs", ""),
|
||||
("Import Folder", ""),
|
||||
("Export", "Exportera"),
|
||||
("Export Logs", "Exportera loggar"),
|
||||
("Import Folder", "Importera mapp"),
|
||||
("Copy to clipboard", "Kppiera till urklipp"),
|
||||
("Enable remote printer", "Aktivera fjärrskrivare"),
|
||||
("Downloading {}", "Laddar ner {}"),
|
||||
|
||||
@@ -1496,8 +1496,8 @@ pub fn rename_exe_cmd(src_exe: &str, path: &str) -> ResultType<String> {
|
||||
.ok_or(anyhow!("Can't get file name of {src_exe}"))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let app_name = crate::get_app_name().to_lowercase();
|
||||
if src_exe_filename.to_lowercase() == format!("{app_name}.exe") {
|
||||
let app_name = crate::get_app_name();
|
||||
if src_exe_filename == format!("{app_name}.exe") {
|
||||
Ok("".to_owned())
|
||||
} else {
|
||||
Ok(format!(
|
||||
|
||||
@@ -92,12 +92,11 @@ pub async fn listen(
|
||||
tokio::select! {
|
||||
Ok((forward, addr)) = listener.accept() => {
|
||||
log::info!("new connection from {:?}", addr);
|
||||
lc.write().unwrap().port_forward = (remote_host.clone(), remote_port);
|
||||
let id = id.clone();
|
||||
let password = password.clone();
|
||||
let mut forward = Framed::new(forward, BytesCodec::new());
|
||||
let mut close_port_forward = false;
|
||||
match connect_and_login(&id, &password, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward).await {
|
||||
match connect_and_login(&id, &password, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward, &remote_host, remote_port).await {
|
||||
Ok(Some(stream)) => {
|
||||
let interface = interface.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -143,6 +142,8 @@ async fn connect_and_login(
|
||||
token: &str,
|
||||
is_rdp: bool,
|
||||
close_port_forward: &mut bool,
|
||||
remote_host: &str,
|
||||
remote_port: i32,
|
||||
) -> ResultType<Option<Stream>> {
|
||||
let conn_type = if is_rdp {
|
||||
ConnType::RDP
|
||||
@@ -160,6 +161,8 @@ async fn connect_and_login(
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut received = false;
|
||||
let mut challenge = None;
|
||||
let mut pending_login = None;
|
||||
|
||||
let _keep_it = hc_connection(feedback, rendezvous_server, token).await;
|
||||
|
||||
@@ -177,7 +180,8 @@ async fn connect_and_login(
|
||||
let msg_in = Message::parse_from_bytes(&bytes)?;
|
||||
match msg_in.union {
|
||||
Some(message::Union::Hash(hash)) => {
|
||||
if !interface.handle_hash(password, hash, &mut stream).await {
|
||||
challenge = Some(hash.clone());
|
||||
if !hash_arrived(&interface, password, hash, pending_login.take(), remote_host, remote_port, &mut stream).await {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
@@ -208,9 +212,10 @@ async fn connect_and_login(
|
||||
},
|
||||
d = ui_receiver.recv() => {
|
||||
match d {
|
||||
Some(Data::Login((os_username, os_password, password, remember))) => {
|
||||
interface.handle_login_from_ui(os_username, os_password, password, remember, &mut stream).await;
|
||||
}
|
||||
Some(Data::Login(login)) => match &challenge {
|
||||
Some(hash) => login_from_ui(&interface, hash, login, remote_host, remote_port, &mut stream).await,
|
||||
None => pending_login = Some(login),
|
||||
},
|
||||
Some(Data::Message(msg)) => {
|
||||
allow_err!(stream.send(&msg).await);
|
||||
}
|
||||
@@ -233,6 +238,76 @@ async fn connect_and_login(
|
||||
Ok(Some(stream))
|
||||
}
|
||||
|
||||
|
||||
/// A mapping's login is built from the window's shared handler:
|
||||
/// `create_login_msg` reads `port_forward` and `handle_login_from_ui` reads
|
||||
/// `hash`. Mappings log in concurrently, so each fills them and sends under
|
||||
/// the window's turn lock, or one login carried another mapping's target or
|
||||
/// answered another's challenge.
|
||||
async fn login_with_hash(
|
||||
interface: &impl Interface,
|
||||
password: &str,
|
||||
hash: Hash,
|
||||
remote_host: &str,
|
||||
remote_port: i32,
|
||||
stream: &mut Stream,
|
||||
) -> bool {
|
||||
let lc = interface.get_lch();
|
||||
let turn = lc.read().unwrap().port_forward_login_turn.clone();
|
||||
let _turn = turn.lock().await;
|
||||
lc.write().unwrap().port_forward = (remote_host.to_owned(), remote_port);
|
||||
interface.handle_hash(password, hash, stream).await
|
||||
}
|
||||
|
||||
type UiLogin = (String, String, String, bool);
|
||||
|
||||
/// This connection's `Hash`. The window's password prompt is broadcast to
|
||||
/// every mapping and can reach this one first, so a password typed while
|
||||
/// the `Hash` was on its way is kept and answers it now, rather than being
|
||||
/// dropped in the hope that the mapping which prompted has already stored
|
||||
/// it in the shared handler.
|
||||
async fn hash_arrived(
|
||||
interface: &impl Interface,
|
||||
password: &str,
|
||||
hash: Hash,
|
||||
pending_login: Option<UiLogin>,
|
||||
remote_host: &str,
|
||||
remote_port: i32,
|
||||
stream: &mut Stream,
|
||||
) -> bool {
|
||||
match pending_login {
|
||||
Some(login) => {
|
||||
login_from_ui(interface, &hash, login, remote_host, remote_port, stream).await;
|
||||
true
|
||||
}
|
||||
None => login_with_hash(interface, password, hash, remote_host, remote_port, stream).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's password prompt is broadcast to every mapping; this one
|
||||
/// answers it with its own challenge.
|
||||
async fn login_from_ui(
|
||||
interface: &impl Interface,
|
||||
hash: &Hash,
|
||||
login: UiLogin,
|
||||
remote_host: &str,
|
||||
remote_port: i32,
|
||||
stream: &mut Stream,
|
||||
) {
|
||||
let lc = interface.get_lch();
|
||||
let turn = lc.read().unwrap().port_forward_login_turn.clone();
|
||||
let _turn = turn.lock().await;
|
||||
{
|
||||
let mut lc = lc.write().unwrap();
|
||||
lc.port_forward = (remote_host.to_owned(), remote_port);
|
||||
lc.set_hash(hash.clone());
|
||||
}
|
||||
let (os_username, os_password, password, remember) = login;
|
||||
interface
|
||||
.handle_login_from_ui(os_username, os_password, password, remember, stream)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn run_forward(forward: Framed<TcpStream, BytesCodec>, stream: Stream) -> ResultType<()> {
|
||||
log::info!("new port forwarding connection started");
|
||||
let mut forward = forward;
|
||||
@@ -257,3 +332,172 @@ async fn run_forward(forward: Framed<TcpStream, BytesCodec>, stream: Stream) ->
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod login_tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use hbb_common::{
|
||||
tcp::FramedStream,
|
||||
tokio::time::{sleep, Duration},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// A window's interface over its shared handler. `handle_hash` can pause
|
||||
/// before building the login, where the real one looks passwords up.
|
||||
#[derive(Clone)]
|
||||
struct Ui {
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
pause: Duration,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Interface for Ui {
|
||||
fn send(&self, _data: Data) {}
|
||||
fn msgbox(&self, _msgtype: &str, _title: &str, _text: &str, _link: &str) {}
|
||||
fn handle_login_error(&self, _err: &str) -> bool {
|
||||
false
|
||||
}
|
||||
fn handle_peer_info(&self, _pi: PeerInfo) {}
|
||||
fn set_multiple_windows_session(&self, _sessions: Vec<WindowsSession>) {}
|
||||
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) -> bool {
|
||||
sleep(self.pause).await;
|
||||
crate::client::handle_hash(self.lc.clone(), pass, hash, self, peer).await
|
||||
}
|
||||
async fn handle_login_from_ui(
|
||||
&self,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: String,
|
||||
remember: bool,
|
||||
peer: &mut Stream,
|
||||
) {
|
||||
crate::client::handle_login_from_ui(
|
||||
self.lc.clone(),
|
||||
os_username,
|
||||
os_password,
|
||||
password,
|
||||
remember,
|
||||
peer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn handle_test_delay(&self, _t: TestDelay, _peer: &mut Stream) {}
|
||||
fn get_lch(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
||||
self.lc.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn window() -> Ui {
|
||||
let mut lc = LoginConfigHandler::default();
|
||||
lc.conn_type = ConnType::PORT_FORWARD;
|
||||
Ui {
|
||||
lc: Arc::new(RwLock::new(lc)),
|
||||
pause: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// (our end, the peer's end) of one connection.
|
||||
async fn loopback() -> (Stream, Stream) {
|
||||
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = l.local_addr().unwrap();
|
||||
let client = tokio::net::TcpStream::connect(addr).await.unwrap();
|
||||
let (server, _) = l.accept().await.unwrap();
|
||||
(
|
||||
Stream::Tcp(FramedStream::from(client, addr)),
|
||||
Stream::Tcp(FramedStream::from(server, addr)),
|
||||
)
|
||||
}
|
||||
|
||||
async fn login_at(peer: &mut Stream) -> LoginRequest {
|
||||
let bytes = peer.next().await.unwrap().unwrap();
|
||||
Message::parse_from_bytes(&bytes)
|
||||
.unwrap()
|
||||
.login_request()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn target(lr: &LoginRequest) -> (String, i32) {
|
||||
(lr.port_forward().host.clone(), lr.port_forward().port)
|
||||
}
|
||||
|
||||
fn hash(challenge: &str) -> Hash {
|
||||
Hash {
|
||||
salt: "salt".to_owned(),
|
||||
challenge: challenge.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// What the peer expects for password `pw` under `hash(challenge)`.
|
||||
fn digest(challenge: &str) -> Vec<u8> {
|
||||
let mut h = Sha256::new();
|
||||
h.update("pw");
|
||||
h.update("salt");
|
||||
let salted = h.finalize();
|
||||
let mut h2 = Sha256::new();
|
||||
h2.update(&salted[..]);
|
||||
h2.update(challenge);
|
||||
h2.finalize()[..].to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mappings_logging_in_at_once_each_carry_their_own_target() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
let mut ui = window();
|
||||
ui.pause = Duration::from_millis(50);
|
||||
let (mut a, mut a_peer) = loopback().await;
|
||||
let (mut b, mut b_peer) = loopback().await;
|
||||
tokio::join!(
|
||||
login_with_hash(&ui, "pw", hash("a"), "a", 1, &mut a),
|
||||
login_with_hash(&ui, "pw", hash("b"), "b", 2, &mut b),
|
||||
);
|
||||
assert_eq!(target(&login_at(&mut a_peer).await), ("a".to_owned(), 1));
|
||||
assert_eq!(target(&login_at(&mut b_peer).await), ("b".to_owned(), 2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mapping_answers_the_prompt_with_its_own_challenge() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
let ui = window();
|
||||
let (mut a, mut a_peer) = loopback().await;
|
||||
let (mut b, mut b_peer) = loopback().await;
|
||||
// A's hash arrived last, so it is the one the handler holds.
|
||||
assert!(login_with_hash(&ui, "pw", hash("a"), "a", 1, &mut a).await);
|
||||
login_at(&mut a_peer).await;
|
||||
let typed = (String::new(), String::new(), "pw".to_owned(), false);
|
||||
login_from_ui(&ui, &hash("b"), typed, "b", 2, &mut b).await;
|
||||
let lr = login_at(&mut b_peer).await;
|
||||
assert_eq!(lr.password, digest("b"));
|
||||
assert_eq!(target(&lr), ("b".to_owned(), 2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_password_typed_before_this_connections_hash_answers_it_when_it_comes() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
let ui = window();
|
||||
let (mut b, mut b_peer) = loopback().await;
|
||||
// The prompt's password reached B before its hash, and no other
|
||||
// mapping has stored it in the handler yet.
|
||||
let typed = (String::new(), String::new(), "pw".to_owned(), false);
|
||||
assert!(hash_arrived(&ui, "", hash("b"), Some(typed), "b", 2, &mut b).await);
|
||||
let lr = login_at(&mut b_peer).await;
|
||||
assert_eq!(lr.password, digest("b"));
|
||||
assert_eq!(target(&lr), ("b".to_owned(), 2));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,13 +585,9 @@ impl Connection {
|
||||
crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT));
|
||||
let mut last_recv_time = Instant::now();
|
||||
|
||||
conn.stream.set_send_timeout(
|
||||
if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() || conn.terminal {
|
||||
SEND_TIMEOUT_OTHER
|
||||
} else {
|
||||
SEND_TIMEOUT_VIDEO
|
||||
},
|
||||
);
|
||||
// The connection type is not known until the login request arrives;
|
||||
// `on_message` picks the type-specific timeout then.
|
||||
conn.stream.set_send_timeout(SEND_TIMEOUT_VIDEO);
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned));
|
||||
@@ -2766,6 +2762,17 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
self.stream.set_send_timeout(
|
||||
if self.file_transfer.is_some()
|
||||
|| self.terminal
|
||||
|| matches!(self.lr.union, Some(login_request::Union::PortForward(_)))
|
||||
{
|
||||
SEND_TIMEOUT_OTHER
|
||||
} else {
|
||||
SEND_TIMEOUT_VIDEO
|
||||
},
|
||||
);
|
||||
|
||||
if !crate::common::is_direct_ip_access(&lr.username) && lr.username != Config::get_id()
|
||||
{
|
||||
self.send_login_error(crate::client::LOGIN_MSG_OFFLINE)
|
||||
|
||||
Reference in New Issue
Block a user