1use anyhow::Context;
4use lanflare_core::config::AppConfig;
5use lanflare_core::crypto::cert::TlsIdentity;
6use lanflare_core::discovery::interface::{local_interfaces, InterfaceFilter};
7use lanflare_core::discovery::mdns::{start_mdns_engine, MdnsConfig, MdnsDiscoveryEvent, MdnsNodeInfo};
8use lanflare_core::discovery::multicast::{
9 start_multicast_engine, MulticastConfig, MulticastDiscoveryEvent, MulticastNodeInfo,
10};
11use lanflare_core::discovery::registry::{DiscoveredPeer, PeerRegistry};
12use lanflare_proto::discovery::{DeviceType, ProtocolType};
13use std::path::PathBuf;
14use std::time::Duration;
15use tokio::sync::{mpsc, oneshot};
16
17use crate::tui::render_peer_table;
18
19pub async fn status(config: &AppConfig, identity: &TlsIdentity) -> anyhow::Result<()> {
23 println!("LanFlare Node Status:");
24 println!(" Alias: {}", config.alias);
25 println!(" Port: {}", config.port);
26 println!(" Download Dir: {}", config.download_dir.display());
27 println!(" Auto Accept: {}", config.auto_accept);
28 println!(
29 " PIN Protect: {}",
30 if config.pin.is_some() { "Enabled" } else { "Disabled" }
31 );
32 println!(" Fingerprint: {}", identity.fingerprint);
33 println!();
34
35 println!("Local Network Interfaces:");
36 let filter = InterfaceFilter::default();
37 match local_interfaces(&filter) {
38 Ok(interfaces) => {
39 if interfaces.v4.is_empty() && interfaces.v6.is_empty() {
40 println!(" (No active non-loopback network interfaces found)");
41 } else {
42 for iface in &interfaces.v4 {
43 println!(" - IPv4: {} ({})", iface.address, iface.name);
44 }
45 for iface in &interfaces.v6 {
46 println!(" - IPv6: index {} ({})", iface.index, iface.name);
47 }
48 }
49 }
50 Err(err) => {
51 println!(" Failed to enumerate network interfaces: {err}");
52 }
53 }
54
55 Ok(())
56}
57
58#[derive(Clone, Debug)]
60pub struct ScanOptions {
61 pub duration_secs: u64,
63 pub multicast: bool,
65 pub mdns: bool,
67 pub subnet: bool,
69 pub fallback_subnet: bool,
71}
72
73impl Default for ScanOptions {
74 fn default() -> Self {
75 Self {
76 duration_secs: 3,
77 multicast: true,
78 mdns: true,
79 subnet: false,
80 fallback_subnet: true,
81 }
82 }
83}
84
85pub async fn scan(
90 config: &AppConfig,
91 identity: &TlsIdentity,
92 options: &ScanOptions,
93) -> anyhow::Result<Vec<DiscoveredPeer>> {
94 println!(
95 "Scanning local network for LanFlare / LocalSend peers (timeout: {}s)...",
96 options.duration_secs
97 );
98
99 let registry = PeerRegistry::new();
100
101 let (mc_stop_tx, mc_stop_rx) = oneshot::channel();
103 let (mc_event_tx, mut mc_event_rx) = mpsc::channel::<MulticastDiscoveryEvent>(100);
104
105 let mc_handle = if options.multicast {
106 let mc_config = MulticastConfig {
107 port: config.port,
108 node_info: MulticastNodeInfo {
109 alias: config.alias.clone(),
110 version: "2.0".to_string(),
111 device_model: Some("LanFlare Node".to_string()),
112 device_type: Some(DeviceType::Desktop),
113 fingerprint: identity.fingerprint.clone(),
114 port: config.port,
115 protocol: ProtocolType::Https,
116 download: false,
117 },
118 ..MulticastConfig::default()
119 };
120
121 match start_multicast_engine(mc_config, mc_event_tx, mc_stop_rx).await {
122 Ok(handle) => {
123 let h_clone = handle.clone();
125 tokio::spawn(async move {
126 h_clone.announce_burst().await;
127 });
128 Some(handle)
129 }
130 Err(e) => {
131 tracing::warn!("Failed to start multicast engine: {e}");
132 None
133 }
134 }
135 } else {
136 None
137 };
138
139 let (mdns_stop_tx, mdns_stop_rx) = oneshot::channel();
141 let (mdns_event_tx, mut mdns_event_rx) = mpsc::channel::<MdnsDiscoveryEvent>(100);
142
143 let mdns_handle = if options.mdns {
144 let mdns_config = MdnsConfig {
145 node_info: MdnsNodeInfo {
146 alias: config.alias.clone(),
147 instance_name: String::new(),
148 version: "2.0".to_string(),
149 device_model: Some("LanFlare Node".to_string()),
150 device_type: Some(DeviceType::Desktop),
151 fingerprint: identity.fingerprint.clone(),
152 port: config.port,
153 protocol: ProtocolType::Https,
154 download: false,
155 },
156 enable_responder: true,
157 enable_browser: true,
158 ..MdnsConfig::default()
159 };
160
161 match start_mdns_engine(mdns_config, mdns_event_tx, mdns_stop_rx).await {
162 Ok(handle) => Some(handle),
163 Err(e) => {
164 tracing::warn!("Failed to start mDNS engine: {e}");
165 None
166 }
167 }
168 } else {
169 None
170 };
171
172 let registry_clone = registry.clone();
174 let collector_task = tokio::spawn(async move {
175 loop {
176 tokio::select! {
177 Some(event) = mc_event_rx.recv() => {
178 if let MulticastDiscoveryEvent::PeerDiscovered { remote_ip, scope_id, message } = event {
179 registry_clone.handle_multicast(remote_ip, scope_id, &message).await;
180 }
181 }
182 Some(event) = mdns_event_rx.recv() => {
183 match event {
184 MdnsDiscoveryEvent::PeerDiscovered(peer) => {
185 registry_clone.handle_mdns(&peer).await;
186 }
187 MdnsDiscoveryEvent::PeerRemoved { fullname, .. } => {
188 tracing::debug!("mDNS service removed: {fullname}");
190 }
191 }
192 }
193 else => break,
194 }
195 }
196 });
197
198 let registry_subnet = registry.clone();
201 let identity_subnet = identity.clone();
202 let port_subnet = config.port;
203 let should_run_subnet = options.subnet || options.fallback_subnet;
204 let immediate_subnet = options.subnet;
205
206 let subnet_task = if should_run_subnet {
207 Some(tokio::spawn(async move {
208 if !immediate_subnet {
209 tokio::time::sleep(Duration::from_millis(500)).await;
211 if !registry_subnet.get_all().await.is_empty() {
213 return;
214 }
215 }
216
217 let filter = InterfaceFilter::default();
218 if let Ok(interfaces) = local_interfaces(&filter) {
219 let valid_ifaces: Vec<_> = interfaces
220 .v4
221 .into_iter()
222 .filter(|iface| is_sweepable_interface(&iface.name, &iface.address))
223 .collect();
224
225 if !valid_ifaces.is_empty() {
226 let descs: Vec<String> = valid_ifaces
227 .iter()
228 .map(|i| format!("{}/24 via {}", i.address, i.name))
229 .collect();
230 println!("Probing subnets in parallel: {}...", descs.join(", "));
231
232 let scan_tasks = valid_ifaces.into_iter().map(|iface| {
233 let reg = registry_subnet.clone();
234 let ident = identity_subnet.clone();
235 async move {
236 lanflare_core::discovery::scanner::scan_subnet(
237 iface.address,
238 port_subnet,
239 &ident,
240 ®,
241 )
242 .await
243 }
244 });
245
246 futures_util::future::join_all(scan_tasks).await;
247 }
248 }
249 }))
250 } else {
251 None
252 };
253
254 let start = std::time::Instant::now();
259 let scan_timeout = Duration::from_secs(options.duration_secs);
260
261 loop {
262 tokio::time::sleep(Duration::from_millis(100)).await;
263
264 if !registry.get_all().await.is_empty() {
265 tokio::time::sleep(Duration::from_millis(250)).await;
266 break;
267 }
268
269 if let Some(ref st) = subnet_task {
270 if st.is_finished() && (immediate_subnet || start.elapsed() >= scan_timeout) {
271 break;
272 }
273 } else if start.elapsed() >= scan_timeout {
274 break;
275 }
276
277 if start.elapsed() >= scan_timeout.max(Duration::from_secs(5)) {
278 break;
279 }
280 }
281
282 if let Some(st) = subnet_task {
283 let _ = st.await;
284 }
285
286 let _ = mc_stop_tx.send(());
288 let _ = mdns_stop_tx.send(());
289
290 if let Some(h) = mc_handle {
291 h.stop().await;
292 }
293 if let Some(h) = mdns_handle {
294 h.stop().await;
295 }
296
297 collector_task.abort();
298
299 let peers = registry.get_all().await;
301
302 print!("{}", render_peer_table(&peers));
303
304 Ok(peers)
305}
306
307fn is_sweepable_interface(name: &str, ip: &std::net::Ipv4Addr) -> bool {
312 if ip.is_loopback() {
313 return false;
314 }
315
316 let octets = ip.octets();
318 if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
319 return false;
320 }
321
322 let lower = name.to_ascii_lowercase();
323 if lower.starts_with("tailscale")
324 || lower.starts_with("docker")
325 || lower.starts_with("vboxnet")
326 || lower.starts_with("veth")
327 || lower.starts_with("br-")
328 {
329 return false;
330 }
331
332 true
333}
334
335use std::io::Write;
336use std::net::IpAddr;
337use lanflare_core::client::transfer::{
338 OutgoingFile, TransferOptions, TransferOrchestrator, TransferProgress,
339};
340use lanflare_core::client::{LsClient, LsClientConfig};
341use lanflare_core::server::{start_server, PrepareUploadDecision, ServerTransferEvent};
342use lanflare_proto::discovery::RegisterDtoV2;
343
344use crate::tui::{shorten_fingerprint, TransferProgressTracker};
345
346pub fn prompt_user_input(prompt: &str) -> anyhow::Result<String> {
348 print!("{prompt}");
349 std::io::stdout().flush().context("Failed to flush stdout")?;
350 let mut input = String::new();
351 std::io::stdin()
352 .read_line(&mut input)
353 .context("Failed to read user input from stdin")?;
354 Ok(input.trim().to_string())
355}
356
357pub fn match_target_peer<'a>(
359 query: &str,
360 peers: &'a [DiscoveredPeer],
361) -> Option<&'a DiscoveredPeer> {
362 let q = query.trim();
363 if q.is_empty() {
364 return None;
365 }
366
367 let q_upper = q.to_ascii_uppercase();
369 if let Some(p) = peers.iter().find(|p| p.fingerprint == q_upper || p.fingerprint.starts_with(&q_upper)) {
370 return Some(p);
371 }
372
373 if let Ok(ip) = q.parse::<IpAddr>()
375 && let Some(p) = peers.iter().find(|p| p.endpoints.iter().any(|ep| ep.ip == ip))
376 {
377 return Some(p);
378 }
379
380 if let Some(p) = peers.iter().find(|p| p.alias.eq_ignore_ascii_case(q)) {
382 return Some(p);
383 }
384
385 peers.iter().find(|p| p.alias.to_ascii_lowercase().contains(&q.to_ascii_lowercase()))
387}
388
389pub async fn discover_or_select_target(
391 config: &AppConfig,
392 identity: &TlsIdentity,
393 query: Option<&str>,
394) -> anyhow::Result<DiscoveredPeer> {
395 if let Some(q) = query {
396 let trimmed = q.trim();
398 if let Ok(ip) = trimmed.parse::<IpAddr>() {
399 println!("Probing direct IP address {ip}...");
400 let probe_config = LsClientConfig {
401 timeout: Duration::from_secs(3),
402 connect_timeout: Duration::from_secs(2),
403 tls_identity: Some(identity.clone()),
404 target_fingerprint: None,
405 };
406 if let Ok(probe_client) = LsClient::new(probe_config) {
407 for proto in [ProtocolType::Https, ProtocolType::Http] {
408 let proto_str = match proto {
409 ProtocolType::Https => "https",
410 ProtocolType::Http => "http",
411 };
412 let base_url = match ip {
413 IpAddr::V4(v4) => format!("{proto_str}://{v4}:{}", config.port),
414 IpAddr::V6(v6) => format!("{proto_str}://[{v6}]:{}", config.port),
415 };
416 if let Ok(info) = probe_client.get_info(&base_url).await {
417 let peer_port = info.port.unwrap_or(config.port);
418 let peer_proto = info.protocol.unwrap_or(proto);
419 let ep = lanflare_core::discovery::registry::PeerEndpoint::new(
420 ip,
421 None,
422 lanflare_core::discovery::DiscoverySource::ManualScan,
423 0,
424 );
425 return Ok(DiscoveredPeer {
426 fingerprint: info.fingerprint,
427 alias: info.alias,
428 version: info.version,
429 device_model: info.device_model,
430 device_type: info.device_type,
431 port: peer_port,
432 protocol: peer_proto,
433 download: info.download,
434 endpoints: vec![ep],
435 discovery_sources: std::collections::HashSet::new(),
436 first_seen_ms: 0,
437 last_seen_ms: 0,
438 metadata: std::collections::HashMap::new(),
439 });
440 }
441 }
442 }
443
444 let ep = lanflare_core::discovery::registry::PeerEndpoint::new(
446 ip,
447 None,
448 lanflare_core::discovery::DiscoverySource::ManualScan,
449 0,
450 );
451 return Ok(DiscoveredPeer {
452 fingerprint: String::new(),
453 alias: format!("Direct Peer ({ip})"),
454 version: "2.0".to_string(),
455 device_model: None,
456 device_type: None,
457 port: config.port,
458 protocol: ProtocolType::Https,
459 download: false,
460 endpoints: vec![ep],
461 discovery_sources: std::collections::HashSet::new(),
462 first_seen_ms: 0,
463 last_seen_ms: 0,
464 metadata: std::collections::HashMap::new(),
465 });
466 }
467 }
468
469 println!("Scanning network to find target peer...");
471 let scan_opts = ScanOptions {
472 duration_secs: 2,
473 multicast: true,
474 mdns: true,
475 subnet: false,
476 fallback_subnet: true,
477 };
478 let peers = scan(config, identity, &scan_opts).await?;
479
480 if peers.is_empty() {
481 if let Some(q) = query {
482 anyhow::bail!("No peers found matching query '{q}'. Use explicit IP if known.");
484 } else {
485 anyhow::bail!("No peers discovered on local network.");
486 }
487 }
488
489 if let Some(q) = query {
490 if let Some(matched) = match_target_peer(q, &peers) {
491 return Ok(matched.clone());
492 }
493 anyhow::bail!("No discovered peer matched query '{q}'.");
494 }
495
496 if peers.len() == 1 {
498 let p = &peers[0];
499 println!("Single peer found: {} ({})", p.alias, p.primary_ip().map(|ip| ip.to_string()).unwrap_or_default());
500 return Ok(p.clone());
501 }
502
503 println!("\nSelect a peer to send files to:");
504 for (idx, p) in peers.iter().enumerate() {
505 let ip_str = p.primary_ip().map(|ip| ip.to_string()).unwrap_or_else(|| "-".to_string());
506 let fp_str = shorten_fingerprint(&p.fingerprint);
507 println!(" [{}] {} | IP: {} | Port: {} | FP: {}", idx + 1, p.alias, ip_str, p.port, fp_str);
508 }
509
510 loop {
511 let choice_str = prompt_user_input(&format!("Enter peer number (1-{}): ", peers.len()))?;
512 if let Ok(choice) = choice_str.parse::<usize>()
513 && (1..=peers.len()).contains(&choice)
514 {
515 return Ok(peers[choice - 1].clone());
516 }
517 println!("Invalid choice. Please enter a number between 1 and {}.", peers.len());
518 }
519}
520
521#[derive(Clone, Debug)]
523pub struct SendOptions {
524 pub files: Vec<PathBuf>,
525 pub to: Option<String>,
526 pub pin: Option<String>,
527}
528
529pub async fn send(
535 config: &AppConfig,
536 identity: &TlsIdentity,
537 options: &SendOptions,
538) -> anyhow::Result<()> {
539 if options.files.is_empty() {
540 anyhow::bail!("No files specified to send");
541 }
542
543 let mut outgoing_files = Vec::with_capacity(options.files.len());
545 let mut total_bytes = 0u64;
546
547 for f in &options.files {
548 if !f.exists() {
549 anyhow::bail!("File not found: {}", f.display());
550 }
551 println!("Preparing file: {}...", f.display());
552 let outgoing = OutgoingFile::from_path(f)
553 .await
554 .with_context(|| format!("Failed to read file metadata for: {}", f.display()))?
555 .with_computed_sha256()
556 .await
557 .with_context(|| format!("Failed to compute SHA-256 for: {}", f.display()))?;
558
559 total_bytes += outgoing.size;
560 outgoing_files.push(outgoing);
561 }
562
563 println!(
564 "Prepared {} file(s) ({:.2} MB total).",
565 outgoing_files.len(),
566 total_bytes as f64 / (1024.0 * 1024.0)
567 );
568
569 let target = discover_or_select_target(config, identity, options.to.as_deref()).await?;
571 let base_url = target
572 .primary_base_url()
573 .context("Target peer does not have a valid IP endpoint")?;
574
575 println!("Target resolved: {} at {}", target.alias, base_url);
576
577 let target_fp = if target.fingerprint.is_empty() {
579 None
580 } else {
581 Some(target.fingerprint.clone())
582 };
583
584 let client_config = LsClientConfig {
585 timeout: Duration::from_secs(60),
586 connect_timeout: Duration::from_secs(5),
587 tls_identity: Some(identity.clone()),
588 target_fingerprint: target_fp,
589 };
590 let client = LsClient::new(client_config)
591 .context("Failed to initialize LocalSend client with TLS identity")?;
592
593 let sender_info = RegisterDtoV2 {
594 alias: config.alias.clone(),
595 version: "2.0".to_string(),
596 device_model: Some("LanFlare CLI".to_string()),
597 device_type: Some(DeviceType::Desktop),
598 fingerprint: identity.fingerprint.clone(),
599 port: config.port,
600 protocol: ProtocolType::Https,
601 download: false,
602 };
603
604 let orchestrator = TransferOrchestrator::new(client, sender_info);
605
606 let (progress_tx, mut progress_rx) = mpsc::channel::<TransferProgress>(100);
608 let mut tracker = TransferProgressTracker::new(true);
609
610 let transfer_opts = TransferOptions {
611 max_concurrency: 4,
612 pin: options.pin.clone().or_else(|| config.pin.clone()),
613 cancel_token: None,
614 progress_interval: Duration::from_millis(50),
615 ..Default::default()
616 };
617
618 let orchestrator_task = tokio::spawn(async move {
620 orchestrator
621 .transfer(&base_url, outgoing_files, transfer_opts, Some(progress_tx))
622 .await
623 });
624
625 while let Some(ev) = progress_rx.recv().await {
627 tracker.handle_event(&ev);
628 }
629
630 match orchestrator_task.await {
631 Ok(Ok(())) => {
632 println!("All files transferred successfully!");
633 Ok(())
634 }
635 Ok(Err(err)) => {
636 anyhow::bail!("Transfer failed: {err}");
637 }
638 Err(join_err) => {
639 anyhow::bail!("Transfer task panicked or aborted: {join_err}");
640 }
641 }
642}
643
644#[derive(Clone, Debug)]
646pub struct ReceiveOptions {
647 pub dir: Option<PathBuf>,
648 pub port: Option<u16>,
649 pub auto_accept: bool,
650 pub pin: Option<String>,
651}
652
653pub async fn receive(
659 config: &AppConfig,
660 identity: &TlsIdentity,
661 options: &ReceiveOptions,
662) -> anyhow::Result<()> {
663 let dest_dir = options
664 .dir
665 .as_ref()
666 .unwrap_or(&config.download_dir);
667
668 tokio::fs::create_dir_all(dest_dir)
669 .await
670 .with_context(|| format!("Failed to create download directory: {}", dest_dir.display()))?;
671
672 let port = options.port.unwrap_or(config.port);
673 let auto_accept = options.auto_accept || config.auto_accept;
674 let pin = options.pin.clone().or_else(|| config.pin.clone());
675
676 let server_config = AppConfig {
677 alias: config.alias.clone(),
678 port,
679 download_dir: dest_dir.clone(),
680 auto_accept,
681 pin: pin.clone(),
682 };
683
684 let registry = PeerRegistry::new();
685 let (event_tx, mut event_rx) = mpsc::channel::<ServerTransferEvent>(100);
686
687 let server_handle = start_server(
688 server_config,
689 Some(identity.clone()),
690 registry.clone(),
691 Some(event_tx),
692 )
693 .await
694 .with_context(|| format!("Failed to start receive server on port {port}"))?;
695
696 let bound_port = server_handle.bound_port();
697 println!("Receiver active and listening on port {bound_port}");
698 println!("Saving files to: {}", dest_dir.display());
699 println!("Auto-accept transfers: {}", if auto_accept { "Enabled" } else { "Disabled (interactive prompt)" });
700 if let Some(ref p) = pin {
701 println!("PIN protection enabled: required PIN is '{p}'");
702 }
703 println!("Certificate Fingerprint: {}", identity.fingerprint);
704 println!("Press Ctrl+C to terminate receiver daemon.\n");
705
706 while let Some(event) = event_rx.recv().await {
708 match event {
709 ServerTransferEvent::DeviceRegistered { ip, info } => {
710 println!("Device registered: {} ({}) at {}", info.alias, info.version, ip);
711 }
712 ServerTransferEvent::PrepareUpload {
713 session_id,
714 sender_ip,
715 sender_info,
716 files,
717 decision_tx,
718 ..
719 } => {
720 println!("\n[Incoming Transfer Request]");
721 println!(" From: {} ({})", sender_info.alias, sender_ip);
722 println!(" Session ID: {session_id}");
723 println!(" Files ({}) :", files.len());
724
725 let mut total_size = 0u64;
726 for (id, file) in &files {
727 total_size += file.size;
728 println!(
729 " - {} ({:.2} MB) [ID: {}]",
730 file.file_name,
731 file.size as f64 / (1024.0 * 1024.0),
732 id
733 );
734 }
735 println!(" Total Size: {:.2} MB", total_size as f64 / (1024.0 * 1024.0));
736
737 if auto_accept {
738 println!("Auto-accepting incoming transfer...");
739 let all_ids = files.keys().cloned().collect();
740 let _ = decision_tx.send(PrepareUploadDecision::Accept(all_ids));
741 } else {
742 let ans = prompt_user_input("Accept this transfer? [Y/n]: ")
743 .unwrap_or_else(|_| "n".to_string());
744 if ans.is_empty() || ans.eq_ignore_ascii_case("y") || ans.eq_ignore_ascii_case("yes") {
745 let all_ids = files.keys().cloned().collect();
746 let _ = decision_tx.send(PrepareUploadDecision::Accept(all_ids));
747 println!("Accepted transfer.");
748 } else {
749 let _ = decision_tx.send(PrepareUploadDecision::Decline);
750 println!("Declined transfer.");
751 }
752 }
753 }
754 ServerTransferEvent::FileStarted { file_name, total_bytes, .. } => {
755 println!(
756 "Receiving file: {} ({:.2} MB)...",
757 file_name,
758 total_bytes as f64 / (1024.0 * 1024.0)
759 );
760 }
761 ServerTransferEvent::FileCompleted { result, .. } => {
762 println!(
763 "File saved successfully: {} (SHA-256: {}) ✓",
764 result.target_path.display(),
765 result.sha256
766 );
767 }
768 ServerTransferEvent::FileFailed { file_id, error, .. } => {
769 eprintln!("File transfer failed [ID: {file_id}]: {error}");
770 }
771 ServerTransferEvent::SessionEnded { session_id, reason } => {
772 println!("Transfer session ended [ID: {session_id}]: {reason:?}\n");
773 }
774 }
775 }
776
777 server_handle.wait().await;
778 Ok(())
779}