mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Resolve dealer
and spclient
access points (#795)
This commit is contained in:
parent
7ed35396f8
commit
6244515879
5 changed files with 86 additions and 62 deletions
|
@ -31,7 +31,6 @@ version = "0.2.0"
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "core"
|
path = "core"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
features = ["apresolve"]
|
|
||||||
|
|
||||||
[dependencies.librespot-metadata]
|
[dependencies.librespot-metadata]
|
||||||
path = "metadata"
|
path = "metadata"
|
||||||
|
|
|
@ -23,8 +23,8 @@ futures-util = { version = "0.3", default-features = false, features = ["alloc",
|
||||||
hmac = "0.11"
|
hmac = "0.11"
|
||||||
httparse = "1.3"
|
httparse = "1.3"
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] }
|
hyper = { version = "0.14", features = ["client", "tcp", "http1"] }
|
||||||
hyper-proxy = { version = "0.9.1", optional = true, default-features = false }
|
hyper-proxy = { version = "0.9.1", default-features = false }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-bigint = { version = "0.4", features = ["rand"] }
|
num-bigint = { version = "0.4", features = ["rand"] }
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
|
@ -53,6 +53,3 @@ vergen = "3.0.4"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
tokio = {version = "1.0", features = ["macros"] }
|
tokio = {version = "1.0", features = ["macros"] }
|
||||||
|
|
||||||
[features]
|
|
||||||
apresolve = ["hyper", "hyper-proxy"]
|
|
||||||
|
|
|
@ -1,30 +1,67 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use hyper::client::HttpConnector;
|
use hyper::client::HttpConnector;
|
||||||
use hyper::{Body, Client, Method, Request};
|
use hyper::{Body, Client, Request};
|
||||||
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
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 {
|
struct ApResolveData {
|
||||||
ap_list: Vec<String>,
|
accesspoint: Vec<String>,
|
||||||
|
dealer: Vec<String>,
|
||||||
|
spclient: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn try_apresolve(
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
proxy: Option<&Url>,
|
pub struct AccessPoints {
|
||||||
ap_port: Option<u16>,
|
pub accesspoint: SocketAddress,
|
||||||
) -> Result<(String, u16), Box<dyn Error>> {
|
pub dealer: SocketAddress,
|
||||||
let port = ap_port.unwrap_or(443);
|
pub spclient: SocketAddress,
|
||||||
|
}
|
||||||
|
|
||||||
let mut req = Request::new(Body::empty());
|
fn select_ap(data: Vec<String>, fallback: &str, ap_port: Option<u16>) -> SocketAddress {
|
||||||
*req.method_mut() = Method::GET;
|
let port = ap_port.unwrap_or(FALLBACK_PORT);
|
||||||
// panic safety: APRESOLVE_ENDPOINT above is valid url.
|
|
||||||
*req.uri_mut() = APRESOLVE_ENDPOINT.parse().expect("invalid AP resolve URL");
|
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<ApResolveData, Box<dyn Error>> {
|
||||||
|
let req = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri(APRESOLVE_ENDPOINT)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let response = if let Some(url) = proxy {
|
let response = if let Some(url) = proxy {
|
||||||
// Panic safety: all URLs are valid URIs
|
// 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 body = hyper::body::to_bytes(response.into_body()).await?;
|
||||||
let data: ApResolveData = serde_json::from_slice(body.as_ref())?;
|
let data: ApResolveData = serde_json::from_slice(body.as_ref())?;
|
||||||
|
|
||||||
let mut aps = data.ap_list.into_iter().filter_map(|ap| {
|
Ok(data)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn apresolve(proxy: Option<&Url>, ap_port: Option<u16>) -> (String, u16) {
|
pub async fn apresolve(proxy: Option<&Url>, ap_port: Option<u16>) -> AccessPoints {
|
||||||
try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| {
|
let data = try_apresolve(proxy).await.unwrap_or_else(|e| {
|
||||||
warn!("Failed to resolve Access Point: {}, using fallback.", e);
|
warn!("Failed to resolve access points: {}, using fallbacks.", e);
|
||||||
ap_fallback()
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
|
|
||||||
use super::try_apresolve;
|
use super::apresolve;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_apresolve() {
|
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
|
// 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]
|
#[tokio::test]
|
||||||
async fn test_apresolve_port_443() {
|
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);
|
assert_eq!(port, 443);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use librespot_protocol as protocol;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod component;
|
mod component;
|
||||||
|
|
||||||
|
mod apresolve;
|
||||||
pub mod audio_key;
|
pub mod audio_key;
|
||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
@ -27,17 +28,3 @@ pub mod spotify_id;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod version;
|
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<u16>) -> (String, u16) {
|
|
||||||
super::ap_fallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -67,7 +67,9 @@ impl Session {
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
) -> Result<Session, SessionError> {
|
) -> Result<Session, SessionError> {
|
||||||
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);
|
info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
|
||||||
let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?;
|
let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?;
|
||||||
|
|
Loading…
Reference in a new issue