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]
|
||||
path = "core"
|
||||
version = "0.2.0"
|
||||
features = ["apresolve"]
|
||||
|
||||
[dependencies.librespot-metadata]
|
||||
path = "metadata"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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<String>,
|
||||
accesspoint: Vec<String>,
|
||||
dealer: Vec<String>,
|
||||
spclient: Vec<String>,
|
||||
}
|
||||
|
||||
async fn try_apresolve(
|
||||
proxy: Option<&Url>,
|
||||
ap_port: Option<u16>,
|
||||
) -> Result<(String, u16), Box<dyn Error>> {
|
||||
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<String>, fallback: &str, ap_port: Option<u16>) -> 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<ApResolveData, Box<dyn Error>> {
|
||||
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<u16>) -> (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<u16>) -> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<u16>) -> (String, u16) {
|
||||
super::ap_fallback()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,9 @@ impl Session {
|
|||
credentials: Credentials,
|
||||
cache: Option<Cache>,
|
||||
) -> 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);
|
||||
let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?;
|
||||
|
|
Loading…
Reference in a new issue