diff --git a/Cargo.lock b/Cargo.lock index 54ba474e..955044b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,6 +1143,7 @@ dependencies = [ "librespot-audio", "librespot-connect", "librespot-core", + "librespot-discovery", "librespot-metadata", "librespot-playback", "librespot-protocol", @@ -1172,15 +1173,8 @@ dependencies = [ name = "librespot-connect" version = "0.2.0" dependencies = [ - "aes-ctr", - "base64", - "dns-sd", "form_urlencoded", - "futures-core", "futures-util", - "hmac", - "hyper", - "libmdns", "librespot-core", "librespot-playback", "librespot-protocol", @@ -1189,10 +1183,8 @@ dependencies = [ "rand", "serde", "serde_json", - "sha-1", "tokio", "tokio-stream", - "url", ] [[package]] @@ -1235,6 +1227,27 @@ dependencies = [ "vergen", ] +[[package]] +name = "librespot-discovery" +version = "0.2.0" +dependencies = [ + "aes-ctr", + "base64", + "cfg-if 1.0.0", + "dns-sd", + "form_urlencoded", + "futures-core", + "hmac", + "hyper", + "libmdns", + "librespot-core", + "log", + "rand", + "serde_json", + "sha-1", + "tokio", +] + [[package]] name = "librespot-metadata" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 081cacae..80903698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,10 @@ path = "core" version = "0.2.0" features = ["apresolve"] +[dependencies.librespot-discovery] +path = "discovery" +version = "0.2.0" + [dependencies.librespot-metadata] path = "metadata" version = "0.2.0" @@ -72,7 +76,7 @@ gstreamer-backend = ["librespot-playback/gstreamer-backend"] with-tremor = ["librespot-playback/with-tremor"] with-vorbis = ["librespot-playback/with-vorbis"] -with-dns-sd = ["librespot-connect/with-dns-sd"] +with-dns-sd = ["librespot-discovery/with-dns-sd"] default = ["rodio-backend"] diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 8e9589fc..a46b70e4 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -8,25 +8,15 @@ repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies] -aes-ctr = "0.6" -base64 = "0.13" form_urlencoded = "1.0" -futures-core = "0.3" futures-util = { version = "0.3.5", default_features = false } -hmac = "0.11" -hyper = { version = "0.14", features = ["server", "http1", "tcp"] } -libmdns = "0.6" log = "0.4" protobuf = "2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.25" -sha-1 = "0.9" -tokio = { version = "1.0", features = ["macros", "rt", "sync"] } +serde_json = "1.0" +tokio = { version = "1.0", features = ["macros", "sync"] } tokio-stream = "0.1.1" -url = "2.1" - -dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" @@ -39,7 +29,3 @@ version = "0.2.0" [dependencies.librespot-protocol] path = "../protocol" version = "0.2.0" - -[features] -with-dns-sd = ["dns-sd"] - diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs deleted file mode 100644 index 7d559f0a..00000000 --- a/connect/src/discovery.rs +++ /dev/null @@ -1,263 +0,0 @@ -use aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; -use aes_ctr::Aes128Ctr; -use futures_core::Stream; -use hmac::{Hmac, Mac, NewMac}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, StatusCode}; -use serde_json::json; -use sha1::{Digest, Sha1}; -use tokio::sync::{mpsc, oneshot}; - -#[cfg(feature = "with-dns-sd")] -use dns_sd::DNSService; - -use librespot_core::authentication::Credentials; -use librespot_core::config::ConnectConfig; -use librespot_core::diffie_hellman::DhLocalKeys; - -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::convert::Infallible; -use std::io; -use std::net::{Ipv4Addr, SocketAddr}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; - -type HmacSha1 = Hmac; - -#[derive(Clone)] -struct Discovery(Arc); -struct DiscoveryInner { - config: ConnectConfig, - device_id: String, - keys: DhLocalKeys, - tx: mpsc::UnboundedSender, -} - -impl Discovery { - fn new( - config: ConnectConfig, - device_id: String, - ) -> (Discovery, mpsc::UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); - - let discovery = Discovery(Arc::new(DiscoveryInner { - config, - device_id, - keys: DhLocalKeys::random(&mut rand::thread_rng()), - tx, - })); - - (discovery, rx) - } - - fn handle_get_info(&self, _: BTreeMap, Cow<'_, str>>) -> Response { - let public_key = base64::encode(&self.0.keys.public_key()); - - let result = json!({ - "status": 101, - "statusString": "ERROR-OK", - "spotifyError": 0, - "version": "2.7.1", - "deviceID": (self.0.device_id), - "remoteName": (self.0.config.name), - "activeUser": "", - "publicKey": (public_key), - "deviceType": (self.0.config.device_type.to_string().to_uppercase()), - "libraryVersion": "0.1.0", - "accountReq": "PREMIUM", - "brandDisplayName": "librespot", - "modelDisplayName": "librespot", - "resolverVersion": "0", - "groupStatus": "NONE", - "voiceSupport": "NO", - }); - - let body = result.to_string(); - Response::new(Body::from(body)) - } - - fn handle_add_user( - &self, - params: BTreeMap, Cow<'_, str>>, - ) -> Response { - let username = params.get("userName").unwrap().as_ref(); - let encrypted_blob = params.get("blob").unwrap(); - let client_key = params.get("clientKey").unwrap(); - - let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); - - let shared_key = self - .0 - .keys - .shared_secret(&base64::decode(client_key.as_bytes()).unwrap()); - - let iv = &encrypted_blob[0..16]; - let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; - let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; - - let base_key = Sha1::digest(&shared_key); - let base_key = &base_key[..16]; - - let checksum_key = { - let mut h = HmacSha1::new_from_slice(base_key).expect("HMAC can take key of any size"); - h.update(b"checksum"); - h.finalize().into_bytes() - }; - - let encryption_key = { - let mut h = HmacSha1::new_from_slice(&base_key).expect("HMAC can take key of any size"); - h.update(b"encryption"); - h.finalize().into_bytes() - }; - - let mut h = HmacSha1::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); - h.update(encrypted); - if h.verify(cksum).is_err() { - warn!("Login error for user {:?}: MAC mismatch", username); - let result = json!({ - "status": 102, - "spotifyError": 1, - "statusString": "ERROR-MAC" - }); - - let body = result.to_string(); - return Response::new(Body::from(body)); - } - - let decrypted = { - let mut data = encrypted.to_vec(); - let mut cipher = Aes128Ctr::new( - &GenericArray::from_slice(&encryption_key[0..16]), - &GenericArray::from_slice(iv), - ); - cipher.apply_keystream(&mut data); - String::from_utf8(data).unwrap() - }; - - let credentials = - Credentials::with_blob(username.to_string(), &decrypted, &self.0.device_id); - - self.0.tx.send(credentials).unwrap(); - - let result = json!({ - "status": 101, - "spotifyError": 0, - "statusString": "ERROR-OK" - }); - - let body = result.to_string(); - Response::new(Body::from(body)) - } - - fn not_found(&self) -> Response { - let mut res = Response::default(); - *res.status_mut() = StatusCode::NOT_FOUND; - res - } - - async fn call(self, request: Request) -> hyper::Result> { - let mut params = BTreeMap::new(); - - let (parts, body) = request.into_parts(); - - if let Some(query) = parts.uri.query() { - let query_params = url::form_urlencoded::parse(query.as_bytes()); - params.extend(query_params); - } - - if parts.method != Method::GET { - debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); - } - - let body = hyper::body::to_bytes(body).await?; - - params.extend(url::form_urlencoded::parse(&body)); - - Ok( - match (parts.method, params.get("action").map(AsRef::as_ref)) { - (Method::GET, Some("getInfo")) => self.handle_get_info(params), - (Method::POST, Some("addUser")) => self.handle_add_user(params), - _ => self.not_found(), - }, - ) - } -} - -#[cfg(feature = "with-dns-sd")] -pub struct DiscoveryStream { - credentials: mpsc::UnboundedReceiver, - _svc: DNSService, - _close_tx: oneshot::Sender, -} - -#[cfg(not(feature = "with-dns-sd"))] -pub struct DiscoveryStream { - credentials: mpsc::UnboundedReceiver, - _svc: libmdns::Service, - _close_tx: oneshot::Sender, -} - -pub fn discovery( - config: ConnectConfig, - device_id: String, - port: u16, -) -> io::Result { - let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); - let (close_tx, close_rx) = oneshot::channel(); - - let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port); - - let make_service = make_service_fn(move |_| { - let discovery = discovery.clone(); - async move { Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().call(request))) } - }); - - let server = hyper::Server::bind(&address).serve(make_service); - - let s_port = server.local_addr().port(); - debug!("Zeroconf server listening on 0.0.0.0:{}", s_port); - - tokio::spawn(server.with_graceful_shutdown(async { - close_rx.await.unwrap_err(); - debug!("Shutting down discovery server"); - })); - - #[cfg(feature = "with-dns-sd")] - let svc = DNSService::register( - Some(&*config.name), - "_spotify-connect._tcp", - None, - None, - s_port, - &["VERSION=1.0", "CPath=/"], - ) - .unwrap(); - - #[cfg(not(feature = "with-dns-sd"))] - let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; - - #[cfg(not(feature = "with-dns-sd"))] - let svc = responder.register( - "_spotify-connect._tcp".to_owned(), - config.name, - s_port, - &["VERSION=1.0", "CPath=/"], - ); - - Ok(DiscoveryStream { - credentials: creds_rx, - _svc: svc, - _close_tx: close_tx, - }) -} - -impl Stream for DiscoveryStream { - type Item = Credentials; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.credentials.poll_recv(cx) - } -} diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 600dd033..5ddfeba9 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -6,5 +6,4 @@ use librespot_playback as playback; use librespot_protocol as protocol; pub mod context; -pub mod discovery; pub mod spirc; diff --git a/core/src/config.rs b/core/src/config.rs index 9c70c25b..4ab81c6a 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -71,30 +71,43 @@ impl FromStr for DeviceType { } } +impl From<&DeviceType> for &str { + fn from(d: &DeviceType) -> &'static str { + use self::DeviceType::*; + match d { + Unknown => "Unknown", + Computer => "Computer", + Tablet => "Tablet", + Smartphone => "Smartphone", + Speaker => "Speaker", + Tv => "TV", + Avr => "AVR", + Stb => "STB", + AudioDongle => "AudioDongle", + GameConsole => "GameConsole", + CastAudio => "CastAudio", + CastVideo => "CastVideo", + Automobile => "Automobile", + Smartwatch => "Smartwatch", + Chromebook => "Chromebook", + UnknownSpotify => "UnknownSpotify", + CarThing => "CarThing", + Observer => "Observer", + HomeThing => "HomeThing", + } + } +} + +impl From for &str { + fn from(d: DeviceType) -> &'static str { + (&d).into() + } +} + impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::DeviceType::*; - match *self { - Unknown => f.write_str("Unknown"), - Computer => f.write_str("Computer"), - Tablet => f.write_str("Tablet"), - Smartphone => f.write_str("Smartphone"), - Speaker => f.write_str("Speaker"), - Tv => f.write_str("TV"), - Avr => f.write_str("AVR"), - Stb => f.write_str("STB"), - AudioDongle => f.write_str("AudioDongle"), - GameConsole => f.write_str("GameConsole"), - CastAudio => f.write_str("CastAudio"), - CastVideo => f.write_str("CastVideo"), - Automobile => f.write_str("Automobile"), - Smartwatch => f.write_str("Smartwatch"), - Chromebook => f.write_str("Chromebook"), - UnknownSpotify => f.write_str("UnknownSpotify"), - CarThing => f.write_str("CarThing"), - Observer => f.write_str("Observer"), - HomeThing => f.write_str("HomeThing"), - } + let str: &str = self.into(); + f.write_str(str) } } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml new file mode 100644 index 00000000..1946c710 --- /dev/null +++ b/discovery/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "librespot-discovery" +version = "0.2.0" +authors = ["Paul Lietar "] +description = "The discovery logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2018" + +[dependencies] +aes-ctr = "0.6" +base64 = "0.13" +cfg-if = "1.0" +form_urlencoded = "1.0" +futures-core = "0.3" +hmac = "0.11" +hyper = { version = "0.14", features = ["server", "http1", "tcp"] } +libmdns = "0.6" +log = "0.4" +rand = "0.8" +serde_json = "1.0.25" +sha-1 = "0.9" +tokio = { version = "1.0", features = ["sync", "rt"] } + +dns-sd = { version = "0.1.3", optional = true } + +[dependencies.librespot-core] +path = "../core" +default_features = false +version = "0.2.0" + +[features] +with-dns-sd = ["dns-sd"] diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs new file mode 100644 index 00000000..cd78345d --- /dev/null +++ b/discovery/src/lib.rs @@ -0,0 +1,134 @@ +//! Advertises this device to Spotify clients in the local network. +//! +//! This device will show up in the list of "available devices". +//! Once it is selected from the list, [`Credentials`] are received. +//! Those can be used to establish a new Session with [`librespot_core`]. + +#![warn(clippy::all, missing_docs, rust_2018_idioms)] + +mod server; + +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use cfg_if::cfg_if; +use futures_core::Stream; +use librespot_core as core; + +use self::server::DiscoveryServer; + +/// Credentials to be used in [`librespot`](`librespot_core`). +pub use crate::core::authentication::Credentials; + +/// Determining the icon in the list of available devices. +pub use crate::core::config::DeviceType; + +/// Makes this device visible to Spotify clients in the local network. +/// +/// `Discovery` implements the [`Stream`] trait. Every time this device +/// is selected in the list of available devices, it yields [`Credentials`]. +pub struct Discovery { + server: DiscoveryServer, + + #[cfg(not(feature = "with-dns-sd"))] + _svc: libmdns::Service, + #[cfg(feature = "with-dns-sd")] + _svc: dns_sd::DNSService, +} + +/// A builder for [`Discovery`]. +pub struct Builder { + server_config: server::Config, + port: u16, +} + +impl Builder { + /// Starts a new builder using the provided device id. + pub fn new(device_id: String) -> Self { + Self { + server_config: server::Config { + name: "Librespot".into(), + device_type: DeviceType::default(), + device_id, + }, + port: 0, + } + } + + /// Sets the name to be displayed. Default is `"Librespot"`. + pub fn name(mut self, name: String) -> Self { + self.server_config.name = name.into(); + self + } + + /// Sets the device type which is visible as icon in other Spotify clients. Default is `Speaker`. + pub fn device_type(mut self, device_type: DeviceType) -> Self { + self.server_config.device_type = device_type; + self + } + + /// Sets the port on which it should listen to incoming connections. + /// The default value `0` means any port. + pub fn port(mut self, port: u16) -> Self { + self.port = port; + self + } + + /// Sets up the [`Discovery`] instance. + /// + /// # Errors + /// If setting up the mdns service or creating the server fails, this function returns an error. + pub fn launch(self) -> io::Result { + Discovery::new(self) + } +} + +impl Discovery { + /// Starts a [`Builder`] with the provided device id. + pub fn builder(device_id: String) -> Builder { + Builder::new(device_id) + } + + fn new(builder: Builder) -> io::Result { + let name = builder.server_config.name.clone(); + let mut port = builder.port; + let server = DiscoveryServer::new(builder.server_config, &mut port) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + let svc; + + cfg_if! { + if #[cfg(feature = "with-dns-sd")] { + svc = dns_sd::DNSService::register( + Some(name.as_ref()), + "_spotify-connect._tcp", + None, + None, + port, + &["VERSION=1.0", "CPath=/"], + ) + .unwrap(); + + } else { + let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; + svc = responder.register( + "_spotify-connect._tcp".to_owned(), + name.into_owned(), + port, + &["VERSION=1.0", "CPath=/"], + ) + } + }; + + Ok(Self { server, _svc: svc }) + } +} + +impl Stream for Discovery { + type Item = Credentials; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.server).poll_next(cx) + } +} diff --git a/discovery/src/server.rs b/discovery/src/server.rs new file mode 100644 index 00000000..867ac3c9 --- /dev/null +++ b/discovery/src/server.rs @@ -0,0 +1,236 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::convert::Infallible; +use std::net::{Ipv4Addr, SocketAddr}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use aes_ctr::cipher::generic_array::GenericArray; +use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; +use aes_ctr::Aes128Ctr; +use futures_core::Stream; +use hmac::{Hmac, Mac, NewMac}; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Method, Request, Response, StatusCode}; +use log::{debug, warn}; +use serde_json::json; +use sha1::{Digest, Sha1}; +use tokio::sync::{mpsc, oneshot}; + +use crate::core::authentication::Credentials; +use crate::core::config::DeviceType; +use crate::core::diffie_hellman::DhLocalKeys; + +type Params<'a> = BTreeMap, Cow<'a, str>>; + +pub struct Config { + pub name: Cow<'static, str>, + pub device_type: DeviceType, + pub device_id: String, +} + +struct RequestHandler { + config: Config, + keys: DhLocalKeys, + tx: mpsc::UnboundedSender, +} + +impl RequestHandler { + fn new(config: Config) -> (Self, mpsc::UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded_channel(); + + let discovery = Self { + config, + keys: DhLocalKeys::random(&mut rand::thread_rng()), + tx, + }; + + (discovery, rx) + } + + fn handle_get_info(&self) -> Response { + let public_key = base64::encode(&self.keys.public_key()); + let device_type: &str = self.config.device_type.into(); + + let body = json!({ + "status": 101, + "statusString": "ERROR-OK", + "spotifyError": 0, + "version": "2.7.1", + "deviceID": (self.config.device_id), + "remoteName": (self.config.name), + "activeUser": "", + "publicKey": (public_key), + "deviceType": (device_type), + "libraryVersion": "0.1.0", + "accountReq": "PREMIUM", + "brandDisplayName": "librespot", + "modelDisplayName": "librespot", + "resolverVersion": "0", + "groupStatus": "NONE", + "voiceSupport": "NO", + }) + .to_string(); + + Response::new(Body::from(body)) + } + + fn handle_add_user(&self, params: &Params<'_>) -> Response { + let username = params.get("userName").unwrap().as_ref(); + let encrypted_blob = params.get("blob").unwrap(); + let client_key = params.get("clientKey").unwrap(); + + let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); + + let client_key = base64::decode(client_key.as_bytes()).unwrap(); + let shared_key = self.keys.shared_secret(&client_key); + + let iv = &encrypted_blob[0..16]; + let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; + let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; + + let base_key = Sha1::digest(&shared_key); + let base_key = &base_key[..16]; + + let checksum_key = { + let mut h = + Hmac::::new_from_slice(base_key).expect("HMAC can take key of any size"); + h.update(b"checksum"); + h.finalize().into_bytes() + }; + + let encryption_key = { + let mut h = + Hmac::::new_from_slice(base_key).expect("HMAC can take key of any size"); + h.update(b"encryption"); + h.finalize().into_bytes() + }; + + let mut h = + Hmac::::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); + h.update(encrypted); + if h.verify(cksum).is_err() { + warn!("Login error for user {:?}: MAC mismatch", username); + let result = json!({ + "status": 102, + "spotifyError": 1, + "statusString": "ERROR-MAC" + }); + + let body = result.to_string(); + return Response::new(Body::from(body)); + } + + let decrypted = { + let mut data = encrypted.to_vec(); + let mut cipher = Aes128Ctr::new( + GenericArray::from_slice(&encryption_key[0..16]), + GenericArray::from_slice(iv), + ); + cipher.apply_keystream(&mut data); + String::from_utf8(data).unwrap() + }; + + let credentials = + Credentials::with_blob(username.to_string(), &decrypted, &self.config.device_id); + + self.tx.send(credentials).unwrap(); + + let result = json!({ + "status": 101, + "spotifyError": 0, + "statusString": "ERROR-OK" + }); + + let body = result.to_string(); + Response::new(Body::from(body)) + } + + fn not_found(&self) -> Response { + let mut res = Response::default(); + *res.status_mut() = StatusCode::NOT_FOUND; + res + } + + async fn handle(self: Arc, request: Request) -> hyper::Result> { + let mut params = Params::new(); + + let (parts, body) = request.into_parts(); + + if let Some(query) = parts.uri.query() { + let query_params = form_urlencoded::parse(query.as_bytes()); + params.extend(query_params); + } + + if parts.method != Method::GET { + debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); + } + + let body = hyper::body::to_bytes(body).await?; + + params.extend(form_urlencoded::parse(&body)); + + let action = params.get("action").map(Cow::as_ref); + + Ok(match (parts.method, action) { + (Method::GET, Some("getInfo")) => self.handle_get_info(), + (Method::POST, Some("addUser")) => self.handle_add_user(¶ms), + _ => self.not_found(), + }) + } +} + +pub struct DiscoveryServer { + cred_rx: mpsc::UnboundedReceiver, + _close_tx: oneshot::Sender, +} + +impl DiscoveryServer { + pub fn new(config: Config, port: &mut u16) -> hyper::Result { + let (discovery, cred_rx) = RequestHandler::new(config); + let discovery = Arc::new(discovery); + + let (close_tx, close_rx) = oneshot::channel(); + + let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port); + + let make_service = make_service_fn(move |_| { + let discovery = discovery.clone(); + async move { + Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request))) + } + }); + + let server = hyper::Server::try_bind(&address)?.serve(make_service); + + *port = server.local_addr().port(); + debug!("Zeroconf server listening on 0.0.0.0:{}", *port); + + tokio::spawn(async { + let result = server + .with_graceful_shutdown(async { + close_rx.await.unwrap_err(); + debug!("Shutting down discovery server"); + }) + .await; + + if let Err(e) = result { + warn!("Discovery server failed: {}", e); + } + }); + + Ok(Self { + cred_rx, + _close_tx: close_tx, + }) + } +} + +impl Stream for DiscoveryServer { + type Item = Credentials; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.cred_rx.poll_recv(cx) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7722e93e..75211282 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub use librespot_audio as audio; pub use librespot_connect as connect; pub use librespot_core as core; +pub use librespot_discovery as discovery; pub use librespot_metadata as metadata; pub use librespot_playback as playback; pub use librespot_protocol as protocol; diff --git a/src/main.rs b/src/main.rs index a5106af2..b401f9b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -645,11 +645,14 @@ async fn main() { let mut connecting: Pin>> = Box::pin(future::pending()); if setup.enable_discovery { - let config = setup.connect_config.clone(); let device_id = setup.session_config.device_id.clone(); discovery = Some( - librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port) + librespot::discovery::Discovery::builder(device_id) + .name(setup.connect_config.name.clone()) + .device_type(setup.connect_config.device_type) + .port(setup.zeroconf_port) + .launch() .unwrap(), ); }