2022-08-29 21:09:51 +00:00
|
|
|
use std::{env::consts::OS, time::Duration};
|
2021-12-26 20:18:42 +00:00
|
|
|
|
2021-11-27 07:30:51 +00:00
|
|
|
use bytes::Bytes;
|
2021-12-26 20:18:42 +00:00
|
|
|
use futures_util::{future::IntoStream, FutureExt};
|
2022-08-29 21:09:51 +00:00
|
|
|
use governor::{
|
|
|
|
clock::MonotonicClock,
|
|
|
|
middleware::NoOpMiddleware,
|
|
|
|
state::{InMemoryState, NotKeyed},
|
|
|
|
Jitter, Quota, RateLimiter,
|
|
|
|
};
|
2022-08-04 16:37:32 +00:00
|
|
|
use http::{header::HeaderValue, Uri};
|
2021-12-26 20:18:42 +00:00
|
|
|
use hyper::{
|
|
|
|
client::{HttpConnector, ResponseFuture},
|
|
|
|
header::USER_AGENT,
|
|
|
|
Body, Client, Request, Response, StatusCode,
|
|
|
|
};
|
2021-06-20 21:09:27 +00:00
|
|
|
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
|
2022-01-08 22:28:46 +00:00
|
|
|
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
2022-08-29 21:09:51 +00:00
|
|
|
use nonzero_ext::nonzero;
|
2022-08-04 16:37:32 +00:00
|
|
|
use once_cell::sync::OnceCell;
|
2022-08-26 19:14:43 +00:00
|
|
|
use sysinfo::{System, SystemExt};
|
2021-11-26 22:21:27 +00:00
|
|
|
use thiserror::Error;
|
2021-06-20 21:09:27 +00:00
|
|
|
use url::Url;
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
use crate::{
|
2022-08-25 23:51:00 +00:00
|
|
|
version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING},
|
2021-12-26 20:18:42 +00:00
|
|
|
Error,
|
2021-12-18 22:44:13 +00:00
|
|
|
};
|
2021-12-09 18:00:27 +00:00
|
|
|
|
2022-08-29 21:09:51 +00:00
|
|
|
// The 30 seconds interval is documented by Spotify, but the calls per interval
|
|
|
|
// is a guesstimate and probably subject to licensing (purchasing extra calls)
|
|
|
|
// and may change at any time.
|
2022-08-31 19:08:11 +00:00
|
|
|
pub const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(30);
|
|
|
|
pub const RATE_LIMIT_MAX_WAIT: Duration = Duration::from_secs(10);
|
2022-08-29 21:09:51 +00:00
|
|
|
pub const RATE_LIMIT_CALLS_PER_INTERVAL: u32 = 300;
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
#[derive(Debug, Error)]
|
2021-11-26 22:21:27 +00:00
|
|
|
pub enum HttpClientError {
|
2021-12-26 20:18:42 +00:00
|
|
|
#[error("Response status code: {0}")]
|
|
|
|
StatusCode(hyper::StatusCode),
|
2021-11-26 22:21:27 +00:00
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
impl From<HttpClientError> for Error {
|
|
|
|
fn from(err: HttpClientError) -> Self {
|
|
|
|
match err {
|
|
|
|
HttpClientError::StatusCode(code) => {
|
|
|
|
// not exhaustive, but what reasonably could be expected
|
|
|
|
match code {
|
|
|
|
StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => {
|
|
|
|
Error::deadline_exceeded(err)
|
|
|
|
}
|
|
|
|
StatusCode::GONE
|
|
|
|
| StatusCode::NOT_FOUND
|
|
|
|
| StatusCode::MOVED_PERMANENTLY
|
|
|
|
| StatusCode::PERMANENT_REDIRECT
|
|
|
|
| StatusCode::TEMPORARY_REDIRECT => Error::not_found(err),
|
|
|
|
StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => {
|
|
|
|
Error::permission_denied(err)
|
|
|
|
}
|
|
|
|
StatusCode::NETWORK_AUTHENTICATION_REQUIRED
|
|
|
|
| StatusCode::PROXY_AUTHENTICATION_REQUIRED
|
|
|
|
| StatusCode::UNAUTHORIZED => Error::unauthenticated(err),
|
|
|
|
StatusCode::EXPECTATION_FAILED
|
|
|
|
| StatusCode::PRECONDITION_FAILED
|
|
|
|
| StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err),
|
|
|
|
StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err),
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
|
|
| StatusCode::MISDIRECTED_REQUEST
|
|
|
|
| StatusCode::SERVICE_UNAVAILABLE
|
|
|
|
| StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err),
|
|
|
|
StatusCode::BAD_REQUEST
|
|
|
|
| StatusCode::HTTP_VERSION_NOT_SUPPORTED
|
|
|
|
| StatusCode::LENGTH_REQUIRED
|
|
|
|
| StatusCode::METHOD_NOT_ALLOWED
|
|
|
|
| StatusCode::NOT_ACCEPTABLE
|
|
|
|
| StatusCode::PAYLOAD_TOO_LARGE
|
|
|
|
| StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
|
|
|
|
| StatusCode::UNSUPPORTED_MEDIA_TYPE
|
|
|
|
| StatusCode::URI_TOO_LONG => Error::invalid_argument(err),
|
|
|
|
StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err),
|
|
|
|
StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err),
|
|
|
|
_ => Error::unknown(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-11-27 07:30:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-04 16:37:32 +00:00
|
|
|
type HyperClient = Client<ProxyConnector<HttpsConnector<HttpConnector>>, Body>;
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub struct HttpClient {
|
|
|
|
user_agent: HeaderValue,
|
2022-08-04 16:37:32 +00:00
|
|
|
proxy_url: Option<Url>,
|
|
|
|
hyper_client: OnceCell<HyperClient>,
|
2022-08-29 21:09:51 +00:00
|
|
|
rate_limiter: RateLimiter<NotKeyed, InMemoryState, MonotonicClock, NoOpMiddleware>,
|
2021-12-26 20:18:42 +00:00
|
|
|
}
|
|
|
|
|
2021-06-20 21:09:27 +00:00
|
|
|
impl HttpClient {
|
2022-08-04 16:37:32 +00:00
|
|
|
pub fn new(proxy_url: Option<&Url>) -> Self {
|
2022-08-26 19:14:43 +00:00
|
|
|
let zero_str = String::from("0");
|
|
|
|
let os_version = System::new()
|
|
|
|
.os_version()
|
|
|
|
.unwrap_or_else(|| zero_str.clone());
|
|
|
|
|
|
|
|
let (spotify_platform, os_version) = match OS {
|
|
|
|
"android" => ("Android", os_version),
|
|
|
|
"ios" => ("iOS", os_version),
|
|
|
|
"macos" => ("OSX", zero_str),
|
|
|
|
"windows" => ("Win32", zero_str),
|
|
|
|
_ => ("Linux", zero_str),
|
2021-12-18 11:39:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let user_agent_str = &format!(
|
2022-08-26 19:14:43 +00:00
|
|
|
"Spotify/{} {}/{} ({})",
|
2022-08-25 23:51:00 +00:00
|
|
|
spotify_version(),
|
|
|
|
spotify_platform,
|
2022-08-26 19:14:43 +00:00
|
|
|
os_version,
|
2022-08-25 23:51:00 +00:00
|
|
|
VERSION_STRING
|
2021-12-18 11:39:16 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
|
|
|
|
error!("Invalid user agent <{}>: {}", user_agent_str, err);
|
2021-12-18 22:44:13 +00:00
|
|
|
HeaderValue::from_static(FALLBACK_USER_AGENT)
|
2021-12-18 11:39:16 +00:00
|
|
|
});
|
|
|
|
|
2022-08-31 19:08:11 +00:00
|
|
|
let replenish_interval_ns =
|
|
|
|
RATE_LIMIT_INTERVAL.as_nanos() / RATE_LIMIT_CALLS_PER_INTERVAL as u128;
|
2022-08-29 21:09:51 +00:00
|
|
|
let quota = Quota::with_period(Duration::from_nanos(replenish_interval_ns as u64))
|
|
|
|
.expect("replenish interval should be valid")
|
|
|
|
.allow_burst(nonzero![RATE_LIMIT_CALLS_PER_INTERVAL]);
|
|
|
|
let rate_limiter = RateLimiter::direct(quota);
|
|
|
|
|
2022-08-04 16:37:32 +00:00
|
|
|
Self {
|
|
|
|
user_agent,
|
|
|
|
proxy_url: proxy_url.cloned(),
|
|
|
|
hyper_client: OnceCell::new(),
|
2022-08-29 21:09:51 +00:00
|
|
|
rate_limiter,
|
2022-08-04 16:37:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result<HyperClient, Error> {
|
2021-12-16 21:42:37 +00:00
|
|
|
// configuring TLS is expensive and should be done once per process
|
2022-01-08 22:28:46 +00:00
|
|
|
let https_connector = HttpsConnectorBuilder::new()
|
|
|
|
.with_native_roots()
|
|
|
|
.https_or_http()
|
|
|
|
.enable_http1()
|
|
|
|
.enable_http2()
|
|
|
|
.build();
|
2021-12-16 21:42:37 +00:00
|
|
|
|
2022-08-04 16:37:32 +00:00
|
|
|
// When not using a proxy a dummy proxy is configured that will not intercept any traffic.
|
|
|
|
// This prevents needing to carry the Client Connector generics through the whole project
|
|
|
|
let proxy = match &proxy_url {
|
|
|
|
Some(proxy_url) => Proxy::new(Intercept::All, proxy_url.to_string().parse()?),
|
|
|
|
None => Proxy::new(Intercept::None, Uri::from_static("0.0.0.0")),
|
|
|
|
};
|
|
|
|
let proxy_connector = ProxyConnector::from_proxy(https_connector, proxy)?;
|
|
|
|
|
|
|
|
let client = Client::builder()
|
|
|
|
.http2_adaptive_window(true)
|
|
|
|
.build(proxy_connector);
|
|
|
|
Ok(client)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn hyper_client(&self) -> Result<&HyperClient, Error> {
|
|
|
|
self.hyper_client
|
|
|
|
.get_or_try_init(|| Self::try_create_hyper_client(self.proxy_url.as_ref()))
|
2021-12-16 21:42:37 +00:00
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub async fn request(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
|
2021-12-30 21:36:38 +00:00
|
|
|
debug!("Requesting {}", req.uri().to_string());
|
2021-12-18 11:39:16 +00:00
|
|
|
|
2022-08-29 21:09:51 +00:00
|
|
|
// `Request` does not implement `Clone` because its `Body` may be a single-shot stream.
|
|
|
|
// As correct as that may be technically, we now need all this boilerplate to clone it
|
|
|
|
// ourselves, as any `Request` is moved in the loop.
|
|
|
|
let (parts, body) = req.into_parts();
|
|
|
|
let body_as_bytes = hyper::body::to_bytes(body)
|
|
|
|
.await
|
|
|
|
.unwrap_or_else(|_| Bytes::new());
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let mut req = Request::new(Body::from(body_as_bytes.clone()));
|
|
|
|
*req.method_mut() = parts.method.clone();
|
|
|
|
*req.uri_mut() = parts.uri.clone();
|
|
|
|
*req.version_mut() = parts.version;
|
|
|
|
*req.headers_mut() = parts.headers.clone();
|
|
|
|
|
|
|
|
// For rate limiting we cannot *just* depend on Spotify sending us HTTP/429
|
|
|
|
// Retry-After headers. For example, when there is a service interruption
|
|
|
|
// and HTTP/500 is returned, we don't want to DoS the Spotify infrastructure.
|
|
|
|
self.rate_limiter
|
|
|
|
.until_ready_with_jitter(Jitter::up_to(Duration::from_secs(5)))
|
|
|
|
.await;
|
|
|
|
|
|
|
|
let request = self.request_fut(req)?;
|
|
|
|
let response = request.await;
|
|
|
|
|
|
|
|
if let Ok(response) = &response {
|
|
|
|
let code = response.status();
|
|
|
|
|
|
|
|
if code == StatusCode::TOO_MANY_REQUESTS {
|
|
|
|
if let Some(retry_after) = response.headers().get("Retry-After") {
|
|
|
|
if let Ok(retry_after_str) = retry_after.to_str() {
|
|
|
|
if let Ok(retry_after_secs) = retry_after_str.parse::<u64>() {
|
|
|
|
let duration = Duration::from_secs(retry_after_secs);
|
2022-08-31 19:08:11 +00:00
|
|
|
if duration <= RATE_LIMIT_MAX_WAIT {
|
|
|
|
warn!(
|
|
|
|
"Rate limiting, retrying in {} seconds...",
|
|
|
|
retry_after_secs
|
|
|
|
);
|
|
|
|
tokio::time::sleep(duration).await;
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
debug!("Not going to wait {} seconds", retry_after_secs);
|
|
|
|
}
|
2022-08-29 21:09:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-12-26 20:18:42 +00:00
|
|
|
|
2022-08-29 21:09:51 +00:00
|
|
|
if code != StatusCode::OK {
|
|
|
|
return Err(HttpClientError::StatusCode(code).into());
|
|
|
|
}
|
2021-12-16 21:42:37 +00:00
|
|
|
}
|
2021-12-26 20:18:42 +00:00
|
|
|
|
2022-08-29 21:09:51 +00:00
|
|
|
return Ok(response?);
|
|
|
|
}
|
2021-06-20 21:09:27 +00:00
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub async fn request_body(&self, req: Request<Body>) -> Result<Bytes, Error> {
|
2021-12-16 21:42:37 +00:00
|
|
|
let response = self.request(req).await?;
|
2021-12-26 20:18:42 +00:00
|
|
|
Ok(hyper::body::to_bytes(response.into_body()).await?)
|
2021-12-16 21:42:37 +00:00
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub fn request_stream(&self, req: Request<Body>) -> Result<IntoStream<ResponseFuture>, Error> {
|
2021-12-16 21:42:37 +00:00
|
|
|
Ok(self.request_fut(req)?.into_stream())
|
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub fn request_fut(&self, mut req: Request<Body>) -> Result<ResponseFuture, Error> {
|
2021-11-27 07:30:51 +00:00
|
|
|
let headers_mut = req.headers_mut();
|
2021-12-18 11:39:16 +00:00
|
|
|
headers_mut.insert(USER_AGENT, self.user_agent.clone());
|
2021-11-27 07:30:51 +00:00
|
|
|
|
2022-08-04 16:37:32 +00:00
|
|
|
let request = self.hyper_client()?.request(req);
|
2021-12-16 21:42:37 +00:00
|
|
|
Ok(request)
|
2021-06-20 21:09:27 +00:00
|
|
|
}
|
|
|
|
}
|