feat: multicast in Rust
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
Tien Do Nam
2026-07-27 18:01:50 +02:00
parent 2a92f57f6c
commit 20f7849aa5
31 changed files with 2949 additions and 618 deletions
+12 -1
View File
@@ -75,7 +75,7 @@ Codegen has a habit of rewriting `app/test/mocks.mocks.dart` at 80 columns; reve
## Core crate features
`packages/core` gates almost everything behind Cargo features (`crypto`, `http`, `webrtc`, `webrtc-signaling`, `full`), and `default = []`. **Always build and test it with `--features full`.** A bare `cargo check`/`cargo build` fails because modules are declared unconditionally while their dependencies are optional — that is pre-existing and expected, not a regression.
`packages/core` gates almost everything behind Cargo features (`crypto`, `http`, `multicast`, `webrtc`, `webrtc-signaling`, `full`), and `default = []`. **Always build and test it with `--features full`.** A bare `cargo check`/`cargo build` fails because modules are declared unconditionally while their dependencies are optional — that is pre-existing and expected, not a regression.
## Architecture
@@ -106,12 +106,23 @@ The FRB layer (`packages/localsend_isolates/rust/src/api/server.rs`) exposes `st
Save targets are decided in Dart (`prepareFileSaveTarget`) and written by Rust: a path, or an Android SAF file descriptor obtained through the `org.localsend.localsend_app/localsend` method channel. Gallery saves go through a cache file first.
Server event `ip`s are `PeerIp` (IP + IPv6 scope): a link-local peer renders as `fe80::1%3`, which the HTTP client accepts back as a host, so event ips stay dialable.
TLS uses per-device on-the-fly certificates with **mandatory client certificates**; the peer identity is the uppercase-hex SHA-256 of the client cert DER, and `Register` is simply not emitted when a payload's claimed fingerprint disagrees with the cert. Prefer `event.certFingerprint ?? event.info.fingerprint` — the payload fallback only exists for encryption-off mode.
Both the receive pin and the web-send pin are fixed at server start, so changing either restarts the server.
Web assets for the browser download page are embedded from `packages/core/assets/web/`.
### Multicast discovery (Rust)
`packages/core/src/multicast/` (feature `multicast`, independent of `http`) implements UDP multicast discovery for protocol v2.1 — v1 messages are not parsed.
Integration mirrors the HTTP server: `multicast::start` takes a `MulticastConfig { group, group_v6, port, interface_filter, device, event_tx }` and emits `MulticastEvent::Discovered { ip, message }`; the returned `MulticastHandle` offers `announce` (the announcement burst) and `wait_stopped`.
UDP is **announce-only**: responses go back over HTTP as a unicast register request to the announcing device.
One socket is bound per interface IPv4 address (`SO_REUSEPORT`/`SO_REUSEADDR` + `IP_MULTICAST_IF`), because a single socket only sends on one interface. Multicast loopback stays on so that instances on one host see each other; own messages are dropped by fingerprint. IPv6 is a LocalSend extension (group `ff12::fd3a:e420`, `DEFAULT_MULTICAST_GROUP_V6`), enabled by setting `group_v6`: one `IPV6_V6ONLY` socket per interface, joined by interface index. `Discovered` carries the source's scope ID (interface index), which link-local IPv6 sources need for the HTTP answer.
### i18n
Slang, source files in `app/assets/i18n/` (`<locale>.json` plus `_missing_translations_<locale>.json`), generated output in `app/lib/gen/`. Translations are managed on Weblate; fields prefixed with `@` are metadata for translators and are not used by the app. `app/test/unit/i18n_test.dart` guards the locale set.
+11
View File
@@ -1111,6 +1111,16 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "if-addrs"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -1247,6 +1257,7 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-util",
"if-addrs",
"lru",
"pem",
"percent-encoding",
+3 -1
View File
@@ -13,6 +13,7 @@ form_urlencoded = { version = "1.2", optional = true }
futures-util = { version = "0.3.31", features = ["sink"] }
http-body-util = { version = "0.1.3", optional = true }
hyper = { version = "1.8.1", optional = true }
if-addrs = { version = "0.15.0", optional = true }
hyper-util = { version = "0.1.19", features = ["server"], optional = true }
lru = "0.16.3"
pem = { version = "3.0.6", optional = true }
@@ -45,6 +46,7 @@ rcgen = "0.13.2"
default = []
crypto = ["ed25519-dalek", "rsa", "sha2", "tokio-util"]
http = ["crypto", "form_urlencoded", "http-body-util", "hyper", "hyper-util", "pem", "percent-encoding", "reqwest", "rustls", "socket2", "tokio-rustls", "tokio-util", "x509-parser"]
multicast = ["if-addrs", "socket2", "tokio-util"]
webrtc-signaling = ["tokio-tungstenite"]
webrtc = ["crypto", "flate2", "dep:webrtc", "webrtc-signaling", "x509-parser"]
full = ["crypto", "http", "webrtc"]
full = ["crypto", "http", "multicast", "webrtc"]
+24 -1
View File
@@ -1,3 +1,4 @@
mod scoped_host;
mod server_cert_verifier;
mod url;
pub mod v2;
@@ -223,7 +224,8 @@ pub(super) fn create_reqwest_client(
let mut builder = reqwest::Client::builder()
.tls_backend_preconfigured(tls_config)
.tls_info(true);
.tls_info(true)
.dns_resolver(Arc::new(ScopedHostResolver));
if let Some(timeout) = timeout {
builder = builder.timeout(timeout);
@@ -234,6 +236,27 @@ pub(super) fn create_reqwest_client(
Ok(client)
}
/// DNS resolver that turns the synthetic host names produced by
/// [`scoped_host::encode`] back into their scoped IPv6 socket address.
/// Every other name is resolved by the system resolver, like by default.
struct ScopedHostResolver;
impl reqwest::dns::Resolve for ScopedHostResolver {
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
Box::pin(async move {
if let Some(addr) = scoped_host::decode(name.as_str()) {
return Ok(Box::new(std::iter::once(addr)) as reqwest::dns::Addrs);
}
// The port is a placeholder, reqwest replaces it with the URL's.
let addrs = tokio::net::lookup_host((name.as_str(), 0))
.await?
.collect::<Vec<_>>();
Ok(Box::new(addrs.into_iter()) as reqwest::dns::Addrs)
})
}
}
/// Verifies the certificate from the response.
/// Returns the public key extracted from the certificate.
pub(super) fn verify_cert_from_res(
@@ -0,0 +1,96 @@
//! Support for connecting to scoped IPv6 addresses (`fe80::1%3`).
//!
//! Link-local IPv6 peers are only reachable together with their scope (the
//! interface index), but URLs cannot carry a zone identifier.
//!
//! [`encode`] therefore turns `fe80::1%3` into the synthetic host name
//! `fe80--1s3.scoped.localsend.internal`, and [`decode`], called by the custom
//! DNS resolver of the reqwest client, turns it back into a scoped address.
//! The name is also a valid TLS server name; certificates are verified by
//! fingerprint, never by name.
use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
/// Marks a synthetic host name produced by [`encode`]. The `.internal` TLD is
/// reserved for private use and never resolves in public DNS.
const SUFFIX: &str = ".scoped.localsend.internal";
/// Encodes a `<IPv6 address>%<scope>` host into a synthetic host name.
/// Returns `None` for every host that URLs can represent directly.
///
/// `:` becomes `-` and `s` separates the scope, both unambiguous because IPv6
/// addresses consist of hex digits and colons only.
pub(crate) fn encode(host: &str) -> Option<String> {
let (address, scope) = host.split_once('%')?;
// Only accept what [decode] can reverse.
let address: Ipv6Addr = address.parse().ok()?;
let scope: u32 = scope.parse().ok()?;
if address.to_string().contains('.') {
// An IPv4-mapped address; those never carry a scope in practice.
return None;
}
Some(format!(
"{}s{scope}{SUFFIX}",
address.to_string().replace(':', "-")
))
}
/// Decodes a host name produced by [`encode`] back into a scoped socket
/// address. Returns `None` for every other host name.
pub(crate) fn decode(name: &str) -> Option<SocketAddr> {
let encoded = name.strip_suffix(SUFFIX)?;
let (address, scope) = encoded.split_once('s')?;
let address: Ipv6Addr = address.replace('-', ":").parse().ok()?;
let scope: u32 = scope.parse().ok()?;
// Port 0 is a placeholder; reqwest replaces it with the port of the URL.
Some(SocketAddr::from(SocketAddrV6::new(address, 0, 0, scope)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_scoped_host() {
assert_eq!(
encode("fe80::1%3").as_deref(),
Some("fe80--1s3.scoped.localsend.internal")
);
assert_eq!(
encode("fe80::abcd:ef12:3456:789a%4294967295").as_deref(),
Some("fe80--abcd-ef12-3456-789as4294967295.scoped.localsend.internal")
);
}
#[test]
fn test_encode_rejects_unscoped_and_invalid_hosts() {
assert_eq!(encode("192.168.1.1"), None);
assert_eq!(encode("fe80::1"), None);
assert_eq!(encode("example.com"), None);
assert_eq!(encode("fe80::1%eth0"), None); // only numeric scopes
assert_eq!(encode("not-an-address%3"), None);
assert_eq!(encode("::ffff:192.168.1.1%3"), None); // IPv4-mapped
}
#[test]
fn test_decode_reverses_encode() {
let encoded = encode("fe80::1%3").unwrap();
let decoded = decode(&encoded).unwrap();
assert_eq!(
decoded,
SocketAddr::from(SocketAddrV6::new("fe80::1".parse().unwrap(), 0, 0, 3))
);
}
#[test]
fn test_decode_rejects_other_names() {
assert_eq!(decode("example.com"), None);
assert_eq!(decode("fe80--1.scoped.localsend.internal"), None); // no scope
assert_eq!(decode("fe80--1sx.scoped.localsend.internal"), None); // bad scope
assert_eq!(decode("s3.scoped.localsend.internal"), None); // no address
}
}
+26 -3
View File
@@ -1,3 +1,4 @@
use crate::http::client::scoped_host;
use std::borrow::Cow;
pub struct TargetUrl<'a> {
@@ -22,9 +23,14 @@ impl<'a> TargetUrl<'a> {
let base = format!(
"{}://{}:{}/api/localsend/{}{}",
self.protocol,
match self.host.contains(':') {
true => Cow::Owned(format!("[{}]", self.host)), // IPv6 addresses need to be enclosed in brackets
false => Cow::Borrowed(&self.host),
// A scoped IPv6 address (`fe80::1%3`) cannot be represented in a
// URL and becomes a synthetic host name instead.
match scoped_host::encode(&self.host) {
Some(encoded) => Cow::Owned(encoded),
None => match self.host.contains(':') {
true => Cow::Owned(format!("[{}]", self.host)), // IPv6 addresses need to be enclosed in brackets
false => Cow::Borrowed(&self.host),
},
},
self.port,
match self.version {
@@ -79,6 +85,23 @@ mod tests {
assert_eq!(url, "https://[::1]:53317/api/localsend/v2/register");
}
#[test]
fn test_build_url_scoped_ipv6() {
let url = TargetUrl {
version: ApiVersion::V2,
protocol: "https",
host: "fe80::1%3".to_string(),
port: 53317,
path: "/register",
params: &[],
}
.to_string();
assert_eq!(
url,
"https://fe80--1s3.scoped.localsend.internal:53317/api/localsend/v2/register"
);
}
#[test]
fn test_build_url_http() {
let url = TargetUrl {
+5 -128
View File
@@ -3,110 +3,11 @@ use crate::model::transfer::FileDto;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// The protocol version (major.minor) implemented by this crate for the v2 protocol.
pub const PROTOCOL_VERSION_V2: &str = "2.1";
/// Serde helpers for `DeviceType` in the v2 protocol.
///
/// The v2 protocol uses lowercase values (e.g. "desktop") on the wire.
/// Unknown values fall back to `Desktop` as required by the protocol (section 7.1).
pub(crate) mod device_type_v2 {
use crate::model::discovery::DeviceType;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(
value: &Option<DeviceType>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(device_type) => serializer.serialize_str(match device_type {
DeviceType::Mobile => "mobile",
DeviceType::Desktop => "desktop",
DeviceType::Web => "web",
DeviceType::Headless => "headless",
DeviceType::Server => "server",
}),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<DeviceType>, D::Error> {
let value = Option::<String>::deserialize(deserializer)?;
Ok(value.map(|value| match value.to_lowercase().as_str() {
"mobile" => DeviceType::Mobile,
"desktop" => DeviceType::Desktop,
"web" => DeviceType::Web,
"headless" => DeviceType::Headless,
"server" => DeviceType::Server,
_ => DeviceType::Desktop,
}))
}
}
/// Protocol type for HTTP or HTTPS connections.
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ProtocolTypeV2 {
Http,
Https,
}
impl ProtocolTypeV2 {
pub fn as_str(&self) -> &'static str {
match self {
ProtocolTypeV2::Http => "http",
ProtocolTypeV2::Https => "https",
}
}
}
/// Multicast announcement/response message for UDP discovery (v2.1).
///
/// Used for both sending announcements and responding to announcements.
/// When `announce` is true, other devices should respond.
/// When `announce` is false, this is a response to an announcement.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MulticastMessageV2 {
/// The display name of the device.
pub alias: String,
/// Protocol version (e.g., "2.1").
pub version: String,
/// Device model (e.g., "Samsung", "Windows"). Optional.
#[serde(skip_serializing_if = "Option::is_none")]
pub device_model: Option<String>,
/// Device type category. Optional.
#[serde(
default,
with = "device_type_v2",
skip_serializing_if = "Option::is_none"
)]
pub device_type: Option<DeviceType>,
/// Fingerprint for device identification.
/// In HTTPS mode: SHA-256 hash of the certificate.
/// In HTTP mode: randomly generated string.
pub fingerprint: String,
/// Port number the device is listening on.
pub port: u16,
/// Protocol type (http or https).
pub protocol: ProtocolTypeV2,
/// Whether the download API (sections 5.2, 5.3) is active.
#[serde(default)]
pub download: bool,
/// Whether this is an announcement (true) or a response (false).
/// Other devices should only respond when this is true.
pub announce: bool,
}
// Discovery types are shared with the (HTTP-independent) multicast module and
// therefore live in `crate::model::discovery`. They are re-exported here so that
// the v2 DTOs remain available under a single path.
pub(crate) use crate::model::discovery::device_type_v2;
pub use crate::model::discovery::{MulticastMessageV2, ProtocolTypeV2, PROTOCOL_VERSION_V2};
/// Register request DTO for v2.1 protocol.
///
@@ -267,30 +168,6 @@ pub struct InfoResponseDtoV2 {
mod tests {
use super::*;
#[test]
fn test_multicast_message_serialization() {
let msg = MulticastMessageV2 {
alias: "Nice Orange".to_string(),
version: "2.1".to_string(),
device_model: Some("Samsung".to_string()),
device_type: Some(DeviceType::Mobile),
fingerprint: "random string".to_string(),
port: 53317,
protocol: ProtocolTypeV2::Https,
download: true,
announce: true,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"alias\":\"Nice Orange\""));
assert!(json.contains("\"version\":\"2.1\""));
assert!(json.contains("\"fingerprint\":\"random string\""));
assert!(json.contains("\"announce\":true"));
assert!(json.contains("\"download\":true"));
assert!(json.contains("\"protocol\":\"https\""));
assert!(json.contains("\"deviceType\":\"mobile\""));
}
#[test]
fn test_register_dto_v2_deserialization() {
let json = r#"{
@@ -1,6 +1,6 @@
use crate::http::server::PeerIp;
use crate::model::transfer::FileDto;
use std::collections::HashMap;
use std::net::IpAddr;
/// State of the single v2 upload session slot.
pub(crate) enum SessionStateV2 {
@@ -15,7 +15,7 @@ pub(crate) struct UploadSessionV2 {
pub(crate) session_id: String,
/// The IP address of the sender. Uploads are only accepted from this address.
pub(crate) sender_ip: IpAddr,
pub(crate) sender_ip: PeerIp,
/// The accepted files, mapped by file ID.
pub(crate) files: HashMap<String, SessionFileV2>,
+7 -4
View File
@@ -1,9 +1,12 @@
pub mod common;
pub mod internal;
mod peer_ip;
pub mod v2;
pub mod v3;
pub mod web;
pub use peer_ip::PeerIp;
use crate::crypto::cert::{fingerprint_from_cert_der, public_key_from_cert_der};
use crate::http::server::internal::{InternalConfig, InternalState};
use crate::http::server::v2::ServerEventV2;
@@ -299,7 +302,7 @@ async fn serve_connection(
let client_info = {
let (_, server_connection) = tls_stream.get_ref();
RequestClientInfo {
ip: remote_addr.ip(),
ip: PeerIp::from_remote_addr(&remote_addr),
cert: server_connection
.deref()
.deref()
@@ -327,7 +330,7 @@ async fn serve_connection(
hyper::service::service_fn(move |mut req: Request<Incoming>| {
req.extensions_mut()
.insert::<RequestClientInfo>(RequestClientInfo {
ip: remote_addr.ip(),
ip: PeerIp::from_remote_addr(&remote_addr),
cert: None,
});
req.extensions_mut().insert::<AppState>(app_state.clone());
@@ -359,8 +362,8 @@ fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::Tls
#[derive(Clone, Debug)]
pub struct RequestClientInfo {
/// The IP address of the client.
ip: IpAddr,
/// The IP address of the client, including the IPv6 scope when present.
ip: PeerIp,
/// The client certificate in DER format.
cert: Option<Vec<u8>>,
+68
View File
@@ -0,0 +1,68 @@
use std::fmt;
use std::net::{IpAddr, SocketAddr};
/// The IP address of a connected peer.
///
/// Unlike a bare [`IpAddr`], it keeps the scope the connection was accepted
/// with, without which a link-local IPv6 peer cannot be dialed back. Renders
/// as `fe80::1%3`, which the HTTP client accepts as a host.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct PeerIp {
pub ip: IpAddr,
/// The scope (interface index) of the connection. Only set for IPv6 peers
/// that carry one, i.e. link-local addresses.
pub scope_id: Option<u32>,
}
impl PeerIp {
pub(crate) fn from_remote_addr(remote_addr: &SocketAddr) -> Self {
PeerIp {
ip: remote_addr.ip(),
scope_id: match remote_addr {
SocketAddr::V6(remote_addr) if remote_addr.scope_id() != 0 => {
Some(remote_addr.scope_id())
}
_ => None,
},
}
}
}
impl fmt::Display for PeerIp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.scope_id {
Some(scope_id) => write!(f, "{}%{scope_id}", self.ip),
None => write!(f, "{}", self.ip),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::SocketAddrV6;
#[test]
fn test_ipv4_has_no_scope() {
let peer = PeerIp::from_remote_addr(&"192.168.1.42:50000".parse().unwrap());
assert_eq!(peer.scope_id, None);
assert_eq!(peer.to_string(), "192.168.1.42");
}
#[test]
fn test_scoped_ipv6_keeps_the_scope() {
let addr = SocketAddrV6::new("fe80::1".parse().unwrap(), 50000, 0, 3);
let peer = PeerIp::from_remote_addr(&addr.into());
assert_eq!(peer.scope_id, Some(3));
assert_eq!(peer.to_string(), "fe80::1%3");
}
#[test]
fn test_unscoped_ipv6_has_no_scope() {
let addr = SocketAddrV6::new("2001:db8::1".parse().unwrap(), 50000, 0, 0);
let peer = PeerIp::from_remote_addr(&addr.into());
assert_eq!(peer.scope_id, None);
assert_eq!(peer.to_string(), "2001:db8::1");
}
}
+11 -5
View File
@@ -11,12 +11,12 @@ use crate::http::server::common::save::{FileUploadTarget, SaveResult};
use crate::http::server::common::session::{
FileStatusV2, SessionFileV2, SessionStateV2, UploadSessionV2,
};
use crate::http::server::PeerIp;
use crate::http::server::{common, AppState, RequestClientInfo, V2State};
use crate::model::transfer::FileDto;
use hyper::body::Incoming;
use hyper::{Request, Response, StatusCode};
use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::sync::Arc;
use tokio::sync::oneshot;
use uuid::Uuid;
@@ -31,7 +31,7 @@ pub enum ServerEventV2 {
/// handshake, so the fingerprint cannot be spoofed.
Register {
/// The IP address of the remote device.
ip: IpAddr,
ip: PeerIp,
/// The device information sent by the remote device.
info: RegisterDtoV2,
@@ -48,7 +48,7 @@ pub enum ServerEventV2 {
session_id: String,
/// The IP address of the sender.
ip: IpAddr,
ip: PeerIp,
/// The device information of the sender.
info: RegisterDtoV2,
@@ -112,7 +112,7 @@ pub enum ServerEventV2 {
/// send session before cancelling it.
CancelReceived {
/// The IP address of the remote device requesting the cancellation.
ip: IpAddr,
ip: PeerIp,
/// The session ID as known by the remote device.
session_id: String,
@@ -212,7 +212,13 @@ pub(crate) async fn prepare_upload(
let v2 = require_v2(&state)?;
let query = parse_query(req.uri().query());
check_pin(v2.pin.as_deref(), &v2.pin_attempts, &query, client_info.ip).await?;
check_pin(
v2.pin.as_deref(),
&v2.pin_attempts,
&query,
client_info.ip.ip,
)
.await?;
let payload = req
.into_body()
+4 -3
View File
@@ -3,6 +3,7 @@ use crate::http::server::common::error::AppError;
use crate::http::server::common::pin::check_pin;
use crate::http::server::common::query::parse_query;
use crate::http::server::common::response::{full_body, BoxedBody, JsonResponse};
use crate::http::server::PeerIp;
use crate::http::server::{AppState, RequestClientInfo};
use crate::model::transfer::{FileContent, FileDto};
use bytes::Bytes;
@@ -31,7 +32,7 @@ pub enum WebSendEvent {
/// Dropping `decision_tx` results in a 500 response.
PrepareDownload {
/// The IP address of the web client.
ip: IpAddr,
ip: PeerIp,
/// The ID of the download session that is created when accepted.
session_id: String,
@@ -168,7 +169,7 @@ impl WebPageState {
/// A download session of a single web client.
pub(crate) struct WebSendSession {
/// The IP address of the web client. Downloads are only allowed from this address.
ip: IpAddr,
ip: PeerIp,
/// `false` while the prepare-download request is waiting for the application's decision.
accepted: bool,
@@ -222,7 +223,7 @@ pub(crate) async fn prepare_download(
web.pin.as_deref(),
&web.pin_attempts,
&query,
client_info.ip,
client_info.ip.ip,
)
.await?;
+2
View File
@@ -3,6 +3,8 @@ pub mod crypto;
#[cfg(feature = "http")]
pub mod http;
pub mod model;
#[cfg(feature = "multicast")]
pub mod multicast;
pub(crate) mod util;
pub mod webrtc;
+167
View File
@@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize};
/// The protocol version (major.minor) implemented by this crate for the v2 protocol.
pub const PROTOCOL_VERSION_V2: &str = "2.1";
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DeviceType {
@@ -9,3 +12,167 @@ pub enum DeviceType {
Headless,
Server,
}
/// Protocol type for HTTP or HTTPS connections.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ProtocolTypeV2 {
Http,
Https,
}
impl ProtocolTypeV2 {
pub fn as_str(&self) -> &'static str {
match self {
ProtocolTypeV2::Http => "http",
ProtocolTypeV2::Https => "https",
}
}
}
/// Serde helpers for `DeviceType` in the v2 protocol.
///
/// The v2 protocol uses lowercase values (e.g. "desktop") on the wire.
/// Unknown values fall back to `Desktop` as required by the protocol (section 7.1).
pub(crate) mod device_type_v2 {
use super::DeviceType;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(
value: &Option<DeviceType>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(device_type) => serializer.serialize_str(match device_type {
DeviceType::Mobile => "mobile",
DeviceType::Desktop => "desktop",
DeviceType::Web => "web",
DeviceType::Headless => "headless",
DeviceType::Server => "server",
}),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<DeviceType>, D::Error> {
let value = Option::<String>::deserialize(deserializer)?;
Ok(value.map(|value| match value.to_lowercase().as_str() {
"mobile" => DeviceType::Mobile,
"desktop" => DeviceType::Desktop,
"web" => DeviceType::Web,
"headless" => DeviceType::Headless,
"server" => DeviceType::Server,
_ => DeviceType::Desktop,
}))
}
}
/// Multicast announcement message for UDP discovery (v2.1).
///
/// Devices that receive an announcement respond over HTTP, so this message is
/// only ever an announcement and never a response.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MulticastMessageV2 {
/// The display name of the device.
pub alias: String,
/// Protocol version (e.g., "2.1").
pub version: String,
/// Device model (e.g., "Samsung", "Windows"). Optional.
#[serde(skip_serializing_if = "Option::is_none")]
pub device_model: Option<String>,
/// Device type category. Optional.
#[serde(
default,
with = "device_type_v2",
skip_serializing_if = "Option::is_none"
)]
pub device_type: Option<DeviceType>,
/// Fingerprint for device identification.
/// In HTTPS mode: SHA-256 hash of the certificate.
/// In HTTP mode: randomly generated string.
pub fingerprint: String,
/// Port number the device is listening on.
pub port: u16,
/// Protocol type (http or https).
pub protocol: ProtocolTypeV2,
/// Whether the download API (sections 5.2, 5.3) is active.
#[serde(default)]
pub download: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multicast_message_serialization() {
let msg = MulticastMessageV2 {
alias: "Nice Orange".to_string(),
version: "2.1".to_string(),
device_model: Some("Samsung".to_string()),
device_type: Some(DeviceType::Mobile),
fingerprint: "random string".to_string(),
port: 53317,
protocol: ProtocolTypeV2::Https,
download: true,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"alias\":\"Nice Orange\""));
assert!(json.contains("\"version\":\"2.1\""));
assert!(json.contains("\"fingerprint\":\"random string\""));
assert!(json.contains("\"download\":true"));
assert!(json.contains("\"protocol\":\"https\""));
assert!(json.contains("\"deviceType\":\"mobile\""));
}
#[test]
fn test_multicast_message_deserialization() {
let json = r#"{
"alias": "Secret Banana",
"version": "2.1",
"deviceModel": "Windows",
"deviceType": "desktop",
"fingerprint": "random string",
"port": 53317,
"protocol": "https",
"download": true
}"#;
let msg: MulticastMessageV2 = serde_json::from_str(json).unwrap();
assert_eq!(msg.alias, "Secret Banana");
assert_eq!(msg.version, "2.1");
assert_eq!(msg.device_model, Some("Windows".to_string()));
assert_eq!(msg.device_type, Some(DeviceType::Desktop));
assert_eq!(msg.port, 53317);
assert_eq!(msg.protocol, ProtocolTypeV2::Https);
assert!(msg.download);
}
#[test]
fn test_multicast_message_without_optional_fields() {
let json = r#"{
"alias": "Secret Banana",
"version": "2.1",
"fingerprint": "random string",
"port": 53317,
"protocol": "http"
}"#;
let msg: MulticastMessageV2 = serde_json::from_str(json).unwrap();
assert_eq!(msg.device_model, None);
assert_eq!(msg.device_type, None);
assert_eq!(msg.protocol, ProtocolTypeV2::Http);
assert!(!msg.download);
}
}
+257
View File
@@ -0,0 +1,257 @@
//! Enumeration and filtering of the local network interfaces used for multicast.
use std::net::{IpAddr, Ipv4Addr};
/// Restricts the network interfaces that multicast sockets are bound to.
///
/// Filters are matched against every address of an interface. A `*` matches one
/// or more characters that are not a `.`, so `192.168.1.*` matches
/// `192.168.1.42` but not `192.168.10.1`.
#[derive(Clone, Debug, Default)]
pub struct InterfaceFilter {
/// When set, only interfaces with a matching address are used.
pub whitelist: Option<Vec<String>>,
/// When set, interfaces with a matching address are skipped.
pub blacklist: Option<Vec<String>>,
}
impl InterfaceFilter {
/// Returns `true` when an interface with the given addresses must not be used.
pub fn is_ignored(&self, addresses: &[IpAddr]) -> bool {
if let Some(whitelist) = &self.whitelist {
if !any_address_matches(addresses, whitelist) {
return true;
}
}
if let Some(blacklist) = &self.blacklist {
if any_address_matches(addresses, blacklist) {
return true;
}
}
false
}
}
/// Returns `true` when at least one of `addresses` matches at least one of `patterns`.
fn any_address_matches(addresses: &[IpAddr], patterns: &[String]) -> bool {
addresses.iter().any(|address| {
let address = address.to_string();
patterns
.iter()
.any(|pattern| matches_ip_filter(pattern, &address))
})
}
/// Matches the whole address against a filter pattern.
fn matches_ip_filter(pattern: &str, address: &str) -> bool {
match pattern.split_once('*') {
None => pattern == address,
Some((prefix, rest)) => {
let Some(remainder) = address.strip_prefix(prefix) else {
return false;
};
// `*` must consume at least one character and may not cross a `.`.
let max_len = remainder
.find('.')
.unwrap_or(remainder.len())
.min(remainder.len());
(1..=max_len).any(|len| matches_ip_filter(rest, &remainder[len..]))
}
}
}
/// A local IPv4 address that a multicast socket can be bound to.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct LocalInterfaceV4 {
/// The name of the interface, only used for logging.
pub(crate) name: String,
/// The IPv4 address of the interface, used to join the multicast group.
pub(crate) address: Ipv4Addr,
}
/// A local interface that an IPv6 multicast socket can be bound to.
///
/// IPv6 group membership is joined per interface rather than per address, so a
/// single socket per interface covers all of its IPv6 addresses.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct LocalInterfaceV6 {
/// The name of the interface, only used for logging.
pub(crate) name: String,
/// The index of the interface, used to join the multicast group.
pub(crate) index: u32,
}
/// The local network interfaces that multicast sockets can be bound to.
pub(crate) struct LocalInterfaces {
/// One entry per IPv4 address of an interface.
pub(crate) v4: Vec<LocalInterfaceV4>,
/// One entry per interface that has at least one IPv6 address.
pub(crate) v6: Vec<LocalInterfaceV6>,
}
/// Returns the addresses of all non-loopback interfaces that pass `filter`.
pub(crate) fn local_interfaces(filter: &InterfaceFilter) -> std::io::Result<LocalInterfaces> {
struct Entry {
name: String,
index: Option<u32>,
addresses: Vec<IpAddr>,
}
// Filters apply to an interface as a whole, so all of its addresses have to
// be known before any of them can be accepted.
let mut by_name: Vec<Entry> = Vec::new();
for interface in if_addrs::get_if_addrs()? {
if interface.is_loopback() {
continue;
}
match by_name
.iter_mut()
.find(|entry| entry.name == interface.name)
{
Some(entry) => {
entry.index = entry.index.or(interface.index);
entry.addresses.push(interface.ip());
}
None => by_name.push(Entry {
name: interface.name.clone(),
index: interface.index,
addresses: vec![interface.ip()],
}),
}
}
let mut result = LocalInterfaces {
v4: Vec::new(),
v6: Vec::new(),
};
for entry in by_name {
if filter.is_ignored(&entry.addresses) {
tracing::debug!(
"Ignoring network interface {} ({:?})",
entry.name,
entry.addresses
);
continue;
}
let mut has_v6 = false;
for address in &entry.addresses {
match address {
// IPv4 group membership is joined per address.
IpAddr::V4(address) => result.v4.push(LocalInterfaceV4 {
name: entry.name.clone(),
address: *address,
}),
IpAddr::V6(_) => has_v6 = true,
}
}
if has_v6 {
match entry.index {
Some(index) => result.v6.push(LocalInterfaceV6 {
name: entry.name,
index,
}),
// Joining an IPv6 group requires the interface index.
None => tracing::debug!(
"Interface {} has no index, not using it for IPv6 multicast",
entry.name
),
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(address: &str) -> IpAddr {
address.parse().unwrap()
}
#[test]
fn test_exact_pattern() {
assert!(matches_ip_filter("192.168.1.42", "192.168.1.42"));
assert!(!matches_ip_filter("192.168.1.42", "192.168.1.43"));
}
#[test]
fn test_wildcard_does_not_cross_dots() {
assert!(matches_ip_filter("192.168.1.*", "192.168.1.42"));
assert!(!matches_ip_filter("192.168.1.*", "192.168.1.42.1"));
assert!(!matches_ip_filter("192.168.*", "192.168.1.42"));
assert!(matches_ip_filter("192.168.*.42", "192.168.1.42"));
}
#[test]
fn test_wildcard_requires_at_least_one_character() {
assert!(!matches_ip_filter("192.168.1.*", "192.168.1."));
}
#[test]
fn test_multiple_wildcards() {
assert!(matches_ip_filter("192.*.*.42", "192.168.1.42"));
assert!(!matches_ip_filter("192.*.*.42", "192.168.1.43"));
}
#[test]
fn test_ipv6_wildcard() {
assert!(matches_ip_filter("1::1:*:3", "1::1:2:3"));
assert!(!matches_ip_filter("1::1:*:3", "1::1:2:4"));
}
#[test]
fn test_no_filter_accepts_everything() {
let filter = InterfaceFilter::default();
assert!(!filter.is_ignored(&[ip("192.168.1.42")]));
}
#[test]
fn test_whitelist_skips_non_matching_interfaces() {
let filter = InterfaceFilter {
whitelist: Some(vec!["192.168.1.*".to_string()]),
blacklist: None,
};
assert!(!filter.is_ignored(&[ip("192.168.1.42")]));
assert!(filter.is_ignored(&[ip("10.0.0.1")]));
// A single matching address is enough to keep the interface.
assert!(!filter.is_ignored(&[ip("10.0.0.1"), ip("192.168.1.42")]));
}
#[test]
fn test_blacklist_skips_matching_interfaces() {
let filter = InterfaceFilter {
whitelist: None,
blacklist: Some(vec!["10.0.0.*".to_string()]),
};
assert!(!filter.is_ignored(&[ip("192.168.1.42")]));
assert!(filter.is_ignored(&[ip("10.0.0.1")]));
// A single matching address is enough to drop the interface.
assert!(filter.is_ignored(&[ip("192.168.1.42"), ip("10.0.0.1")]));
}
#[test]
fn test_blacklist_wins_over_whitelist() {
let filter = InterfaceFilter {
whitelist: Some(vec!["192.168.*.*".to_string()]),
blacklist: Some(vec!["192.168.1.*".to_string()]),
};
assert!(!filter.is_ignored(&[ip("192.168.2.42")]));
assert!(filter.is_ignored(&[ip("192.168.1.42")]));
}
}
+414
View File
@@ -0,0 +1,414 @@
//! UDP multicast discovery for the LocalSend protocol (v2.1).
//!
//! Devices announce themselves by sending a [`MulticastMessageV2`] to the
//! multicast group. Announcements are only ever sent, never answered over UDP:
//! the answer is an HTTP register request to the announcing device, which is
//! left to the application.
//!
//! [`start`] binds the sockets and emits a [`MulticastEvent`] per announcement;
//! the returned [`MulticastHandle`] drives the application-initiated side.
mod interface;
mod socket;
pub use interface::InterfaceFilter;
use crate::model::discovery::{DeviceType, MulticastMessageV2, ProtocolTypeV2};
use serde::Serialize;
use socket::MulticastSocket;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, oneshot, Mutex, RwLock};
use tokio_util::sync::CancellationToken;
/// The multicast group used by LocalSend.
///
/// It is inside `224.0.0.0/24` because on some Android devices this is the only
/// IP range that can receive UDP multicast messages.
pub const DEFAULT_MULTICAST_GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 167);
/// The IPv6 multicast group used by LocalSend, a transient (`ff1x::`) group
/// with link-local scope.
///
/// IPv6 discovery is a LocalSend extension on top of protocol v2.1, so IPv4
/// remains the baseline and IPv6 is announced in parallel.
pub const DEFAULT_MULTICAST_GROUP_V6: Ipv6Addr =
Ipv6Addr::new(0xff12, 0, 0, 0, 0, 0, 0xfd3a, 0xe420);
/// The default multicast port, identical to the default HTTP server port.
pub const DEFAULT_PORT: u16 = 53317;
/// Delays before each message of an announcement burst.
///
/// A single datagram is easily lost and devices that just joined the network
/// may not be ready to answer yet, so an announcement is repeated.
const ANNOUNCE_DELAYS: [Duration; 3] = [
Duration::from_millis(100),
Duration::from_millis(500),
Duration::from_millis(2000),
];
/// The largest datagram that is read at once. Announcements are far smaller;
/// anything bigger is truncated and then fails to parse.
const RECEIVE_BUFFER_SIZE: usize = 65536;
/// How many consecutive receive errors are tolerated before a socket is given
/// up on. Receiving may fail transiently (on Windows an ICMP error of a
/// previous send surfaces on the next receive).
const MAX_CONSECUTIVE_RECEIVE_ERRORS: u32 = 10;
/// The device information that is announced to the network.
#[derive(Clone, Debug)]
pub struct MulticastDevice {
/// The display name of this device.
pub alias: String,
/// Protocol version (major.minor) implemented by this device.
pub version: String,
/// Device model (e.g. "Samsung", "Windows").
pub device_model: Option<String>,
/// Device type category.
pub device_type: Option<DeviceType>,
/// Fingerprint identifying this device.
/// In HTTPS mode the SHA-256 hash of the certificate, otherwise a random string.
///
/// Messages carrying this fingerprint are ignored (multicast loopback).
pub fingerprint: String,
/// The port of this device's HTTP server, which is not necessarily the
/// multicast port.
pub port: u16,
/// Whether this device's HTTP server uses TLS.
pub protocol: ProtocolTypeV2,
/// Whether this device's download API is active.
pub download: bool,
}
impl MulticastDevice {
fn to_message(&self) -> MulticastMessageV2 {
MulticastMessageV2 {
alias: self.alias.clone(),
version: self.version.clone(),
device_model: self.device_model.clone(),
device_type: self.device_type.clone(),
fingerprint: self.fingerprint.clone(),
port: self.port,
protocol: self.protocol,
download: self.download,
}
}
}
/// Configuration of the multicast discovery.
pub struct MulticastConfig {
/// The multicast group to join, usually [`DEFAULT_MULTICAST_GROUP`].
pub group: Ipv4Addr,
/// The IPv6 multicast group to additionally join, usually
/// [`DEFAULT_MULTICAST_GROUP_V6`]. `None` disables IPv6 discovery.
///
/// A dual-stack peer is discovered over both families; deduplicating by
/// fingerprint is left to the application.
pub group_v6: Option<Ipv6Addr>,
/// The port to bind, usually [`DEFAULT_PORT`].
pub port: u16,
/// Restricts the network interfaces that are used.
pub interface_filter: InterfaceFilter,
/// The device information announced to the network.
pub device: MulticastDevice,
/// Channel on which discovery emits events that must be handled by the application.
pub event_tx: mpsc::Sender<MulticastEvent>,
}
/// An event emitted by the multicast discovery.
#[derive(Clone, Debug)]
pub enum MulticastEvent {
/// Another device announced itself.
///
/// The peer expects to be answered with an HTTP register request.
Discovered {
/// The address the datagram was sent from. The peer's HTTP server is
/// reachable at this address on `message.port`.
ip: IpAddr,
/// The scope (interface index) the datagram was received with.
/// Only set for IPv6 sources that carry one, i.e. link-local
/// addresses, which are unreachable without it.
scope_id: Option<u32>,
/// The message as it was received.
message: MulticastMessageV2,
},
}
/// A socket announcements are sent on, together with its target address.
#[derive(Clone)]
struct SendSocket {
target: SocketAddr,
socket: Arc<UdpSocket>,
}
struct MulticastState {
device: MulticastDevice,
/// The sockets used for sending. Emptied once discovery is stopped, which
/// releases the port and turns further sends into no-ops.
sockets: RwLock<Vec<SendSocket>>,
}
/// A [`MulticastMessageV2`] plus the legacy `announce` flag of protocol v2.
#[derive(Serialize)]
struct AnnouncedMessage {
#[serde(flatten)]
message: MulticastMessageV2,
announce: bool,
}
impl MulticastState {
/// Sends one announcement on every interface. Failing interfaces are
/// skipped: discovery on the remaining ones must keep working.
async fn send(&self) {
let message = AnnouncedMessage {
message: self.device.to_message(),
announce: true,
};
let payload = match serde_json::to_vec(&message) {
Ok(payload) => payload,
Err(err) => {
tracing::error!("Could not serialize multicast message: {err:#}");
return;
}
};
let sockets = self.sockets.read().await.clone();
for SendSocket { target, socket } in sockets {
if let Err(err) = socket.send_to(&payload, target).await {
tracing::warn!("Could not send multicast message to {target}: {err:#}");
}
}
}
}
/// A handle to a running multicast discovery, for interactions initiated by
/// the application.
pub struct MulticastHandle {
state: Arc<MulticastState>,
cancel: CancellationToken,
/// The task supervising the receive loops. Completes after a stop has been
/// requested and all sockets have been closed.
task: Mutex<Option<tokio::task::JoinHandle<()>>>,
}
impl MulticastHandle {
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
pub async fn announce(&self) {
for delay in ANNOUNCE_DELAYS {
tokio::select! {
_ = self.cancel.cancelled() => return,
_ = tokio::time::sleep(delay) => {}
}
tracing::debug!("Announcing via UDP multicast");
self.state.send().await;
}
}
/// Waits until discovery has terminated and all sockets have been closed,
/// so that the port can be bound again.
/// Must be called after requesting a stop via the stop channel.
pub async fn wait_stopped(&self) {
if let Some(task) = self.task.lock().await.take() {
let _ = task.await;
}
}
}
/// Binds one socket per usable network interface and starts listening for
/// discovery messages of other devices.
///
/// Nothing is sent until [`MulticastHandle::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: MulticastConfig,
stop_rx: oneshot::Receiver<()>,
) -> anyhow::Result<MulticastHandle> {
let sockets = socket::bind_multicast_sockets(
config.group,
config.group_v6,
config.port,
&config.interface_filter,
)?;
if sockets.is_empty() {
anyhow::bail!(
"No network interface available for multicast on port {}",
config.port
);
}
let state = Arc::new(MulticastState {
device: config.device,
sockets: RwLock::new(
sockets
.iter()
.map(|socket| SendSocket {
target: socket.target,
socket: socket.socket.clone(),
})
.collect(),
),
});
let cancel = CancellationToken::new();
let task = tokio::spawn({
let state = state.clone();
let cancel = cancel.clone();
async move {
let mut receivers = tokio::task::JoinSet::new();
for socket in sockets {
receivers.spawn(receive_loop(
socket,
state.clone(),
config.event_tx.clone(),
cancel.clone(),
));
}
tokio::select! {
// All receive loops gave up on their socket.
_ = async { while receivers.join_next().await.is_some() {} } => {}
_ = stop_rx => {}
}
cancel.cancel();
// Wait for the sockets to be released, so that the port is free
// once this task completes.
receivers.shutdown().await;
state.sockets.write().await.clear();
}
});
Ok(MulticastHandle {
state,
cancel,
task: Mutex::new(Some(task)),
})
}
/// Reads discovery messages from a single interface until discovery is stopped
/// or the socket keeps failing.
async fn receive_loop(
socket: MulticastSocket,
state: Arc<MulticastState>,
event_tx: mpsc::Sender<MulticastEvent>,
cancel: CancellationToken,
) {
let mut buffer = vec![0u8; RECEIVE_BUFFER_SIZE];
let mut consecutive_errors = 0;
loop {
let received = tokio::select! {
_ = cancel.cancelled() => return,
received = socket.socket.recv_from(&mut buffer) => received,
};
let (size, source) = match received {
Ok(received) => {
consecutive_errors = 0;
received
}
Err(err) => {
consecutive_errors += 1;
tracing::warn!(
"Could not receive multicast message on interface {}: {err:#}",
socket.description,
);
if consecutive_errors >= MAX_CONSECUTIVE_RECEIVE_ERRORS {
tracing::error!(
"Giving up on multicast interface {} after {consecutive_errors} consecutive errors",
socket.description,
);
return;
}
continue;
}
};
let message = match serde_json::from_slice::<MulticastMessageV2>(&buffer[..size]) {
Ok(message) => message,
Err(err) => {
tracing::warn!("Could not parse multicast message from {source}: {err:#}");
continue;
}
};
// Loopback is enabled, so our own messages come back as well.
if message.fingerprint == state.device.fingerprint {
continue;
}
let event = MulticastEvent::Discovered {
ip: source.ip(),
scope_id: match &source {
SocketAddr::V6(source) if source.scope_id() != 0 => Some(source.scope_id()),
_ => None,
},
message,
};
if event_tx.send(event).await.is_err() {
tracing::debug!("Multicast event receiver dropped, stopping discovery");
return;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sent_message_carries_legacy_announce_flag() {
let device = MulticastDevice {
alias: "Nice Orange".to_string(),
version: "2.1".to_string(),
device_model: None,
device_type: Some(DeviceType::Desktop),
fingerprint: "my-fingerprint".to_string(),
port: 53317,
protocol: ProtocolTypeV2::Https,
download: false,
};
let json = serde_json::to_string(&AnnouncedMessage {
message: device.to_message(),
announce: true,
})
.unwrap();
assert!(json.contains("\"announce\":true"));
assert!(json.contains("\"alias\":\"Nice Orange\""));
assert!(json.contains("\"fingerprint\":\"my-fingerprint\""));
assert!(json.contains("\"deviceType\":\"desktop\""));
assert!(!json.contains("deviceModel"));
}
}
+155
View File
@@ -0,0 +1,155 @@
//! Binding of the UDP sockets that carry the multicast discovery traffic.
use crate::multicast::interface::{local_interfaces, InterfaceFilter};
use socket2::{Domain, Protocol, Socket, Type};
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use std::sync::Arc;
use tokio::net::UdpSocket;
/// A socket that is joined to the multicast group on a single interface.
pub(crate) struct MulticastSocket {
/// Describes the interface this socket is bound to, only used for logging.
pub(crate) description: String,
/// The group address announcements are sent to through this socket.
pub(crate) target: SocketAddr,
pub(crate) socket: Arc<UdpSocket>,
}
/// Binds and joins the multicast groups on every interface that passes
/// `filter`: one IPv4 socket per interface address, and — when `group_v6` is
/// set — one IPv6 socket per interface.
///
/// One socket per interface is required because a socket only sends on a single
/// interface.
///
/// Interfaces that cannot be bound or joined are skipped, so that a single
/// unusable interface (e.g. a virtual adapter) does not disable discovery.
pub(crate) fn bind_multicast_sockets(
group: Ipv4Addr,
group_v6: Option<Ipv6Addr>,
port: u16,
filter: &InterfaceFilter,
) -> std::io::Result<Vec<MulticastSocket>> {
let interfaces = local_interfaces(filter)?;
let mut sockets = Vec::new();
for interface in interfaces.v4 {
let description = format!("{} ({})", interface.name, interface.address);
match bind_multicast_socket_v4(group, port, interface.address) {
Ok(socket) => {
tracing::info!(
"Bound UDP multicast socket (interface: {description}, group: {group}, port: {port})",
);
sockets.push(MulticastSocket {
description,
target: SocketAddr::from(SocketAddrV4::new(group, port)),
socket: Arc::new(socket),
});
}
Err(err) => {
tracing::warn!(
"Could not bind UDP multicast socket (interface: {description}, group: {group}, port: {port}): {err:#}",
);
}
}
}
if let Some(group) = group_v6 {
for interface in interfaces.v6 {
let description = format!("{} (IPv6, if-index {})", interface.name, interface.index);
match bind_multicast_socket_v6(group, port, interface.index) {
Ok(socket) => {
tracing::info!(
"Bound UDP multicast socket (interface: {description}, group: {group}, port: {port})",
);
sockets.push(MulticastSocket {
description,
target: SocketAddr::from(SocketAddrV6::new(
group,
port,
0,
interface.index,
)),
socket: Arc::new(socket),
});
}
Err(err) => {
tracing::warn!(
"Could not bind UDP multicast socket (interface: {description}, group: {group}, port: {port}): {err:#}",
);
}
}
}
}
Ok(sockets)
}
fn bind_multicast_socket_v4(
group: Ipv4Addr,
port: u16,
interface: Ipv4Addr,
) -> std::io::Result<UdpSocket> {
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
// All sockets share the same port. Windows has no `SO_REUSEPORT`.
socket.set_reuse_address(true)?;
#[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
socket.set_reuse_port(true)?;
// Binding to the wildcard address instead of the interface address is what
// makes the socket receive multicast datagrams on platforms that match the
// destination address against the bound address.
socket.bind(&SocketAddr::from(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)).into())?;
socket.join_multicast_v4(&group, &interface)?;
// Pin outgoing datagrams to this interface, otherwise the routing table
// decides and every socket would announce on the same one.
socket.set_multicast_if_v4(&interface)?;
// Keep loopback enabled so that several instances on the same host can see
// each other; own messages are filtered out by fingerprint.
socket.set_multicast_loop_v4(true)?;
// Discovery is limited to the local subnet.
socket.set_multicast_ttl_v4(1)?;
socket.set_nonblocking(true)?;
UdpSocket::from_std(socket.into())
}
/// Mirrors [`bind_multicast_socket_v4`], but joins the group by interface
/// index instead of by address.
fn bind_multicast_socket_v6(
group: Ipv6Addr,
port: u16,
interface: u32,
) -> std::io::Result<UdpSocket> {
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
// A dual-stack socket would clash with the IPv4 sockets on the same port.
socket.set_only_v6(true)?;
socket.set_reuse_address(true)?;
#[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
socket.set_reuse_port(true)?;
socket.bind(&SocketAddr::from(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, port, 0, 0)).into())?;
socket.join_multicast_v6(&group, interface)?;
socket.set_multicast_if_v6(interface)?;
socket.set_multicast_loop_v6(true)?;
// Discovery is limited to the local link (the group's scope already is).
socket.set_multicast_hops_v6(1)?;
socket.set_nonblocking(true)?;
UdpSocket::from_std(socket.into())
}
+241
View File
@@ -0,0 +1,241 @@
#![cfg(feature = "multicast")]
//! Discovery tests that exercise real multicast traffic between two instances.
//!
//! Whether multicast datagrams are delivered depends on the machine (no network
//! interface, a firewall dropping the group), so these tests skip themselves
//! instead of failing when the environment does not carry the traffic.
use localsend::model::discovery::{DeviceType, ProtocolTypeV2, PROTOCOL_VERSION_V2};
use localsend::multicast::{
self, MulticastConfig, MulticastDevice, MulticastEvent, MulticastHandle,
};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicU16, Ordering};
use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
/// Ports are not reused between tests: a lingering membership of a stopped
/// instance would leak messages into the next test.
static NEXT_PORT: AtomicU16 = AtomicU16::new(54317);
/// Like the group the protocol uses, but distinct from it so that real
/// LocalSend instances on the machine stay out.
const TEST_GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 168);
/// See [TEST_GROUP].
const TEST_GROUP_V6: Ipv6Addr = Ipv6Addr::new(0xff12, 0, 0, 0, 0, 0, 0xfd3a, 0xe421);
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
struct TestInstance {
handle: MulticastHandle,
events: mpsc::Receiver<MulticastEvent>,
_stop_tx: oneshot::Sender<()>,
}
impl TestInstance {
/// Waits for the next discovery of the device with the given fingerprint,
/// ignoring messages of unrelated devices that may be on the network.
async fn next_discovery(&mut self, fingerprint: &str) -> Option<MulticastMessage> {
let deadline = tokio::time::Instant::now() + RECEIVE_TIMEOUT;
loop {
let event = tokio::time::timeout_at(deadline, self.events.recv())
.await
.ok()??;
let MulticastEvent::Discovered {
ip,
scope_id,
message,
} = event;
if message.fingerprint == fingerprint {
return Some(MulticastMessage {
source: ip,
scope_id,
alias: message.alias,
port: message.port,
});
}
}
}
}
/// The parts of a received message the tests assert on.
struct MulticastMessage {
source: IpAddr,
scope_id: Option<u32>,
alias: String,
port: u16,
}
/// Starts an instance on `port`, or returns `None` when this machine has no
/// interface that multicast can be bound to.
async fn start_instance(alias: &str, port: u16) -> Option<TestInstance> {
let (event_tx, events) = mpsc::channel(32);
let (stop_tx, stop_rx) = oneshot::channel();
let handle = multicast::start(
MulticastConfig {
group: TEST_GROUP,
group_v6: Some(TEST_GROUP_V6),
port,
interface_filter: Default::default(),
device: MulticastDevice {
alias: alias.to_string(),
version: PROTOCOL_VERSION_V2.to_string(),
device_model: Some("Test".to_string()),
device_type: Some(DeviceType::Headless),
fingerprint: format!("fingerprint-of-{alias}"),
port,
protocol: ProtocolTypeV2::Https,
download: false,
},
event_tx,
},
stop_rx,
)
.await
.ok()?;
Some(TestInstance {
handle,
events,
_stop_tx: stop_tx,
})
}
fn skip(reason: &str) {
eprintln!("skipping multicast test: {reason}");
}
#[tokio::test]
async fn test_announcement_is_received_by_other_instance() {
let port = NEXT_PORT.fetch_add(1, Ordering::Relaxed);
let Some(sender) = start_instance("Sender", port).await else {
return skip("no network interface available for multicast");
};
let Some(mut receiver) = start_instance("Receiver", port).await else {
return skip("no network interface available for multicast");
};
sender.handle.announce().await;
let Some(message) = receiver.next_discovery("fingerprint-of-Sender").await else {
return skip("multicast traffic is not delivered on this machine");
};
assert_eq!(message.alias, "Sender");
assert_eq!(
message.port, port,
"the peer's HTTP port must be carried in the announcement, \
so that the response can be sent over HTTP"
);
}
#[tokio::test]
async fn test_announcement_is_received_over_ipv6() {
let port = NEXT_PORT.fetch_add(1, Ordering::Relaxed);
let Some(sender) = start_instance("Sender6", port).await else {
return skip("no network interface available for multicast");
};
let Some(mut receiver) = start_instance("Receiver6", port).await else {
return skip("no network interface available for multicast");
};
sender.handle.announce().await;
// The same announcement also arrives over IPv4; wait for an IPv6 source.
// The loop ends because `next_discovery` times out once the finite
// announcement burst has been drained.
loop {
match receiver.next_discovery("fingerprint-of-Sender6").await {
Some(message) if message.source.is_ipv6() => {
assert_eq!(message.alias, "Sender6");
assert_eq!(message.port, port);
if let IpAddr::V6(source) = message.source {
if source.is_unicast_link_local() {
assert!(
message.scope_id.is_some(),
"a link-local source must carry its scope"
);
}
}
return;
}
Some(_) => continue, // Discovered over IPv4, keep waiting.
None => return skip("IPv6 multicast traffic is not delivered on this machine"),
}
}
}
#[tokio::test]
async fn test_own_messages_are_not_discovered() {
let port = NEXT_PORT.fetch_add(1, Ordering::Relaxed);
let Some(mut instance) = start_instance("Lonely", port).await else {
return skip("no network interface available for multicast");
};
instance.handle.announce().await;
// The announcement comes back through the loopback and must be filtered
// out by fingerprint.
let own = tokio::time::timeout(Duration::from_secs(1), async {
instance.next_discovery("fingerprint-of-Lonely").await
})
.await;
assert!(
matches!(own, Err(_) | Ok(None)),
"a device must not discover itself"
);
}
#[tokio::test]
async fn test_port_is_released_after_stop() {
let port = NEXT_PORT.fetch_add(1, Ordering::Relaxed);
let (event_tx, _events) = mpsc::channel(32);
let (stop_tx, stop_rx) = oneshot::channel();
let handle = multicast::start(
MulticastConfig {
group: TEST_GROUP,
group_v6: Some(TEST_GROUP_V6),
port,
interface_filter: Default::default(),
device: MulticastDevice {
alias: "Restarting".to_string(),
version: PROTOCOL_VERSION_V2.to_string(),
device_model: None,
device_type: None,
fingerprint: "fingerprint-of-Restarting".to_string(),
port,
protocol: ProtocolTypeV2::Http,
download: false,
},
event_tx,
},
stop_rx,
)
.await;
let Ok(handle) = handle else {
return skip("no network interface available for multicast");
};
stop_tx.send(()).unwrap();
handle.wait_stopped().await;
// Sending after a stop must not panic, it simply reaches nobody.
handle.announce().await;
assert!(
start_instance("Restarted", port).await.is_some(),
"the port must be bindable again after the discovery stopped"
);
}
@@ -1,55 +1,6 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_isolates/constants.dart';
import 'package:localsend_isolates/model/device.dart';
part 'multicast_dto.mapper.dart';
@MappableEnum(defaultValue: ProtocolType.https)
enum ProtocolType { http, https }
@MappableClass()
class MulticastDto with MulticastDtoMappable {
final String alias;
final String? version; // v2, format: major.minor
final String? deviceModel;
final DeviceType? deviceType; // nullable since v2
final String fingerprint;
final int? port; // v2
final ProtocolType? protocol; // v2
final bool? download; // v2
final bool? announcement; // v1
final bool? announce; // v2
const MulticastDto({
required this.alias,
required this.version,
required this.deviceModel,
required this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
required this.announcement,
required this.announce,
});
static const fromJson = MulticastDtoMapper.fromJson;
}
extension MulticastDtoToDeviceExt on MulticastDto {
Device toDevice(String ip, int ownPort, bool ownHttps) {
return Device(
signalingId: null,
ip: ip,
version: version ?? fallbackProtocolVersion,
port: port ?? ownPort,
https: protocol != null ? protocol == ProtocolType.https : ownHttps,
fingerprint: fingerprint,
alias: alias,
deviceModel: deviceModel,
deviceType: deviceType ?? DeviceType.desktop,
download: download ?? false,
discoveryMethods: {MulticastDiscovery()},
);
}
}
@@ -54,224 +54,3 @@ extension ProtocolTypeMapperExtension on ProtocolType {
}
}
class MulticastDtoMapper extends ClassMapperBase<MulticastDto> {
MulticastDtoMapper._();
static MulticastDtoMapper? _instance;
static MulticastDtoMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = MulticastDtoMapper._());
DeviceTypeMapper.ensureInitialized();
ProtocolTypeMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'MulticastDto';
static String _$alias(MulticastDto v) => v.alias;
static const Field<MulticastDto, String> _f$alias = Field('alias', _$alias);
static String? _$version(MulticastDto v) => v.version;
static const Field<MulticastDto, String> _f$version = Field(
'version',
_$version,
);
static String? _$deviceModel(MulticastDto v) => v.deviceModel;
static const Field<MulticastDto, String> _f$deviceModel = Field(
'deviceModel',
_$deviceModel,
);
static DeviceType? _$deviceType(MulticastDto v) => v.deviceType;
static const Field<MulticastDto, DeviceType> _f$deviceType = Field(
'deviceType',
_$deviceType,
);
static String _$fingerprint(MulticastDto v) => v.fingerprint;
static const Field<MulticastDto, String> _f$fingerprint = Field(
'fingerprint',
_$fingerprint,
);
static int? _$port(MulticastDto v) => v.port;
static const Field<MulticastDto, int> _f$port = Field('port', _$port);
static ProtocolType? _$protocol(MulticastDto v) => v.protocol;
static const Field<MulticastDto, ProtocolType> _f$protocol = Field(
'protocol',
_$protocol,
);
static bool? _$download(MulticastDto v) => v.download;
static const Field<MulticastDto, bool> _f$download = Field(
'download',
_$download,
);
static bool? _$announcement(MulticastDto v) => v.announcement;
static const Field<MulticastDto, bool> _f$announcement = Field(
'announcement',
_$announcement,
);
static bool? _$announce(MulticastDto v) => v.announce;
static const Field<MulticastDto, bool> _f$announce = Field(
'announce',
_$announce,
);
@override
final MappableFields<MulticastDto> fields = const {
#alias: _f$alias,
#version: _f$version,
#deviceModel: _f$deviceModel,
#deviceType: _f$deviceType,
#fingerprint: _f$fingerprint,
#port: _f$port,
#protocol: _f$protocol,
#download: _f$download,
#announcement: _f$announcement,
#announce: _f$announce,
};
static MulticastDto _instantiate(DecodingData data) {
return MulticastDto(
alias: data.dec(_f$alias),
version: data.dec(_f$version),
deviceModel: data.dec(_f$deviceModel),
deviceType: data.dec(_f$deviceType),
fingerprint: data.dec(_f$fingerprint),
port: data.dec(_f$port),
protocol: data.dec(_f$protocol),
download: data.dec(_f$download),
announcement: data.dec(_f$announcement),
announce: data.dec(_f$announce),
);
}
@override
final Function instantiate = _instantiate;
static MulticastDto fromJson(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<MulticastDto>(map);
}
static MulticastDto deserialize(String json) {
return ensureInitialized().decodeJson<MulticastDto>(json);
}
}
mixin MulticastDtoMappable {
String serialize() {
return MulticastDtoMapper.ensureInitialized().encodeJson<MulticastDto>(
this as MulticastDto,
);
}
Map<String, dynamic> toJson() {
return MulticastDtoMapper.ensureInitialized().encodeMap<MulticastDto>(
this as MulticastDto,
);
}
MulticastDtoCopyWith<MulticastDto, MulticastDto, MulticastDto> get copyWith =>
_MulticastDtoCopyWithImpl<MulticastDto, MulticastDto>(
this as MulticastDto,
$identity,
$identity,
);
@override
String toString() {
return MulticastDtoMapper.ensureInitialized().stringifyValue(
this as MulticastDto,
);
}
@override
bool operator ==(Object other) {
return MulticastDtoMapper.ensureInitialized().equalsValue(
this as MulticastDto,
other,
);
}
@override
int get hashCode {
return MulticastDtoMapper.ensureInitialized().hashValue(
this as MulticastDto,
);
}
}
extension MulticastDtoValueCopy<$R, $Out>
on ObjectCopyWith<$R, MulticastDto, $Out> {
MulticastDtoCopyWith<$R, MulticastDto, $Out> get $asMulticastDto =>
$base.as((v, t, t2) => _MulticastDtoCopyWithImpl<$R, $Out>(v, t, t2));
}
abstract class MulticastDtoCopyWith<$R, $In extends MulticastDto, $Out>
implements ClassCopyWith<$R, $In, $Out> {
$R call({
String? alias,
String? version,
String? deviceModel,
DeviceType? deviceType,
String? fingerprint,
int? port,
ProtocolType? protocol,
bool? download,
bool? announcement,
bool? announce,
});
MulticastDtoCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _MulticastDtoCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, MulticastDto, $Out>
implements MulticastDtoCopyWith<$R, MulticastDto, $Out> {
_MulticastDtoCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<MulticastDto> $mapper =
MulticastDtoMapper.ensureInitialized();
@override
$R call({
String? alias,
Object? version = $none,
Object? deviceModel = $none,
Object? deviceType = $none,
String? fingerprint,
Object? port = $none,
Object? protocol = $none,
Object? download = $none,
Object? announcement = $none,
Object? announce = $none,
}) => $apply(
FieldCopyWithData({
if (alias != null) #alias: alias,
if (version != $none) #version: version,
if (deviceModel != $none) #deviceModel: deviceModel,
if (deviceType != $none) #deviceType: deviceType,
if (fingerprint != null) #fingerprint: fingerprint,
if (port != $none) #port: port,
if (protocol != $none) #protocol: protocol,
if (download != $none) #download: download,
if (announcement != $none) #announcement: announcement,
if (announce != $none) #announce: announce,
}),
);
@override
MulticastDto $make(CopyWithData data) => MulticastDto(
alias: data.get(#alias, or: $value.alias),
version: data.get(#version, or: $value.version),
deviceModel: data.get(#deviceModel, or: $value.deviceModel),
deviceType: data.get(#deviceType, or: $value.deviceType),
fingerprint: data.get(#fingerprint, or: $value.fingerprint),
port: data.get(#port, or: $value.port),
protocol: data.get(#protocol, or: $value.protocol),
download: data.get(#download, or: $value.download),
announcement: data.get(#announcement, or: $value.announcement),
announce: data.get(#announce, or: $value.announce),
);
@override
MulticastDtoCopyWith<$R2, MulticastDto, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t,
) => _MulticastDtoCopyWithImpl<$R2, $Out2>($value, $cast, t);
}
@@ -0,0 +1,141 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/frb_generated.dart';
/// Starts UDP multicast discovery: binds the multicast sockets on all usable
/// network interfaces and listens for announcements of other devices.
///
/// Announcements are sent to the IPv4 [group] and, as a LocalSend extension,
/// to the (currently hardcoded) IPv6 group `ff12::fd3a:e420`.
///
/// [port] is used both to bind the multicast sockets and as the HTTP server
/// port announced to other devices.
///
/// Nothing is announced until [RsMulticast::announce] is called.
///
/// Fails when [group] is not a valid IPv4 address or when no network
/// interface could be used at all.
Future<RsMulticast> startMulticast({
required String group,
required int port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
required String alias,
required String version,
String? deviceModel,
DeviceType? deviceType,
required String fingerprint,
required ProtocolTypeV2 protocol,
required bool download,
}) => RustLib.instance.api.crateApiMulticastStartMulticast(
group: group,
port: port,
networkWhitelist: networkWhitelist,
networkBlacklist: networkBlacklist,
alias: alias,
version: version,
deviceModel: deviceModel,
deviceType: deviceType,
fingerprint: fingerprint,
protocol: protocol,
download: download,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>
abstract class RsMulticast implements RustOpaqueInterface {
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
Future<void> announce();
/// Emits a [RsMulticastDiscovered] for every announcement received from
/// another device until discovery is stopped.
/// Can only be listened to once.
Stream<RsMulticastDiscovered> listen();
/// Stops discovery, which also ends the [RsMulticast::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
Future<void> stop();
}
class MulticastMessageV2 {
final String alias;
final String version;
final String? deviceModel;
final DeviceType? deviceType;
final String fingerprint;
final int port;
final ProtocolTypeV2 protocol;
final bool download;
const MulticastMessageV2({
required this.alias,
required this.version,
this.deviceModel,
this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
});
@override
int get hashCode =>
alias.hashCode ^
version.hashCode ^
deviceModel.hashCode ^
deviceType.hashCode ^
fingerprint.hashCode ^
port.hashCode ^
protocol.hashCode ^
download.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MulticastMessageV2 &&
runtimeType == other.runtimeType &&
alias == other.alias &&
version == other.version &&
deviceModel == other.deviceModel &&
deviceType == other.deviceType &&
fingerprint == other.fingerprint &&
port == other.port &&
protocol == other.protocol &&
download == other.download;
}
/// Another device announced itself via UDP multicast.
///
/// The peer expects to be answered with an HTTP register request.
class RsMulticastDiscovered {
/// The address the announcement was sent from. The peer's HTTP server is
/// reachable at this address on `message.port`.
///
/// A link-local IPv6 source carries its interface scope as `fe80::1%3`,
/// which the Rust HTTP client accepts back as a host.
final String ip;
/// The announcement as it was received.
final MulticastMessageV2 message;
const RsMulticastDiscovered({
required this.ip,
required this.message,
});
@override
int get hashCode => ip.hashCode ^ message.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) || other is RsMulticastDiscovered && runtimeType == other.runtimeType && ip == other.ip && message == other.message;
}
@@ -12,6 +12,7 @@ import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/api/stream.dart';
import 'package:localsend_isolates/rust/api/webrtc.dart';
@@ -73,7 +74,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => 1193619754;
int get rustContentHash => -2071906741;
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
stem: 'rust_lib_localsend_app',
@@ -184,6 +185,12 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiServerRsHttpServerStop({required RsHttpServer that});
Future<void> crateApiMulticastRsMulticastAnnounce({required RsMulticast that});
Stream<RsMulticastDiscovered> crateApiMulticastRsMulticastListen({required RsMulticast that});
Future<void> crateApiMulticastRsMulticastStop({required RsMulticast that});
Future<String> crateApiWebrtcRtcFileReceiverGetFileId({required RtcFileReceiver that});
Stream<Uint8List> crateApiWebrtcRtcFileReceiverReceive({required RtcFileReceiver that});
@@ -241,6 +248,20 @@ abstract class RustLibApi extends BaseApi {
Future<String> crateApiCryptoHashFile({String? path, int? fileDescriptor, Uint8List? bytes, required RsCancellationToken cancelToken});
Future<RsMulticast> crateApiMulticastStartMulticast({
required String group,
required int port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
required String alias,
required String version,
String? deviceModel,
DeviceType? deviceType,
required String fingerprint,
required ProtocolTypeV2 protocol,
required bool download,
});
Future<RsHttpServer> crateApiServerStartServer({
required int port,
TlsConfig? tls,
@@ -315,6 +336,12 @@ abstract class RustLibApi extends BaseApi {
RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_RsHttpServer;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpServerPtr;
RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_RsMulticast;
RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_RsMulticast;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsMulticastPtr;
}
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@@ -951,6 +978,86 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ['that'],
);
@override
Future<void> crateApiMulticastRsMulticastAnnounce({required RsMulticast that}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(that, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: null,
),
constMeta: kCrateApiMulticastRsMulticastAnnounceConstMeta,
argValues: [that],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMulticastRsMulticastAnnounceConstMeta => const TaskConstMeta(
debugName: 'RsMulticast_announce',
argNames: ['that'],
);
@override
Stream<RsMulticastDiscovered> crateApiMulticastRsMulticastListen({required RsMulticast that}) {
final sink = RustStreamSink<RsMulticastDiscovered>();
unawaited(
handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(that, serializer);
sse_encode_StreamSink_rs_multicast_discovered_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: null,
),
constMeta: kCrateApiMulticastRsMulticastListenConstMeta,
argValues: [that, sink],
apiImpl: this,
),
),
);
return sink.stream;
}
TaskConstMeta get kCrateApiMulticastRsMulticastListenConstMeta => const TaskConstMeta(
debugName: 'RsMulticast_listen',
argNames: ['that', 'sink'],
);
@override
Future<void> crateApiMulticastRsMulticastStop({required RsMulticast that}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(that, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: null,
),
constMeta: kCrateApiMulticastRsMulticastStopConstMeta,
argValues: [that],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMulticastRsMulticastStopConstMeta => const TaskConstMeta(
debugName: 'RsMulticast_stop',
argNames: ['that'],
);
@override
Future<String> crateApiWebrtcRtcFileReceiverGetFileId({required RtcFileReceiver that}) {
return handler.executeNormal(
@@ -958,7 +1065,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileReceiver(that, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -986,7 +1093,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileReceiver(that, serializer);
sse_encode_StreamSink_list_prim_u_8_strict_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1014,7 +1121,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileSender(that, serializer);
sse_encode_list_prim_u_8_loose(data, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1039,7 +1146,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1067,7 +1174,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
sse_encode_StreamSink_rtc_file_error_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1094,7 +1201,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_file_dto,
@@ -1122,7 +1229,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
sse_encode_StreamSink_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileReceiver_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1152,7 +1259,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
sse_encode_StreamSink_rtc_status_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1180,7 +1287,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
sse_encode_box_autoadd_rtc_send_file_response(status, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1206,7 +1313,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
sse_encode_String(pin, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1232,7 +1339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCReceiveController(that, serializer);
sse_encode_Set_String_None(selection, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1260,7 +1367,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer);
sse_encode_StreamSink_rtc_file_error_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1287,7 +1394,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_Set_String_None,
@@ -1315,7 +1422,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer);
sse_encode_StreamSink_rtc_status_Sse(sink, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1343,7 +1450,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer);
sse_encode_String(fileId, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCFileSender,
@@ -1369,7 +1476,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRTCSendController(that, serializer);
sse_encode_String(pin, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1408,7 +1515,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
onConnection,
serializer,
);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1434,7 +1541,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken,
@@ -1469,7 +1576,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_ls_http_client_version(version, serializer);
sse_encode_opt_String(expectedFingerprint, serializer);
sse_encode_opt_box_autoadd_u_32(timeoutMs, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 41)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient,
@@ -1493,7 +1600,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42, port: port_);
},
codec: SseCodec(
decodeSuccessData:
@@ -1518,7 +1625,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1542,7 +1649,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 41, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_key_pair,
@@ -1570,7 +1677,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_box_autoadd_i_32(fileDescriptor, serializer);
sse_encode_opt_list_prim_u_8_strict(bytes, serializer);
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(cancelToken, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -1588,6 +1695,65 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ['path', 'fileDescriptor', 'bytes', 'cancelToken'],
);
@override
Future<RsMulticast> crateApiMulticastStartMulticast({
required String group,
required int port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
required String alias,
required String version,
String? deviceModel,
DeviceType? deviceType,
required String fingerprint,
required ProtocolTypeV2 protocol,
required bool download,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(group, serializer);
sse_encode_u_16(port, serializer);
sse_encode_opt_list_String(networkWhitelist, serializer);
sse_encode_opt_list_String(networkBlacklist, serializer);
sse_encode_String(alias, serializer);
sse_encode_String(version, serializer);
sse_encode_opt_String(deviceModel, serializer);
sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
sse_encode_String(fingerprint, serializer);
sse_encode_protocol_type_v_2(protocol, serializer);
sse_encode_bool(download, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 46, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateApiMulticastStartMulticastConstMeta,
argValues: [group, port, networkWhitelist, networkBlacklist, alias, version, deviceModel, deviceType, fingerprint, protocol, download],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiMulticastStartMulticastConstMeta => const TaskConstMeta(
debugName: 'start_multicast',
argNames: [
'group',
'port',
'networkWhitelist',
'networkBlacklist',
'alias',
'version',
'deviceModel',
'deviceType',
'fingerprint',
'protocol',
'download',
],
);
@override
Future<RsHttpServer> crateApiServerStartServer({
required int port,
@@ -1615,7 +1781,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_String(pin, serializer);
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
sse_encode_opt_String(showToken, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer,
@@ -1641,7 +1807,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(cert, serializer);
sse_encode_String(publicKey, serializer);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44, port: port_);
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 48, port: port_);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -1749,6 +1915,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_RsHttpServer =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer;
RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_RsMulticast =>
wire.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast;
RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_RsMulticast =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1815,6 +1987,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RsHttpServerImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
RsMulticast dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return RsMulticastImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
Dart2RustStreamSink dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1875,6 +2053,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RsHttpServerImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
RsMulticast dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return RsMulticastImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
FutureOr<void> Function(LsSignalingConnection)
dco_decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -1962,6 +2146,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RsHttpServerImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
RsMulticast dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return RsMulticastImpl.frbInternalDcoDecode(raw as List<dynamic>);
}
@protected
Set<String> dco_decode_Set_String_None(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1988,6 +2178,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError();
}
@protected
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
throw UnimplementedError();
}
@protected
RustStreamSink<RsServerEvent> dco_decode_StreamSink_rs_server_event_Sse(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2296,6 +2492,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return LsHttpClientVersion.values[raw as int];
}
@protected
MulticastMessageV2 dco_decode_multicast_message_v_2(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 8) throw Exception('unexpected arr length: expect 8 but see ${arr.length}');
return MulticastMessageV2(
alias: dco_decode_String(arr[0]),
version: dco_decode_String(arr[1]),
deviceModel: dco_decode_opt_String(arr[2]),
deviceType: dco_decode_opt_box_autoadd_device_type(arr[3]),
fingerprint: dco_decode_String(arr[4]),
port: dco_decode_u_16(arr[5]),
protocol: dco_decode_protocol_type_v_2(arr[6]),
download: dco_decode_bool(arr[7]),
);
}
@protected
String? dco_decode_opt_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2578,6 +2791,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
RsMulticastDiscovered dco_decode_rs_multicast_discovered(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
return RsMulticastDiscovered(
ip: dco_decode_String(arr[0]),
message: dco_decode_multicast_message_v_2(arr[1]),
);
}
@protected
RsServerEvent dco_decode_rs_server_event(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2897,6 +3121,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RsHttpServerImpl.frbInternalSseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer));
}
@protected
RsMulticast sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return RsMulticastImpl.frbInternalSseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer));
}
@protected
Dart2RustStreamSink sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
SseDeserializer deserializer,
@@ -2969,6 +3199,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RsHttpServerImpl.frbInternalSseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer));
}
@protected
RsMulticast sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return RsMulticastImpl.frbInternalSseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer));
}
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -3052,6 +3288,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RsHttpServerImpl.frbInternalSseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer));
}
@protected
RsMulticast sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return RsMulticastImpl.frbInternalSseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer));
}
@protected
Set<String> sse_decode_Set_String_None(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -3079,6 +3321,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError('Unreachable ()');
}
@protected
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
throw UnimplementedError('Unreachable ()');
}
@protected
RustStreamSink<RsServerEvent> sse_decode_StreamSink_rs_server_event_Sse(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -3413,6 +3661,29 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return LsHttpClientVersion.values[inner];
}
@protected
MulticastMessageV2 sse_decode_multicast_message_v_2(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_alias = sse_decode_String(deserializer);
var var_version = sse_decode_String(deserializer);
var var_deviceModel = sse_decode_opt_String(deserializer);
var var_deviceType = sse_decode_opt_box_autoadd_device_type(deserializer);
var var_fingerprint = sse_decode_String(deserializer);
var var_port = sse_decode_u_16(deserializer);
var var_protocol = sse_decode_protocol_type_v_2(deserializer);
var var_download = sse_decode_bool(deserializer);
return MulticastMessageV2(
alias: var_alias,
version: var_version,
deviceModel: var_deviceModel,
deviceType: var_deviceType,
fingerprint: var_fingerprint,
port: var_port,
protocol: var_protocol,
download: var_download,
);
}
@protected
String? sse_decode_opt_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -3742,6 +4013,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
RsMulticastDiscovered sse_decode_rs_multicast_discovered(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_ip = sse_decode_String(deserializer);
var var_message = sse_decode_multicast_message_v_2(deserializer);
return RsMulticastDiscovered(ip: var_ip, message: var_message);
}
@protected
RsServerEvent sse_decode_rs_server_event(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -4058,6 +4337,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_usize((self as RsHttpServerImpl).frbInternalSseEncode(move: true), serializer);
}
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_usize((self as RsMulticastImpl).frbInternalSseEncode(move: true), serializer);
}
@protected
void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
Dart2RustStreamSink self,
@@ -4139,6 +4424,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_usize((self as RsHttpServerImpl).frbInternalSseEncode(move: false), serializer);
}
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_usize((self as RsMulticastImpl).frbInternalSseEncode(move: false), serializer);
}
@protected
void
sse_encode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -4250,6 +4541,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_usize((self as RsHttpServerImpl).frbInternalSseEncode(move: null), serializer);
}
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_usize((self as RsMulticastImpl).frbInternalSseEncode(move: null), serializer);
}
@protected
void sse_encode_Set_String_None(Set<String> self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -4301,6 +4598,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(
self.setupAndSerialize(
codec: SseCodec(
decodeSuccessData: sse_decode_rs_multicast_discovered,
decodeErrorData: sse_decode_AnyhowException,
),
),
serializer,
);
}
@protected
void sse_encode_StreamSink_rs_server_event_Sse(RustStreamSink<RsServerEvent> self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -4635,6 +4946,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_multicast_message_v_2(MulticastMessageV2 self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.alias, serializer);
sse_encode_String(self.version, serializer);
sse_encode_opt_String(self.deviceModel, serializer);
sse_encode_opt_box_autoadd_device_type(self.deviceType, serializer);
sse_encode_String(self.fingerprint, serializer);
sse_encode_u_16(self.port, serializer);
sse_encode_protocol_type_v_2(self.protocol, serializer);
sse_encode_bool(self.download, serializer);
}
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -4909,6 +5233,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_rs_multicast_discovered(RsMulticastDiscovered self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.ip, serializer);
sse_encode_multicast_message_v_2(self.message, serializer);
}
@protected
void sse_encode_rs_server_event(RsServerEvent self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -5386,6 +5717,43 @@ class RsHttpServerImpl extends RustOpaque implements RsHttpServer {
);
}
@sealed
class RsMulticastImpl extends RustOpaque implements RsMulticast {
// Not to be used by end users
RsMulticastImpl.frbInternalDcoDecode(List<dynamic> wire) : super.frbInternalDcoDecode(wire, _kStaticData);
// Not to be used by end users
RsMulticastImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData);
static final _kStaticData = RustArcStaticData(
rustArcIncrementStrongCount: RustLib.instance.api.rust_arc_increment_strong_count_RsMulticast,
rustArcDecrementStrongCount: RustLib.instance.api.rust_arc_decrement_strong_count_RsMulticast,
rustArcDecrementStrongCountPtr: RustLib.instance.api.rust_arc_decrement_strong_count_RsMulticastPtr,
);
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
Future<void> announce() => RustLib.instance.api.crateApiMulticastRsMulticastAnnounce(
that: this,
);
/// Emits a [RsMulticastDiscovered] for every announcement received from
/// another device until discovery is stopped.
/// Can only be listened to once.
Stream<RsMulticastDiscovered> listen() => RustLib.instance.api.crateApiMulticastRsMulticastListen(
that: this,
);
/// Stops discovery, which also ends the [RsMulticast::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
Future<void> stop() => RustLib.instance.api.crateApiMulticastRsMulticastStop(
that: this,
);
}
@sealed
class RtcFileReceiverImpl extends RustOpaque implements RtcFileReceiver {
// Not to be used by end users
@@ -13,6 +13,7 @@ import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/api/stream.dart';
import 'package:localsend_isolates/rust/api/webrtc.dart';
@@ -57,6 +58,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpServerPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServerPtr;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsMulticastPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@@ -90,6 +94,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Dart2RustStreamSink dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(dynamic raw);
@@ -120,6 +127,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
FutureOr<void> Function(LsSignalingConnection)
dco_decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -165,6 +175,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Set<String> dco_decode_Set_String_None(dynamic raw);
@@ -179,6 +192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(dynamic raw);
@protected
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw);
@protected
RustStreamSink<RsServerEvent> dco_decode_StreamSink_rs_server_event_Sse(dynamic raw);
@@ -313,6 +329,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion dco_decode_ls_http_client_version(dynamic raw);
@protected
MulticastMessageV2 dco_decode_multicast_message_v_2(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@@ -401,6 +420,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw);
@protected
RsMulticastDiscovered dco_decode_rs_multicast_discovered(dynamic raw);
@protected
RsServerEvent dco_decode_rs_server_event(dynamic raw);
@@ -494,6 +516,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Dart2RustStreamSink sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
SseDeserializer deserializer,
@@ -534,6 +559,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@@ -575,6 +603,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Set<String> sse_decode_Set_String_None(SseDeserializer deserializer);
@@ -589,6 +620,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsServerEvent> sse_decode_StreamSink_rs_server_event_Sse(SseDeserializer deserializer);
@@ -723,6 +757,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion sse_decode_ls_http_client_version(SseDeserializer deserializer);
@protected
MulticastMessageV2 sse_decode_multicast_message_v_2(SseDeserializer deserializer);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@@ -813,6 +850,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer);
@protected
RsMulticastDiscovered sse_decode_rs_multicast_discovered(SseDeserializer deserializer);
@protected
RsServerEvent sse_decode_rs_server_event(SseDeserializer deserializer);
@@ -918,6 +958,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
Dart2RustStreamSink self,
@@ -969,6 +1012,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void
sse_encode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -1030,6 +1076,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Set_String_None(Set<String> self, SseSerializer serializer);
@@ -1045,6 +1094,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(RustStreamSink<Uint8List> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_server_event_Sse(RustStreamSink<RsServerEvent> self, SseSerializer serializer);
@@ -1180,6 +1232,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_ls_http_client_version(LsHttpClientVersion self, SseSerializer serializer);
@protected
void sse_encode_multicast_message_v_2(MulticastMessageV2 self, SseSerializer serializer);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@@ -1271,6 +1326,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer);
@protected
void sse_encode_rs_multicast_discovered(RsMulticastDiscovered self, SseSerializer serializer);
@protected
void sse_encode_rs_server_event(RsServerEvent self, SseSerializer serializer);
@@ -1650,4 +1708,36 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServerPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_localsend_isolates_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_localsend_isolates_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
}
@@ -15,6 +15,7 @@ import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/api/stream.dart';
import 'package:localsend_isolates/rust/api/webrtc.dart';
@@ -59,6 +60,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpServerPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsMulticastPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@@ -92,6 +96,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Dart2RustStreamSink dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(dynamic raw);
@@ -122,6 +129,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
FutureOr<void> Function(LsSignalingConnection)
dco_decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -167,6 +177,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Set<String> dco_decode_Set_String_None(dynamic raw);
@@ -181,6 +194,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(dynamic raw);
@protected
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw);
@protected
RustStreamSink<RsServerEvent> dco_decode_StreamSink_rs_server_event_Sse(dynamic raw);
@@ -315,6 +331,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion dco_decode_ls_http_client_version(dynamic raw);
@protected
MulticastMessageV2 dco_decode_multicast_message_v_2(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@@ -403,6 +422,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw);
@protected
RsMulticastDiscovered dco_decode_rs_multicast_discovered(dynamic raw);
@protected
RsServerEvent dco_decode_rs_server_event(dynamic raw);
@@ -496,6 +518,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Dart2RustStreamSink sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
SseDeserializer deserializer,
@@ -536,6 +561,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@@ -577,6 +605,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpServer sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Set<String> sse_decode_Set_String_None(SseDeserializer deserializer);
@@ -591,6 +622,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsServerEvent> sse_decode_StreamSink_rs_server_event_Sse(SseDeserializer deserializer);
@@ -725,6 +759,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion sse_decode_ls_http_client_version(SseDeserializer deserializer);
@protected
MulticastMessageV2 sse_decode_multicast_message_v_2(SseDeserializer deserializer);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@@ -815,6 +852,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer);
@protected
RsMulticastDiscovered sse_decode_rs_multicast_discovered(SseDeserializer deserializer);
@protected
RsServerEvent sse_decode_rs_server_event(SseDeserializer deserializer);
@@ -920,6 +960,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
Dart2RustStreamSink self,
@@ -971,6 +1014,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void
sse_encode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -1032,6 +1078,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Set_String_None(Set<String> self, SseSerializer serializer);
@@ -1047,6 +1096,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(RustStreamSink<Uint8List> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_server_event_Sse(RustStreamSink<RsServerEvent> self, SseSerializer serializer);
@@ -1182,6 +1234,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_ls_http_client_version(LsHttpClientVersion self, SseSerializer serializer);
@protected
void sse_encode_multicast_message_v_2(MulticastMessageV2 self, SseSerializer serializer);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@@ -1273,6 +1328,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer);
@protected
void sse_encode_rs_multicast_discovered(RsMulticastDiscovered self, SseSerializer serializer);
@protected
void sse_encode_rs_server_event(RsServerEvent self, SseSerializer serializer);
@@ -1386,6 +1444,12 @@ class RustLibWire implements BaseWire {
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(int ptr) =>
wasmModule.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(ptr);
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr) =>
wasmModule.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(ptr);
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr) =>
wasmModule.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(ptr);
}
@JS('wasm_bindgen')
@@ -1433,4 +1497,8 @@ extension type RustLibWasmModule._(JSObject _) implements JSObject {
external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(int ptr);
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(int ptr);
external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr);
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr);
}
@@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:localsend_isolates/constants.dart';
import 'package:localsend_isolates/isolate.dart';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart' show ProtocolTypeV2;
import 'package:localsend_isolates/src/isolate/child/http_provider.dart';
import 'package:localsend_isolates/util/network_interfaces.dart';
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:localsend_isolates/util/rust.dart';
import 'package:localsend_isolates/util/sleep.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -23,11 +21,13 @@ class MulticastService {
MulticastService(this._ref);
final Ref _ref;
Completer<void> _cancelCompleter = Completer();
RsMulticast? _multicast;
Completer<void> _retryCompleter = Completer();
bool _listening = false;
/// Binds the UDP port and listen to UDP multicast packages
/// It will automatically answer announcement messages
/// Binds the UDP sockets and listens to multicast announcements.
/// Announcements of other devices are answered with an HTTP register
/// request while the server is running.
Stream<Device> startListener() async* {
if (_listening) {
_logger.info('Already listening to multicast');
@@ -37,99 +37,78 @@ class MulticastService {
_listening = true;
while (true) {
final streamController = StreamController<Device>();
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(
whitelist: syncState.networkWhitelist,
blacklist: syncState.networkBlacklist,
multicastGroup: syncState.multicastGroup,
port: syncState.port,
);
for (final socket in sockets) {
socket.socket.listen((_) {
final datagram = socket.socket.receive();
if (datagram == null) {
return;
}
try {
final dto = MulticastDto.fromJson(jsonDecode(utf8.decode(datagram.data)));
if (dto.fingerprint == syncState.securityContext.certificateHash) {
return;
}
final ip = datagram.address.address;
final peer = dto.toDevice(ip, syncState.port, syncState.protocol == ProtocolType.https);
streamController.add(peer);
if ((dto.announcement == true || dto.announce == true) && syncState.serverRunning) {
// only respond when server is running
// ignore: discarded_futures
_answerAnnouncement(peer);
}
} catch (e) {
_logger.warning('Could not parse multicast message', e);
}
});
_logger.info(
'Bind UDP multicast port (ip: ${socket.interface.addresses.map((a) => a.address).toList()}, group: ${syncState.multicastGroup}, port: ${syncState.port})',
final RsMulticast multicast;
try {
multicast = await startMulticast(
group: syncState.multicastGroup,
port: syncState.port,
networkWhitelist: syncState.networkWhitelist,
networkBlacklist: syncState.networkBlacklist,
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType.toRust(),
fingerprint: syncState.securityContext.certificateHash,
protocol: syncState.protocol == ProtocolType.https ? ProtocolTypeV2.https : ProtocolTypeV2.http,
download: syncState.download,
);
} catch (e) {
_logger.warning('Could not start multicast discovery (group: ${syncState.multicastGroup}, port: ${syncState.port})', e);
// Wait for the next restart request instead of hot-looping
_retryCompleter = Completer();
await _retryCompleter.future;
continue;
}
// Tell everyone in the network that I am online
sendAnnouncement(); // ignore: unawaited_futures
_multicast = multicast;
_cancelCompleter = Completer();
// Tell everyone in the network that I am online.
unawaited(multicast.announce());
// ignore: unawaited_futures
_cancelCompleter.future.then((_) {
// ignore: discarded_futures
streamController.close();
for (final socket in sockets) {
socket.socket.close();
await for (final event in multicast.listen()) {
final device = event.message.toDevice(event.ip);
yield device;
if (_ref.read(syncProvider).serverRunning) {
// only respond when server is running
unawaited(_answerAnnouncement(device));
}
});
}
yield* streamController.stream;
// streamController is closed because of cancel
// wait for resources to be released (it works without on macOS, but who knows)
await sleepAsync(500);
// The stream ended because [restartListener] stopped the discovery.
_multicast = null;
}
}
/// Restarts the listener, e.g. after the port or the network settings changed.
void restartListener() {
_cancelCompleter.complete();
final multicast = _multicast;
if (multicast != null) {
// Ends the listen stream, which makes [startListener] rebind.
unawaited(multicast.stop());
} else if (!_retryCompleter.isCompleted) {
// Starting failed previously; let [startListener] try again.
_retryCompleter.complete();
}
}
/// Sends an announcement which triggers a response on every LocalSend member of the network.
Future<void> sendAnnouncement() async {
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(
whitelist: syncState.networkWhitelist,
blacklist: syncState.networkBlacklist,
multicastGroup: syncState.multicastGroup,
);
final dto = _getMulticastDto(announcement: true);
for (final wait in [100, 500, 2000]) {
await sleepAsync(wait);
_logger.info('Announce via UDP');
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(syncState.multicastGroup), syncState.port);
socket.socket.close();
} catch (e) {
_logger.warning('Could not send multicast message', e);
}
}
final multicast = _multicast;
if (multicast == null) {
_logger.info('Multicast discovery is not running, skipping announcement');
return;
}
_logger.info('Announce via UDP');
await multicast.announce();
}
/// Responds to an announcement.
/// Responds to an announcement over HTTP.
Future<void> _answerAnnouncement(Device peer) async {
try {
// Answer with TCP
await _ref
.read(httpProvider)
.discovery
@@ -141,75 +120,7 @@ class MulticastService {
);
_logger.info('Respond to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) via TCP');
} catch (e) {
// Fallback: Answer with UDP
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(
whitelist: syncState.networkWhitelist,
blacklist: syncState.networkBlacklist,
multicastGroup: syncState.multicastGroup,
);
final dto = _getMulticastDto(announcement: false);
for (final socket in sockets) {
try {
socket.socket.send(dto, InternetAddress(syncState.multicastGroup), syncState.port);
socket.socket.close();
} catch (e) {
_logger.warning('Could not send multicast message', e);
}
}
_logger.info('Respond to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) with UDP because TCP failed');
_logger.warning('Could not respond to announcement of ${peer.alias} (${peer.ip})', e);
}
}
/// Returns the MulticastDto of this device in bytes.
List<int> _getMulticastDto({required bool announcement}) {
final syncState = _ref.read(syncProvider);
final dto = MulticastDto(
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType,
fingerprint: syncState.securityContext.certificateHash,
port: syncState.port,
protocol: syncState.protocol,
download: syncState.download,
announcement: announcement,
announce: announcement,
);
return utf8.encode(jsonEncode(dto.toJson()));
}
}
class _SocketResult {
final NetworkInterface interface;
final RawDatagramSocket socket;
_SocketResult(this.interface, this.socket);
}
Future<List<_SocketResult>> _getSockets({
required List<String>? whitelist,
required List<String>? blacklist,
required String multicastGroup,
int? port,
}) async {
final interfaces = await getNetworkInterfaces(
whitelist: whitelist,
blacklist: blacklist,
);
final sockets = <_SocketResult>[];
for (final interface in interfaces) {
try {
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port ?? 0);
socket.joinMulticast(InternetAddress(multicastGroup), interface);
sockets.add(_SocketResult(interface, socket));
} catch (e) {
_logger.warning(
'Could not bind UDP multicast port (ip: ${interface.addresses.map((a) => a.address).toList()}, group: $multicastGroup, port: $port)',
e,
);
}
}
return sockets;
}
@@ -4,6 +4,7 @@ import 'package:localsend_isolates/model/dto/file_dto.dart';
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
import 'package:localsend_isolates/rust/api/http.dart' as rust_http;
import 'package:localsend_isolates/rust/api/model.dart' as rust_model;
import 'package:localsend_isolates/rust/api/multicast.dart' as rust_multicast;
import 'package:localsend_isolates/rust/api/server.dart' as rust_server;
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:mime/mime.dart';
@@ -128,6 +129,24 @@ extension RustFileDtoExt on rust_model.FileDto {
}
}
extension MulticastMessageV2Ext on rust_multicast.MulticastMessageV2 {
Device toDevice(String ip) {
return Device(
signalingId: null,
ip: ip,
version: version,
port: port,
https: protocol == rust_server.ProtocolTypeV2.https,
fingerprint: fingerprint,
alias: alias,
deviceModel: deviceModel,
deviceType: deviceType?.toDart() ?? DeviceType.desktop,
download: download,
discoveryMethods: {MulticastDiscovery()},
);
}
}
extension RegisterDtoV2Ext on rust_server.RegisterDtoV2 {
Device toDevice(String ip, DiscoveryMethod? method) {
return Device(
+11
View File
@@ -1313,6 +1313,16 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "if-addrs"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -1462,6 +1472,7 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-util",
"if-addrs",
"lru",
"pem",
"percent-encoding",
@@ -3,6 +3,7 @@ pub mod crypto;
pub mod http;
pub mod logging;
pub mod model;
pub mod multicast;
pub mod server;
pub mod stream;
pub mod webrtc;
@@ -0,0 +1,155 @@
use crate::frb_generated::StreamSink;
use flutter_rust_bridge::frb;
use localsend::model::discovery::DeviceType;
pub use localsend::model::discovery::{MulticastMessageV2, ProtocolTypeV2};
use localsend::multicast::{
DEFAULT_MULTICAST_GROUP_V6, InterfaceFilter, MulticastConfig, MulticastDevice, MulticastEvent,
MulticastHandle,
};
use tokio::sync::{Mutex, mpsc, oneshot};
/// Another device announced itself via UDP multicast.
///
/// The peer expects to be answered with an HTTP register request.
pub struct RsMulticastDiscovered {
/// The address the announcement was sent from. The peer's HTTP server is
/// reachable at this address on `message.port`.
///
/// A link-local IPv6 source carries its interface scope as `fe80::1%3`,
/// which the Rust HTTP client accepts back as a host.
pub ip: String,
/// The announcement as it was received.
pub message: MulticastMessageV2,
}
pub struct RsMulticast {
handle: MulticastHandle,
event_rx: Mutex<Option<mpsc::Receiver<MulticastEvent>>>,
stop_tx: Mutex<Option<oneshot::Sender<()>>>,
}
/// Starts UDP multicast discovery: binds the multicast sockets on all usable
/// network interfaces and listens for announcements of other devices.
///
/// Announcements are sent to the IPv4 [group] and, as a LocalSend extension,
/// to the (currently hardcoded) IPv6 group `ff12::fd3a:e420`.
///
/// [port] is used both to bind the multicast sockets and as the HTTP server
/// port announced to other devices.
///
/// Nothing is announced until [RsMulticast::announce] is called.
///
/// Fails when [group] is not a valid IPv4 address or when no network
/// interface could be used at all.
pub async fn start_multicast(
group: String,
port: u16,
network_whitelist: Option<Vec<String>>,
network_blacklist: Option<Vec<String>>,
alias: String,
version: String,
device_model: Option<String>,
device_type: Option<DeviceType>,
fingerprint: String,
protocol: ProtocolTypeV2,
download: bool,
) -> anyhow::Result<RsMulticast> {
let group = group
.parse()
.map_err(|_| anyhow::anyhow!("Invalid multicast group: {group}"))?;
let (event_tx, event_rx) = mpsc::channel::<MulticastEvent>(16);
let (stop_tx, stop_rx) = oneshot::channel::<()>();
let handle = localsend::multicast::start(
MulticastConfig {
group,
// Hardcoded for now; becomes a parameter once the app exposes it.
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
port,
interface_filter: InterfaceFilter {
whitelist: network_whitelist,
blacklist: network_blacklist,
},
device: MulticastDevice {
alias,
version,
device_model,
device_type,
fingerprint,
port,
protocol,
download,
},
event_tx,
},
stop_rx,
)
.await?;
Ok(RsMulticast {
handle,
event_rx: Mutex::new(Some(event_rx)),
stop_tx: Mutex::new(Some(stop_tx)),
})
}
impl RsMulticast {
/// Emits a [RsMulticastDiscovered] for every announcement received from
/// another device until discovery is stopped.
/// Can only be listened to once.
pub async fn listen(&self, sink: StreamSink<RsMulticastDiscovered>) {
let Some(mut event_rx) = self.event_rx.lock().await.take() else {
let _ = sink.add_error(anyhow::anyhow!("Multicast events already listened to"));
return;
};
while let Some(event) = event_rx.recv().await {
let MulticastEvent::Discovered {
ip,
scope_id,
message,
} = event;
let _ = sink.add(RsMulticastDiscovered {
ip: match scope_id {
// A link-local source is unreachable without its scope.
Some(scope_id) => format!("{ip}%{scope_id}"),
None => ip.to_string(),
},
message,
});
}
}
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
pub async fn announce(&self) {
self.handle.announce().await;
}
/// Stops discovery, which also ends the [RsMulticast::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
pub async fn stop(&self) {
if let Some(stop_tx) = self.stop_tx.lock().await.take() {
let _ = stop_tx.send(());
self.handle.wait_stopped().await;
}
}
}
#[frb(mirror(MulticastMessageV2))]
pub struct _MulticastMessageV2 {
pub alias: String,
pub version: String,
pub device_model: Option<String>,
pub device_type: Option<DeviceType>,
pub fingerprint: String,
pub port: u16,
pub protocol: ProtocolTypeV2,
pub download: bool,
}
@@ -19,6 +19,10 @@ use tokio::sync::{Mutex, mpsc, oneshot};
///
/// [RsServerEvent::PrepareUpload] must be answered with [RsHttpServer::respond_prepare_upload]
/// and [RsServerEvent::FileUpload] with [RsHttpServer::respond_file_upload].
///
/// The `ip` of an event renders a link-local IPv6 peer as `fe80::1%3`,
/// including the interface scope, which the Rust HTTP client accepts back as
/// a host.
pub enum RsServerEvent {
/// A device registered itself via `POST /api/localsend/v2/register`.
///
@@ -28,6 +28,7 @@
use crate::api::cancel::*;
use crate::api::http::*;
use crate::api::multicast::*;
use crate::api::server::*;
use crate::api::stream::*;
use crate::api::webrtc::*;
@@ -43,7 +44,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1193619754;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2071906741;
// Section: executor
@@ -1282,6 +1283,182 @@ fn wire__crate__api__server__RsHttpServer_stop_impl(
},
)
}
fn wire__crate__api__multicast__RsMulticast_announce_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "RsMulticast_announce",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_that = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>,
>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, ()>(
(move || async move {
let mut api_that_guard = None;
let decode_indices_ =
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
&api_that, 0, false,
)],
);
for i in decode_indices_ {
match i {
0 => {
api_that_guard =
Some(api_that.lockable_decode_async_ref().await)
}
_ => unreachable!(),
}
}
let api_that_guard = api_that_guard.unwrap();
let output_ok = Result::<_, ()>::Ok({
crate::api::multicast::RsMulticast::announce(&*api_that_guard).await;
})?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__multicast__RsMulticast_listen_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "RsMulticast_listen",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_that = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>,
>>::sse_decode(&mut deserializer);
let api_sink = <StreamSink<
crate::api::multicast::RsMulticastDiscovered,
flutter_rust_bridge::for_generated::SseCodec,
>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, ()>(
(move || async move {
let mut api_that_guard = None;
let decode_indices_ =
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
&api_that, 0, false,
)],
);
for i in decode_indices_ {
match i {
0 => {
api_that_guard =
Some(api_that.lockable_decode_async_ref().await)
}
_ => unreachable!(),
}
}
let api_that_guard = api_that_guard.unwrap();
let output_ok = Result::<_, ()>::Ok({
crate::api::multicast::RsMulticast::listen(&*api_that_guard, api_sink)
.await;
})?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__multicast__RsMulticast_stop_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "RsMulticast_stop",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_that = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>,
>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, ()>(
(move || async move {
let mut api_that_guard = None;
let decode_indices_ =
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
&api_that, 0, false,
)],
);
for i in decode_indices_ {
match i {
0 => {
api_that_guard =
Some(api_that.lockable_decode_async_ref().await)
}
_ => unreachable!(),
}
}
let api_that_guard = api_that_guard.unwrap();
let output_ok = Result::<_, ()>::Ok({
crate::api::multicast::RsMulticast::stop(&*api_that_guard).await;
})?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__webrtc__RtcFileReceiver_get_file_id_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -2510,6 +2687,66 @@ fn wire__crate__api__crypto__hash_file_impl(
},
)
}
fn wire__crate__api__multicast__start_multicast_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "start_multicast",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_group = <String>::sse_decode(&mut deserializer);
let api_port = <u16>::sse_decode(&mut deserializer);
let api_network_whitelist = <Option<Vec<String>>>::sse_decode(&mut deserializer);
let api_network_blacklist = <Option<Vec<String>>>::sse_decode(&mut deserializer);
let api_alias = <String>::sse_decode(&mut deserializer);
let api_version = <String>::sse_decode(&mut deserializer);
let api_device_model = <Option<String>>::sse_decode(&mut deserializer);
let api_device_type =
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
let api_fingerprint = <String>::sse_decode(&mut deserializer);
let api_protocol = <crate::api::server::ProtocolTypeV2>::sse_decode(&mut deserializer);
let api_download = <bool>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok = crate::api::multicast::start_multicast(
api_group,
api_port,
api_network_whitelist,
api_network_blacklist,
api_alias,
api_version,
api_device_model,
api_device_type,
api_fingerprint,
api_protocol,
api_download,
)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__server__start_server_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@@ -2642,6 +2879,17 @@ const _: fn() = || {
let _: Option<String> = FileMetadata.modified;
let _: Option<String> = FileMetadata.accessed;
}
{
let MulticastMessageV2 = None::<crate::api::multicast::MulticastMessageV2>.unwrap();
let _: String = MulticastMessageV2.alias;
let _: String = MulticastMessageV2.version;
let _: Option<String> = MulticastMessageV2.device_model;
let _: Option<crate::api::model::DeviceType> = MulticastMessageV2.device_type;
let _: String = MulticastMessageV2.fingerprint;
let _: u16 = MulticastMessageV2.port;
let _: crate::api::server::ProtocolTypeV2 = MulticastMessageV2.protocol;
let _: bool = MulticastMessageV2.download;
}
{
let PinConfig = None::<crate::api::webrtc::PinConfig>.unwrap();
let _: String = PinConfig.pin;
@@ -2829,6 +3077,9 @@ flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServer>
);
flutter_rust_bridge::frb_generated_moi_arc_impl_value!(
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>
);
// Section: dart2rust
@@ -2940,6 +3191,16 @@ impl SseDecode for RsHttpServer {
}
}
impl SseDecode for RsMulticast {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <RustOpaqueMoi<
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>,
>>::sse_decode(deserializer);
return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner);
}
}
impl SseDecode for flutter_rust_bridge::DartOpaque {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -3068,6 +3329,16 @@ impl SseDecode
}
}
impl SseDecode
for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>
{
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <usize>::sse_decode(deserializer);
return decode_rust_opaque_moi(inner);
}
}
impl SseDecode for std::collections::HashSet<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -3100,6 +3371,19 @@ impl SseDecode for StreamSink<Vec<u8>, flutter_rust_bridge::for_generated::SseCo
}
}
impl SseDecode
for StreamSink<
crate::api::multicast::RsMulticastDiscovered,
flutter_rust_bridge::for_generated::SseCodec,
>
{
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <String>::sse_decode(deserializer);
return StreamSink::deserialize(inner);
}
}
impl SseDecode
for StreamSink<crate::api::server::RsServerEvent, flutter_rust_bridge::for_generated::SseCodec>
{
@@ -3384,6 +3668,30 @@ impl SseDecode for crate::api::http::LsHttpClientVersion {
}
}
impl SseDecode for crate::api::multicast::MulticastMessageV2 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_alias = <String>::sse_decode(deserializer);
let mut var_version = <String>::sse_decode(deserializer);
let mut var_deviceModel = <Option<String>>::sse_decode(deserializer);
let mut var_deviceType = <Option<crate::api::model::DeviceType>>::sse_decode(deserializer);
let mut var_fingerprint = <String>::sse_decode(deserializer);
let mut var_port = <u16>::sse_decode(deserializer);
let mut var_protocol = <crate::api::server::ProtocolTypeV2>::sse_decode(deserializer);
let mut var_download = <bool>::sse_decode(deserializer);
return crate::api::multicast::MulticastMessageV2 {
alias: var_alias,
version: var_version,
device_model: var_deviceModel,
device_type: var_deviceType,
fingerprint: var_fingerprint,
port: var_port,
protocol: var_protocol,
download: var_download,
};
}
}
impl SseDecode for Option<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -3768,6 +4076,18 @@ impl SseDecode for crate::api::http::RsHttpClientError {
}
}
impl SseDecode for crate::api::multicast::RsMulticastDiscovered {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_ip = <String>::sse_decode(deserializer);
let mut var_message = <crate::api::multicast::MulticastMessageV2>::sse_decode(deserializer);
return crate::api::multicast::RsMulticastDiscovered {
ip: var_ip,
message: var_message,
};
}
}
impl SseDecode for crate::api::server::RsServerEvent {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -4178,106 +4498,117 @@ fn pde_ffi_dispatcher_primary_impl(
data_len,
),
19 => wire__crate__api__server__RsHttpServer_stop_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__api__webrtc__RtcFileReceiver_get_file_id_impl(
20 => wire__crate__api__multicast__RsMulticast_announce_impl(
port,
ptr,
rust_vec_len,
data_len,
),
21 => wire__crate__api__webrtc__RtcFileReceiver_receive_impl(
21 => {
wire__crate__api__multicast__RsMulticast_listen_impl(port, ptr, rust_vec_len, data_len)
}
22 => wire__crate__api__multicast__RsMulticast_stop_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__api__webrtc__RtcFileReceiver_get_file_id_impl(
port,
ptr,
rust_vec_len,
data_len,
),
22 => wire__crate__api__webrtc__RtcFileSender_send_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__api__webrtc__RtcReceiveController_decline_impl(
24 => wire__crate__api__webrtc__RtcFileReceiver_receive_impl(
port,
ptr,
rust_vec_len,
data_len,
),
24 => wire__crate__api__webrtc__RtcReceiveController_listen_error_impl(
25 => wire__crate__api__webrtc__RtcFileSender_send_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__api__webrtc__RtcReceiveController_decline_impl(
port,
ptr,
rust_vec_len,
data_len,
),
25 => wire__crate__api__webrtc__RtcReceiveController_listen_files_impl(
27 => wire__crate__api__webrtc__RtcReceiveController_listen_error_impl(
port,
ptr,
rust_vec_len,
data_len,
),
26 => wire__crate__api__webrtc__RtcReceiveController_listen_receiving_impl(
28 => wire__crate__api__webrtc__RtcReceiveController_listen_files_impl(
port,
ptr,
rust_vec_len,
data_len,
),
27 => wire__crate__api__webrtc__RtcReceiveController_listen_status_impl(
29 => wire__crate__api__webrtc__RtcReceiveController_listen_receiving_impl(
port,
ptr,
rust_vec_len,
data_len,
),
28 => wire__crate__api__webrtc__RtcReceiveController_send_file_status_impl(
30 => wire__crate__api__webrtc__RtcReceiveController_listen_status_impl(
port,
ptr,
rust_vec_len,
data_len,
),
29 => wire__crate__api__webrtc__RtcReceiveController_send_pin_impl(
31 => wire__crate__api__webrtc__RtcReceiveController_send_file_status_impl(
port,
ptr,
rust_vec_len,
data_len,
),
30 => wire__crate__api__webrtc__RtcReceiveController_send_selection_impl(
32 => wire__crate__api__webrtc__RtcReceiveController_send_pin_impl(
port,
ptr,
rust_vec_len,
data_len,
),
31 => wire__crate__api__webrtc__RtcSendController_listen_error_impl(
33 => wire__crate__api__webrtc__RtcReceiveController_send_selection_impl(
port,
ptr,
rust_vec_len,
data_len,
),
32 => wire__crate__api__webrtc__RtcSendController_listen_selected_files_impl(
34 => wire__crate__api__webrtc__RtcSendController_listen_error_impl(
port,
ptr,
rust_vec_len,
data_len,
),
33 => wire__crate__api__webrtc__RtcSendController_listen_status_impl(
35 => wire__crate__api__webrtc__RtcSendController_listen_selected_files_impl(
port,
ptr,
rust_vec_len,
data_len,
),
34 => wire__crate__api__webrtc__RtcSendController_send_file_impl(
36 => wire__crate__api__webrtc__RtcSendController_listen_status_impl(
port,
ptr,
rust_vec_len,
data_len,
),
35 => wire__crate__api__webrtc__RtcSendController_send_pin_impl(
37 => wire__crate__api__webrtc__RtcSendController_send_file_impl(
port,
ptr,
rust_vec_len,
data_len,
),
36 => wire__crate__api__webrtc__connect_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__api__stream__create_stream_impl(port, ptr, rust_vec_len, data_len),
40 => {
38 => wire__crate__api__webrtc__RtcSendController_send_pin_impl(
port,
ptr,
rust_vec_len,
data_len,
),
39 => wire__crate__api__webrtc__connect_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__api__stream__create_stream_impl(port, ptr, rust_vec_len, data_len),
43 => {
wire__crate__api__logging__enable_debug_logging_impl(port, ptr, rust_vec_len, data_len)
}
41 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__api__crypto__hash_file_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__api__crypto__generate_key_pair_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__api__crypto__hash_file_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__api__multicast__start_multicast_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__api__server__start_server_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__api__crypto__verify_cert_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -4292,8 +4623,8 @@ fn pde_ffi_dispatcher_sync_impl(
match func_id {
2 => wire__crate__api__stream__Dart2RustStreamSink_close_impl(ptr, rust_vec_len, data_len),
6 => wire__crate__api__cancel__RsCancellationToken_cancel_impl(ptr, rust_vec_len, data_len),
37 => wire__crate__api__cancel__create_cancellation_token_impl(ptr, rust_vec_len, data_len),
38 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len),
40 => wire__crate__api__cancel__create_cancellation_token_impl(ptr, rust_vec_len, data_len),
41 => wire__crate__api__http__create_client_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -4469,6 +4800,21 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<RsHttpServer>> for RsHttpServe
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<RsMulticast> {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0)
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper<RsMulticast> {}
impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<RsMulticast>> for RsMulticast {
fn into_into_dart(self) -> FrbWrapper<RsMulticast> {
self.into()
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::webrtc::ClientInfo> {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
@@ -4650,6 +4996,33 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::api::http::LsHttpClient
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::multicast::MulticastMessageV2> {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.0.alias.into_into_dart().into_dart(),
self.0.version.into_into_dart().into_dart(),
self.0.device_model.into_into_dart().into_dart(),
self.0.device_type.into_into_dart().into_dart(),
self.0.fingerprint.into_into_dart().into_dart(),
self.0.port.into_into_dart().into_dart(),
self.0.protocol.into_into_dart().into_dart(),
self.0.download.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for FrbWrapper<crate::api::multicast::MulticastMessageV2>
{
}
impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::api::multicast::MulticastMessageV2>>
for crate::api::multicast::MulticastMessageV2
{
fn into_into_dart(self) -> FrbWrapper<crate::api::multicast::MulticastMessageV2> {
self.into()
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::webrtc::PinConfig> {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
@@ -4938,6 +5311,27 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::http::RsHttpClientError>
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::multicast::RsMulticastDiscovered {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.ip.into_into_dart().into_dart(),
self.message.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::api::multicast::RsMulticastDiscovered
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::api::multicast::RsMulticastDiscovered>
for crate::api::multicast::RsMulticastDiscovered
{
fn into_into_dart(self) -> crate::api::multicast::RsMulticastDiscovered {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::server::RsServerEvent {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
@@ -5355,6 +5749,13 @@ impl SseEncode for RsHttpServer {
}
}
impl SseEncode for RsMulticast {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer);
}
}
impl SseEncode for flutter_rust_bridge::DartOpaque {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -5493,6 +5894,17 @@ impl SseEncode
}
}
impl SseEncode
for RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>
{
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
let (ptr, size) = self.sse_encode_raw();
<usize>::sse_encode(ptr, serializer);
<i32>::sse_encode(size, serializer);
}
}
impl SseEncode for std::collections::HashSet<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -5521,6 +5933,18 @@ impl SseEncode for StreamSink<Vec<u8>, flutter_rust_bridge::for_generated::SseCo
}
}
impl SseEncode
for StreamSink<
crate::api::multicast::RsMulticastDiscovered,
flutter_rust_bridge::for_generated::SseCodec,
>
{
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
unimplemented!("")
}
}
impl SseEncode
for StreamSink<crate::api::server::RsServerEvent, flutter_rust_bridge::for_generated::SseCodec>
{
@@ -5760,6 +6184,20 @@ impl SseEncode for crate::api::http::LsHttpClientVersion {
}
}
impl SseEncode for crate::api::multicast::MulticastMessageV2 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.alias, serializer);
<String>::sse_encode(self.version, serializer);
<Option<String>>::sse_encode(self.device_model, serializer);
<Option<crate::api::model::DeviceType>>::sse_encode(self.device_type, serializer);
<String>::sse_encode(self.fingerprint, serializer);
<u16>::sse_encode(self.port, serializer);
<crate::api::server::ProtocolTypeV2>::sse_encode(self.protocol, serializer);
<bool>::sse_encode(self.download, serializer);
}
}
impl SseEncode for Option<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -6073,6 +6511,14 @@ impl SseEncode for crate::api::http::RsHttpClientError {
}
}
impl SseEncode for crate::api::multicast::RsMulticastDiscovered {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.ip, serializer);
<crate::api::multicast::MulticastMessageV2>::sse_encode(self.message, serializer);
}
}
impl SseEncode for crate::api::server::RsServerEvent {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -6357,6 +6803,7 @@ mod io {
use super::*;
use crate::api::cancel::*;
use crate::api::http::*;
use crate::api::multicast::*;
use crate::api::server::*;
use crate::api::stream::*;
use crate::api::webrtc::*;
@@ -6509,6 +6956,20 @@ mod io {
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServer>>::decrement_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_localsend_isolates_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>::increment_strong_count(ptr as _);
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_localsend_isolates_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>::decrement_strong_count(ptr as _);
}
}
#[cfg(not(target_family = "wasm"))]
pub use io::*;
@@ -6524,6 +6985,7 @@ mod web {
use super::*;
use crate::api::cancel::*;
use crate::api::http::*;
use crate::api::multicast::*;
use crate::api::server::*;
use crate::api::stream::*;
use crate::api::webrtc::*;
@@ -6678,6 +7140,20 @@ mod web {
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsHttpServer>>::decrement_strong_count(ptr as _);
}
#[wasm_bindgen]
pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>::increment_strong_count(ptr as _);
}
#[wasm_bindgen]
pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr: *const std::ffi::c_void,
) {
MoiArc::<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>::decrement_strong_count(ptr as _);
}
}
#[cfg(target_family = "wasm")]
pub use web::*;