1
+ use base64:: { prelude:: BASE64_STANDARD_NO_PAD , Engine } ;
1
2
use clap:: { Parser , Subcommand } ;
2
3
use colored:: Colorize ;
4
+ use copypasta:: { ClipboardContext , ClipboardProvider } ;
3
5
use dialoguer:: theme:: ColorfulTheme ;
4
6
use indicatif:: { HumanBytes , MultiProgress , ProgressBar , ProgressStyle } ;
5
- use local_ip_address :: local_ip ;
7
+ use iroh :: { Endpoint , RelayMode , SecretKey } ;
6
8
use qs_core:: {
7
9
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 ,
11
13
} ;
12
14
use std:: {
13
15
cell:: RefCell ,
14
16
io:: { self , Write } ,
15
- net:: { IpAddr , Ipv4Addr , SocketAddr , ToSocketAddrs , UdpSocket } ,
16
17
path:: PathBuf ,
17
18
rc:: Rc ,
19
+ str:: FromStr ,
20
+ time:: Duration ,
18
21
} ;
19
22
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 ;
22
24
23
25
#[ derive( Parser , Debug ) ]
24
26
#[ clap( version = QS_VERSION , author = env!( "CARGO_PKG_AUTHORS" ) ) ]
25
27
struct Args {
26
28
/// Log level
27
- #[ clap( long, short, default_value = "info " ) ]
29
+ #[ clap( long, short, default_value = "error " ) ]
28
30
log_level : tracing:: Level ,
29
- /// Direct mode (no rendezvous server)
30
- #[ clap( long, short, default_value = "false" , conflicts_with = "server_addr" ) ]
31
- direct : bool ,
32
31
/// Send or receive files
33
32
#[ clap( subcommand) ]
34
33
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 ,
38
34
}
39
35
40
36
#[ derive( Subcommand , Debug ) ]
@@ -75,16 +71,19 @@ enum AppError {
75
71
#[ tokio:: main]
76
72
async fn main ( ) -> color_eyre:: Result < ( ) > {
77
73
let args: Args = Args :: parse ( ) ;
74
+
78
75
color_eyre:: install ( ) ?;
79
76
tracing_subscriber:: fmt ( )
80
- . with_max_level ( args. log_level )
77
+ . with_max_level ( Level :: from_str ( & args. log_level . to_string ( ) ) . unwrap ( ) )
81
78
. init ( ) ;
82
79
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 ) ;
88
87
89
88
// Check if the files even exist
90
89
if let Mode :: Send { files, .. } = & args. mode {
@@ -95,100 +94,53 @@ async fn main() -> color_eyre::Result<()> {
95
94
}
96
95
}
97
96
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 ) ;
176
98
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 ( ) ) ) ) ?;
178
106
179
107
let progress_bars: Rc < RefCell < Option < CliProgressBars > > > = Rc :: new ( RefCell :: new ( None ) ) ;
180
108
let rc_clone = Rc :: clone ( & progress_bars) ;
181
109
182
110
match args. mode {
183
111
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) ) ;
187
139
188
140
sender
189
141
. send_files (
190
142
|| {
191
- print ! ( "waiting for the other peer to accept the files..." ) ;
143
+ print ! ( "Waiting for the other peer to accept the files..." ) ;
192
144
io:: stdout ( ) . flush ( ) . unwrap ( ) ;
193
145
} ,
194
146
|_accepted| { } ,
@@ -208,13 +160,37 @@ async fn main() -> color_eyre::Result<()> {
208
160
Mode :: Receive {
209
161
overwrite,
210
162
output,
163
+ code,
211
164
auto_accept,
212
- ..
213
165
} => {
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) ) ;
218
194
219
195
receiver
220
196
. receive_files (
@@ -224,6 +200,7 @@ async fn main() -> color_eyre::Result<()> {
224
200
|files_offered| {
225
201
if auto_accept {
226
202
println ! ( "auto accepting files" ) ;
203
+ tracing:: debug!( "auto accepting files" ) ;
227
204
Some ( output. clone ( ) )
228
205
} else if accept_files ( files_offered) {
229
206
Some ( output. clone ( ) )
@@ -382,3 +359,16 @@ impl CliProgressBars {
382
359
}
383
360
}
384
361
}
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
+ }
0 commit comments