Put apresolve behind feature flag

This commit is contained in:
johannesd3 2021-02-10 22:50:08 +01:00 committed by Johannesd3
parent 9253be7bc9
commit 8cff10e983
6 changed files with 124 additions and 101 deletions

2
Cargo.lock generated
View file

@ -1450,6 +1450,7 @@ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
"bytes", "bytes",
"cfg-if 1.0.0",
"env_logger", "env_logger",
"error-chain", "error-chain",
"futures", "futures",
@ -1473,7 +1474,6 @@ dependencies = [
"shannon", "shannon",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower-service",
"url 1.7.2", "url 1.7.2",
"vergen", "vergen",
] ]

View file

@ -61,6 +61,9 @@ sha-1 = "0.8"
hex = "0.4" hex = "0.4"
[features] [features]
apresolve = ["librespot-core/apresolve"]
apresolve-http2 = ["librespot-core/apresolve-http2"]
alsa-backend = ["librespot-playback/alsa-backend"] alsa-backend = ["librespot-playback/alsa-backend"]
portaudio-backend = ["librespot-playback/portaudio-backend"] portaudio-backend = ["librespot-playback/portaudio-backend"]
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
@ -75,7 +78,7 @@ with-vorbis = ["librespot-audio/with-vorbis"]
# with-dns-sd = ["librespot-connect/with-dns-sd"] # with-dns-sd = ["librespot-connect/with-dns-sd"]
default = ["librespot-playback/rodio-backend"] default = ["rodio-backend", "apresolve"]
[package.metadata.deb] [package.metadata.deb]
maintainer = "librespot-org" maintainer = "librespot-org"

View file

@ -17,11 +17,12 @@ aes = "0.6"
base64 = "0.13" base64 = "0.13"
byteorder = "1.4" byteorder = "1.4"
bytes = "1.0" bytes = "1.0"
cfg-if = "1"
error-chain = { version = "0.12", default-features = false } error-chain = { version = "0.12", default-features = false }
futures = { version = "0.3", features = ["bilock", "unstable"] } futures = { version = "0.3", features = ["bilock", "unstable"] }
hmac = "0.10" hmac = "0.10"
httparse = "1.3" httparse = "1.3"
hyper = { version = "0.14", features = ["client", "tcp", "http1", "http2"] } hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] }
log = "0.4" log = "0.4"
num-bigint = "0.3" num-bigint = "0.3"
num-integer = "0.1" num-integer = "0.1"
@ -38,7 +39,6 @@ sha-1 = "0.9"
shannon = "0.2.0" shannon = "0.2.0"
tokio = { version = "1.0", features = ["io-util", "rt-multi-thread"] } tokio = { version = "1.0", features = ["io-util", "rt-multi-thread"] }
tokio-util = { version = "0.6", features = ["codec"] } tokio-util = { version = "0.6", features = ["codec"] }
tower-service = "0.3"
url = "1.7" url = "1.7"
[build-dependencies] [build-dependencies]
@ -47,4 +47,8 @@ vergen = "3.0.4"
[dev-dependencies] [dev-dependencies]
env_logger = "*" env_logger = "*"
tokio = {version = "1.0", features = ["macros"] } tokio = {version = "1.0", features = ["macros"] }
[features]
apresolve = ["hyper"]
apresolve-http2 = ["apresolve", "hyper/http2"]

View file

@ -1,61 +1,72 @@
const AP_FALLBACK: &'static str = "ap.spotify.com:443"; const AP_FALLBACK: &'static str = "ap.spotify.com:443";
const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com:80";
use hyper::{Body, Client, Method, Request, Uri};
use std::error::Error;
use url::Url; use url::Url;
use crate::proxytunnel::ProxyTunnel; cfg_if! {
if #[cfg(feature = "apresolve")] {
const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com:80";
#[derive(Clone, Debug, Serialize, Deserialize)] use std::error::Error;
pub struct APResolveData {
ap_list: Vec<String>,
}
async fn apresolve(proxy: &Option<Url>, ap_port: &Option<u16>) -> Result<String, Box<dyn Error>> { use hyper::{Body, Client, Method, Request, Uri};
let port = ap_port.unwrap_or(443);
let req = Request::builder() use crate::proxytunnel::ProxyTunnel;
.method(Method::GET)
.uri(
APRESOLVE_ENDPOINT
.parse::<Uri>()
.expect("invalid AP resolve URL"),
)
.body(Body::empty())?;
let response = if let Some(url) = proxy { #[derive(Clone, Debug, Serialize, Deserialize)]
Client::builder() pub struct APResolveData {
.build(ProxyTunnel::new(url)?) ap_list: Vec<String>,
.request(req) }
.await?
} else {
Client::new().request(req).await?
};
let body = hyper::body::to_bytes(response.into_body()).await?; async fn apresolve(proxy: &Option<Url>, ap_port: &Option<u16>) -> Result<String, Box<dyn Error>> {
let data: APResolveData = serde_json::from_slice(body.as_ref())?; let port = ap_port.unwrap_or(443);
let ap = if ap_port.is_some() || proxy.is_some() { let req = Request::builder()
data.ap_list.into_iter().find_map(|ap| { .method(Method::GET)
if ap.parse::<Uri>().ok()?.port()? == port { .uri(
Some(ap) APRESOLVE_ENDPOINT
.parse::<Uri>()
.expect("invalid AP resolve URL"),
)
.body(Body::empty())?;
let response = if let Some(url) = proxy {
Client::builder()
.build(ProxyTunnel::new(url)?)
.request(req)
.await?
} else { } else {
None Client::new().request(req).await?
};
let body = hyper::body::to_bytes(response.into_body()).await?;
let data: APResolveData = serde_json::from_slice(body.as_ref())?;
let ap = if ap_port.is_some() || proxy.is_some() {
data.ap_list.into_iter().find_map(|ap| {
if ap.parse::<Uri>().ok()?.port()? == port {
Some(ap)
} else {
None
}
})
} else {
data.ap_list.into_iter().next()
} }
}) .ok_or("empty AP List")?;
Ok(ap)
}
pub async fn apresolve_or_fallback(proxy: &Option<Url>, ap_port: &Option<u16>) -> String {
apresolve(proxy, ap_port).await.unwrap_or_else(|e| {
warn!("Failed to resolve Access Point: {}", e);
warn!("Using fallback \"{}\"", AP_FALLBACK);
AP_FALLBACK.into()
})
}
} else { } else {
data.ap_list.into_iter().next() pub async fn apresolve_or_fallback(_: &Option<Url>, _: &Option<u16>) -> String {
AP_FALLBACK.to_string()
}
} }
.ok_or("empty AP List")?;
Ok(ap)
}
pub async fn apresolve_or_fallback(proxy: &Option<Url>, ap_port: &Option<u16>) -> String {
apresolve(proxy, ap_port).await.unwrap_or_else(|e| {
warn!("Failed to resolve Access Point: {}", e);
warn!("Using fallback \"{}\"", AP_FALLBACK);
AP_FALLBACK.into()
})
} }

View file

@ -3,6 +3,8 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
#[macro_use] #[macro_use]
extern crate cfg_if;
#[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate pin_project_lite; extern crate pin_project_lite;

View file

@ -1,16 +1,6 @@
use futures::Future; use std::io;
use hyper::Uri;
use std::{ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
io,
net::{SocketAddr, ToSocketAddrs},
pin::Pin,
task::Poll,
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::TcpStream,
};
use tower_service::Service;
pub async fn connect<T: AsyncRead + AsyncWrite + Unpin>( pub async fn connect<T: AsyncRead + AsyncWrite + Unpin>(
mut proxy_connection: T, mut proxy_connection: T,
@ -64,43 +54,56 @@ pub async fn connect<T: AsyncRead + AsyncWrite + Unpin>(
} }
} }
#[derive(Clone)] cfg_if! {
pub struct ProxyTunnel { if #[cfg(feature = "apresolve")] {
proxy_addr: SocketAddr, use std::future::Future;
} use std::net::{SocketAddr, ToSocketAddrs};
use std::pin::Pin;
use std::task::Poll;
impl ProxyTunnel { use hyper::service::Service;
pub fn new<T: ToSocketAddrs>(addr: T) -> io::Result<Self> { use hyper::Uri;
let addr = addr.to_socket_addrs()?.next().ok_or_else(|| { use tokio::net::TcpStream;
io::Error::new(io::ErrorKind::InvalidInput, "No socket address given")
})?; #[derive(Clone)]
Ok(Self { proxy_addr: addr }) pub struct ProxyTunnel {
} proxy_addr: SocketAddr,
} }
impl Service<Uri> for ProxyTunnel { impl ProxyTunnel {
type Response = TcpStream; pub fn new<T: ToSocketAddrs>(addr: T) -> io::Result<Self> {
type Error = io::Error; let addr = addr.to_socket_addrs()?.next().ok_or_else(|| {
type Future = Pin<Box<dyn Future<Output = io::Result<TcpStream>> + Send>>; io::Error::new(io::ErrorKind::InvalidInput, "No socket address given")
})?;
fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll<io::Result<()>> { Ok(Self { proxy_addr: addr })
Poll::Ready(Ok(())) }
} }
fn call(&mut self, url: Uri) -> Self::Future { impl Service<Uri> for ProxyTunnel {
let proxy_addr = self.proxy_addr; type Response = TcpStream;
let fut = async move { type Error = io::Error;
let host = url type Future = Pin<Box<dyn Future<Output = io::Result<TcpStream>> + Send>>;
.host()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Host is missing"))?; fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll<io::Result<()>> {
let port = url Poll::Ready(Ok(()))
.port() }
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Port is missing"))?;
fn call(&mut self, url: Uri) -> Self::Future {
let conn = TcpStream::connect(proxy_addr).await?; let proxy_addr = self.proxy_addr;
connect(conn, host, port.as_u16()).await let fut = async move {
}; let host = url
.host()
Box::pin(fut) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Host is missing"))?;
let port = url
.port()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Port is missing"))?;
let conn = TcpStream::connect(proxy_addr).await?;
connect(conn, host, port.as_u16()).await
};
Box::pin(fut)
}
}
} }
} }