librespot/core/src/apresolve.rs

109 lines
3.2 KiB
Rust
Raw Normal View History

2021-03-17 20:24:28 +00:00
use std::error::Error;
2021-03-17 20:24:28 +00:00
use hyper::client::HttpConnector;
use hyper::{Body, Client, Method, Request, Uri};
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use serde::Deserialize;
use url::Url;
2021-03-17 20:24:28 +00:00
const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80";
2021-06-01 18:33:10 +00:00
const AP_FALLBACK: &str = "ap.spotify.com:443";
const AP_BLACKLIST: [&str; 2] = ["ap-gew4.spotify.com", "ap-gue1.spotify.com"];
2021-03-17 20:24:28 +00:00
#[derive(Clone, Debug, Deserialize)]
2021-03-31 18:05:32 +00:00
struct ApResolveData {
2021-03-17 20:24:28 +00:00
ap_list: Vec<String>,
}
2021-03-17 20:24:28 +00:00
async fn try_apresolve(
proxy: Option<&Url>,
ap_port: Option<u16>,
) -> Result<String, Box<dyn Error>> {
let port = ap_port.unwrap_or(443);
2021-03-17 20:24:28 +00:00
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");
2021-02-10 21:50:08 +00:00
2021-03-17 20:24:28 +00:00
let response = if let Some(url) = proxy {
// Panic safety: all URLs are valid URIs
let uri = url.to_string().parse().unwrap();
let proxy = Proxy::new(Intercept::All, uri);
let connector = HttpConnector::new();
let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy);
Client::builder()
.build(proxy_connector)
.request(req)
.await?
} else {
Client::new().request(req).await?
};
2021-02-10 21:50:08 +00:00
2021-03-17 20:24:28 +00:00
let body = hyper::body::to_bytes(response.into_body()).await?;
2021-03-31 18:05:32 +00:00
let data: ApResolveData = serde_json::from_slice(body.as_ref())?;
2021-02-10 21:50:08 +00:00
2022-07-28 16:51:49 +00:00
// filter APs that are known to cause channel errors
let aps: Vec<String> = data
.ap_list
.into_iter()
.filter_map(|ap| {
let host = ap.parse::<Uri>().ok()?.host()?.to_owned();
if !AP_BLACKLIST.iter().any(|&blacklisted| host == blacklisted) {
Some(ap)
} else {
warn!("Ignoring blacklisted access point {}", ap);
None
}
})
.collect();
2021-03-17 20:24:28 +00:00
let ap = if ap_port.is_some() || proxy.is_some() {
2022-07-28 16:51:49 +00:00
// filter on ports if specified on the command line...
aps.into_iter().find_map(|ap| {
2021-03-17 20:24:28 +00:00
if ap.parse::<Uri>().ok()?.port()? == port {
Some(ap)
2021-02-10 21:50:08 +00:00
} else {
2021-03-17 20:24:28 +00:00
None
2018-03-23 15:52:24 +00:00
}
2021-03-17 20:24:28 +00:00
})
2021-02-10 21:50:08 +00:00
} else {
2022-07-28 16:51:49 +00:00
// ...or pick the first on the list
aps.into_iter().next()
2021-02-10 21:50:08 +00:00
}
2022-07-28 16:51:49 +00:00
.ok_or("Unable to resolve any viable access points.")?;
2021-03-17 20:24:28 +00:00
Ok(ap)
}
pub async fn apresolve(proxy: Option<&Url>, ap_port: Option<u16>) -> String {
try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| {
warn!("Failed to resolve Access Point: {}", e);
warn!("Using fallback \"{}\"", AP_FALLBACK);
AP_FALLBACK.into()
})
}
#[cfg(test)]
mod test {
use std::net::ToSocketAddrs;
use super::try_apresolve;
#[tokio::test]
async fn test_apresolve() {
let ap = try_apresolve(None, None).await.unwrap();
// Assert that the result contains a valid host and port
ap.to_socket_addrs().unwrap().next().unwrap();
}
#[tokio::test]
async fn test_apresolve_port_443() {
let ap = try_apresolve(None, Some(443)).await.unwrap();
let port = ap.to_socket_addrs().unwrap().next().unwrap().port();
assert_eq!(port, 443);
}
}