diff --git a/Cargo.toml b/Cargo.toml index 081cacae..5df27872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ version = "0.2.0" [dependencies.librespot-core] path = "core" version = "0.2.0" -features = ["apresolve"] [dependencies.librespot-metadata] path = "metadata" diff --git a/core/Cargo.toml b/core/Cargo.toml index 8ed21273..7eb4051c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,8 +23,8 @@ futures-util = { version = "0.3", default-features = false, features = ["alloc", hmac = "0.11" httparse = "1.3" http = "0.2" -hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } -hyper-proxy = { version = "0.9.1", optional = true, default-features = false } +hyper = { version = "0.14", features = ["client", "tcp", "http1"] } +hyper-proxy = { version = "0.9.1", default-features = false } log = "0.4" num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" @@ -53,6 +53,3 @@ vergen = "3.0.4" [dev-dependencies] env_logger = "0.8" tokio = {version = "1.0", features = ["macros"] } - -[features] -apresolve = ["hyper", "hyper-proxy"] diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 8dced22d..975e0e18 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,30 +1,67 @@ use std::error::Error; use hyper::client::HttpConnector; -use hyper::{Body, Client, Method, Request}; +use hyper::{Body, Client, Request}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use serde::Deserialize; use url::Url; -use super::ap_fallback; +const APRESOLVE_ENDPOINT: &str = + "http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient"; -const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; +// These addresses probably do some geo-location based traffic management or at least DNS-based +// load balancing. They are known to fail when the normal resolvers are up, so that's why they +// should only be used as fallback. +const AP_FALLBACK: &str = "ap.spotify.com"; +const DEALER_FALLBACK: &str = "dealer.spotify.com"; +const SPCLIENT_FALLBACK: &str = "spclient.wg.spotify.com"; -#[derive(Clone, Debug, Deserialize)] +const FALLBACK_PORT: u16 = 443; + +pub type SocketAddress = (String, u16); + +#[derive(Clone, Debug, Default, Deserialize)] struct ApResolveData { - ap_list: Vec, + accesspoint: Vec, + dealer: Vec, + spclient: Vec, } -async fn try_apresolve( - proxy: Option<&Url>, - ap_port: Option, -) -> Result<(String, u16), Box> { - let port = ap_port.unwrap_or(443); +#[derive(Clone, Debug, Deserialize)] +pub struct AccessPoints { + pub accesspoint: SocketAddress, + pub dealer: SocketAddress, + pub spclient: SocketAddress, +} - let mut req = Request::new(Body::empty()); - *req.method_mut() = Method::GET; - // panic safety: APRESOLVE_ENDPOINT above is valid url. - *req.uri_mut() = APRESOLVE_ENDPOINT.parse().expect("invalid AP resolve URL"); +fn select_ap(data: Vec, fallback: &str, ap_port: Option) -> SocketAddress { + let port = ap_port.unwrap_or(FALLBACK_PORT); + + let mut aps = data.into_iter().filter_map(|ap| { + let mut split = ap.rsplitn(2, ':'); + let port = split + .next() + .expect("rsplitn should not return empty iterator"); + let host = split.next()?.to_owned(); + let port: u16 = port.parse().ok()?; + Some((host, port)) + }); + + let ap = if ap_port.is_some() { + aps.find(|(_, p)| *p == port) + } else { + aps.next() + }; + + ap.unwrap_or_else(|| (String::from(fallback), port)) +} + +async fn try_apresolve(proxy: Option<&Url>) -> Result> { + let req = Request::builder() + .method("GET") + .uri(APRESOLVE_ENDPOINT) + .body(Body::empty()) + .unwrap(); let response = if let Some(url) = proxy { // Panic safety: all URLs are valid URIs @@ -43,51 +80,53 @@ async fn try_apresolve( let body = hyper::body::to_bytes(response.into_body()).await?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?; - let mut aps = data.ap_list.into_iter().filter_map(|ap| { - let mut split = ap.rsplitn(2, ':'); - let port = split - .next() - .expect("rsplitn should not return empty iterator"); - let host = split.next()?.to_owned(); - let port: u16 = port.parse().ok()?; - Some((host, port)) - }); - let ap = if ap_port.is_some() || proxy.is_some() { - aps.find(|(_, p)| *p == port) - } else { - aps.next() - } - .ok_or("no valid AP in list")?; - - Ok(ap) + Ok(data) } -pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> (String, u16) { - try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| { - warn!("Failed to resolve Access Point: {}, using fallback.", e); - ap_fallback() - }) +pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> AccessPoints { + let data = try_apresolve(proxy).await.unwrap_or_else(|e| { + warn!("Failed to resolve access points: {}, using fallbacks.", e); + ApResolveData::default() + }); + + let accesspoint = select_ap(data.accesspoint, AP_FALLBACK, ap_port); + let dealer = select_ap(data.dealer, DEALER_FALLBACK, ap_port); + let spclient = select_ap(data.spclient, SPCLIENT_FALLBACK, ap_port); + + AccessPoints { + accesspoint, + dealer, + spclient, + } } #[cfg(test)] mod test { use std::net::ToSocketAddrs; - use super::try_apresolve; + use super::apresolve; #[tokio::test] async fn test_apresolve() { - let ap = try_apresolve(None, None).await.unwrap(); + let aps = apresolve(None, None).await; // Assert that the result contains a valid host and port - ap.to_socket_addrs().unwrap().next().unwrap(); + aps.accesspoint.to_socket_addrs().unwrap().next().unwrap(); + aps.dealer.to_socket_addrs().unwrap().next().unwrap(); + aps.spclient.to_socket_addrs().unwrap().next().unwrap(); } #[tokio::test] async fn test_apresolve_port_443() { - let ap = try_apresolve(None, Some(443)).await.unwrap(); + let aps = apresolve(None, Some(443)).await; - let port = ap.to_socket_addrs().unwrap().next().unwrap().port(); + let port = aps + .accesspoint + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + .port(); assert_eq!(port, 443); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index c6f6e190..f26caf3d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,6 +8,7 @@ use librespot_protocol as protocol; #[macro_use] mod component; +mod apresolve; pub mod audio_key; pub mod authentication; pub mod cache; @@ -27,17 +28,3 @@ pub mod spotify_id; #[doc(hidden)] pub mod util; pub mod version; - -fn ap_fallback() -> (String, u16) { - (String::from("ap.spotify.com"), 443) -} - -#[cfg(feature = "apresolve")] -mod apresolve; - -#[cfg(not(feature = "apresolve"))] -mod apresolve { - pub async fn apresolve(_: Option<&url::Url>, _: Option) -> (String, u16) { - super::ap_fallback() - } -} diff --git a/core/src/session.rs b/core/src/session.rs index f43a4cc0..17452b20 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -67,7 +67,9 @@ impl Session { credentials: Credentials, cache: Option, ) -> Result { - let ap = apresolve(config.proxy.as_ref(), config.ap_port).await; + let ap = apresolve(config.proxy.as_ref(), config.ap_port) + .await + .accesspoint; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?;