From 55cbcfe154403ab1108c128369e29a4d41f128f0 Mon Sep 17 00:00:00 2001 From: Walker Crouse Date: Thu, 24 Sep 2020 18:03:10 -0400 Subject: [PATCH] Move from magiclip project Signed-off-by: Walker Crouse --- .gitignore | 1 + Cargo.lock | 506 +++++++++++++++++++++++++++++ Cargo.toml | 5 + README.md | 89 +++++ examples/Cargo.lock | 520 ++++++++++++++++++++++++++++++ examples/Cargo.toml | 5 + examples/browser/Cargo.toml | 8 + examples/browser/src/main.rs | 18 ++ examples/service/Cargo.toml | 8 + examples/service/src/main.rs | 36 +++ zeroconf-macros/Cargo.toml | 12 + zeroconf-macros/src/lib.rs | 70 ++++ zeroconf/Cargo.toml | 19 ++ zeroconf/src/builder.rs | 9 + zeroconf/src/discovery.rs | 25 ++ zeroconf/src/ffi.rs | 62 ++++ zeroconf/src/lib.rs | 128 ++++++++ zeroconf/src/linux/avahi_util.rs | 42 +++ zeroconf/src/linux/browser.rs | 225 +++++++++++++ zeroconf/src/linux/client.rs | 72 +++++ zeroconf/src/linux/constants.rs | 6 + zeroconf/src/linux/entry_group.rs | 110 +++++++ zeroconf/src/linux/mod.rs | 21 ++ zeroconf/src/linux/poll.rs | 39 +++ zeroconf/src/linux/raw_browser.rs | 64 ++++ zeroconf/src/linux/resolver.rs | 86 +++++ zeroconf/src/linux/service.rs | 188 +++++++++++ zeroconf/src/macos/browser.rs | 207 ++++++++++++ zeroconf/src/macos/compat.rs | 13 + zeroconf/src/macos/mod.rs | 14 + zeroconf/src/macos/service.rs | 104 ++++++ zeroconf/src/macos/service_ref.rs | 230 +++++++++++++ zeroconf/src/registration.rs | 22 ++ 33 files changed, 2964 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 examples/Cargo.lock create mode 100644 examples/Cargo.toml create mode 100644 examples/browser/Cargo.toml create mode 100644 examples/browser/src/main.rs create mode 100644 examples/service/Cargo.toml create mode 100644 examples/service/src/main.rs create mode 100644 zeroconf-macros/Cargo.toml create mode 100644 zeroconf-macros/src/lib.rs create mode 100644 zeroconf/Cargo.toml create mode 100644 zeroconf/src/builder.rs create mode 100644 zeroconf/src/discovery.rs create mode 100644 zeroconf/src/ffi.rs create mode 100644 zeroconf/src/lib.rs create mode 100644 zeroconf/src/linux/avahi_util.rs create mode 100644 zeroconf/src/linux/browser.rs create mode 100644 zeroconf/src/linux/client.rs create mode 100644 zeroconf/src/linux/constants.rs create mode 100644 zeroconf/src/linux/entry_group.rs create mode 100644 zeroconf/src/linux/mod.rs create mode 100644 zeroconf/src/linux/poll.rs create mode 100644 zeroconf/src/linux/raw_browser.rs create mode 100644 zeroconf/src/linux/resolver.rs create mode 100644 zeroconf/src/linux/service.rs create mode 100644 zeroconf/src/macos/browser.rs create mode 100644 zeroconf/src/macos/compat.rs create mode 100644 zeroconf/src/macos/mod.rs create mode 100644 zeroconf/src/macos/service.rs create mode 100644 zeroconf/src/macos/service_ref.rs create mode 100644 zeroconf/src/registration.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b60de5b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2d2dd04 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,506 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "avahi-sys" +version = "0.9.0" +dependencies = [ + "bindgen", + "libc", +] + +[[package]] +name = "bindgen" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b13ce559e6433d360c26305643803cb52cfbabbc2b9c47ce04a58493dfb443" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bonjour-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "clang-sys" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da1484c6a890e374ca5086062d4847e0a2c1e5eba9afa5d48c09e8eb39b2519" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive-getters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5905670fd9c320154f3a4a01c9e609733cd7b753f3c58777ab7d5ce26686b3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hermit-abi" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" + +[[package]] +name = "libloading" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2443d8f0478b16759158b2f66d525991a05491138bc05814ef52a250148ef4f9" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "proc-macro2" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "serde" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "syn" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroconf" +version = "0.1.0" +dependencies = [ + "avahi-sys", + "bonjour-sys", + "derive-getters", + "derive_builder", + "libc", + "log", + "serde", + "zeroconf-macros", +] + +[[package]] +name = "zeroconf-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..941fc40 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "zeroconf", + "zeroconf-macros", +] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ffbb92 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# zeroconf + +`zeroconf` is a cross-platform library that wraps underlying [ZeroConf/mDNS] implementations +such as [Bonjour] or [Avahi], providing an easy and idiomatic way to both register and +browse services. + +[ZeroConf/mDNS]: https://en.wikipedia.org/wiki/Zero-configuration_networking +[Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) +[Avahi]: https://en.wikipedia.org/wiki/Avahi_(software) + +## Prerequisites + +```bash +$ sudo apt install xorg-dev libxcb-shape0-dev libxcb-xfixes0-dev clang +``` + +## Examples + + ## Register a service + + When registering a service, you may optionally pass a "context" to pass state through the + callback. The only requirement is that this context implements the [`Any`] trait, which most + types will automatically. See [`MdnsService`] for more information about contexts. + +``` +use std::any::Any; +use std::sync::{Arc, Mutex}; +use zeroconf::{MdnsService, ServiceRegistration}; + +#[derive(Default, Debug)] +pub struct Context { + service_name: String, +} + +fn main() { + let mut service = MdnsService::new("_http._tcp", 8080); + let context: Arc> = Arc::default(); + + service.set_registered_callback(Box::new(on_service_registered)); + service.set_context(Box::new(context)); + + // blocks current thread, must keep-alive to keep service active + service.start().unwrap(); +} + +fn on_service_registered(service: ServiceRegistration, context: Option>) { + println!("Service registered: {:?}", service); + + let context = context + .as_ref() + .unwrap() + .downcast_ref::>>() + .unwrap() + .clone(); + + context.lock().unwrap().service_name = service.name().clone(); + + println!("Context: {:?}", context); + + // ... +} +``` + +## Browsing services + +``` +use std::any::Any; +use std::sync::Arc; +use zeroconf::{MdnsBrowser, ServiceDiscovery}; + +fn main() { + let mut browser = MdnsBrowser::new("_http._tcp"); + + browser.set_service_discovered_callback(Box::new(on_service_discovered)); + + // blocks current thread, must keep-alive to keep browser active + browser.start().unwrap() +} + +fn on_service_discovered(service: ServiceDiscovery, _context: Option>) { + println!("Service discovered: {:?}", &service); + + // ... +} +``` + +## TODO + +## Caveats diff --git a/examples/Cargo.lock b/examples/Cargo.lock new file mode 100644 index 0000000..3487abc --- /dev/null +++ b/examples/Cargo.lock @@ -0,0 +1,520 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "avahi-sys" +version = "0.9.0" +dependencies = [ + "bindgen", + "libc", +] + +[[package]] +name = "bindgen" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b13ce559e6433d360c26305643803cb52cfbabbc2b9c47ce04a58493dfb443" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bonjour-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "clang-sys" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da1484c6a890e374ca5086062d4847e0a2c1e5eba9afa5d48c09e8eb39b2519" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive-getters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5905670fd9c320154f3a4a01c9e609733cd7b753f3c58777ab7d5ce26686b3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hermit-abi" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" + +[[package]] +name = "libloading" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2443d8f0478b16759158b2f66d525991a05491138bc05814ef52a250148ef4f9" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "proc-macro2" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "serde" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "syn" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroconf" +version = "0.1.0" +dependencies = [ + "avahi-sys", + "bonjour-sys", + "derive-getters", + "derive_builder", + "libc", + "log", + "serde", + "zeroconf-macros", +] + +[[package]] +name = "zeroconf-browser-example" +version = "0.1.0" +dependencies = [ + "zeroconf", +] + +[[package]] +name = "zeroconf-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "zeroconf-service-example" +version = "0.1.0" +dependencies = [ + "zeroconf", +] diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 0000000..6ba93f1 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "browser", + "service", +] diff --git a/examples/browser/Cargo.toml b/examples/browser/Cargo.toml new file mode 100644 index 0000000..9248b96 --- /dev/null +++ b/examples/browser/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "zeroconf-browser-example" +version = "0.1.0" +authors = ["Walker Crouse "] +edition = "2018" + +[dependencies] +zeroconf = { path = "../../zeroconf" } diff --git a/examples/browser/src/main.rs b/examples/browser/src/main.rs new file mode 100644 index 0000000..b2bf939 --- /dev/null +++ b/examples/browser/src/main.rs @@ -0,0 +1,18 @@ +use std::any::Any; +use std::sync::Arc; +use zeroconf::{MdnsBrowser, ServiceDiscovery}; + +fn main() { + let mut browser = MdnsBrowser::new("_http._tcp"); + + browser.set_service_discovered_callback(Box::new(on_service_discovered)); + + // blocks current thread, must keep-alive to keep browser active + browser.start().unwrap() +} + +fn on_service_discovered(service: ServiceDiscovery, _context: Option>) { + println!("Service discovered: {:?}", &service); + + // ... +} diff --git a/examples/service/Cargo.toml b/examples/service/Cargo.toml new file mode 100644 index 0000000..45c0182 --- /dev/null +++ b/examples/service/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "zeroconf-service-example" +version = "0.1.0" +authors = ["Walker Crouse "] +edition = "2018" + +[dependencies] +zeroconf = { path = "../../zeroconf" } diff --git a/examples/service/src/main.rs b/examples/service/src/main.rs new file mode 100644 index 0000000..2798e49 --- /dev/null +++ b/examples/service/src/main.rs @@ -0,0 +1,36 @@ +use std::any::Any; +use std::sync::{Arc, Mutex}; +use zeroconf::{MdnsService, ServiceRegistration}; + +#[derive(Default, Debug)] +pub struct Context { + service_name: String, +} + +fn main() { + let mut service = MdnsService::new("_http._tcp", 8080); + let context: Arc> = Arc::default(); + + service.set_registered_callback(Box::new(on_service_registered)); + service.set_context(Box::new(context)); + + // blocks current thread, must keep-alive to keep service active + service.start().unwrap(); +} + +fn on_service_registered(service: ServiceRegistration, context: Option>) { + println!("Service registered: {:?}", service); + + let context = context + .as_ref() + .unwrap() + .downcast_ref::>>() + .unwrap() + .clone(); + + context.lock().unwrap().service_name = service.name().clone(); + + println!("Context: {:?}", context); + + // ... +} diff --git a/zeroconf-macros/Cargo.toml b/zeroconf-macros/Cargo.toml new file mode 100644 index 0000000..e933a10 --- /dev/null +++ b/zeroconf-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "zeroconf-macros" +version = "0.1.0" +authors = ["Walker Crouse "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0.41" +quote = "1.0.7" diff --git a/zeroconf-macros/src/lib.rs b/zeroconf-macros/src/lib.rs new file mode 100644 index 0000000..4000720 --- /dev/null +++ b/zeroconf-macros/src/lib.rs @@ -0,0 +1,70 @@ +extern crate proc_macro; + +use crate::proc_macro::TokenStream; +use quote::quote; +use syn::{self, DeriveInput, Ident}; + +#[proc_macro_derive(FromRaw)] +pub fn from_raw_macro_derive(input: TokenStream) -> TokenStream { + impl_from_raw(&syn::parse(input).unwrap()) +} + +fn impl_from_raw(ast: &DeriveInput) -> TokenStream { + let name = &ast.ident; + let generics = &ast.generics; + + let gen = quote! { + impl #generics crate::ffi::FromRaw<#name #generics> for #name #generics {} + }; + + gen.into() +} + +#[proc_macro_derive(CloneRaw)] +pub fn clone_raw_macro_derive(input: TokenStream) -> TokenStream { + impl_clone_raw(&syn::parse(input).unwrap()) +} + +fn impl_clone_raw(ast: &DeriveInput) -> TokenStream { + let name = &ast.ident; + let generics = &ast.generics; + + let gen = quote! { + impl #generics crate::ffi::CloneRaw<#name #generics> for #name #generics {} + }; + + gen.into() +} + +#[proc_macro_derive(AsRaw)] +pub fn as_raw_macro_derive(input: TokenStream) -> TokenStream { + impl_as_raw(&syn::parse(input).unwrap()) +} + +fn impl_as_raw(ast: &DeriveInput) -> TokenStream { + let name = &ast.ident; + let generics = &ast.generics; + + let gen = quote! { + impl #generics crate::ffi::AsRaw for #name #generics {} + }; + + gen.into() +} + +#[proc_macro_derive(BuilderDelegate)] +pub fn builder_delegate_macro_derive(input: TokenStream) -> TokenStream { + impl_builder_delegate(&syn::parse(input).unwrap()) +} + +fn impl_builder_delegate(ast: &DeriveInput) -> TokenStream { + let name = &ast.ident; + let builder: Ident = syn::parse_str(&format!("{}Builder", name)).unwrap(); + let generics = &ast.generics; + + let gen = quote! { + impl #generics crate::builder::BuilderDelegate<#builder #generics> for #name #generics {} + }; + + gen.into() +} diff --git a/zeroconf/Cargo.toml b/zeroconf/Cargo.toml new file mode 100644 index 0000000..801894e --- /dev/null +++ b/zeroconf/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "zeroconf" +version = "0.1.0" +authors = ["Walker Crouse "] +edition = "2018" + +[dependencies] +serde = { version = "1.0.116", features = ["derive"] } +derive-getters = "0.2.0" +libc = "*" +derive_builder = "0.9.0" +zeroconf-macros = { path = "../zeroconf-macros" } +log = "0.4.11" + +[target.'cfg(unix)'.dependencies] +avahi-sys = { path = "../../avahi-sys" } + +[target.'cfg(target_os = "macos")'.dependencies] +bonjour-sys = { path = "../../bonjour-sys" } diff --git a/zeroconf/src/builder.rs b/zeroconf/src/builder.rs new file mode 100644 index 0000000..64d3153 --- /dev/null +++ b/zeroconf/src/builder.rs @@ -0,0 +1,9 @@ +//! Provides builder related helpers + +/// Implements a `builder()` function for the specified type +pub trait BuilderDelegate { + /// Initializes a new default builder of type `T` + fn builder() -> T { + T::default() + } +} diff --git a/zeroconf/src/discovery.rs b/zeroconf/src/discovery.rs new file mode 100644 index 0000000..5439663 --- /dev/null +++ b/zeroconf/src/discovery.rs @@ -0,0 +1,25 @@ +use std::any::Any; +use std::sync::Arc; + +/// Callback invoked from [`MdnsBrowser`] once a service has been discovered and resolved. +/// +/// # Arguments +/// * `discovered_service` - The service that was disovered +/// * `context` - The optional user context passed through using [`MdnsBrowser::set_context()`] +/// +/// [`MdnsBrowser`]: struct.MdnsBrowser.html +/// [`MdnsBrowser::set_context()`]: struct.MdnsBrowser.html#method.set_context +pub type ServiceDiscoveredCallback = dyn Fn(ServiceDiscovery, Option>); + +/// Represents a service that has been discovered by a [`MdnsBrowser`]. +/// +/// [`MdnsBrowser`]: struct.MdnsBrowser.html +#[derive(Debug, Getters, Builder, BuilderDelegate, Serialize, Deserialize)] +pub struct ServiceDiscovery { + name: String, + kind: String, + domain: String, + host_name: String, + address: String, + port: u16, +} diff --git a/zeroconf/src/ffi.rs b/zeroconf/src/ffi.rs new file mode 100644 index 0000000..f710915 --- /dev/null +++ b/zeroconf/src/ffi.rs @@ -0,0 +1,62 @@ +//! Utilities related to FFI bindings + +use libc::c_void; + +/// Helper trait to convert a raw `*mut c_void` to it's rust type +pub trait FromRaw { + /// Converts the specified `*mut c_void` to a `&'a mut T`. + /// + /// # Unsafe + /// This function is unsafe due to the dereference of the specified raw pointer. + unsafe fn from_raw<'a>(raw: *mut c_void) -> &'a mut T { + &mut *(raw as *mut T) + } +} + +/// Helper trait to convert and clone a raw `*mut c_void` to it's rust type +pub trait CloneRaw + Clone> { + /// Converts and clones the specified `*mut c_void` to a `Box`. + /// + /// # Unsafe + /// This function is unsafe due to a call to the unsafe function [`FromRaw::from_raw()`]. + /// + /// [`FromRaw::from_raw()`]: trait.FromRaw.html#method.from_raw + unsafe fn clone_raw<'a>(raw: *mut c_void) -> Box { + Box::new(T::from_raw(raw).clone()) + } +} + +/// Helper trait to convert self to a raw `*mut c_void` +pub trait AsRaw { + /// Converts self to a raw `*mut c_void` by cast. + fn as_raw(&mut self) -> *mut c_void { + self as *mut _ as *mut c_void + } +} + +pub mod cstr { + //! FFI utilities related to c-strings + + use libc::c_char; + use std::ffi::CStr; + + /// Returns the specified `*const c_char` as a `&'a str`. Ownership is not taken. + /// + /// # Unsafe + /// This function is unsafe due to a call to the unsafe function [`CStr::from_ptr()`]. + /// + /// [`CStr::from_ptr()`]: https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr + pub unsafe fn raw_to_str<'a>(s: *const c_char) -> &'a str { + CStr::from_ptr(s).to_str().unwrap() + } + + /// Copies the specified `*const c_char` into a `String`. + /// + /// # Unsafe + /// This function is unsafe due to a call to the unsafe function [`raw_to_str()`]. + /// + /// [`raw_to_str()`]: fn.raw_to_str.html + pub unsafe fn copy_raw(s: *const c_char) -> String { + String::from(raw_to_str(s)) + } +} diff --git a/zeroconf/src/lib.rs b/zeroconf/src/lib.rs new file mode 100644 index 0000000..af245b3 --- /dev/null +++ b/zeroconf/src/lib.rs @@ -0,0 +1,128 @@ +//! `zeroconf` is a cross-platform library that wraps underlying [ZeroConf/mDNS] implementations +//! such as [Bonjour] or [Avahi], providing an easy and idiomatic way to both register and +//! browse services. +//! +//! This crate provides the cross-platform [`MdnsService`] and [`MdnsBrowser`] available for each +//! supported platform as well as platform-specific modules for lower-level access to the mDNS +//! implementation should that be necessary. +//! +//! Most users of this crate need only [`MdnsService`] and [`MdnsBrowser`]. +//! +//! # Examples +//! +//! ## Register a service +//! +//! When registering a service, you may optionally pass a "context" to pass state through the +//! callback. The only requirement is that this context implements the [`Any`] trait, which most +//! types will automatically. See [`MdnsService`] for more information about contexts. +//! +//! ``` +//! use std::any::Any; +//! use std::sync::{Arc, Mutex}; +//! use zeroconf::{MdnsService, ServiceRegistration}; +//! +//! #[derive(Default, Debug)] +//! pub struct Context { +//! service_name: String, +//! } +//! +//! fn main() { +//! let mut service = MdnsService::new("_http._tcp", 8080); +//! let context: Arc> = Arc::default(); +//! +//! service.set_registered_callback(Box::new(on_service_registered)); +//! service.set_context(Box::new(context)); +//! +//! // blocks current thread, must keep-alive to keep service active +//! service.start().unwrap(); +//! } +//! +//! fn on_service_registered(service: ServiceRegistration, context: Option>) { +//! println!("Service registered: {:?}", service); +//! +//! let context = context +//! .as_ref() +//! .unwrap() +//! .downcast_ref::>>() +//! .unwrap() +//! .clone(); +//! +//! context.lock().unwrap().service_name = service.name().clone(); +//! +//! println!("Context: {:?}", context); +//! +//! // ... +//! } +//! ``` +//! +//! ## Browsing services +//! +//! ``` +//! use std::any::Any; +//! use std::sync::Arc; +//! use zeroconf::{MdnsBrowser, ServiceDiscovery}; +//! +//! fn main() { +//! let mut browser = MdnsBrowser::new("_http._tcp"); +//! +//! browser.set_service_discovered_callback(Box::new(on_service_discovered)); +//! +//! // blocks current thread, must keep-alive to keep browser active +//! browser.start().unwrap() +//! } +//! +//! fn on_service_discovered(service: ServiceDiscovery, _context: Option>) { +//! println!("Service discovered: {:?}", &service); +//! +//! // ... +//! } +//! ``` +//! [ZeroConf/mDNS]: https://en.wikipedia.org/wiki/Zero-configuration_networking +//! [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) +//! [Avahi]: https://en.wikipedia.org/wiki/Avahi_(software) +//! [`MdnsService`]: struct.MdnsService.html +//! [`MdnsBrowser`]: struct.MdnsBrowser.html +//! [`Any`]: https://doc.rust-lang.org/std/any/trait.Any.html + +#[macro_use] +extern crate serde; +#[macro_use] +extern crate derive_builder; +#[macro_use] +extern crate zeroconf_macros; +#[cfg(target_os = "linux")] +extern crate avahi_sys; +#[cfg(target_os = "macos")] +extern crate bonjour_sys; +#[macro_use] +extern crate derive_getters; +#[macro_use] +extern crate log; +extern crate libc; + +mod discovery; +mod registration; + +pub mod builder; +pub mod ffi; + +#[cfg(target_os = "linux")] +pub mod linux; +#[cfg(target_os = "macos")] +pub mod macos; + +pub use discovery::*; +pub use registration::*; + +/// Type alias for the platform-specific mDNS browser implementation +#[cfg(target_os = "linux")] +pub type MdnsBrowser = linux::browser::AvahiMdnsBrowser; +/// Type alias for the platform-specific mDNS service implementation +#[cfg(target_os = "linux")] +pub type MdnsService = linux::service::AvahiMdnsService; +/// Type alias for the platform-specific mDNS browser implementation +#[cfg(target_os = "macos")] +pub type MdnsBrowser = macos::browser::BonjourMdnsBrowser; +/// Type alias for the platform-specific mDNS service implementation +#[cfg(target_os = "macos")] +pub type MdnsService = macos::service::BonjourMdnsService; diff --git a/zeroconf/src/linux/avahi_util.rs b/zeroconf/src/linux/avahi_util.rs new file mode 100644 index 0000000..133c9a8 --- /dev/null +++ b/zeroconf/src/linux/avahi_util.rs @@ -0,0 +1,42 @@ +//! Utilities related to Avahi + +use super::constants; +use avahi_sys::{avahi_address_snprint, avahi_strerror, AvahiAddress}; +use libc::c_char; +use std::ffi::{CStr, CString}; +use std::mem; + +/// Converts the specified `*const AvahiAddress` to a `String`. +/// +/// The new `String` is constructed through allocating a new `CString`, passing it to +/// `avahi_address_snprint` and then converting it to a Rust-type `String`. +pub fn avahi_address_to_string(addr: *const AvahiAddress) -> String { + let addr_str = unsafe { + let str = CString::from_vec_unchecked(vec![0; constants::AVAHI_ADDRESS_STR_MAX]); + avahi_address_snprint(str.as_ptr() as *mut c_char, mem::size_of_val(&str), addr); + str + }; + + String::from(addr_str.to_str().unwrap()) + .trim_matches(char::from(0)) + .to_string() +} + +/// Returns the `&str` message associated with the specified error code. +pub fn get_error<'a>(code: i32) -> &'a str { + unsafe { + CStr::from_ptr(avahi_strerror(code)) + .to_str() + .expect("could not fetch Avahi error string") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_error_returns_valid_error_string() { + assert_eq!(get_error(avahi_sys::AVAHI_ERR_FAILURE), "Operation failed"); + } +} diff --git a/zeroconf/src/linux/browser.rs b/zeroconf/src/linux/browser.rs new file mode 100644 index 0000000..a9e652f --- /dev/null +++ b/zeroconf/src/linux/browser.rs @@ -0,0 +1,225 @@ +use super::avahi_util; +use super::client::{ManagedAvahiClient, ManagedAvahiClientParams}; +use super::constants; +use super::poll::ManagedAvahiSimplePoll; +use super::raw_browser::{ManagedAvahiServiceBrowser, ManagedAvahiServiceBrowserParams}; +use super::resolver::{ + ManagedAvahiServiceResolver, ManagedAvahiServiceResolverParams, ServiceResolverSet, +}; +use crate::builder::BuilderDelegate; +use crate::ffi::{cstr, FromRaw}; +use crate::{ServiceDiscoveredCallback, ServiceDiscovery}; +use avahi_sys::{ + AvahiAddress, AvahiBrowserEvent, AvahiClient, AvahiClientFlags, AvahiClientState, AvahiIfIndex, + AvahiLookupResultFlags, AvahiProtocol, AvahiResolverEvent, AvahiServiceBrowser, + AvahiServiceResolver, AvahiStringList, +}; +use libc::{c_char, c_void}; +use std::any::Any; +use std::ffi::CString; +use std::sync::Arc; +use std::{fmt, ptr}; + +#[derive(Debug)] +pub struct AvahiMdnsBrowser { + poll: Option, + browser: Option, + kind: CString, + context: *mut AvahiBrowserContext, +} + +impl AvahiMdnsBrowser { + pub fn new(kind: &str) -> Self { + Self { + poll: None, + browser: None, + kind: CString::new(kind.to_string()).unwrap(), + context: Box::into_raw(Box::default()), + } + } + + pub fn set_service_discovered_callback( + &mut self, + service_discovered_callback: Box, + ) { + unsafe { (*self.context).service_discovered_callback = Some(service_discovered_callback) }; + } + + pub fn set_context(&mut self, context: Box) { + unsafe { (*self.context).user_context = Some(Arc::from(context)) }; + } + + pub fn start(&mut self) -> Result<(), String> { + debug!("Browsing services: {:?}", self); + + self.poll = Some(ManagedAvahiSimplePoll::new()?); + + let client = ManagedAvahiClient::new( + ManagedAvahiClientParams::builder() + .poll(self.poll.as_ref().unwrap()) + .flags(AvahiClientFlags(0)) + .callback(Some(client_callback)) + .userdata(ptr::null_mut()) + .build()?, + )?; + + unsafe { + (*self.context).client = Some(client); + + self.browser = Some(ManagedAvahiServiceBrowser::new( + ManagedAvahiServiceBrowserParams::builder() + .client(&(*self.context).client.as_ref().unwrap()) + .interface(constants::AVAHI_IF_UNSPEC) + .protocol(constants::AVAHI_PROTO_UNSPEC) + .kind(self.kind.as_ptr()) + .domain(ptr::null_mut()) + .flags(0) + .callback(Some(browse_callback)) + .userdata(self.context as *mut c_void) + .build()?, + )?); + } + + self.poll.as_ref().unwrap().start_loop() + } +} + +impl Drop for AvahiMdnsBrowser { + fn drop(&mut self) { + unsafe { + Box::from_raw(self.context); + } + } +} + +#[derive(FromRaw)] +struct AvahiBrowserContext { + client: Option, + resolvers: ServiceResolverSet, + service_discovered_callback: Option>, + user_context: Option>, +} + +impl Default for AvahiBrowserContext { + fn default() -> Self { + AvahiBrowserContext { + client: None, + resolvers: ServiceResolverSet::default(), + service_discovered_callback: None, + user_context: None, + } + } +} + +impl fmt::Debug for AvahiBrowserContext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("AvahiBrowserContext") + .field("client", &self.client) + .field("resolvers", &self.resolvers) + .finish() + } +} + +unsafe extern "C" fn browse_callback( + _browser: *mut AvahiServiceBrowser, + interface: AvahiIfIndex, + protocol: AvahiProtocol, + event: AvahiBrowserEvent, + name: *const c_char, + kind: *const c_char, + domain: *const c_char, + _flags: AvahiLookupResultFlags, + userdata: *mut c_void, +) { + let context = AvahiBrowserContext::from_raw(userdata); + + match event { + avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_NEW => { + context.resolvers.insert( + ManagedAvahiServiceResolver::new( + ManagedAvahiServiceResolverParams::builder() + .client(context.client.as_ref().unwrap()) + .interface(interface) + .protocol(protocol) + .name(name) + .kind(kind) + .domain(domain) + .aprotocol(constants::AVAHI_PROTO_UNSPEC) + .flags(0) + .callback(Some(resolve_callback)) + .userdata(userdata) + .build() + .unwrap(), + ) + .unwrap(), + ); + } + avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_FAILURE => panic!("browser failure"), + _ => {} + }; +} + +unsafe extern "C" fn resolve_callback( + resolver: *mut AvahiServiceResolver, + _interface: AvahiIfIndex, + _protocol: AvahiProtocol, + event: AvahiResolverEvent, + name: *const c_char, + kind: *const c_char, + domain: *const c_char, + host_name: *const c_char, + addr: *const AvahiAddress, + port: u16, + _txt: *mut AvahiStringList, + _flags: AvahiLookupResultFlags, + userdata: *mut c_void, +) { + let name = cstr::raw_to_str(name); + let kind = cstr::raw_to_str(kind); + let domain = cstr::raw_to_str(domain); + + let context = AvahiBrowserContext::from_raw(userdata); + + match event { + avahi_sys::AvahiResolverEvent_AVAHI_RESOLVER_FAILURE => warn!( + "failed to resolve service `{}` of type `{}` in domain `{}`", + name, kind, domain + ), + avahi_sys::AvahiResolverEvent_AVAHI_RESOLVER_FOUND => { + let host_name = cstr::raw_to_str(host_name); + let address = avahi_util::avahi_address_to_string(addr); + + let result = ServiceDiscovery::builder() + .name(name.to_string()) + .kind(kind.to_string()) + .domain(domain.to_string()) + .host_name(host_name.to_string()) + .address(address) + .port(port) + .build() + .unwrap(); + + debug!("Service resolved: {:?}", result); + + if let Some(f) = &context.service_discovered_callback { + f(result, context.user_context.clone()); + } else { + warn!("Service resolved but no callback was set"); + } + } + _ => {} + }; + + context.resolvers.remove_raw(resolver); +} + +extern "C" fn client_callback( + _client: *mut AvahiClient, + state: AvahiClientState, + _userdata: *mut c_void, +) { + match state { + avahi_sys::AvahiClientState_AVAHI_CLIENT_FAILURE => panic!("client failure"), + _ => {} + } +} diff --git a/zeroconf/src/linux/client.rs b/zeroconf/src/linux/client.rs new file mode 100644 index 0000000..643a9bd --- /dev/null +++ b/zeroconf/src/linux/client.rs @@ -0,0 +1,72 @@ +use super::avahi_util; +use super::poll::ManagedAvahiSimplePoll; +use crate::ffi::cstr; +use avahi_sys::{ + avahi_client_free, avahi_client_get_host_name, avahi_client_new, avahi_simple_poll_get, + AvahiClient, AvahiClientCallback, AvahiClientFlags, +}; +use libc::{c_int, c_void}; +use std::ptr; + +#[derive(Debug)] +pub struct ManagedAvahiClient { + pub(super) client: *mut AvahiClient, +} + +impl ManagedAvahiClient { + pub fn new( + ManagedAvahiClientParams { + poll, + flags, + callback, + userdata, + }: ManagedAvahiClientParams, + ) -> Result { + let mut err: c_int = 0; + + let client = unsafe { + avahi_client_new( + avahi_simple_poll_get(poll.poll), + flags, + callback, + userdata, + &mut err, + ) + }; + + if client == ptr::null_mut() { + return Err("could not initialize AvahiClient".to_string()); + } + + match err { + 0 => Ok(Self { client }), + _ => Err(format!( + "could not initialize AvahiClient: {}", + avahi_util::get_error(err) + )), + } + } +} + +impl Drop for ManagedAvahiClient { + fn drop(&mut self) { + unsafe { avahi_client_free(self.client) }; + } +} + +#[derive(Builder, BuilderDelegate)] +pub struct ManagedAvahiClientParams<'a> { + poll: &'a ManagedAvahiSimplePoll, + flags: AvahiClientFlags, + callback: AvahiClientCallback, + userdata: *mut c_void, +} + +pub unsafe fn get_host_name<'a>(client: *mut AvahiClient) -> Result<&'a str, String> { + let host_name = avahi_client_get_host_name(client); + if host_name != ptr::null_mut() { + Ok(cstr::raw_to_str(host_name)) + } else { + Err("could not get host name from AvahiClient".to_string()) + } +} diff --git a/zeroconf/src/linux/constants.rs b/zeroconf/src/linux/constants.rs new file mode 100644 index 0000000..772685f --- /dev/null +++ b/zeroconf/src/linux/constants.rs @@ -0,0 +1,6 @@ +use avahi_sys::{AvahiIfIndex, AvahiProtocol}; + +pub const AVAHI_IF_UNSPEC: AvahiIfIndex = -1; +pub const AVAHI_PROTO_UNSPEC: AvahiProtocol = -1; +pub const AVAHI_ERR_COLLISION: i32 = -8; +pub const AVAHI_ADDRESS_STR_MAX: usize = 40; diff --git a/zeroconf/src/linux/entry_group.rs b/zeroconf/src/linux/entry_group.rs new file mode 100644 index 0000000..1f69cc0 --- /dev/null +++ b/zeroconf/src/linux/entry_group.rs @@ -0,0 +1,110 @@ +use super::avahi_util; +use avahi_sys::{ + avahi_entry_group_add_service, avahi_entry_group_commit, avahi_entry_group_free, + avahi_entry_group_is_empty, avahi_entry_group_new, avahi_entry_group_reset, AvahiClient, + AvahiEntryGroup, AvahiEntryGroupCallback, AvahiIfIndex, AvahiProtocol, AvahiPublishFlags, +}; +use libc::{c_char, c_void}; +use std::ptr; + +#[derive(Debug)] +pub struct ManagedAvahiEntryGroup { + group: *mut AvahiEntryGroup, +} + +impl ManagedAvahiEntryGroup { + pub fn new( + ManagedAvahiEntryGroupParams { + client, + callback, + userdata, + }: ManagedAvahiEntryGroupParams, + ) -> Result { + let group = unsafe { avahi_entry_group_new(client, callback, userdata) }; + if group == ptr::null_mut() { + Err("could not initialize AvahiEntryGroup".to_string()) + } else { + Ok(Self { group }) + } + } + + pub fn is_empty(&self) -> bool { + unsafe { avahi_entry_group_is_empty(self.group) != 0 } + } + + pub fn add_service( + &mut self, + AddServiceParams { + interface, + protocol, + flags, + name, + kind, + domain, + host, + port, + }: AddServiceParams, + ) -> Result<(), String> { + let err = unsafe { + avahi_entry_group_add_service( + self.group, + interface, + protocol, + flags, + name, + kind, + domain, + host, + port, + ptr::null_mut() as *const c_char, // null terminated txt record list + ) + }; + + if err < 0 { + return Err(format!( + "could not register service: `{}`", + avahi_util::get_error(err) + )); + } + + let err = unsafe { avahi_entry_group_commit(self.group) }; + + if err < 0 { + Err(format!( + "could not commit service: `{}`", + avahi_util::get_error(err) + )) + } else { + Ok(()) + } + } + + pub fn reset(&mut self) { + unsafe { avahi_entry_group_reset(self.group) }; + } +} + +impl Drop for ManagedAvahiEntryGroup { + fn drop(&mut self) { + unsafe { avahi_entry_group_free(self.group) }; + } +} + +#[derive(Builder, BuilderDelegate)] +pub struct ManagedAvahiEntryGroupParams { + client: *mut AvahiClient, + callback: AvahiEntryGroupCallback, + userdata: *mut c_void, +} + +#[derive(Builder, BuilderDelegate)] +pub struct AddServiceParams { + interface: AvahiIfIndex, + protocol: AvahiProtocol, + flags: AvahiPublishFlags, + name: *const c_char, + kind: *const c_char, + domain: *const c_char, + host: *const c_char, + port: u16, +} diff --git a/zeroconf/src/linux/mod.rs b/zeroconf/src/linux/mod.rs new file mode 100644 index 0000000..d195b40 --- /dev/null +++ b/zeroconf/src/linux/mod.rs @@ -0,0 +1,21 @@ +//! Linux-specific ZeroConf bindings +//! +//! This module wraps the [Avahi] mDNS implementation which can be found in most major Linux +//! distributions. It is a sufficient (and often more featured) replacement for Apple's [Bonjour]. +//! +//! [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) +//! [Avahi]: https://en.wikipedia.org/wiki/Avahi_(software) + +pub(crate) mod browser; +pub(crate) mod service; + +pub mod avahi_util; +pub mod client; +pub mod constants; +pub mod entry_group; +pub mod poll; +pub mod raw_browser; +pub mod resolver; + +pub use browser::*; +pub use service::*; diff --git a/zeroconf/src/linux/poll.rs b/zeroconf/src/linux/poll.rs new file mode 100644 index 0000000..9e79a91 --- /dev/null +++ b/zeroconf/src/linux/poll.rs @@ -0,0 +1,39 @@ +use super::avahi_util; +use avahi_sys::{ + avahi_simple_poll_free, avahi_simple_poll_loop, avahi_simple_poll_new, AvahiSimplePoll, +}; +use std::ptr; + +#[derive(Debug)] +pub struct ManagedAvahiSimplePoll { + pub(super) poll: *mut AvahiSimplePoll, +} + +impl ManagedAvahiSimplePoll { + pub fn new() -> Result { + let poll = unsafe { avahi_simple_poll_new() }; + if poll == ptr::null_mut() { + Err("could not initialize AvahiSimplePoll".to_string()) + } else { + Ok(Self { poll }) + } + } + + pub fn start_loop(&self) -> Result<(), String> { + let err = unsafe { avahi_simple_poll_loop(self.poll) }; + if err != 0 { + Err(format!( + "could not start AvahiSimplePoll: {}", + avahi_util::get_error(err) + )) + } else { + Ok(()) + } + } +} + +impl Drop for ManagedAvahiSimplePoll { + fn drop(&mut self) { + unsafe { avahi_simple_poll_free(self.poll) }; + } +} diff --git a/zeroconf/src/linux/raw_browser.rs b/zeroconf/src/linux/raw_browser.rs new file mode 100644 index 0000000..40ebdb6 --- /dev/null +++ b/zeroconf/src/linux/raw_browser.rs @@ -0,0 +1,64 @@ +use super::client::ManagedAvahiClient; +use avahi_sys::{ + avahi_service_browser_free, avahi_service_browser_new, AvahiIfIndex, AvahiLookupFlags, + AvahiProtocol, AvahiServiceBrowser, AvahiServiceBrowserCallback, +}; +use libc::{c_char, c_void}; +use std::ptr; + +#[derive(Debug)] +pub struct ManagedAvahiServiceBrowser { + browser: *mut AvahiServiceBrowser, +} + +impl ManagedAvahiServiceBrowser { + pub fn new( + ManagedAvahiServiceBrowserParams { + client, + interface, + protocol, + kind, + domain, + flags, + callback, + userdata, + }: ManagedAvahiServiceBrowserParams, + ) -> Result { + let browser = unsafe { + avahi_service_browser_new( + client.client, + interface, + protocol, + kind, + domain, + flags, + callback, + userdata, + ) + }; + + if browser == ptr::null_mut() { + Err("could not initialize Avahi service browser".to_string()) + } else { + Ok(Self { browser }) + } + } +} + +impl Drop for ManagedAvahiServiceBrowser { + fn drop(&mut self) { + unsafe { avahi_service_browser_free(self.browser) }; + } +} + +#[derive(Builder, BuilderDelegate)] +pub struct ManagedAvahiServiceBrowserParams<'a> { + client: &'a ManagedAvahiClient, + interface: AvahiIfIndex, + protocol: AvahiProtocol, + kind: *const c_char, + domain: *const c_char, + flags: AvahiLookupFlags, + callback: AvahiServiceBrowserCallback, + userdata: *mut c_void, +} diff --git a/zeroconf/src/linux/resolver.rs b/zeroconf/src/linux/resolver.rs new file mode 100644 index 0000000..5fae692 --- /dev/null +++ b/zeroconf/src/linux/resolver.rs @@ -0,0 +1,86 @@ +use super::client::ManagedAvahiClient; +use avahi_sys::{ + avahi_service_resolver_free, avahi_service_resolver_new, AvahiIfIndex, AvahiLookupFlags, + AvahiProtocol, AvahiServiceResolver, AvahiServiceResolverCallback, +}; +use libc::{c_char, c_void}; +use std::collections::HashMap; +use std::ptr; + +#[derive(Debug)] +pub struct ManagedAvahiServiceResolver { + resolver: *mut AvahiServiceResolver, +} + +impl ManagedAvahiServiceResolver { + pub fn new( + ManagedAvahiServiceResolverParams { + client, + interface, + protocol, + name, + kind, + domain, + aprotocol, + flags, + callback, + userdata, + }: ManagedAvahiServiceResolverParams, + ) -> Result { + let resolver = unsafe { + avahi_service_resolver_new( + client.client, + interface, + protocol, + name, + kind, + domain, + aprotocol, + flags, + callback, + userdata, + ) + }; + + if resolver == ptr::null_mut() { + Err("could not initialize AvahiServiceResolver".to_string()) + } else { + Ok(Self { resolver }) + } + } +} + +impl Drop for ManagedAvahiServiceResolver { + fn drop(&mut self) { + unsafe { avahi_service_resolver_free(self.resolver) }; + } +} + +#[derive(Builder, BuilderDelegate)] +pub struct ManagedAvahiServiceResolverParams<'a> { + client: &'a ManagedAvahiClient, + interface: AvahiIfIndex, + protocol: AvahiProtocol, + name: *const c_char, + kind: *const c_char, + domain: *const c_char, + aprotocol: AvahiProtocol, + flags: AvahiLookupFlags, + callback: AvahiServiceResolverCallback, + userdata: *mut c_void, +} + +#[derive(Default, Debug)] +pub struct ServiceResolverSet { + resolvers: HashMap<*mut AvahiServiceResolver, ManagedAvahiServiceResolver>, +} + +impl ServiceResolverSet { + pub fn insert(&mut self, resolver: ManagedAvahiServiceResolver) { + self.resolvers.insert(resolver.resolver, resolver); + } + + pub fn remove_raw(&mut self, raw: *mut AvahiServiceResolver) { + self.resolvers.remove(&raw); + } +} diff --git a/zeroconf/src/linux/service.rs b/zeroconf/src/linux/service.rs new file mode 100644 index 0000000..3edff7c --- /dev/null +++ b/zeroconf/src/linux/service.rs @@ -0,0 +1,188 @@ +use super::client::{self, ManagedAvahiClient, ManagedAvahiClientParams}; +use super::constants; +use super::entry_group::{AddServiceParams, ManagedAvahiEntryGroup, ManagedAvahiEntryGroupParams}; +use super::poll::ManagedAvahiSimplePoll; +use crate::builder::BuilderDelegate; +use crate::ffi::{cstr, AsRaw, FromRaw}; +use crate::{ServiceRegisteredCallback, ServiceRegistration}; +use avahi_sys::{ + AvahiClient, AvahiClientFlags, AvahiClientState, AvahiEntryGroup, AvahiEntryGroupState, +}; +use libc::c_void; +use std::any::Any; +use std::ffi::CString; +use std::fmt::{self, Formatter}; +use std::ptr; +use std::sync::Arc; + +#[derive(Debug)] +pub struct AvahiMdnsService { + client: Option, + poll: Option, + context: *mut AvahiServiceContext, +} + +impl AvahiMdnsService { + pub fn new(kind: &str, port: u16) -> Self { + Self { + client: None, + poll: None, + context: Box::into_raw(Box::new(AvahiServiceContext::new(kind, port))), + } + } + + pub fn set_registered_callback(&mut self, registered_callback: Box) { + unsafe { (*self.context).registered_callback = Some(registered_callback) }; + } + + pub fn set_context(&mut self, context: Box) { + unsafe { (*self.context).user_context = Some(Arc::from(context)) }; + } + + pub fn start(&mut self) -> Result<(), String> { + debug!("Registering service: {:?}", self); + + self.poll = Some(ManagedAvahiSimplePoll::new()?); + + self.client = Some(ManagedAvahiClient::new( + ManagedAvahiClientParams::builder() + .poll(self.poll.as_ref().unwrap()) + .flags(AvahiClientFlags(0)) + .callback(Some(client_callback)) + .userdata(self.context as *mut c_void) + .build()?, + )?); + + self.poll.as_ref().unwrap().start_loop() + } +} + +impl Drop for AvahiMdnsService { + fn drop(&mut self) { + unsafe { Box::from_raw(self.context) }; + } +} + +#[derive(FromRaw, AsRaw)] +struct AvahiServiceContext { + name: Option, + kind: CString, + port: u16, + group: Option, + registered_callback: Option>, + user_context: Option>, +} + +impl AvahiServiceContext { + fn new(kind: &str, port: u16) -> Self { + Self { + name: None, + kind: CString::new(kind).unwrap(), + port, + group: None, + registered_callback: None, + user_context: None, + } + } +} + +impl fmt::Debug for AvahiServiceContext { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("AvahiServiceContext") + .field("name", &self.name) + .field("kind", &self.kind) + .field("port", &self.port) + .field("group", &self.group) + .finish() + } +} + +unsafe extern "C" fn client_callback( + client: *mut AvahiClient, + state: AvahiClientState, + userdata: *mut c_void, +) { + let context = AvahiServiceContext::from_raw(userdata); + + match state { + avahi_sys::AvahiClientState_AVAHI_CLIENT_S_RUNNING => create_service(client, context), + avahi_sys::AvahiClientState_AVAHI_CLIENT_FAILURE => panic!("client failure"), + avahi_sys::AvahiClientState_AVAHI_CLIENT_S_REGISTERING => { + if let Some(g) = &mut context.group { + debug!("Group reset"); + g.reset(); + } + } + _ => {} + }; +} + +unsafe fn create_service(client: *mut AvahiClient, context: &mut AvahiServiceContext) { + context.name = Some(CString::new(client::get_host_name(client).unwrap().to_string()).unwrap()); + + if context.group.is_none() { + debug!("Creating group"); + + context.group = Some( + ManagedAvahiEntryGroup::new( + ManagedAvahiEntryGroupParams::builder() + .client(client) + .callback(Some(entry_group_callback)) + .userdata(context.as_raw()) + .build() + .unwrap(), + ) + .unwrap(), + ); + } + + let group = context.group.as_mut().unwrap(); + + if group.is_empty() { + debug!("Adding service"); + + group + .add_service( + AddServiceParams::builder() + .interface(constants::AVAHI_IF_UNSPEC) + .protocol(constants::AVAHI_PROTO_UNSPEC) + .flags(0) + .name(context.name.as_ref().unwrap().as_ptr()) + .kind(context.kind.as_ptr()) + .domain(ptr::null_mut()) + .host(ptr::null_mut()) + .port(context.port) + .build() + .unwrap(), + ) + .unwrap(); + } +} + +unsafe extern "C" fn entry_group_callback( + _group: *mut AvahiEntryGroup, + state: AvahiEntryGroupState, + userdata: *mut c_void, +) { + match state { + avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_ESTABLISHED => { + debug!("Group established"); + + let context = AvahiServiceContext::from_raw(userdata); + + let result = ServiceRegistration::builder() + .name(cstr::copy_raw(context.name.as_ref().unwrap().as_ptr())) + .kind(cstr::copy_raw(context.kind.as_ptr())) + .domain("local".to_string()) + .build() + .expect("could not build ServiceRegistration"); + + if let Some(f) = &context.registered_callback { + f(result, context.user_context.clone()); + } else { + warn!("Service registered but no callback was set: {:?}", result); + } + } + _ => {} + }; +} diff --git a/zeroconf/src/macos/browser.rs b/zeroconf/src/macos/browser.rs new file mode 100644 index 0000000..cfc618f --- /dev/null +++ b/zeroconf/src/macos/browser.rs @@ -0,0 +1,207 @@ +use super::compat; +use super::service_ref::{ + BrowseServicesParams, GetAddressInfoParams, ManagedDNSServiceRef, ServiceResolveParams, +}; +use crate::builder::BuilderDelegate; +use crate::ffi::{cstr, FromRaw}; +use crate::{ServiceDiscoveredCallback, ServiceDiscovery}; +use bonjour_sys::{sockaddr, DNSServiceErrorType, DNSServiceFlags, DNSServiceRef}; +use libc::{c_char, c_uchar, c_void, in_addr, sockaddr_in}; +use std::any::Any; +use std::ffi::CString; +use std::fmt::{self, Formatter}; +use std::ptr; +use std::sync::Arc; + +#[derive(Debug)] +pub struct BonjourMdnsBrowser { + service: ManagedDNSServiceRef, + kind: CString, + context: *mut BonjourBrowserContext, +} + +impl BonjourMdnsBrowser { + pub fn new(kind: &str) -> Self { + Self { + service: ManagedDNSServiceRef::default(), + kind: CString::new(kind).unwrap(), + context: Box::into_raw(Box::default()), + } + } + + pub fn set_service_discovered_callback( + &self, + service_discovered_callback: Box, + ) { + unsafe { (*self.context).service_discovered_callback = Some(service_discovered_callback) }; + } + + pub fn set_context(&mut self, context: Box) { + unsafe { (*self.context).user_context = Some(Arc::from(context)) }; + } + + pub fn start(&mut self) -> Result<(), String> { + debug!("Browsing services: {:?}", self); + + self.service.browse_services( + BrowseServicesParams::builder() + .flags(0) + .interface_index(0) + .regtype(self.kind.as_ptr()) + .domain(ptr::null_mut()) + .callback(Some(browse_callback)) + .context(self.context as *mut c_void) + .build()?, + ) + } +} + +impl Drop for BonjourMdnsBrowser { + fn drop(&mut self) { + unsafe { Box::from_raw(self.context) }; + } +} + +#[derive(Default, FromRaw)] +struct BonjourBrowserContext { + service_discovered_callback: Option>, + resolved_name: Option, + resolved_kind: Option, + resolved_domain: Option, + resolved_port: u16, + user_context: Option>, +} + +impl fmt::Debug for BonjourBrowserContext { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("BonjourResolverContext") + .field("resolved_name", &self.resolved_name) + .field("resolved_kind", &self.resolved_kind) + .field("resolved_domain", &self.resolved_domain) + .field("resolved_port", &self.resolved_port) + .finish() + } +} + +unsafe extern "C" fn browse_callback( + _sd_ref: DNSServiceRef, + _flags: DNSServiceFlags, + interface_index: u32, + error: DNSServiceErrorType, + name: *const c_char, + regtype: *const c_char, + domain: *const c_char, + context: *mut c_void, +) { + let ctx = BonjourBrowserContext::from_raw(context); + + if error != 0 { + panic!("browse_callback() reported error (code: {})", error); + } + + ctx.resolved_name = Some(cstr::copy_raw(name)); + ctx.resolved_kind = Some(cstr::copy_raw(regtype)); + ctx.resolved_domain = Some(cstr::copy_raw(domain)); + + ManagedDNSServiceRef::default() + .resolve_service( + ServiceResolveParams::builder() + .flags(bonjour_sys::kDNSServiceFlagsForceMulticast) + .interface_index(interface_index) + .name(name) + .regtype(regtype) + .domain(domain) + .callback(Some(resolve_callback)) + .context(context) + .build() + .expect("could not build ServiceResolveParams"), + ) + .unwrap(); +} + +unsafe extern "C" fn resolve_callback( + _sd_ref: DNSServiceRef, + _flags: DNSServiceFlags, + interface_index: u32, + error: DNSServiceErrorType, + _fullname: *const c_char, + host_target: *const c_char, + port: u16, + _txt_len: u16, + _txt_record: *const c_uchar, + context: *mut c_void, +) { + let ctx = BonjourBrowserContext::from_raw(context); + + if error != 0 { + panic!("error reported by resolve_callback: (code: {})", error); + } + + ctx.resolved_port = port; + + ManagedDNSServiceRef::default() + .get_address_info( + GetAddressInfoParams::builder() + .flags(bonjour_sys::kDNSServiceFlagsForceMulticast) + .interface_index(interface_index) + .protocol(0) + .hostname(host_target) + .callback(Some(get_address_info_callback)) + .context(context) + .build() + .expect("could not build GetAddressInfoParams"), + ) + .unwrap(); +} + +unsafe extern "C" fn get_address_info_callback( + _sd_ref: DNSServiceRef, + _flags: DNSServiceFlags, + _interface_index: u32, + error: DNSServiceErrorType, + hostname: *const c_char, + address: *const sockaddr, + _ttl: u32, + context: *mut c_void, +) { + let ctx = BonjourBrowserContext::from_raw(context); + + // this callback runs multiple times for some reason + if ctx.resolved_name.is_none() { + return; + } + + if error != 0 { + panic!( + "get_address_info_callback() reported error (code: {})", + error + ); + } + + let ip = get_ip(address as *const sockaddr_in); + let hostname = cstr::copy_raw(hostname); + let domain = compat::normalize_domain(&ctx.resolved_domain.take().unwrap()); + + let result = ServiceDiscovery::builder() + .name(ctx.resolved_name.take().unwrap()) + .kind(ctx.resolved_kind.take().unwrap()) + .domain(domain) + .host_name(hostname) + .address(ip) + .port(ctx.resolved_port) + .build() + .expect("could not build ServiceResolution"); + + if let Some(f) = &ctx.service_discovered_callback { + f(result, ctx.user_context.clone()); + } +} + +extern "C" { + fn inet_ntoa(addr: *const libc::in_addr) -> *const c_char; +} + +unsafe fn get_ip(address: *const sockaddr_in) -> String { + let raw = inet_ntoa(&(*address).sin_addr as *const in_addr); + String::from(cstr::raw_to_str(raw)) +} diff --git a/zeroconf/src/macos/compat.rs b/zeroconf/src/macos/compat.rs new file mode 100644 index 0000000..2e97b15 --- /dev/null +++ b/zeroconf/src/macos/compat.rs @@ -0,0 +1,13 @@ +//! Utilities related to compatibility between platforms + +/// Normalizes the specified domain `&str` to conform to a standard enforced by this crate. +/// +/// Bonjour suffixes domains with a final `'.'` character in some contexts but is not required by +/// the standard. This function removes the final dot if present. +pub fn normalize_domain(domain: &str) -> String { + if domain.chars().nth(domain.len() - 1).unwrap() == '.' { + String::from(&domain[..domain.len() - 1]) + } else { + String::from(domain) + } +} diff --git a/zeroconf/src/macos/mod.rs b/zeroconf/src/macos/mod.rs new file mode 100644 index 0000000..91189a8 --- /dev/null +++ b/zeroconf/src/macos/mod.rs @@ -0,0 +1,14 @@ +//! macOS-specific ZeroConf bindings +//! +//! This module wraps the [Bonjour] mDNS implementation which is distributed with macOS. +//! +//! [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) + +pub(crate) mod browser; +pub(crate) mod service; + +pub mod compat; +pub mod service_ref; + +pub use browser::*; +pub use service::*; diff --git a/zeroconf/src/macos/service.rs b/zeroconf/src/macos/service.rs new file mode 100644 index 0000000..f40c44b --- /dev/null +++ b/zeroconf/src/macos/service.rs @@ -0,0 +1,104 @@ +use super::compat; +use super::service_ref::{ManagedDNSServiceRef, RegisterServiceParams}; +use crate::builder::BuilderDelegate; +use crate::ffi::{cstr, FromRaw}; +use crate::{ServiceRegisteredCallback, ServiceRegistration}; +use bonjour_sys::{DNSServiceErrorType, DNSServiceFlags, DNSServiceRef}; +use libc::{c_char, c_void}; +use std::any::Any; +use std::ffi::CString; +use std::ptr; +use std::sync::Arc; + +const BONJOUR_IF_UNSPEC: u32 = 0; +const BONJOUR_RENAME_FLAGS: DNSServiceFlags = 0; + +#[derive(Debug)] +pub struct BonjourMdnsService { + service: ManagedDNSServiceRef, + kind: CString, + port: u16, + context: *mut BonjourServiceContext, +} + +impl BonjourMdnsService { + pub fn new(kind: &str, port: u16) -> Self { + Self { + service: ManagedDNSServiceRef::default(), + kind: CString::new(kind).unwrap(), + port, + context: Box::into_raw(Box::default()), + } + } + + pub fn set_registered_callback(&mut self, registered_callback: Box) { + unsafe { (*self.context).registered_callback = Some(registered_callback) }; + } + + pub fn set_context(&mut self, context: Box) { + unsafe { (*self.context).user_context = Some(Arc::from(context)) }; + } + + pub fn start(&mut self) -> Result<(), String> { + debug!("Registering service: {:?}", self); + + self.service.register_service( + RegisterServiceParams::builder() + .flags(BONJOUR_RENAME_FLAGS) + .interface_index(BONJOUR_IF_UNSPEC) + .name(ptr::null()) + .regtype(self.kind.as_ptr()) + .domain(ptr::null()) + .host(ptr::null()) + .port(self.port) + .txt_len(0) + .txt_record(ptr::null()) + .callback(Some(register_callback)) + .context(self.context as *mut c_void) + .build()?, + ) + } +} + +impl Drop for BonjourMdnsService { + fn drop(&mut self) { + unsafe { Box::from_raw(self.context) }; + } +} + +#[derive(Default, FromRaw)] +struct BonjourServiceContext { + registered_callback: Option>, + user_context: Option>, +} + +unsafe extern "C" fn register_callback( + _sd_ref: DNSServiceRef, + _flags: DNSServiceFlags, + error: DNSServiceErrorType, + name: *const c_char, + regtype: *const c_char, + domain: *const c_char, + context: *mut c_void, +) { + if error != 0 { + panic!("register_callback() reported error (code: {0})", error); + } + + let domain = compat::normalize_domain(cstr::raw_to_str(domain)); + + let result = ServiceRegistration::builder() + .name(cstr::copy_raw(name)) + .kind(cstr::copy_raw(regtype)) + .domain(domain) + .build() + .expect("could not build ServiceRegistration"); + + let context = BonjourServiceContext::from_raw(context); + + if let Some(f) = &mut context.registered_callback { + f(result, context.user_context.clone()); + } else { + warn!("Service registered but no callback has been set"); + } +} diff --git a/zeroconf/src/macos/service_ref.rs b/zeroconf/src/macos/service_ref.rs new file mode 100644 index 0000000..5266083 --- /dev/null +++ b/zeroconf/src/macos/service_ref.rs @@ -0,0 +1,230 @@ +use bonjour_sys::{ + DNSServiceBrowse, DNSServiceBrowseReply, DNSServiceFlags, DNSServiceGetAddrInfo, + DNSServiceGetAddrInfoReply, DNSServiceProcessResult, DNSServiceProtocol, DNSServiceRef, + DNSServiceRefDeallocate, DNSServiceRegister, DNSServiceRegisterReply, DNSServiceResolve, + DNSServiceResolveReply, +}; +use libc::{c_char, c_void}; +use std::ptr; + +#[derive(Debug)] +pub struct ManagedDNSServiceRef { + service: DNSServiceRef, +} + +#[derive(Builder, BuilderDelegate)] +pub struct RegisterServiceParams { + flags: DNSServiceFlags, + interface_index: u32, + name: *const c_char, + regtype: *const c_char, + domain: *const c_char, + host: *const c_char, + port: u16, + txt_len: u16, + txt_record: *const c_void, + callback: DNSServiceRegisterReply, + context: *mut c_void, +} + +#[derive(Builder, BuilderDelegate)] +pub struct BrowseServicesParams { + flags: DNSServiceFlags, + interface_index: u32, + regtype: *const c_char, + domain: *const c_char, + callback: DNSServiceBrowseReply, + context: *mut c_void, +} + +#[derive(Builder, BuilderDelegate)] +pub struct ServiceResolveParams { + flags: DNSServiceFlags, + interface_index: u32, + name: *const c_char, + regtype: *const c_char, + domain: *const c_char, + callback: DNSServiceResolveReply, + context: *mut c_void, +} + +#[derive(Builder, BuilderDelegate)] +pub struct GetAddressInfoParams { + flags: DNSServiceFlags, + interface_index: u32, + protocol: DNSServiceProtocol, + hostname: *const c_char, + callback: DNSServiceGetAddrInfoReply, + context: *mut c_void, +} + +impl ManagedDNSServiceRef { + pub fn register_service( + &mut self, + RegisterServiceParams { + flags, + interface_index, + name, + regtype, + domain, + host, + port, + txt_len, + txt_record, + callback, + context, + }: RegisterServiceParams, + ) -> Result<(), String> { + let err = unsafe { + DNSServiceRegister( + &mut self.service as *mut DNSServiceRef, + flags, + interface_index, + name, + regtype, + domain, + host, + port, + txt_len, + txt_record, + callback, + context, + ) + }; + + if err != 0 { + return Err(format!("could not register service (code: {})", err).to_string()); + } + + loop { + self.process_result()? + } + } + + pub fn browse_services( + &mut self, + BrowseServicesParams { + flags, + interface_index, + regtype, + domain, + callback, + context, + }: BrowseServicesParams, + ) -> Result<(), String> { + let err = unsafe { + DNSServiceBrowse( + &mut self.service as *mut DNSServiceRef, + flags, + interface_index, + regtype, + domain, + callback, + context, + ) + }; + + if err != 0 { + return Err(format!("could not browse services (code: {})", err).to_string()); + } + + loop { + self.process_result()? + } + } + + pub fn resolve_service( + &mut self, + ServiceResolveParams { + flags, + interface_index, + name, + regtype, + domain, + callback, + context, + }: ServiceResolveParams, + ) -> Result<(), String> { + let error = unsafe { + DNSServiceResolve( + &mut self.service as *mut DNSServiceRef, + flags, + interface_index, + name, + regtype, + domain, + callback, + context, + ) + }; + + if error != 0 { + return Err(format!( + "DNSServiceResolve() reported error (code: {})", + error + )); + } + + self.process_result() + } + + pub fn get_address_info( + &mut self, + GetAddressInfoParams { + flags, + interface_index, + protocol, + hostname, + callback, + context, + }: GetAddressInfoParams, + ) -> Result<(), String> { + let err = unsafe { + DNSServiceGetAddrInfo( + &mut self.service as *mut DNSServiceRef, + flags, + interface_index, + protocol, + hostname, + callback, + context, + ) + }; + + if err != 0 { + return Err(format!( + "DNSServiceGetAddrInfo() reported error (code: {})", + err + )); + } + + self.process_result() + } + + fn process_result(&self) -> Result<(), String> { + let err = unsafe { DNSServiceProcessResult(self.service) }; + if err != 0 { + Err(format!("could not process service result (code: {})", err)) + } else { + Ok(()) + } + } +} + +impl Default for ManagedDNSServiceRef { + fn default() -> Self { + Self { + service: ptr::null_mut(), + } + } +} + +impl Drop for ManagedDNSServiceRef { + fn drop(&mut self) { + unsafe { + if self.service != ptr::null_mut() { + DNSServiceRefDeallocate(self.service); + } + } + } +} diff --git a/zeroconf/src/registration.rs b/zeroconf/src/registration.rs new file mode 100644 index 0000000..bf71807 --- /dev/null +++ b/zeroconf/src/registration.rs @@ -0,0 +1,22 @@ +use std::any::Any; +use std::sync::Arc; + +/// Callback invoked from [`MdnsService`] once it has successfully registered. +/// +/// # Arguments +/// `service` - The service information that was registered +/// `context` - The optional user context passed through using [`MdnsService::set_context()`] +/// +/// [`MdnsService`]: struct.MdnsService.html +/// [`MdnsService::set_context()`]: struct.MdnsService.html#method.set_context +pub type ServiceRegisteredCallback = dyn Fn(ServiceRegistration, Option>); + +/// Represents a registration event for a [`MdnsService`]. +/// +/// [`MdnsService`]: struct.MdnsService.html +#[derive(Builder, BuilderDelegate, Debug, Getters)] +pub struct ServiceRegistration { + name: String, + kind: String, + domain: String, +}