mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: bind cli to core/discovery
This commit is contained in:
+25
-13
@@ -5,6 +5,7 @@ use super::App;
|
||||
use crate::device_list::{DeviceList, DeviceListOutcome, DeviceRow, Row};
|
||||
use crate::ui::Category;
|
||||
use crossterm::event::KeyEvent;
|
||||
use localsend::discovery::StatefulDevice;
|
||||
|
||||
impl App {
|
||||
pub(super) fn open_device_list(&mut self) {
|
||||
@@ -28,14 +29,15 @@ impl App {
|
||||
let mut empty = true;
|
||||
for (fingerprint, paired) in self.storage.paired.iter() {
|
||||
empty = false;
|
||||
let discovered = self.registry.by_fingerprint(fingerprint);
|
||||
let discovered = self.discovery.device_by_fingerprint(fingerprint);
|
||||
rows.push(Row::Device(DeviceRow {
|
||||
fingerprint: fingerprint.clone(),
|
||||
alias: discovered
|
||||
.map(|device| device.alias.clone())
|
||||
.as_ref()
|
||||
.map(|stored| stored.device.alias.clone())
|
||||
.unwrap_or_else(|| paired.alias.clone()),
|
||||
slot: discovered.and_then(|device| device.slot),
|
||||
host: discovered.map(|device| device.host.clone()),
|
||||
slot: self.slots.get(fingerprint),
|
||||
hosts: discovered.as_ref().map(channel_hosts).unwrap_or_default(),
|
||||
paired: true,
|
||||
}));
|
||||
}
|
||||
@@ -46,16 +48,16 @@ impl App {
|
||||
rows.push(Row::Spacer);
|
||||
rows.push(Row::Header("Discovered"));
|
||||
let mut empty = true;
|
||||
for device in self.registry.devices() {
|
||||
if self.storage.paired.contains(&device.fingerprint) {
|
||||
for stored in self.discovery.devices() {
|
||||
if self.storage.paired.contains(&stored.device.fingerprint) {
|
||||
continue;
|
||||
}
|
||||
empty = false;
|
||||
rows.push(Row::Device(DeviceRow {
|
||||
fingerprint: device.fingerprint.clone(),
|
||||
alias: device.alias.clone(),
|
||||
slot: device.slot,
|
||||
host: Some(device.host.clone()),
|
||||
fingerprint: stored.device.fingerprint.clone(),
|
||||
alias: stored.device.alias.clone(),
|
||||
slot: self.slots.get(&stored.device.fingerprint),
|
||||
hosts: channel_hosts(&stored),
|
||||
paired: false,
|
||||
}));
|
||||
}
|
||||
@@ -81,7 +83,7 @@ impl App {
|
||||
self.close_device_list();
|
||||
if !self.preselected.is_empty() {
|
||||
self.start_send(&fingerprint, self.preselected.clone());
|
||||
} else if let Some(device) = self.registry.by_fingerprint(&fingerprint).cloned() {
|
||||
} else if let Some(device) = self.discovery.device_by_fingerprint(&fingerprint) {
|
||||
self.open_picker(device);
|
||||
}
|
||||
}
|
||||
@@ -102,10 +104,10 @@ impl App {
|
||||
/// device moves up into "Paired"). The log line shows up once the list
|
||||
/// is closed.
|
||||
fn pair(&mut self, fingerprint: &str) {
|
||||
let Some(device) = self.registry.by_fingerprint(fingerprint) else {
|
||||
let Some(stored) = self.discovery.device_by_fingerprint(fingerprint) else {
|
||||
return;
|
||||
};
|
||||
let alias = device.alias.clone();
|
||||
let alias = stored.device.alias;
|
||||
match self
|
||||
.storage
|
||||
.paired
|
||||
@@ -151,3 +153,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The addresses a stored device can be dialed at, best first.
|
||||
fn channel_hosts(stored: &StatefulDevice) -> Vec<String> {
|
||||
stored
|
||||
.get_ranked_channels()
|
||||
.into_iter()
|
||||
.filter_map(|channel| channel.http())
|
||||
.map(|http| http.host.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
+43
-80
@@ -1,91 +1,54 @@
|
||||
//! Device discovery: multicast announcements are answered with an HTTP
|
||||
//! register request, and confirmed devices get a slot in the registry.
|
||||
//! Device discovery: the core discovery answers multicast announcements with
|
||||
//! an HTTP register request and stores confirmed devices; the CLI assigns a
|
||||
//! hotkey slot to every device entering the store.
|
||||
|
||||
use super::{App, AppEvent};
|
||||
use super::App;
|
||||
use crate::slots::slot_label;
|
||||
use crate::ui::Category;
|
||||
use localsend::http::client::v2::LsHttpClientV2;
|
||||
use localsend::http::dto::ProtocolType;
|
||||
use localsend::http::dto_v2::ProtocolTypeV2;
|
||||
use localsend::multicast::MulticastEvent;
|
||||
use std::time::Duration;
|
||||
use localsend::discovery::{DeviceChannel, DiscoveredDevice, DiscoveryEvent, HttpChannel};
|
||||
use localsend::http::dto_v2::RegisterDtoV2;
|
||||
|
||||
impl App {
|
||||
pub(super) fn handle_multicast(&mut self, event: MulticastEvent) {
|
||||
let MulticastEvent::Discovered {
|
||||
ip,
|
||||
scope_id,
|
||||
message,
|
||||
} = event;
|
||||
if message.fingerprint == self.storage.identity.fingerprint {
|
||||
pub(super) fn handle_discovery(&mut self, event: DiscoveryEvent) {
|
||||
let DiscoveryEvent::Discovered { device } = event else {
|
||||
// Re-confirmations update the store silently; multi-homed peers
|
||||
// re-announce with a different address all the time.
|
||||
return;
|
||||
};
|
||||
let slot = self.slots.assign(&device.fingerprint);
|
||||
let host = device
|
||||
.http()
|
||||
.map(|http| http.host.as_str())
|
||||
.unwrap_or("unknown address");
|
||||
self.ui.log(
|
||||
Category::Discovery,
|
||||
&format!("[{}] {} ({host})", slot_label(slot), device.alias),
|
||||
);
|
||||
}
|
||||
|
||||
/// Feeds a device confirmed outside of discovery — it registered with our
|
||||
/// HTTP server, or it sent a transfer request — into the discovery store;
|
||||
/// a device new to the store comes back as a `Discovered` event.
|
||||
pub(super) fn device_confirmed(&self, host: String, info: RegisterDtoV2) {
|
||||
if info.fingerprint == self.storage.identity.fingerprint {
|
||||
return;
|
||||
}
|
||||
let host = match scope_id {
|
||||
Some(scope_id) => format!("{ip}%{scope_id}"),
|
||||
None => ip.to_string(),
|
||||
let device = DiscoveredDevice {
|
||||
alias: info.alias,
|
||||
version: info.version,
|
||||
device_model: info.device_model,
|
||||
device_type: info.device_type,
|
||||
fingerprint: info.fingerprint,
|
||||
channel: DeviceChannel::Http(HttpChannel {
|
||||
host,
|
||||
port: info.port,
|
||||
protocol: info.protocol,
|
||||
}),
|
||||
download: info.download,
|
||||
};
|
||||
|
||||
// Answer the announcement with an HTTP register request; the device
|
||||
// is only shown once that request succeeds.
|
||||
let identity = self.storage.identity.clone();
|
||||
let events_tx = self.events_tx.clone();
|
||||
let discovery = self.discovery.clone();
|
||||
tokio::spawn(async move {
|
||||
let expected_fingerprint = match message.protocol {
|
||||
ProtocolTypeV2::Https => Some(message.fingerprint.clone()),
|
||||
ProtocolTypeV2::Http => None,
|
||||
};
|
||||
let Ok(client) = LsHttpClientV2::try_new(
|
||||
&identity.key_pem,
|
||||
&identity.cert_pem,
|
||||
expected_fingerprint,
|
||||
Some(Duration::from_secs(5)),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let protocol = match message.protocol {
|
||||
ProtocolTypeV2::Http => ProtocolType::Http,
|
||||
ProtocolTypeV2::Https => ProtocolType::Https,
|
||||
};
|
||||
let result = client
|
||||
.register(protocol, &host, message.port, identity.register_dto())
|
||||
.await;
|
||||
if let Ok(response) = result {
|
||||
let _ = events_tx
|
||||
.send(AppEvent::DeviceUp {
|
||||
alias: response.body.alias,
|
||||
host,
|
||||
port: message.port,
|
||||
protocol: message.protocol,
|
||||
fingerprint: message.fingerprint,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
discovery.add_device(device).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn device_up(
|
||||
&mut self,
|
||||
alias: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
protocol: ProtocolTypeV2,
|
||||
fingerprint: String,
|
||||
) {
|
||||
if fingerprint == self.storage.identity.fingerprint {
|
||||
return;
|
||||
}
|
||||
if let Some(device) = self
|
||||
.registry
|
||||
.upsert(alias, host, port, protocol, fingerprint)
|
||||
{
|
||||
self.ui.log(
|
||||
Category::Discovery,
|
||||
&format!(
|
||||
"[{}] {} ({})",
|
||||
device.slot_label(),
|
||||
device.alias,
|
||||
device.host
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-64
@@ -6,17 +6,18 @@ mod status;
|
||||
|
||||
use crate::Args;
|
||||
use crate::device_list::DeviceList;
|
||||
use crate::devices::DeviceRegistry;
|
||||
use crate::picker::Picker;
|
||||
use crate::slots::Slots;
|
||||
use crate::storage;
|
||||
use crate::ui::{Category, Ui};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use localsend::http::dto_v2::ProtocolTypeV2;
|
||||
use localsend::discovery::{
|
||||
DEFAULT_DISCOVERY_TIMEOUT, DeviceIdentity, DiscoveryConfig, DiscoveryEvent, DiscoveryHandle,
|
||||
};
|
||||
use localsend::http::server::v2::ServerEventV2;
|
||||
use localsend::http::server::{ServerConfigV2, ServerHandle, start_with_port};
|
||||
use localsend::multicast::{
|
||||
self, DEFAULT_MULTICAST_GROUP, DEFAULT_MULTICAST_GROUP_V6, DEFAULT_PORT, InterfaceFilter,
|
||||
MulticastConfig, MulticastEvent,
|
||||
DEFAULT_MULTICAST_GROUP, DEFAULT_MULTICAST_GROUP_V6, DEFAULT_PORT, InterfaceFilter,
|
||||
};
|
||||
use receive::{Answer, PendingReceive, ReceiveSession};
|
||||
use sending::SendState;
|
||||
@@ -30,16 +31,6 @@ pub enum AppEvent {
|
||||
/// A key was pressed.
|
||||
Key(KeyEvent),
|
||||
|
||||
/// A device was confirmed reachable (it registered with us, or it
|
||||
/// announced itself and answered our register request).
|
||||
DeviceUp {
|
||||
alias: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
protocol: ProtocolTypeV2,
|
||||
fingerprint: String,
|
||||
},
|
||||
|
||||
/// A file of the active receive session finished (or failed).
|
||||
ReceiveFileResult {
|
||||
session_id: String,
|
||||
@@ -63,7 +54,13 @@ pub enum AppEvent {
|
||||
struct App {
|
||||
ui: Ui,
|
||||
server: Arc<ServerHandle>,
|
||||
registry: DeviceRegistry,
|
||||
|
||||
/// The core discovery: the store of confirmed devices, and the multicast
|
||||
/// side when it could be started.
|
||||
discovery: Arc<DiscoveryHandle>,
|
||||
|
||||
/// The hotkeys (1-9) of the devices in the discovery store.
|
||||
slots: Slots,
|
||||
|
||||
/// Config, identity and paired devices, see [`storage::Repository`].
|
||||
storage: storage::Repository,
|
||||
@@ -106,34 +103,37 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
let server = Arc::new(server);
|
||||
|
||||
// Multicast discovery. Failure is not fatal: transfers to this device
|
||||
// still work for peers that know its address.
|
||||
let (multicast_tx, multicast_rx) = mpsc::channel::<MulticastEvent>(16);
|
||||
let (multicast_stop_tx, multicast_stop_rx) = oneshot::channel::<()>();
|
||||
let multicast = multicast::start(
|
||||
MulticastConfig {
|
||||
group: DEFAULT_MULTICAST_GROUP,
|
||||
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
|
||||
port: DEFAULT_PORT,
|
||||
interface_filter: InterfaceFilter::default(),
|
||||
device: identity.multicast_device(),
|
||||
event_tx: multicast_tx,
|
||||
},
|
||||
multicast_stop_rx,
|
||||
)
|
||||
.await;
|
||||
let multicast = match multicast {
|
||||
Ok(handle) => Some(Arc::new(handle)),
|
||||
Err(err) => {
|
||||
eprintln!("Multicast discovery unavailable: {err:#}");
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut multicast_rx = multicast.is_some().then_some(multicast_rx);
|
||||
if let Some(handle) = &multicast {
|
||||
// Discovery: multicast, plus the register requests answering other
|
||||
// devices' announcements. Multicast failure is not fatal: the store keeps
|
||||
// collecting the devices that contact this device over HTTP.
|
||||
let (discovery_tx, mut discovery_rx) = mpsc::channel::<DiscoveryEvent>(16);
|
||||
let (discovery_stop_tx, discovery_stop_rx) = oneshot::channel::<()>();
|
||||
let discovery = Arc::new(
|
||||
localsend::discovery::start(
|
||||
DiscoveryConfig {
|
||||
group: DEFAULT_MULTICAST_GROUP,
|
||||
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
|
||||
port: DEFAULT_PORT,
|
||||
interface_filter: InterfaceFilter::default(),
|
||||
device: identity.multicast_device(),
|
||||
identity: DeviceIdentity {
|
||||
cert_pem: identity.cert_pem.clone(),
|
||||
private_key_pem: identity.key_pem.clone(),
|
||||
},
|
||||
timeout: DEFAULT_DISCOVERY_TIMEOUT,
|
||||
event_tx: Some(discovery_tx),
|
||||
},
|
||||
discovery_stop_rx,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
if let Some(err) = discovery.multicast_error() {
|
||||
eprintln!("Multicast unavailable: {err:#}");
|
||||
}
|
||||
{
|
||||
// Announce this device; peers answer with an HTTP register request.
|
||||
let handle = handle.clone();
|
||||
tokio::spawn(async move { handle.announce().await });
|
||||
let discovery = discovery.clone();
|
||||
tokio::spawn(async move { discovery.announce().await });
|
||||
}
|
||||
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
@@ -159,7 +159,8 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
|
||||
let mut app = App {
|
||||
ui: Ui::new(),
|
||||
server: server.clone(),
|
||||
registry: DeviceRegistry::new(),
|
||||
discovery: discovery.clone(),
|
||||
slots: Slots::new(),
|
||||
storage,
|
||||
pending: None,
|
||||
receive: None,
|
||||
@@ -187,8 +188,8 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
|
||||
Some(event) = server_rx.recv() => {
|
||||
app.handle_server_event(event);
|
||||
}
|
||||
Some(event) = recv_opt(&mut multicast_rx) => {
|
||||
app.handle_multicast(event);
|
||||
Some(event) = discovery_rx.recv() => {
|
||||
app.handle_discovery(event);
|
||||
}
|
||||
_ = tick.tick() => {
|
||||
app.tick();
|
||||
@@ -206,34 +207,17 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
|
||||
app.ui.set_status(None);
|
||||
let _ = crossterm::terminal::disable_raw_mode();
|
||||
let _ = server_stop_tx.send(());
|
||||
let _ = multicast_stop_tx.send(());
|
||||
let _ = discovery_stop_tx.send(());
|
||||
let _ = tokio::time::timeout(Duration::from_secs(1), server.wait_stopped()).await;
|
||||
if let Some(multicast) = &multicast {
|
||||
let _ = tokio::time::timeout(Duration::from_secs(1), multicast.wait_stopped()).await;
|
||||
}
|
||||
let _ = tokio::time::timeout(Duration::from_secs(1), discovery.wait_stopped()).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receives from an optional channel, pending forever when there is none.
|
||||
async fn recv_opt<T>(rx: &mut Option<mpsc::Receiver<T>>) -> Option<T> {
|
||||
match rx {
|
||||
Some(rx) => rx.recv().await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Handles an event; returns `true` when the application should quit.
|
||||
async fn handle_event(&mut self, event: AppEvent) -> bool {
|
||||
match event {
|
||||
AppEvent::Key(key) => return self.handle_key(key),
|
||||
AppEvent::DeviceUp {
|
||||
alias,
|
||||
host,
|
||||
port,
|
||||
protocol,
|
||||
fingerprint,
|
||||
} => self.device_up(alias, host, port, protocol, fingerprint),
|
||||
AppEvent::ReceiveFileResult {
|
||||
session_id,
|
||||
file_id,
|
||||
|
||||
+2
-14
@@ -99,13 +99,7 @@ impl App {
|
||||
pub(super) fn handle_server_event(&mut self, event: ServerEventV2) {
|
||||
match event {
|
||||
ServerEventV2::Register { ip, info } => {
|
||||
self.device_up(
|
||||
info.alias,
|
||||
ip.to_string(),
|
||||
info.port,
|
||||
info.protocol,
|
||||
info.fingerprint,
|
||||
);
|
||||
self.device_confirmed(ip.to_string(), info);
|
||||
}
|
||||
ServerEventV2::PrepareUpload {
|
||||
session_id,
|
||||
@@ -116,13 +110,7 @@ impl App {
|
||||
decision_tx,
|
||||
} => {
|
||||
// The sender is clearly reachable; make sure it has a slot.
|
||||
self.device_up(
|
||||
info.alias.clone(),
|
||||
ip.to_string(),
|
||||
info.port,
|
||||
info.protocol,
|
||||
info.fingerprint.clone(),
|
||||
);
|
||||
self.device_confirmed(ip.to_string(), info.clone());
|
||||
|
||||
let sender = SenderTarget {
|
||||
host: ip.to_string(),
|
||||
|
||||
+30
-9
@@ -2,12 +2,12 @@
|
||||
//! transfer driven by the [`crate::send_task`].
|
||||
|
||||
use super::App;
|
||||
use crate::devices::Device;
|
||||
use crate::picker::{Picker, PickerOutcome};
|
||||
use crate::send_task;
|
||||
use crate::ui::Category;
|
||||
use crate::util::SpeedMeter;
|
||||
use crossterm::event::KeyEvent;
|
||||
use localsend::discovery::StatefulDevice;
|
||||
use localsend::model::transfer::FileDto;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -28,24 +28,31 @@ pub(super) struct SendState {
|
||||
|
||||
impl App {
|
||||
pub(super) fn start_picking(&mut self, slot: u8) {
|
||||
let Some(device) = self.registry.by_slot(slot).cloned() else {
|
||||
let device = self
|
||||
.slots
|
||||
.fingerprint_by_slot(slot)
|
||||
.and_then(|fingerprint| self.discovery.device_by_fingerprint(fingerprint));
|
||||
let Some(device) = device else {
|
||||
self.ui
|
||||
.log(Category::Send, &format!("No device on [{slot}]"));
|
||||
return;
|
||||
};
|
||||
if !self.preselected.is_empty() {
|
||||
self.start_send(&device.fingerprint, self.preselected.clone());
|
||||
self.start_send(&device.device.fingerprint, self.preselected.clone());
|
||||
return;
|
||||
}
|
||||
self.open_picker(device);
|
||||
}
|
||||
|
||||
pub(super) fn open_picker(&mut self, device: Device) {
|
||||
pub(super) fn open_picker(&mut self, device: StatefulDevice) {
|
||||
if self.send.is_some() {
|
||||
self.ui.log(Category::Send, "A send is already in progress");
|
||||
return;
|
||||
}
|
||||
match Picker::open(device.fingerprint, device.alias.clone()) {
|
||||
match Picker::open(
|
||||
device.device.fingerprint.clone(),
|
||||
device.device.alias.clone(),
|
||||
) {
|
||||
Ok(picker) => {
|
||||
self.ui.suspend();
|
||||
self.picker = Some(picker);
|
||||
@@ -53,7 +60,10 @@ impl App {
|
||||
Err(err) => {
|
||||
self.ui.log(
|
||||
Category::Send,
|
||||
&format!("{}: could not open the file picker: {err}", device.alias),
|
||||
&format!(
|
||||
"{}: could not open the file picker: {err}",
|
||||
device.device.alias
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +95,18 @@ impl App {
|
||||
self.ui.log(Category::Send, "A send is already in progress");
|
||||
return;
|
||||
}
|
||||
let Some(device) = self.registry.by_fingerprint(fingerprint).cloned() else {
|
||||
let Some(device) = self.discovery.device_by_fingerprint(fingerprint) else {
|
||||
return;
|
||||
};
|
||||
let Some(host) = device
|
||||
.get_best_channel()
|
||||
.and_then(|channel| channel.http())
|
||||
.map(|http| http.host.clone())
|
||||
else {
|
||||
self.ui.log(
|
||||
Category::Send,
|
||||
&format!("{}: No dialable address", device.device.alias),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -134,8 +155,8 @@ impl App {
|
||||
let cancel = send_task::SendCancel::new();
|
||||
self.send = Some(SendState {
|
||||
session_id: None,
|
||||
alias: device.alias.clone(),
|
||||
host: device.host.clone(),
|
||||
alias: device.device.alias.clone(),
|
||||
host,
|
||||
total_bytes,
|
||||
sent: progress.clone(),
|
||||
cancel: cancel.clone(),
|
||||
|
||||
+51
-8
@@ -30,9 +30,10 @@ pub struct DeviceRow {
|
||||
/// The send hotkey (1-9) of the discovered device, if one was free.
|
||||
pub slot: Option<u8>,
|
||||
|
||||
/// Where the device was last seen; `None` for a paired device that has
|
||||
/// not been discovered in this run and therefore cannot be sent to.
|
||||
pub host: Option<String>,
|
||||
/// The addresses the device was seen on, the best first; empty for a paired
|
||||
/// device that has not been discovered in this run and therefore cannot
|
||||
/// be sent to.
|
||||
pub hosts: Vec<String>,
|
||||
|
||||
/// Whether the device is paired; the pair hotkey toggles it.
|
||||
pub paired: bool,
|
||||
@@ -112,7 +113,7 @@ impl DeviceList {
|
||||
KeyCode::Enter => {
|
||||
// A paired device that was never discovered has no address.
|
||||
if let Some(device) = self.selected_device()
|
||||
&& device.host.is_some()
|
||||
&& !device.hosts.is_empty()
|
||||
{
|
||||
return DeviceListOutcome::Send {
|
||||
fingerprint: device.fingerprint.clone(),
|
||||
@@ -221,11 +222,17 @@ fn row_item(row: &Row) -> ListItem<'_> {
|
||||
Some(slot) => slot.to_string(),
|
||||
None => "-".to_string(),
|
||||
};
|
||||
match &device.host {
|
||||
Some(host) => {
|
||||
ListItem::new(Span::raw(format!(" [{slot}] {} ({host})", device.alias)))
|
||||
match device.hosts.is_empty() {
|
||||
false => {
|
||||
let hosts: Vec<String> =
|
||||
device.hosts.iter().map(|host| shorten_host(host)).collect();
|
||||
ListItem::new(Span::raw(format!(
|
||||
" [{slot}] {} ({})",
|
||||
device.alias,
|
||||
hosts.join(", ")
|
||||
)))
|
||||
}
|
||||
None => ListItem::new(Span::styled(
|
||||
true => ListItem::new(Span::styled(
|
||||
format!(" [{slot}] {} (offline)", device.alias),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)),
|
||||
@@ -233,3 +240,39 @@ fn row_item(row: &Row) -> ListItem<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortens a host for the row: an IPv6 address is reduced to its last group
|
||||
/// (keeping the scope), so `fe80::6888:8aff:febd:8578%15` renders as
|
||||
/// `::8578%15`. Display only; sending always uses the full address.
|
||||
fn shorten_host(host: &str) -> String {
|
||||
let (address, scope) = match host.split_once('%') {
|
||||
Some((address, scope)) => (address, Some(scope)),
|
||||
None => (host, None),
|
||||
};
|
||||
if !address.contains(':') {
|
||||
return host.to_string();
|
||||
}
|
||||
let last_group = address.rsplit(':').next().unwrap_or_default();
|
||||
match scope {
|
||||
Some(scope) => format!("::{last_group}%{scope}"),
|
||||
None => format!("::{last_group}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_shorten_host() {
|
||||
assert_eq!(shorten_host("fe80::6888:8aff:febd:8578%15"), "::8578%15");
|
||||
assert_eq!(shorten_host("fe80::6888:8aff:febd:8578%7"), "::8578%7");
|
||||
assert_eq!(shorten_host("fe80::1%3"), "::1%3");
|
||||
assert_eq!(shorten_host("2a00:1450:4001:829::200e"), "::200e");
|
||||
assert_eq!(
|
||||
shorten_host("192.168.178.183"),
|
||||
"192.168.178.183",
|
||||
"IPv4 must stay untouched"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
use localsend::http::dto_v2::ProtocolTypeV2;
|
||||
|
||||
/// A discovered LocalSend device.
|
||||
#[derive(Clone)]
|
||||
pub struct Device {
|
||||
/// The hotkey (1-9) assigned to this device, if one was free.
|
||||
pub slot: Option<u8>,
|
||||
pub alias: String,
|
||||
|
||||
/// The host to dial the device at: an IP address, or the scoped form
|
||||
/// `fe80::1%3` for link-local IPv6 (the HTTP client accepts both).
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub protocol: ProtocolTypeV2,
|
||||
pub fingerprint: String,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn slot_label(&self) -> String {
|
||||
match self.slot {
|
||||
Some(slot) => slot.to_string(),
|
||||
None => "-".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All devices seen in this run, identified by fingerprint.
|
||||
pub struct DeviceRegistry {
|
||||
devices: Vec<Device>,
|
||||
}
|
||||
|
||||
impl DeviceRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
devices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds or updates a device. Returns the device only when it is new
|
||||
/// (i.e. should be logged): a known device is updated silently, because
|
||||
/// multi-homed peers re-announce with a different address all the time.
|
||||
pub fn upsert(
|
||||
&mut self,
|
||||
alias: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
protocol: ProtocolTypeV2,
|
||||
fingerprint: String,
|
||||
) -> Option<Device> {
|
||||
if let Some(device) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|device| device.fingerprint == fingerprint)
|
||||
{
|
||||
device.alias = alias;
|
||||
device.host = host;
|
||||
device.port = port;
|
||||
device.protocol = protocol;
|
||||
return None;
|
||||
}
|
||||
|
||||
let slot =
|
||||
(1..=9u8).find(|slot| !self.devices.iter().any(|device| device.slot == Some(*slot)));
|
||||
let device = Device {
|
||||
slot,
|
||||
alias,
|
||||
host,
|
||||
port,
|
||||
protocol,
|
||||
fingerprint,
|
||||
};
|
||||
self.devices.push(device.clone());
|
||||
Some(device)
|
||||
}
|
||||
|
||||
pub fn by_slot(&self, slot: u8) -> Option<&Device> {
|
||||
self.devices.iter().find(|device| device.slot == Some(slot))
|
||||
}
|
||||
|
||||
pub fn by_fingerprint(&self, fingerprint: &str) -> Option<&Device> {
|
||||
self.devices
|
||||
.iter()
|
||||
.find(|device| device.fingerprint == fingerprint)
|
||||
}
|
||||
|
||||
/// All devices in discovery order.
|
||||
pub fn devices(&self) -> &[Device] {
|
||||
&self.devices
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
mod app;
|
||||
mod banner;
|
||||
mod device_list;
|
||||
mod devices;
|
||||
mod picker;
|
||||
mod send_task;
|
||||
mod slots;
|
||||
mod storage;
|
||||
mod ui;
|
||||
mod util;
|
||||
|
||||
+19
-15
@@ -1,10 +1,10 @@
|
||||
use crate::app::AppEvent;
|
||||
use crate::devices::Device;
|
||||
use crate::storage::Identity;
|
||||
use crate::ui::Category;
|
||||
use crate::util;
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
use localsend::discovery::StatefulDevice;
|
||||
use localsend::http::client::ClientError;
|
||||
use localsend::http::client::v2::LsHttpClientV2;
|
||||
use localsend::http::dto::ProtocolType;
|
||||
@@ -47,7 +47,7 @@ impl SendCancel {
|
||||
/// Always ends by emitting [AppEvent::SendEnded].
|
||||
pub async fn run_send(
|
||||
identity: Arc<Identity>,
|
||||
device: Device,
|
||||
device: StatefulDevice,
|
||||
files: HashMap<String, FileDto>,
|
||||
paths: HashMap<String, PathBuf>,
|
||||
progress: Arc<AtomicU64>,
|
||||
@@ -60,14 +60,14 @@ pub async fn run_send(
|
||||
|
||||
async fn send_inner(
|
||||
identity: Arc<Identity>,
|
||||
device: Device,
|
||||
device: StatefulDevice,
|
||||
files: HashMap<String, FileDto>,
|
||||
paths: HashMap<String, PathBuf>,
|
||||
progress: Arc<AtomicU64>,
|
||||
cancel: SendCancel,
|
||||
events: &mpsc::Sender<AppEvent>,
|
||||
) {
|
||||
let alias = device.alias.clone();
|
||||
let alias = device.device.alias.clone();
|
||||
let log = |text: String| {
|
||||
let events = events.clone();
|
||||
async move {
|
||||
@@ -80,12 +80,16 @@ async fn send_inner(
|
||||
}
|
||||
};
|
||||
|
||||
let protocol = match device.protocol {
|
||||
let Some(http) = device.get_best_channel().and_then(|channel| channel.http()) else {
|
||||
log(format!("{alias}: No dialable address")).await;
|
||||
return;
|
||||
};
|
||||
let protocol = match http.protocol {
|
||||
ProtocolTypeV2::Http => ProtocolType::Http,
|
||||
ProtocolTypeV2::Https => ProtocolType::Https,
|
||||
};
|
||||
let expected_fingerprint = match device.protocol {
|
||||
ProtocolTypeV2::Https => Some(device.fingerprint.clone()),
|
||||
let expected_fingerprint = match http.protocol {
|
||||
ProtocolTypeV2::Https => Some(device.device.fingerprint.clone()),
|
||||
ProtocolTypeV2::Http => None,
|
||||
};
|
||||
let client = match LsHttpClientV2::try_new(
|
||||
@@ -109,8 +113,8 @@ async fn send_inner(
|
||||
let prepared = match client
|
||||
.prepare_upload(
|
||||
protocol.clone(),
|
||||
&device.host,
|
||||
device.port,
|
||||
&http.host,
|
||||
http.port,
|
||||
None,
|
||||
payload,
|
||||
None,
|
||||
@@ -193,8 +197,8 @@ async fn send_inner(
|
||||
match client
|
||||
.upload(
|
||||
protocol.clone(),
|
||||
&device.host,
|
||||
device.port,
|
||||
&http.host,
|
||||
http.port,
|
||||
None,
|
||||
&response.session_id,
|
||||
file_id,
|
||||
@@ -220,8 +224,8 @@ async fn send_inner(
|
||||
let _ = client
|
||||
.cancel(
|
||||
protocol.clone(),
|
||||
&device.host,
|
||||
device.port,
|
||||
&http.host,
|
||||
http.port,
|
||||
&response.session_id,
|
||||
)
|
||||
.await;
|
||||
@@ -238,8 +242,8 @@ async fn send_inner(
|
||||
let _ = client
|
||||
.cancel(
|
||||
protocol.clone(),
|
||||
&device.host,
|
||||
device.port,
|
||||
&http.host,
|
||||
http.port,
|
||||
&response.session_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/// The hotkeys (1-9) assigned to discovered devices, in discovery order.
|
||||
pub struct Slots {
|
||||
/// One entry per discovered fingerprint. A device beyond the ninth gets
|
||||
/// `None` and keeps it: slots are never released, so a hotkey means the
|
||||
/// same device for the whole run.
|
||||
assigned: Vec<(String, Option<u8>)>,
|
||||
}
|
||||
|
||||
impl Slots {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
assigned: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Assigns the next free slot to a fingerprint; returns the assigned
|
||||
/// slot, also when the fingerprint already has one.
|
||||
pub fn assign(&mut self, fingerprint: &str) -> Option<u8> {
|
||||
if let Some((_, slot)) = self.assigned.iter().find(|(known, _)| known == fingerprint) {
|
||||
return *slot;
|
||||
}
|
||||
let slot = (1..=9u8).find(|slot| {
|
||||
!self
|
||||
.assigned
|
||||
.iter()
|
||||
.any(|(_, assigned)| *assigned == Some(*slot))
|
||||
});
|
||||
self.assigned.push((fingerprint.to_string(), slot));
|
||||
slot
|
||||
}
|
||||
|
||||
pub fn get(&self, fingerprint: &str) -> Option<u8> {
|
||||
self.assigned
|
||||
.iter()
|
||||
.find(|(known, _)| known == fingerprint)
|
||||
.and_then(|(_, slot)| *slot)
|
||||
}
|
||||
|
||||
pub fn fingerprint_by_slot(&self, slot: u8) -> Option<&str> {
|
||||
self.assigned
|
||||
.iter()
|
||||
.find(|(_, assigned)| *assigned == Some(slot))
|
||||
.map(|(fingerprint, _)| fingerprint.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slot_label(slot: Option<u8>) -> String {
|
||||
match slot {
|
||||
Some(slot) => slot.to_string(),
|
||||
None => "-".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
mod store;
|
||||
|
||||
pub use store::{
|
||||
DeviceChannel, DeviceLog, DiscoveredDevice, DiscoveredDeviceWithLogs, HttpChannel,
|
||||
ChannelStatus, DeviceChannel, DeviceLog, DiscoveredDevice, HttpChannel, StatefulDevice,
|
||||
};
|
||||
|
||||
use crate::http::client::{ClientError, LsHttpClientV2};
|
||||
@@ -77,7 +77,7 @@ pub struct DiscoveryConfig {
|
||||
}
|
||||
|
||||
/// An event emitted by the discovery. Every event is also logged in
|
||||
/// [`DiscoveredDeviceWithLogs::logs`]; the accumulated state is read from
|
||||
/// [`StatefulDevice::logs`]; the accumulated state is read from
|
||||
/// [`DiscoveryHandle::devices`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DiscoveryEvent {
|
||||
@@ -144,7 +144,7 @@ impl DiscoveryState {
|
||||
host: &str,
|
||||
port: u16,
|
||||
protocol: ProtocolTypeV2,
|
||||
) -> Result<Option<DiscoveredDeviceWithLogs>, ClientError> {
|
||||
) -> Result<Option<StatefulDevice>, ClientError> {
|
||||
let response = client
|
||||
.register(client_protocol(protocol), host, port, self.register_dto())
|
||||
.await?;
|
||||
@@ -170,7 +170,7 @@ impl DiscoveryState {
|
||||
/// Puts a device into the store, logging the confirmation on it, and
|
||||
/// emits the resulting event. Returns whether the device is new, and
|
||||
/// its stored state after the merge.
|
||||
async fn found(&self, device: DiscoveredDevice) -> (bool, DiscoveredDeviceWithLogs) {
|
||||
async fn found(&self, device: DiscoveredDevice) -> (bool, StatefulDevice) {
|
||||
let (event, merged) = self.store.upsert(device, SystemTime::now());
|
||||
let is_new = matches!(event, DiscoveryEvent::Discovered { .. });
|
||||
if let Some(event_tx) = &self.event_tx {
|
||||
@@ -183,7 +183,9 @@ impl DiscoveryState {
|
||||
/// A handle to a running discovery: the store of discovered devices and the
|
||||
/// application-initiated operations.
|
||||
pub struct DiscoveryHandle {
|
||||
multicast: MulticastHandle,
|
||||
/// The multicast side, or the reason it could not be started; discovery
|
||||
/// keeps working without it, see [`DiscoveryHandle::multicast_error`].
|
||||
multicast: Result<MulticastHandle, anyhow::Error>,
|
||||
state: Arc<DiscoveryState>,
|
||||
}
|
||||
|
||||
@@ -196,9 +198,22 @@ impl DiscoveryHandle {
|
||||
/// themselves. Feed them back via [`DiscoveryHandle::add_device`].
|
||||
///
|
||||
/// Returns once the whole announcement burst has been sent, which takes a
|
||||
/// few seconds, or immediately once discovery has been stopped.
|
||||
/// few seconds, or immediately once discovery has been stopped or
|
||||
/// multicast is unavailable.
|
||||
pub async fn announce(&self) {
|
||||
self.multicast.announce().await;
|
||||
if let Ok(multicast) = &self.multicast {
|
||||
multicast.announce().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// The reason the multicast sockets could not be bound (e.g. the port is
|
||||
/// taken, or there is no usable network interface), when they could not.
|
||||
///
|
||||
/// Discovery then neither hears nor sends announcements; it still learns
|
||||
/// about devices through [`DiscoveryHandle::discover`],
|
||||
/// [`DiscoveryHandle::scan_subnet`] and [`DiscoveryHandle::add_device`].
|
||||
pub fn multicast_error(&self) -> Option<&anyhow::Error> {
|
||||
self.multicast.as_ref().err()
|
||||
}
|
||||
|
||||
/// Discovers a device at a known address, e.g. a favorite or a peer that
|
||||
@@ -214,7 +229,7 @@ impl DiscoveryHandle {
|
||||
host: &str,
|
||||
port: u16,
|
||||
protocol: ProtocolTypeV2,
|
||||
) -> Result<Option<DiscoveredDeviceWithLogs>, ClientError> {
|
||||
) -> Result<Option<StatefulDevice>, ClientError> {
|
||||
let client = self.state.unpinned_client()?;
|
||||
self.state.probe(&client, host, port, protocol).await
|
||||
}
|
||||
@@ -230,7 +245,7 @@ impl DiscoveryHandle {
|
||||
interface_ip: Ipv4Addr,
|
||||
port: u16,
|
||||
protocol: ProtocolTypeV2,
|
||||
) -> Result<Vec<DiscoveredDeviceWithLogs>, ClientError> {
|
||||
) -> Result<Vec<StatefulDevice>, ClientError> {
|
||||
if !self.state.scanning.lock().unwrap().insert(interface_ip) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -273,19 +288,22 @@ impl DiscoveryHandle {
|
||||
}
|
||||
|
||||
/// All discovered devices in discovery order.
|
||||
pub fn devices(&self) -> Vec<DiscoveredDeviceWithLogs> {
|
||||
pub fn devices(&self) -> Vec<StatefulDevice> {
|
||||
self.state.store.devices()
|
||||
}
|
||||
|
||||
pub fn device_by_fingerprint(&self, fingerprint: &str) -> Option<DiscoveredDeviceWithLogs> {
|
||||
pub fn device_by_fingerprint(&self, fingerprint: &str) -> Option<StatefulDevice> {
|
||||
self.state.store.by_fingerprint(fingerprint)
|
||||
}
|
||||
|
||||
/// Waits until discovery has terminated and the multicast sockets have
|
||||
/// been closed, so that the port can be bound again.
|
||||
/// Must be called after requesting a stop via the stop channel.
|
||||
/// Returns immediately when multicast is unavailable.
|
||||
pub async fn wait_stopped(&self) {
|
||||
self.multicast.wait_stopped().await;
|
||||
if let Ok(multicast) = &self.multicast {
|
||||
multicast.wait_stopped().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,14 +327,13 @@ impl Drop for ScanGuard<'_> {
|
||||
/// Binds the multicast sockets and starts answering announcements of other
|
||||
/// devices. Nothing is announced until [`DiscoveryHandle::announce`] is called.
|
||||
///
|
||||
/// Fails when no network interface could be used, e.g. because the port is
|
||||
/// already bound by another process or because there is no network at all.
|
||||
pub async fn start(
|
||||
config: DiscoveryConfig,
|
||||
stop_rx: oneshot::Receiver<()>,
|
||||
) -> anyhow::Result<DiscoveryHandle> {
|
||||
/// Starting cannot fail: when no multicast socket could be bound, e.g. because
|
||||
/// the port is already bound by another process or because there is no network
|
||||
/// at all, discovery runs without multicast.
|
||||
pub async fn start(config: DiscoveryConfig, stop_rx: oneshot::Receiver<()>) -> DiscoveryHandle {
|
||||
let (multicast_tx, mut multicast_rx) = mpsc::channel(MULTICAST_CHANNEL_SIZE);
|
||||
|
||||
// On failure `multicast_tx` is dropped, which ends the answering task.
|
||||
let multicast = multicast::start(
|
||||
MulticastConfig {
|
||||
group: config.group,
|
||||
@@ -328,7 +345,7 @@ pub async fn start(
|
||||
},
|
||||
stop_rx,
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
let state = Arc::new(DiscoveryState {
|
||||
device: config.device,
|
||||
@@ -358,7 +375,7 @@ pub async fn start(
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DiscoveryHandle { multicast, state })
|
||||
DiscoveryHandle { multicast, state }
|
||||
}
|
||||
|
||||
/// Answers an announcement with a register request, as the protocol requires.
|
||||
@@ -437,11 +454,11 @@ fn confirmed_device(
|
||||
device_model: response.device_model,
|
||||
device_type: response.device_type,
|
||||
fingerprint,
|
||||
channels: vec![DeviceChannel::Http(HttpChannel {
|
||||
channel: DeviceChannel::Http(HttpChannel {
|
||||
host,
|
||||
port,
|
||||
protocol,
|
||||
})],
|
||||
}),
|
||||
download: response.download,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use super::DiscoveryEvent;
|
||||
use crate::model::discovery::{DeviceType, ProtocolTypeV2};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -29,26 +31,41 @@ pub struct DiscoveredDevice {
|
||||
/// In HTTPS mode the SHA-256 hash of the certificate, otherwise a random string.
|
||||
pub fingerprint: String,
|
||||
|
||||
/// The channels the device is reachable on, in discovery order.
|
||||
/// A transfer can jump to another channel when one fails.
|
||||
pub channels: Vec<DeviceChannel>,
|
||||
/// The channel the device was confirmed over.
|
||||
pub channel: DeviceChannel,
|
||||
|
||||
/// Whether the device's download API is active.
|
||||
pub download: bool,
|
||||
}
|
||||
|
||||
/// A [`DiscoveredDevice`] as kept in the store, together with the history of
|
||||
/// events that affected it.
|
||||
/// A [`DiscoveredDevice`] as kept in the store.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiscoveredDeviceWithLogs {
|
||||
pub struct StatefulDevice {
|
||||
/// The device as it was most recently confirmed.
|
||||
pub device: DiscoveredDevice,
|
||||
|
||||
/// Every channel the device was confirmed on, with its current status.
|
||||
/// Starts with `Available`, the application is responsible
|
||||
/// to set it to `NotReachable` on error.
|
||||
pub channels: HashMap<DeviceChannel, ChannelStatus>,
|
||||
|
||||
/// The events that affected this device, oldest first, at most
|
||||
/// [`MAX_LOGS`]. Every confirmation is logged, so the last entry is when
|
||||
/// the device was last seen.
|
||||
pub logs: Vec<DeviceLog>,
|
||||
}
|
||||
|
||||
/// Whether a [`DeviceChannel`] of a [`StatefulDevice`] is believed to work.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ChannelStatus {
|
||||
/// The channel worked when it was last used.
|
||||
Available,
|
||||
|
||||
/// The channel failed when it was last used. A re-confirmation over the
|
||||
/// channel makes it available again.
|
||||
NotReachable,
|
||||
}
|
||||
|
||||
/// A [`DiscoveryEvent`] that affected a device, with the time it happened.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeviceLog {
|
||||
@@ -56,20 +73,38 @@ pub struct DeviceLog {
|
||||
pub event: DiscoveryEvent,
|
||||
}
|
||||
|
||||
impl DiscoveredDevice {
|
||||
/// The device's HTTP channels — the addresses it is reachable on — in
|
||||
/// discovery order. A multi-homed device has one per address it was
|
||||
/// discovered on.
|
||||
pub fn http_channels(&self) -> impl Iterator<Item = &HttpChannel> {
|
||||
// With more channel kinds this becomes a `filter_map`.
|
||||
self.channels.iter().map(|channel| match channel {
|
||||
DeviceChannel::Http(http) => http,
|
||||
})
|
||||
impl StatefulDevice {
|
||||
pub fn get_best_channel(&self) -> Option<&DeviceChannel> {
|
||||
self.get_ranked_channels().into_iter().next()
|
||||
}
|
||||
|
||||
/// The device's first HTTP channel, when it has one.
|
||||
pub fn get_ranked_channels(&self) -> Vec<&DeviceChannel> {
|
||||
let mut channels: Vec<_> = self.channels.iter().collect();
|
||||
channels.sort_by_key(|(channel, status)| {
|
||||
std::cmp::Reverse((
|
||||
**status == ChannelStatus::Available,
|
||||
channel.is_ipv6(),
|
||||
self.last_confirmed(channel),
|
||||
))
|
||||
});
|
||||
channels.into_iter().map(|(channel, _)| channel).collect()
|
||||
}
|
||||
|
||||
/// The position of the channel's most recent confirmation in the logs;
|
||||
/// `None` when its confirmations have been dropped from the log cap.
|
||||
fn last_confirmed(&self, channel: &DeviceChannel) -> Option<usize> {
|
||||
self.logs.iter().rposition(|log| {
|
||||
let (DiscoveryEvent::Discovered { device } | DiscoveryEvent::Updated { device }) =
|
||||
&log.event;
|
||||
device.channel == *channel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DiscoveredDevice {
|
||||
/// The device's HTTP channel, when it was confirmed over HTTP.
|
||||
pub fn http(&self) -> Option<&HttpChannel> {
|
||||
self.http_channels().next()
|
||||
self.channel.http()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +112,20 @@ impl DiscoveredDevice {
|
||||
///
|
||||
/// Only HTTP exists so far; other transports (e.g. WebRTC, Bluetooth) will
|
||||
/// become further variants.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum DeviceChannel {
|
||||
/// The device's HTTP server (protocol v2), reachable at one address.
|
||||
Http(HttpChannel),
|
||||
}
|
||||
|
||||
impl DeviceChannel {
|
||||
/// The HTTP address, when this is an HTTP channel.
|
||||
pub fn http(&self) -> Option<&HttpChannel> {
|
||||
match self {
|
||||
DeviceChannel::Http(http) => Some(http),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether two channels address the same endpoint, so that a
|
||||
/// re-confirmation updates the known channel instead of adding one.
|
||||
fn same_endpoint(&self, other: &DeviceChannel) -> bool {
|
||||
@@ -91,10 +133,21 @@ impl DeviceChannel {
|
||||
(DeviceChannel::Http(own), DeviceChannel::Http(other)) => own.host == other.host,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the channel dials an IPv6 address.
|
||||
fn is_ipv6(&self) -> bool {
|
||||
match self {
|
||||
DeviceChannel::Http(http) => {
|
||||
// Strip the scope of a link-local address like `fe80::1%3`.
|
||||
let host = http.host.split('%').next().unwrap_or(&http.host);
|
||||
matches!(host.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The address of a device's HTTP server.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct HttpChannel {
|
||||
/// The host to dial: an IP address, or the scoped form `fe80::1%3` for
|
||||
/// link-local IPv6 (the HTTP client accepts both).
|
||||
@@ -110,7 +163,7 @@ pub struct HttpChannel {
|
||||
/// All devices discovered in this run, identified by fingerprint, in
|
||||
/// discovery order.
|
||||
pub(super) struct DeviceStore {
|
||||
devices: Mutex<Vec<DiscoveredDeviceWithLogs>>,
|
||||
devices: Mutex<Vec<StatefulDevice>>,
|
||||
}
|
||||
|
||||
impl DeviceStore {
|
||||
@@ -126,12 +179,13 @@ impl DeviceStore {
|
||||
/// stored state after the merge.
|
||||
///
|
||||
/// Channels are merged by endpoint: a rediscovery over a known address
|
||||
/// updates its channel in place, an unknown address adds one.
|
||||
/// replaces its channel (and makes it available again), an unknown
|
||||
/// address adds one.
|
||||
pub(super) fn upsert(
|
||||
&self,
|
||||
device: DiscoveredDevice,
|
||||
timestamp: SystemTime,
|
||||
) -> (DiscoveryEvent, DiscoveredDeviceWithLogs) {
|
||||
) -> (DiscoveryEvent, StatefulDevice) {
|
||||
let mut devices = self.devices.lock().unwrap();
|
||||
match devices
|
||||
.iter_mut()
|
||||
@@ -142,13 +196,12 @@ impl DeviceStore {
|
||||
device: device.clone(),
|
||||
};
|
||||
|
||||
let mut channels = std::mem::take(&mut known.device.channels);
|
||||
for channel in device.channels {
|
||||
match channels.iter_mut().find(|c| c.same_endpoint(&channel)) {
|
||||
Some(known) => *known = channel,
|
||||
None => channels.push(channel),
|
||||
}
|
||||
}
|
||||
known
|
||||
.channels
|
||||
.retain(|channel, _| !channel.same_endpoint(&device.channel));
|
||||
known
|
||||
.channels
|
||||
.insert(device.channel.clone(), ChannelStatus::Available);
|
||||
|
||||
known.logs.push(DeviceLog {
|
||||
timestamp,
|
||||
@@ -159,14 +212,15 @@ impl DeviceStore {
|
||||
known.logs.drain(..excess);
|
||||
}
|
||||
|
||||
known.device = DiscoveredDevice { channels, ..device };
|
||||
known.device = device;
|
||||
(event, known.clone())
|
||||
}
|
||||
None => {
|
||||
let event = DiscoveryEvent::Discovered {
|
||||
device: device.clone(),
|
||||
};
|
||||
let known = DiscoveredDeviceWithLogs {
|
||||
let known = StatefulDevice {
|
||||
channels: HashMap::from([(device.channel.clone(), ChannelStatus::Available)]),
|
||||
device,
|
||||
logs: vec![DeviceLog {
|
||||
timestamp,
|
||||
@@ -180,11 +234,11 @@ impl DeviceStore {
|
||||
}
|
||||
|
||||
/// All discovered devices in discovery order.
|
||||
pub(super) fn devices(&self) -> Vec<DiscoveredDeviceWithLogs> {
|
||||
pub(super) fn devices(&self) -> Vec<StatefulDevice> {
|
||||
self.devices.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub(super) fn by_fingerprint(&self, fingerprint: &str) -> Option<DiscoveredDeviceWithLogs> {
|
||||
pub(super) fn by_fingerprint(&self, fingerprint: &str) -> Option<StatefulDevice> {
|
||||
self.devices
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -205,15 +259,19 @@ mod tests {
|
||||
device_model: None,
|
||||
device_type: Some(DeviceType::Desktop),
|
||||
fingerprint: fingerprint.to_string(),
|
||||
channels: vec![DeviceChannel::Http(HttpChannel {
|
||||
channel: DeviceChannel::Http(HttpChannel {
|
||||
host: host.to_string(),
|
||||
port: 53317,
|
||||
protocol: ProtocolTypeV2::Https,
|
||||
})],
|
||||
}),
|
||||
download: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_host(channel: &DeviceChannel) -> &str {
|
||||
channel.http().unwrap().host.as_str()
|
||||
}
|
||||
|
||||
/// The marker telling log entries apart: the host the logged snapshot
|
||||
/// was confirmed on.
|
||||
fn log_marker(log: &DeviceLog) -> &str {
|
||||
@@ -282,64 +340,137 @@ mod tests {
|
||||
|
||||
let devices = store.devices();
|
||||
assert_eq!(devices.len(), 1);
|
||||
let hosts: Vec<&str> = devices[0]
|
||||
.device
|
||||
.http_channels()
|
||||
.map(|http| http.host.as_str())
|
||||
.collect();
|
||||
let known = &devices[0];
|
||||
assert!(
|
||||
known
|
||||
.channels
|
||||
.values()
|
||||
.all(|status| *status == ChannelStatus::Available),
|
||||
"a confirmed channel must be available"
|
||||
);
|
||||
let mut hosts: Vec<&str> = known.channels.keys().map(channel_host).collect();
|
||||
hosts.sort();
|
||||
assert_eq!(
|
||||
hosts,
|
||||
["192.168.0.10", "fe80::1%3"],
|
||||
"every address the device was confirmed on must be kept"
|
||||
);
|
||||
assert_eq!(
|
||||
channel_host(&known.device.channel),
|
||||
"fe80::1%3",
|
||||
"the device must carry the channel of the last confirmation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_updates_channel_of_known_endpoint_in_place() {
|
||||
fn test_upsert_replaces_channel_of_known_endpoint() {
|
||||
let store = DeviceStore::new();
|
||||
|
||||
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
|
||||
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
|
||||
|
||||
let mut update = device("a", "192.168.0.10");
|
||||
match &mut update.channels[0] {
|
||||
match &mut update.channel {
|
||||
DeviceChannel::Http(http) => http.port = 54000,
|
||||
}
|
||||
store.upsert(update, SystemTime::now());
|
||||
|
||||
let known = store.by_fingerprint("a").unwrap();
|
||||
let channels: Vec<(&str, u16)> = known
|
||||
.device
|
||||
.http_channels()
|
||||
.map(|http| (http.host.as_str(), http.port))
|
||||
let mut channels: Vec<(&str, u16)> = known
|
||||
.channels
|
||||
.keys()
|
||||
.map(|channel| {
|
||||
let http = channel.http().unwrap();
|
||||
(http.host.as_str(), http.port)
|
||||
})
|
||||
.collect();
|
||||
channels.sort();
|
||||
assert_eq!(
|
||||
channels,
|
||||
[("192.168.0.10", 54000), ("fe80::1%3", 53317)],
|
||||
"a known address must be updated in place, not duplicated"
|
||||
"a known address must be replaced, not duplicated"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_keeps_channels_missing_from_the_update() {
|
||||
fn test_get_channel_prefers_the_most_recent_confirmation() {
|
||||
let store = DeviceStore::new();
|
||||
|
||||
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
|
||||
store.upsert(device("a", "10.0.0.10"), SystemTime::now());
|
||||
let known = store.by_fingerprint("a").unwrap();
|
||||
assert_eq!(channel_host(known.get_best_channel().unwrap()), "10.0.0.10");
|
||||
|
||||
let mut update = device("a", "10.0.0.10");
|
||||
update.channels.clear();
|
||||
store.upsert(update, SystemTime::now());
|
||||
|
||||
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
|
||||
let known = store.by_fingerprint("a").unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.by_fingerprint("a")
|
||||
.unwrap()
|
||||
.device
|
||||
.http()
|
||||
.unwrap()
|
||||
.host,
|
||||
channel_host(known.get_best_channel().unwrap()),
|
||||
"192.168.0.10",
|
||||
"an update without an HTTP channel must not drop the known one"
|
||||
"the latest confirmation must win among equals"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_channel_prefers_ipv6_over_ipv4() {
|
||||
let store = DeviceStore::new();
|
||||
|
||||
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
|
||||
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
|
||||
|
||||
let known = store.by_fingerprint("a").unwrap();
|
||||
assert_eq!(
|
||||
channel_host(known.get_best_channel().unwrap()),
|
||||
"fe80::1%3",
|
||||
"IPv6 must beat a more recent IPv4 confirmation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_channel_prefers_available_channels() {
|
||||
let store = DeviceStore::new();
|
||||
|
||||
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
|
||||
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
|
||||
|
||||
let mut known = store.by_fingerprint("a").unwrap();
|
||||
for (channel, status) in known.channels.iter_mut() {
|
||||
if channel_host(channel) == "fe80::1%3" {
|
||||
*status = ChannelStatus::NotReachable;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
channel_host(known.get_best_channel().unwrap()),
|
||||
"192.168.0.10",
|
||||
"an available IPv4 channel must beat a not-reachable IPv6 one"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_channels_sorts_best_first() {
|
||||
let store = DeviceStore::new();
|
||||
|
||||
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
|
||||
store.upsert(device("a", "10.0.0.10"), SystemTime::now());
|
||||
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
|
||||
|
||||
let mut known = store.by_fingerprint("a").unwrap();
|
||||
let hosts: Vec<&str> = known.get_ranked_channels().into_iter().map(channel_host).collect();
|
||||
assert_eq!(
|
||||
hosts,
|
||||
["fe80::1%3", "10.0.0.10", "192.168.0.10"],
|
||||
"IPv6 must come first, then the more recent IPv4 confirmation"
|
||||
);
|
||||
|
||||
for (channel, status) in known.channels.iter_mut() {
|
||||
if channel_host(channel) == "fe80::1%3" {
|
||||
*status = ChannelStatus::NotReachable;
|
||||
}
|
||||
}
|
||||
let hosts: Vec<&str> = known.get_ranked_channels().into_iter().map(channel_host).collect();
|
||||
assert_eq!(
|
||||
hosts,
|
||||
["10.0.0.10", "192.168.0.10", "fe80::1%3"],
|
||||
"a not-reachable channel must sort last"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ pub enum DeviceType {
|
||||
}
|
||||
|
||||
/// Protocol type for HTTP or HTTPS connections.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Serialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProtocolTypeV2 {
|
||||
Http,
|
||||
|
||||
@@ -15,7 +15,7 @@ use localsend::discovery::{
|
||||
use localsend::http::server::{start_with_port, ServerConfigV2, TlsConfig};
|
||||
use localsend::http::state::ClientInfo;
|
||||
use localsend::model::discovery::{DeviceType, ProtocolTypeV2, PROTOCOL_VERSION_V2};
|
||||
use localsend::multicast::MulticastDevice;
|
||||
use localsend::multicast::{InterfaceFilter, MulticastDevice};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::atomic::{AtomicU16, Ordering};
|
||||
use std::time::Duration;
|
||||
@@ -161,8 +161,10 @@ async fn start_instance(
|
||||
},
|
||||
stop_rx,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
.await;
|
||||
if handle.multicast_error().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(TestInstance {
|
||||
fingerprint: cert.fingerprint,
|
||||
@@ -296,8 +298,9 @@ async fn test_subnet_scan_finds_device_on_loopback() {
|
||||
assert_eq!(stored.device.alias, "ScanTarget");
|
||||
assert!(
|
||||
stored
|
||||
.device
|
||||
.http_channels()
|
||||
.channels
|
||||
.keys()
|
||||
.filter_map(|channel| channel.http())
|
||||
.all(|http| http.host != "127.0.0.99"),
|
||||
"the interface address itself must not be probed"
|
||||
);
|
||||
@@ -353,6 +356,66 @@ async fn test_targeted_discovery_reads_fingerprint_from_certificate_on_https() {
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_discovery_works_without_multicast() {
|
||||
let server_port = free_port();
|
||||
let _server_stop =
|
||||
start_register_server(server_port, "Target", "target-fingerprint", None).await;
|
||||
|
||||
// An interface whitelist matching no interface makes the multicast side
|
||||
// fail deterministically.
|
||||
let cert = generate_self_signed().expect("Failed to generate an identity");
|
||||
let (stop_tx, stop_rx) = oneshot::channel();
|
||||
let handle = discovery::start(
|
||||
DiscoveryConfig {
|
||||
group: TEST_GROUP,
|
||||
group_v6: Some(TEST_GROUP_V6),
|
||||
port: NEXT_MULTICAST_PORT.fetch_add(1, Ordering::Relaxed),
|
||||
interface_filter: InterfaceFilter {
|
||||
whitelist: Some(vec!["203.0.113.1".to_string()]),
|
||||
blacklist: None,
|
||||
},
|
||||
device: MulticastDevice {
|
||||
alias: "NoMulticast".to_string(),
|
||||
version: PROTOCOL_VERSION_V2.to_string(),
|
||||
device_model: Some("Rust".to_string()),
|
||||
device_type: Some(DeviceType::Headless),
|
||||
fingerprint: cert.fingerprint.clone(),
|
||||
port: free_port(),
|
||||
protocol: ProtocolTypeV2::Http,
|
||||
download: false,
|
||||
},
|
||||
identity: DeviceIdentity {
|
||||
cert_pem: cert.certificate_pem,
|
||||
private_key_pem: cert.private_key_pem,
|
||||
},
|
||||
timeout: discovery::DEFAULT_DISCOVERY_TIMEOUT,
|
||||
event_tx: None,
|
||||
},
|
||||
stop_rx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
handle.multicast_error().is_some(),
|
||||
"no interface matches the whitelist, so multicast must be unavailable"
|
||||
);
|
||||
|
||||
// Announcing is a no-op, the active operations still work.
|
||||
handle.announce().await;
|
||||
let device = handle
|
||||
.discover("127.0.0.1", server_port, ProtocolTypeV2::Http)
|
||||
.await
|
||||
.expect("Targeted discovery failed")
|
||||
.expect("The target must not be mistaken for the device itself");
|
||||
assert_eq!(device.device.alias, "Target");
|
||||
assert!(handle.device_by_fingerprint("target-fingerprint").is_some());
|
||||
|
||||
// Stopping must not hang without multicast sockets to close.
|
||||
drop(stop_tx);
|
||||
handle.wait_stopped().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_announcement_is_answered_and_device_stored() {
|
||||
let multicast_port = NEXT_MULTICAST_PORT.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
Generated
+192
-149
@@ -25,9 +25,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -42,7 +42,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
@@ -62,7 +62,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
@@ -98,6 +98,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
@@ -113,6 +119,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
@@ -127,9 +142,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
@@ -146,6 +161,17 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.39"
|
||||
@@ -160,6 +186,12 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -175,6 +207,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "croner"
|
||||
version = "2.1.0"
|
||||
@@ -194,6 +235,15 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.6.0"
|
||||
@@ -206,8 +256,19 @@ version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid",
|
||||
"crypto-common 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -248,45 +309,44 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -308,26 +368,26 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
@@ -380,6 +440,15 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.5.2"
|
||||
@@ -462,20 +531,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "localsend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.23.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"lru",
|
||||
"rand 0.9.2",
|
||||
"rand 0.10.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
@@ -483,7 +552,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tungstenite 0.28.0",
|
||||
"tungstenite 0.30.0",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -505,9 +574,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
@@ -532,13 +601,13 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -547,7 +616,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -558,7 +627,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -611,12 +680,6 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
@@ -646,9 +709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
@@ -657,18 +720,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_chacha",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -681,16 +745,6 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@@ -702,12 +756,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
@@ -738,9 +789,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
@@ -748,34 +799,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.143"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -806,7 +858,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"futures-util",
|
||||
"localsend",
|
||||
"serde",
|
||||
@@ -825,8 +877,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
"cpufeatures 0.2.16",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -870,12 +933,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -889,6 +952,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -897,22 +971,22 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -927,9 +1001,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -939,7 +1013,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -959,20 +1033,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -1039,7 +1113,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1065,9 +1139,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"sharded-slab",
|
||||
@@ -1090,33 +1164,32 @@ dependencies = [
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"thiserror",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.28.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
|
||||
checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"rand 0.10.2",
|
||||
"sha1 0.11.0",
|
||||
"thiserror",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -1132,11 +1205,11 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.20.0"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -1160,15 +1233,6 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
@@ -1191,7 +1255,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.95",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -1213,7 +1277,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.95",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -1242,24 +1306,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -1333,15 +1379,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
@@ -1360,5 +1397,11 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
Reference in New Issue
Block a user