Skip to content

Commit 4779043

Browse files
authored
Merge pull request #24 from maxomatic458/iroh
Rewrite with Iroh
2 parents 4a99239 + c574433 commit 4779043

File tree

13 files changed

+2689
-983
lines changed

13 files changed

+2689
-983
lines changed

Cargo.lock

Lines changed: 2494 additions & 268 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,34 @@
22
members = [
33
"qs-core",
44
"qs-cli",
5-
"qs-roundezvous",
65
"qs-gui/src-tauri"
76
]
87

98
resolver = "2"
109

1110
[workspace.dependencies]
1211
# Its not possible to use a path here if the qs-cli package needs to be published to crates.io
13-
qs-core = { git = "https://github.com/maxomatic458/quic-send", version = "0.3.0" }
12+
# qs-core = { git = "https://github.com/maxomatic458/quic-send", version = "0.3.0" }
13+
qs-core = { path = "./qs-core" } # todo remove path
1414

15-
quinn = { version = "0.11.4", features = ["runtime-tokio"] }
16-
rustls = { version = "0.23.12", default-features = false, features = ["ring"] }
17-
rcgen = "0.13.1"
18-
bincode = "=2.0.0-rc.3"
15+
quinn = { version = "0.11.6", features = ["runtime-tokio"] }
16+
rustls = { version = "0.23.23", default-features = false, features = ["ring"] }
17+
rcgen = "0.13.2"
18+
bincode = { version = "=2.0.0-rc.3", features = ["serde"] }
1919
tokio = { version = "1.40.0", features = ["full"] }
20-
tracing = "0.1.40"
21-
tracing-subscriber = "0.3.18"
22-
thiserror = "1.0.63"
23-
pretty_assertions = "1.4.0"
20+
tracing = "0.1.41"
21+
tracing-subscriber = "0.3.19"
22+
thiserror = "2.0.12"
23+
pretty_assertions = "1.4.1"
2424
rand = "0.8.5"
25-
clap = { version = "4.5.16", features = ["derive"] }
26-
semver = "1.0.23"
27-
serde = { version = "1.0.215", features = ["derive"] }
28-
local-ip-address = "0.6.3"
25+
clap = { version = "4.5.31", features = ["derive"] }
26+
semver = "1.0.25"
27+
serde = { version = "1", features = ["derive"] }
28+
iroh = { version = "0.33.0" }
29+
hex = "0.4.3"
30+
copypasta = "0.10.1"
2931

3032
[profile.release]
3133
lto = true
3234
codegen-units = 1
33-
strip = "debuginfo"
35+
opt-level = "s"

qs-cli/Cargo.toml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "qs-cli"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
description = "A quic based peer-to-peer file transfer tool"
55
authors = ["Maxomatic458"]
66
edition = "2021"
@@ -17,15 +17,20 @@ tracing-subscriber = { workspace = true }
1717
quinn = { workspace = true }
1818
clap = { workspace = true }
1919
semver = { workspace = true }
20-
qs-core = "0.3.0"
20+
iroh = { workspace = true }
21+
rand = { workspace = true }
22+
qs-core = { workspace = true } # todo remove later
23+
bincode = { workspace = true, features = ["serde"] }
24+
copypasta = { workspace = true }
2125

2226
async-compression = { version = "0.4.12", features = ["tokio", "gzip"] }
2327
indicatif = "0.17.8"
2428
dialoguer = "0.11.0"
25-
colored = "2.1.0"
29+
colored = "3.0.0"
2630
color-eyre = "0.6.3"
27-
local-ip-address = { workspace = true }
31+
base64 = "0.22.1"
2832

2933
[[bin]]
3034
path = "src/main.rs"
3135
name = "qs"
36+

qs-cli/src/main.rs

Lines changed: 98 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,36 @@
1+
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
12
use clap::{Parser, Subcommand};
23
use colored::Colorize;
4+
use copypasta::{ClipboardContext, ClipboardProvider};
35
use dialoguer::theme::ColorfulTheme;
46
use indicatif::{HumanBytes, MultiProgress, ProgressBar, ProgressStyle};
5-
use local_ip_address::local_ip;
7+
use iroh::{Endpoint, RelayMode, SecretKey};
68
use qs_core::{
79
common::FilesAvailable,
8-
receive::{roundezvous_connect, ReceiveError, Receiver, ReceiverArgs},
9-
send::{roundezvous_announce, SendError, Sender, SenderArgs},
10-
utils, QuicSendError, CODE_LEN, QS_VERSION, ROUNDEZVOUS_PROTO_VERSION, STUN_SERVERS,
10+
receive::{ReceiveError, Receiver, ReceiverArgs},
11+
send::{SendError, Sender, SenderArgs},
12+
QuicSendError, QS_ALPN, QS_VERSION,
1113
};
1214
use std::{
1315
cell::RefCell,
1416
io::{self, Write},
15-
net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket},
1617
path::PathBuf,
1718
rc::Rc,
19+
str::FromStr,
20+
time::Duration,
1821
};
1922
use thiserror::Error;
20-
// const DEFAULT_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 178, 47)), 9090);
21-
const DEFAULT_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(209, 25, 141, 16)), 1172);
23+
use tracing::Level;
2224

2325
#[derive(Parser, Debug)]
2426
#[clap(version = QS_VERSION, author = env!("CARGO_PKG_AUTHORS"))]
2527
struct Args {
2628
/// Log level
27-
#[clap(long, short, default_value = "info")]
29+
#[clap(long, short, default_value = "error")]
2830
log_level: tracing::Level,
29-
/// Direct mode (no rendezvous server)
30-
#[clap(long, short, default_value = "false", conflicts_with = "server_addr")]
31-
direct: bool,
3231
/// Send or receive files
3332
#[clap(subcommand)]
3433
mode: Mode,
35-
/// override the default roundezvous server address, incompatible with direct mode
36-
#[clap(long, short, conflicts_with = "direct", default_value_t = DEFAULT_ADDR)]
37-
server_addr: SocketAddr,
3834
}
3935

4036
#[derive(Subcommand, Debug)]
@@ -75,16 +71,19 @@ enum AppError {
7571
#[tokio::main]
7672
async fn main() -> color_eyre::Result<()> {
7773
let args: Args = Args::parse();
74+
7875
color_eyre::install()?;
7976
tracing_subscriber::fmt()
80-
.with_max_level(args.log_level)
77+
.with_max_level(Level::from_str(&args.log_level.to_string()).unwrap())
8178
.init();
8279

83-
tracing::debug!(
84-
"qs-ver {}, roundezvous-proto-ver {}",
85-
QS_VERSION,
86-
ROUNDEZVOUS_PROTO_VERSION
87-
);
80+
// Make sure colors work correctly in cmd.exe.
81+
#[cfg(windows)]
82+
{
83+
colored::control::set_virtual_terminal(true).unwrap();
84+
}
85+
86+
tracing::debug!("qs {}", QS_VERSION);
8887

8988
// Check if the files even exist
9089
if let Mode::Send { files, .. } = &args.mode {
@@ -95,100 +94,53 @@ async fn main() -> color_eyre::Result<()> {
9594
}
9695
}
9796

98-
let socket = UdpSocket::bind(SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0))?;
99-
100-
let external_addr = utils::external_addr(
101-
&socket,
102-
STUN_SERVERS[0]
103-
.to_socket_addrs()?
104-
.find(|x| x.is_ipv4())
105-
.unwrap(),
106-
Some(
107-
STUN_SERVERS[1]
108-
.to_socket_addrs()?
109-
.find(|x| x.is_ipv4())
110-
.unwrap(),
111-
),
112-
)
113-
.map_err(QuicSendError::Stun)?;
114-
115-
let other;
116-
117-
if args.direct {
118-
let local_ip = local_ip().unwrap();
119-
println!(
120-
"Local address (if the other peer is in the same network): {}",
121-
local_ip.to_string().green()
122-
);
123-
println!("External address: {}", external_addr.to_string().green());
124-
125-
other =
126-
dialoguer::Input::<SocketAddr>::with_theme(&dialoguer::theme::ColorfulTheme::default())
127-
.with_prompt("Enter the remote address")
128-
.interact()
129-
.unwrap();
130-
} else {
131-
let socket_clone = socket.try_clone().unwrap();
132-
match args.mode {
133-
Mode::Send { .. } => {
134-
other = roundezvous_announce(socket_clone, external_addr, args.server_addr, |c| {
135-
let code = String::from_utf8(c.to_vec()).unwrap();
136-
println!("code: {}", code.bright_white());
137-
println!("on the other peer, run the following command:\n");
138-
println!(
139-
"{}",
140-
format!(
141-
"qs {}receive {}\n",
142-
if args.server_addr != DEFAULT_ADDR {
143-
format!("-s {} ", args.server_addr)
144-
} else {
145-
"".to_string()
146-
},
147-
code
148-
)
149-
.yellow()
150-
);
151-
})
152-
.await
153-
.map_err(QuicSendError::Send)?;
154-
}
155-
Mode::Receive { ref code, .. } => {
156-
let code = code.clone().unwrap_or_else(|| {
157-
dialoguer::Input::<String>::with_theme(
158-
&dialoguer::theme::ColorfulTheme::default(),
159-
)
160-
.with_prompt("Enter the code")
161-
.interact()
162-
.unwrap()
163-
});
164-
165-
let code: [u8; CODE_LEN] = match code.as_bytes().try_into() {
166-
Ok(c) => c,
167-
Err(_) => return Err(QuicSendError::Receive(ReceiveError::InvalidCode).into()),
168-
};
169-
170-
other = roundezvous_connect(socket_clone, external_addr, args.server_addr, code)
171-
.await
172-
.map_err(QuicSendError::Receive)?;
173-
}
174-
}
175-
}
97+
let secret_key = SecretKey::generate(rand::rngs::OsRng);
17698

177-
utils::hole_punch(&socket, other)?;
99+
let endpoint = Endpoint::builder()
100+
.secret_key(secret_key)
101+
.alpns(vec![QS_ALPN.to_vec()])
102+
.relay_mode(RelayMode::Default)
103+
.bind()
104+
.await
105+
.map_err(|e| AppError::QuicSendCore(QuicSendError::Bind(e.to_string())))?;
178106

179107
let progress_bars: Rc<RefCell<Option<CliProgressBars>>> = Rc::new(RefCell::new(None));
180108
let rc_clone = Rc::clone(&progress_bars);
181109

182110
match args.mode {
183111
Mode::Send { files } => {
184-
let mut sender = Sender::connect(socket, other, SenderArgs { files })
185-
.await
186-
.map_err(QuicSendError::Send)?;
112+
let node_addr = endpoint.node_addr().await.map_err(|e| {
113+
AppError::QuicSendCore(QuicSendError::Send(SendError::NodeAddr(e.to_string())))
114+
})?;
115+
116+
let serialized =
117+
bincode::serde::encode_to_vec(node_addr, bincode::config::standard()).unwrap();
118+
let ticket: String = BASE64_STANDARD_NO_PAD.encode(&serialized);
119+
120+
println!(
121+
"Ticket (copied to your clipboard):\n\n{}\n",
122+
ticket.bright_white()
123+
);
124+
println!("on the other peer, run the following command:\n");
125+
println!("{}", "qs receive <ticket>".yellow());
126+
127+
if let Ok(mut ctx) = ClipboardContext::new() {
128+
let _ = ctx.set_contents(ticket);
129+
}
130+
131+
let sender_args = SenderArgs { files };
132+
let mut sender = Sender::connect(endpoint, sender_args).await?;
133+
134+
// Give iroh some time to switch the connection to direct
135+
std::thread::sleep(Duration::from_secs(4));
136+
let conn_type = sender.connection_type().await;
137+
tracing::debug!("connected with type: {:?}", conn_type);
138+
println!("Connection type: {}", connection_type_info_msg(conn_type));
187139

188140
sender
189141
.send_files(
190142
|| {
191-
print!("waiting for the other peer to accept the files...");
143+
print!("Waiting for the other peer to accept the files...");
192144
io::stdout().flush().unwrap();
193145
},
194146
|_accepted| {},
@@ -208,13 +160,37 @@ async fn main() -> color_eyre::Result<()> {
208160
Mode::Receive {
209161
overwrite,
210162
output,
163+
code,
211164
auto_accept,
212-
..
213165
} => {
214-
let mut receiver =
215-
Receiver::connect(socket, other, ReceiverArgs { resume: !overwrite })
216-
.await
217-
.map_err(QuicSendError::Receive)?;
166+
let ticket = match code {
167+
Some(code) => code,
168+
None => dialoguer::Input::new()
169+
.with_prompt("Enter the ticket to connect")
170+
.interact()?,
171+
};
172+
173+
let node_addr = BASE64_STANDARD_NO_PAD
174+
.decode(ticket.as_bytes())
175+
.map_err(|_| {
176+
AppError::QuicSendCore(QuicSendError::Receive(ReceiveError::InvalidCode))
177+
})?;
178+
179+
let node_addr: iroh::NodeAddr =
180+
bincode::serde::decode_from_slice(&node_addr, bincode::config::standard())
181+
.map_err(|_| {
182+
AppError::QuicSendCore(QuicSendError::Receive(ReceiveError::InvalidCode))
183+
})?
184+
.0;
185+
186+
let receiver_args = ReceiverArgs { resume: !overwrite };
187+
let mut receiver = Receiver::connect(endpoint, node_addr, receiver_args).await?;
188+
189+
// Give iroh some time to switch the connection to direct
190+
std::thread::sleep(Duration::from_secs(4));
191+
let conn_type = receiver.connection_type().await;
192+
tracing::debug!("connected with type: {:?}", conn_type);
193+
println!("Connection type: {}", connection_type_info_msg(conn_type));
218194

219195
receiver
220196
.receive_files(
@@ -224,6 +200,7 @@ async fn main() -> color_eyre::Result<()> {
224200
|files_offered| {
225201
if auto_accept {
226202
println!("auto accepting files");
203+
tracing::debug!("auto accepting files");
227204
Some(output.clone())
228205
} else if accept_files(files_offered) {
229206
Some(output.clone())
@@ -382,3 +359,16 @@ impl CliProgressBars {
382359
}
383360
}
384361
}
362+
363+
fn connection_type_info_msg(connection_type: Option<iroh::endpoint::ConnectionType>) -> String {
364+
if let Some(conn_type) = connection_type {
365+
return match conn_type {
366+
iroh::endpoint::ConnectionType::Direct(_) => "Direct".green().to_string(),
367+
iroh::endpoint::ConnectionType::Relay(_) => "Relay".red().to_string(),
368+
iroh::endpoint::ConnectionType::Mixed(_, _) => "Mixed".yellow().to_string(),
369+
iroh::endpoint::ConnectionType::None => "None".red().to_string(),
370+
};
371+
};
372+
373+
"???".red().to_string()
374+
}

qs-core/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "qs-core"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
edition = "2021"
55
authors = ["Maxomatic458"]
66
description = "quic send core library"
@@ -21,7 +21,7 @@ clap = { workspace = true }
2121
semver = { workspace = true }
2222
serde = { workspace = true, features = ["derive"] }
2323
async-compression = { version = "0.4.12", features = ["tokio", "gzip"] }
24-
stunclient = "0.4.0"
24+
iroh = { workspace = true }
2525

2626
[dev-dependencies]
2727
pretty_assertions = { workspace = true }

0 commit comments

Comments
 (0)