mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: multicast in Rust
This commit is contained in:
@@ -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.
|
||||
|
||||
Generated
+11
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user