mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 08:34:12 +00:00
* add the base crate and repoint the moved modules at it `libs/base` (crate `base`) takes the parts of hbb_common that only this app uses: `fs`, `platform`, `keyboard`, `message.proto`, and 145 of the 177 `config::keys` constants. hbb_common keeps what the server names, and the 32 keys it reads itself are re-exported from `base::config::keys` so call sites still see the full set through one path. Sources move verbatim. The only edits inside them are `crate::` prefixes that now have to say `hbb_common::`; `keyboard.rs` and `platform/windows.rs` are byte-identical. The crate stays on edition 2018, the edition the moved code was written under. `log`, `lazy_static` and `anyhow` become direct dependencies so the bare paths in that code resolve exactly as before, and its winapi features are spelled out rather than left to feature unification. Two call sites outside Rust and Cargo had to follow the move: the Android protobuf source dir, which still pointed at hbb_common/protos for message.proto, and the three AGENTS.md entries that named hbb_common for options, protos and file transfer. `scrap`'s `drm` feature now forwards to `base/wayland_probe`. Left pointing at hbb_common it would still have compiled, silently dropping the Wayland socket-probe fallback, so that forward is verified by a build with and without the feature. `config::keys` carries a test asserting its names stay disjoint from the ones hbb_common kept: the glob re-export and the local constants share a namespace, and Rust prefers the local item silently, so a name added to both sides would otherwise let client and server disagree with no diagnostic. Verified: macOS and Linux, debug and release, `--all-targets`; the 177 key constants diffed name-for-name and value-for-value; the generated protobuf types compared before and after; every `#[cfg]` gate on a moved import checked against its original; and every file that was `rustfmt`-clean before this change still is, compared against master file by file. Windows is checked by inspection only -- it cannot be compiled here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * one `use` per crate, and write the rule down `fs.rs` came out of the move with two ungated `use hbb_common::` statements, because the original single `use crate::{...}` had to give up `message_proto` to the new crate and the rest was left in a second block. Fold it back into one. A scan of the whole tree for the same shape finds nothing else: every other file with more than one top-level `use base::` or `use hbb_common::` is split by a `#[cfg]` that does not cover the whole block, or by `pub use` next to `use`. Those are the cases that cannot merge, so AGENTS.md now states both the rule and the exemption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
856 lines
31 KiB
Rust
856 lines
31 KiB
Rust
use std::sync::{Arc, RwLock};
|
|
|
|
use crate::client::*;
|
|
use crate::port_forward_mux::{Claim, Tunnel, CHANNEL_WINDOW};
|
|
use hbb_common::{
|
|
allow_err, bail,
|
|
config::READ_TIMEOUT,
|
|
futures::{SinkExt, StreamExt},
|
|
log,
|
|
protobuf::Message as _,
|
|
rendezvous_proto::ConnType,
|
|
tcp, timeout,
|
|
tokio::{self, net::TcpStream, sync::mpsc},
|
|
tokio_util::codec::{BytesCodec, Framed},
|
|
ResultType, Stream,
|
|
};
|
|
use base::message_proto::*;
|
|
|
|
fn run_rdp(port: u16, name: &str) {
|
|
std::process::Command::new("cmdkey")
|
|
.arg("/delete:localhost")
|
|
.output()
|
|
.ok();
|
|
let username = std::env::var("rdp_username").unwrap_or_default();
|
|
let password = std::env::var("rdp_password").unwrap_or_default();
|
|
if !username.is_empty() || !password.is_empty() {
|
|
let mut args = vec!["/generic:localhost".to_owned()];
|
|
if !username.is_empty() {
|
|
args.push(format!("/user:{}", username));
|
|
}
|
|
if !password.is_empty() {
|
|
args.push(format!("/pass:{}", password));
|
|
}
|
|
std::process::Command::new("cmdkey")
|
|
.args(&args)
|
|
.output()
|
|
.ok();
|
|
}
|
|
// Keep using /v instead of a generated .rdp file: mstsc then preserves the
|
|
// user's Default.rdp settings and avoids unsigned-file warnings or policies.
|
|
match std::process::Command::new("mstsc")
|
|
.arg(format!("/v:localhost:{}", port))
|
|
.spawn()
|
|
{
|
|
Ok(child) => {
|
|
#[cfg(windows)]
|
|
crate::platform::set_rdp_window_title(child, name.to_owned());
|
|
#[cfg(not(windows))]
|
|
let _ = (child, name);
|
|
}
|
|
Err(err) => log::warn!("Failed to launch mstsc: {}", err),
|
|
}
|
|
}
|
|
|
|
// Show the peer identity with its hostname, using the ID when no alias exists.
|
|
fn rdp_display_name(lc: &Arc<RwLock<LoginConfigHandler>>, id: &str) -> String {
|
|
let lc = lc.read().unwrap();
|
|
let alias = lc
|
|
.options
|
|
.get("alias")
|
|
.map(|s| s.trim())
|
|
.unwrap_or_default();
|
|
let hostname = lc.info.hostname.trim();
|
|
let identity = if !alias.is_empty() { alias } else { id };
|
|
if hostname.is_empty() || hostname == identity {
|
|
identity.to_owned()
|
|
} else {
|
|
format!("{} ({})", identity, hostname)
|
|
}
|
|
}
|
|
|
|
pub async fn listen(
|
|
id: String,
|
|
password: String,
|
|
port: i32,
|
|
interface: impl Interface,
|
|
ui_receiver: mpsc::UnboundedReceiver<Data>,
|
|
key: &str,
|
|
token: &str,
|
|
lc: Arc<RwLock<LoginConfigHandler>>,
|
|
remote_host: String,
|
|
remote_port: i32,
|
|
) -> ResultType<()> {
|
|
let listener = tcp::new_listener(format!("127.0.0.1:{}", port), true).await?;
|
|
let addr = listener.local_addr()?;
|
|
log::info!("listening on port {:?}", addr);
|
|
let is_rdp = port == 0;
|
|
if is_rdp {
|
|
run_rdp(addr.port(), &rdp_display_name(&lc, &id));
|
|
}
|
|
let mut ui_receiver = ui_receiver;
|
|
// One tunnel per mapping; the listener drops it on its way out, and that
|
|
// ends the tunnel.
|
|
let tunnel = Tunnel::new();
|
|
loop {
|
|
tokio::select! {
|
|
Ok((forward, addr)) = listener.accept() => {
|
|
log::info!("new connection from {:?}", addr);
|
|
// A multiplexed window takes the connection on the mapping's
|
|
// tunnel, or probes for one on its first accept. Everything
|
|
// else, the setting off or a peer without the feature, is the
|
|
// raw pipe below, as it always was.
|
|
let claim = if lc.read().unwrap().port_forward_mux { tunnel.claim() } else { Claim::Legacy };
|
|
match claim {
|
|
Claim::Muxed(handle) => {
|
|
if let Err(e) = handle.open(&remote_host, remote_port, forward, Vec::new()) {
|
|
log::debug!("cannot open channel for {:?}: {}", addr, e);
|
|
}
|
|
continue;
|
|
}
|
|
Claim::Claimed => {
|
|
if establish_tunnel(&tunnel, &id, &password, &mut ui_receiver, &interface, forward, addr, key, token, is_rdp, &remote_host, remote_port).await {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
Claim::Legacy => {}
|
|
}
|
|
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, &remote_host, remote_port).await {
|
|
Ok(Some(stream)) => {
|
|
let interface = interface.clone();
|
|
tokio::spawn(async move {
|
|
if let Err(err) = run_forward(forward, stream).await {
|
|
interface.msgbox("error", "Error", &err.to_string(), "");
|
|
}
|
|
log::info!("connection from {:?} closed", addr);
|
|
});
|
|
}
|
|
_ if close_port_forward => {
|
|
break;
|
|
}
|
|
Err(err) => {
|
|
interface.on_establish_connection_error(err.to_string());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
d = ui_receiver.recv() => {
|
|
match d {
|
|
Some(Data::Close) => {
|
|
break;
|
|
}
|
|
Some(Data::NewRDP) => {
|
|
println!("receive run_rdp from ui_receiver");
|
|
run_rdp(addr.port(), &rdp_display_name(&lc, &id));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn connect_and_login(
|
|
id: &str,
|
|
password: &str,
|
|
ui_receiver: &mut mpsc::UnboundedReceiver<Data>,
|
|
interface: impl Interface,
|
|
forward: &mut Framed<TcpStream, BytesCodec>,
|
|
key: &str,
|
|
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
|
|
} else {
|
|
ConnType::PORT_FORWARD
|
|
};
|
|
let ((mut stream, direct, _pk, _kcp, _stream_type), (feedback, rendezvous_server)) =
|
|
Client::start(id, key, token, conn_type, interface.clone()).await?;
|
|
interface.update_direct(Some(direct));
|
|
if !stream.is_secured() && !crate::common::is_direct_ip_access(id) {
|
|
if !confirm_insecure_connection(&interface, ui_receiver).await {
|
|
*close_port_forward = true;
|
|
return Ok(None);
|
|
}
|
|
}
|
|
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;
|
|
|
|
loop {
|
|
tokio::select! {
|
|
res = timeout(READ_TIMEOUT, stream.next()) => match res {
|
|
Err(_) => {
|
|
bail!("Timeout");
|
|
}
|
|
Ok(Some(Ok(bytes))) => {
|
|
if !received {
|
|
received = true;
|
|
interface.update_received(true);
|
|
}
|
|
let msg_in = Message::parse_from_bytes(&bytes)?;
|
|
match msg_in.union {
|
|
Some(message::Union::Hash(hash)) => {
|
|
challenge = Some(hash.clone());
|
|
if !hash_arrived(&interface, password, hash, pending_login.take(), remote_host, remote_port, false, &mut stream).await {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
Some(message::Union::LoginResponse(lr)) => match lr.union {
|
|
Some(login_response::Union::Error(err)) => {
|
|
if !interface.handle_login_error(&err) {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
Some(login_response::Union::PeerInfo(pi)) => {
|
|
interface.handle_peer_info(pi);
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
Some(message::Union::TestDelay(t)) => {
|
|
interface.handle_test_delay(t, &mut stream).await;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(Some(Err(err))) => {
|
|
bail!("Connection closed: {}", err);
|
|
}
|
|
_ => {
|
|
bail!("Reset by the peer");
|
|
}
|
|
},
|
|
d = ui_receiver.recv() => {
|
|
match d {
|
|
Some(Data::Login(login)) => match &challenge {
|
|
Some(hash) => login_from_ui(&interface, hash, login, remote_host, remote_port, false, &mut stream).await,
|
|
None => pending_login = Some(login),
|
|
},
|
|
Some(Data::Message(msg)) => {
|
|
allow_err!(stream.send(&msg).await);
|
|
}
|
|
_ => {}
|
|
}
|
|
},
|
|
res = forward.next() => {
|
|
if let Some(Ok(bytes)) = res {
|
|
buffer.extend(bytes);
|
|
} else {
|
|
return Ok(None);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
stream.set_raw();
|
|
if !buffer.is_empty() {
|
|
allow_err!(stream.send_bytes(buffer.into()).await);
|
|
}
|
|
Ok(Some(stream))
|
|
}
|
|
|
|
|
|
/// A mapping's login is built from the window's shared handler:
|
|
/// `create_login_msg` reads `port_forward` and `port_forward_multiplex`,
|
|
/// `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,
|
|
mux: bool,
|
|
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);
|
|
lc.write().unwrap().port_forward_multiplex = mux;
|
|
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,
|
|
mux: bool,
|
|
stream: &mut Stream,
|
|
) -> bool {
|
|
match pending_login {
|
|
Some(login) => {
|
|
login_from_ui(interface, &hash, login, remote_host, remote_port, mux, stream).await;
|
|
true
|
|
}
|
|
None => login_with_hash(interface, password, hash, remote_host, remote_port, mux, 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,
|
|
mux: bool,
|
|
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.port_forward_multiplex = mux;
|
|
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;
|
|
}
|
|
|
|
/// The first accept of a multiplexed mapping. It logs in asking for the
|
|
/// tunnel, and the peer's answer fixes this listener's mode until it closes:
|
|
/// a peer with the feature gets a tunnel every later accept joins, one
|
|
/// without gets today's raw pipe for this connection and `Legacy` for the
|
|
/// rest. Re-adding the mapping is how a user picks up an upgraded peer;
|
|
/// nothing switches modes underneath live connections. Returns `true` when
|
|
/// the listener should stop.
|
|
async fn establish_tunnel(
|
|
tunnel: &Tunnel,
|
|
id: &str,
|
|
password: &str,
|
|
ui_receiver: &mut mpsc::UnboundedReceiver<Data>,
|
|
interface: &impl Interface,
|
|
forward: TcpStream,
|
|
addr: std::net::SocketAddr,
|
|
key: &str,
|
|
token: &str,
|
|
is_rdp: bool,
|
|
remote_host: &str,
|
|
remote_port: i32,
|
|
) -> bool {
|
|
let mut forward = Framed::new(forward, BytesCodec::new());
|
|
let mut close_port_forward = false;
|
|
match connect_and_login_mux(id, password, ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward, remote_host, remote_port).await {
|
|
Ok(Some(outcome)) if outcome.mux => {
|
|
let handle = tunnel.set_muxed(outcome.stream, interface.clone());
|
|
if !outcome.local_eof {
|
|
let (socket, prebuf) = take_socket(forward, outcome.prebuf);
|
|
if let Err(e) = handle.open(remote_host, remote_port, socket, prebuf) {
|
|
log::debug!("cannot open channel for {:?}: {}", addr, e);
|
|
}
|
|
}
|
|
}
|
|
Ok(Some(outcome)) => {
|
|
tunnel.set_legacy();
|
|
if outcome.local_eof {
|
|
log::debug!("legacy peer and local {:?} already gone", addr);
|
|
} else {
|
|
run_legacy(outcome, forward, addr, interface.clone());
|
|
}
|
|
}
|
|
_ if close_port_forward => {
|
|
tunnel.set_failed();
|
|
return true;
|
|
}
|
|
Err(err) => {
|
|
tunnel.set_failed();
|
|
interface.on_establish_connection_error(err.to_string());
|
|
}
|
|
_ => tunnel.set_failed(),
|
|
}
|
|
false
|
|
}
|
|
|
|
/// `connect_and_login` for a mapping that wants the tunnel: the pre-read
|
|
/// stops at one window rather than growing without bound, and a local EOF
|
|
/// no longer ends the login, since the tunnel may still be wanted. It
|
|
/// reports what the peer answered rather than a raw stream, because the
|
|
/// caller's next step depends on it. The login itself is the raw pipe's,
|
|
/// told to ask for the tunnel.
|
|
async fn connect_and_login_mux(
|
|
id: &str,
|
|
password: &str,
|
|
ui_receiver: &mut mpsc::UnboundedReceiver<Data>,
|
|
interface: impl Interface,
|
|
forward: &mut Framed<TcpStream, BytesCodec>,
|
|
key: &str,
|
|
token: &str,
|
|
is_rdp: bool,
|
|
close_port_forward: &mut bool,
|
|
remote_host: &str,
|
|
remote_port: i32,
|
|
) -> ResultType<Option<LoginOutcome>> {
|
|
let conn_type = if is_rdp {
|
|
ConnType::RDP
|
|
} else {
|
|
ConnType::PORT_FORWARD
|
|
};
|
|
let ((mut stream, direct, _pk, _kcp, _stream_type), (feedback, rendezvous_server)) =
|
|
Client::start(id, key, token, conn_type, interface.clone()).await?;
|
|
interface.update_direct(Some(direct));
|
|
if !stream.is_secured() && !crate::common::is_direct_ip_access(id) {
|
|
if !confirm_insecure_connection(&interface, ui_receiver).await {
|
|
*close_port_forward = true;
|
|
return Ok(None);
|
|
}
|
|
}
|
|
let mut buffer = Vec::new();
|
|
let mut local_eof = false;
|
|
let mux;
|
|
let mut received = false;
|
|
let mut challenge = None;
|
|
let mut pending_login = None;
|
|
|
|
let _keep_it = hc_connection(feedback, rendezvous_server, token).await;
|
|
|
|
loop {
|
|
tokio::select! {
|
|
res = timeout(READ_TIMEOUT, stream.next()) => match res {
|
|
Err(_) => {
|
|
bail!("Timeout");
|
|
}
|
|
Ok(Some(Ok(bytes))) => {
|
|
if !received {
|
|
received = true;
|
|
interface.update_received(true);
|
|
}
|
|
let msg_in = Message::parse_from_bytes(&bytes)?;
|
|
match msg_in.union {
|
|
Some(message::Union::Hash(hash)) => {
|
|
challenge = Some(hash.clone());
|
|
if !hash_arrived(&interface, password, hash, pending_login.take(), remote_host, remote_port, true, &mut stream).await {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
Some(message::Union::LoginResponse(lr)) => match lr.union {
|
|
Some(login_response::Union::Error(err)) => {
|
|
if !interface.handle_login_error(&err) {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
Some(login_response::Union::PeerInfo(pi)) => {
|
|
mux = peer_supports_mux(&pi);
|
|
interface.handle_peer_info(pi);
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
Some(message::Union::TestDelay(t)) => {
|
|
interface.handle_test_delay(t, &mut stream).await;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(Some(Err(err))) => {
|
|
bail!("Connection closed: {}", err);
|
|
}
|
|
_ => {
|
|
bail!("Reset by the peer");
|
|
}
|
|
},
|
|
d = ui_receiver.recv() => {
|
|
match d {
|
|
Some(Data::Login(login)) => match &challenge {
|
|
Some(hash) => login_from_ui(&interface, hash, login, remote_host, remote_port, true, &mut stream).await,
|
|
None => pending_login = Some(login),
|
|
},
|
|
Some(Data::Message(msg)) => {
|
|
allow_err!(stream.send(&msg).await);
|
|
}
|
|
_ => {}
|
|
}
|
|
},
|
|
// Stop pulling once the pre-read buffer is a window deep; the
|
|
// rest waits in the kernel until the channel opens. A local EOF
|
|
// no longer aborts the login: the tunnel may still be wanted.
|
|
res = forward.next(), if !local_eof && buffer.len() < CHANNEL_WINDOW as usize => {
|
|
if let Some(Ok(bytes)) = res {
|
|
buffer.extend(bytes);
|
|
} else {
|
|
local_eof = true;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
Ok(Some(LoginOutcome {
|
|
stream,
|
|
mux,
|
|
prebuf: buffer,
|
|
local_eof,
|
|
}))
|
|
}
|
|
|
|
/// Today's raw pipe, for peers without multiplexing.
|
|
fn run_legacy(
|
|
outcome: LoginOutcome,
|
|
forward: Framed<TcpStream, BytesCodec>,
|
|
addr: std::net::SocketAddr,
|
|
interface: impl Interface,
|
|
) {
|
|
let mut stream = outcome.stream;
|
|
let prebuf = outcome.prebuf;
|
|
tokio::spawn(async move {
|
|
stream.set_raw();
|
|
if !prebuf.is_empty() {
|
|
allow_err!(stream.send_bytes(prebuf.into()).await);
|
|
}
|
|
if let Err(err) = run_forward(forward, stream).await {
|
|
interface.msgbox("error", "Error", &err.to_string(), "");
|
|
}
|
|
log::info!("connection from {:?} closed", addr);
|
|
});
|
|
}
|
|
|
|
struct LoginOutcome {
|
|
stream: Stream,
|
|
mux: bool,
|
|
prebuf: Vec<u8>,
|
|
local_eof: bool,
|
|
}
|
|
|
|
fn peer_supports_mux(pi: &PeerInfo) -> bool {
|
|
pi.features.as_ref().map(|f| f.port_forward_mux).unwrap_or(false)
|
|
}
|
|
|
|
/// `into_inner()` would drop bytes the codec pulled but never yielded.
|
|
fn take_socket(forward: Framed<TcpStream, BytesCodec>, mut prebuf: Vec<u8>) -> (TcpStream, Vec<u8>) {
|
|
let parts = forward.into_parts();
|
|
prebuf.extend_from_slice(&parts.read_buf);
|
|
(parts.io, prebuf)
|
|
}
|
|
|
|
/// The controlling side's `enable-port-forward-mux`: on unless set to `N`.
|
|
pub fn mux_enabled() -> bool {
|
|
use hbb_common::config::{option2bool, LocalConfig};
|
|
use base::config::keys;
|
|
option2bool(
|
|
keys::OPTION_ENABLE_PORT_FORWARD_MUX,
|
|
&LocalConfig::get_option(keys::OPTION_ENABLE_PORT_FORWARD_MUX),
|
|
)
|
|
}
|
|
|
|
async fn run_forward(forward: Framed<TcpStream, BytesCodec>, stream: Stream) -> ResultType<()> {
|
|
log::info!("new port forwarding connection started");
|
|
let mut forward = forward;
|
|
let mut stream = stream;
|
|
loop {
|
|
tokio::select! {
|
|
res = forward.next() => {
|
|
if let Some(Ok(bytes)) = res {
|
|
allow_err!(stream.send_bytes(bytes.into()).await);
|
|
} else {
|
|
break;
|
|
}
|
|
},
|
|
res = stream.next() => {
|
|
if let Some(Ok(bytes)) = res {
|
|
allow_err!(forward.send(bytes).await);
|
|
} else {
|
|
break;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
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, false, &mut a),
|
|
login_with_hash(&ui, "pw", hash("b"), "b", 2, false, &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, false, &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, false, &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, false, &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_raw_pipe_login_on_a_multiplexed_window_does_not_ask_for_the_tunnel() {
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
rt.block_on(async {
|
|
let ui = window();
|
|
// The window probes for the tunnel, but this mapping latched to
|
|
// the raw pipe: its login must read as the raw pipe's, or an
|
|
// upgraded peer answers with a tunnel it then never gets.
|
|
ui.lc.write().unwrap().port_forward_mux = true;
|
|
let (mut a, mut a_peer) = loopback().await;
|
|
assert!(login_with_hash(&ui, "pw", hash("a"), "a", 1, false, &mut a).await);
|
|
assert!(!login_at(&mut a_peer).await.port_forward().multiplex);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_password_typed_at_the_prompt_keeps_a_raw_pipe_login_raw() {
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
rt.block_on(async {
|
|
let ui = window();
|
|
ui.lc.write().unwrap().port_forward_mux = true;
|
|
let (mut b, mut b_peer) = loopback().await;
|
|
let typed = (String::new(), String::new(), "pw".to_owned(), false);
|
|
login_from_ui(&ui, &hash("b"), typed, "b", 2, false, &mut b).await;
|
|
assert!(!login_at(&mut b_peer).await.port_forward().multiplex);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_probing_login_asks_for_the_tunnel() {
|
|
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;
|
|
assert!(login_with_hash(&ui, "pw", hash("a"), "a", 1, true, &mut a).await);
|
|
assert!(login_at(&mut a_peer).await.port_forward().multiplex);
|
|
});
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn peer_supports_mux_reads_the_features_bit() {
|
|
let mut pi = PeerInfo::new();
|
|
assert!(!peer_supports_mux(&pi));
|
|
pi.features = Some(Features { port_forward_mux: false, ..Default::default() }).into();
|
|
assert!(!peer_supports_mux(&pi));
|
|
pi.features = Some(Features { port_forward_mux: true, ..Default::default() }).into();
|
|
assert!(peer_supports_mux(&pi));
|
|
}
|
|
|
|
#[test]
|
|
fn port_forward_mux_defaults_to_on() {
|
|
use hbb_common::config::option2bool;
|
|
use base::config::keys;
|
|
// option2bool's fallback branch is also "on unless N", so the value
|
|
// assertions below would pass for a prefixless key too. The `enable-`
|
|
// prefix is what actually guarantees the default, and renaming the key
|
|
// to an `allow-` one would silently flip it — pin the prefix itself.
|
|
assert!(keys::OPTION_ENABLE_PORT_FORWARD_MUX.starts_with("enable-"));
|
|
assert!(option2bool(keys::OPTION_ENABLE_PORT_FORWARD_MUX, ""));
|
|
assert!(option2bool(keys::OPTION_ENABLE_PORT_FORWARD_MUX, "Y"));
|
|
assert!(!option2bool(keys::OPTION_ENABLE_PORT_FORWARD_MUX, "N"));
|
|
}
|
|
|
|
#[test]
|
|
fn take_socket_hands_back_a_working_socket_and_the_prebuf() {
|
|
use hbb_common::tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
|
|
rt.block_on(async {
|
|
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = l.local_addr().unwrap();
|
|
let mut client = TcpStream::connect(addr).await.unwrap();
|
|
let (server, _) = l.accept().await.unwrap();
|
|
let mut framed = Framed::new(server, BytesCodec::new());
|
|
client.write_all(b"abc").await.unwrap();
|
|
// Read through the codec, as connect_and_login does during login.
|
|
let pulled = framed.next().await.unwrap().unwrap();
|
|
assert_eq!(&pulled[..], b"abc");
|
|
let (mut sock, prebuf) = take_socket(framed, pulled.to_vec());
|
|
assert_eq!(prebuf, b"abc".to_vec());
|
|
// Bytes written after the handoff arrive on the bare socket.
|
|
client.write_all(b"def").await.unwrap();
|
|
let mut buf = [0u8; 3];
|
|
sock.read_exact(&mut buf).await.unwrap();
|
|
assert_eq!(&buf, b"def");
|
|
});
|
|
}
|
|
}
|