Resolve dealer and spclient access points (#795)

This commit is contained in:
Roderick van Domburg 2021-06-10 22:24:40 +02:00 committed by GitHub
parent 7ed35396f8
commit 6244515879
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 86 additions and 62 deletions

View file

@ -31,7 +31,6 @@ version = "0.2.0"
[dependencies.librespot-core]
path = "core"
version = "0.2.0"
features = ["apresolve"]
[dependencies.librespot-metadata]
path = "metadata"

View file

@ -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"]

View file

@ -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))
Ok(data)
}
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 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)
}
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);
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()
})
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);
}
}

View file

@ -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()
}
}

View file

@ -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?;