librespot/core/src/http_client.rs

180 lines
6.5 KiB
Rust
Raw Normal View History

use std::env::consts::OS;
2021-11-27 07:30:51 +00:00
use bytes::Bytes;
use futures_util::{future::IntoStream, FutureExt};
use http::{header::HeaderValue, Uri};
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};
use once_cell::sync::OnceCell;
2022-08-26 19:14:43 +00:00
use sysinfo::{System, SystemExt};
use thiserror::Error;
2021-06-20 21:09:27 +00:00
use url::Url;
use crate::{
version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING},
Error,
2021-12-18 22:44:13 +00:00
};
#[derive(Debug, Error)]
pub enum HttpClientError {
#[error("Response status code: {0}")]
StatusCode(hyper::StatusCode),
}
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
}
}
type HyperClient = Client<ProxyConnector<HttpsConnector<HttpConnector>>, Body>;
2022-01-08 22:28:46 +00:00
#[derive(Clone)]
pub struct HttpClient {
user_agent: HeaderValue,
proxy_url: Option<Url>,
hyper_client: OnceCell<HyperClient>,
}
2021-06-20 21:09:27 +00:00
impl HttpClient {
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/{} {}/{} ({})",
spotify_version(),
spotify_platform,
2022-08-26 19:14:43 +00:00
os_version,
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
});
Self {
user_agent,
proxy_url: proxy_url.cloned(),
hyper_client: OnceCell::new(),
}
}
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
// 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
}
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
2021-12-16 21:42:37 +00:00
let request = self.request_fut(req)?;
let response = request.await;
if let Ok(response) = &response {
let code = response.status();
if code != StatusCode::OK {
return Err(HttpClientError::StatusCode(code).into());
2021-12-16 21:42:37 +00:00
}
2021-06-20 21:09:27 +00:00
}
Ok(response?)
2021-06-20 21:09:27 +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?;
Ok(hyper::body::to_bytes(response.into_body()).await?)
2021-12-16 21:42:37 +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())
}
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
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
}
}