librespot_core/
apresolve.rs

1use std::collections::VecDeque;
2
3use bytes::Bytes;
4use hyper::{Method, Request};
5use serde::Deserialize;
6
7use crate::Error;
8
9pub type SocketAddress = (String, u16);
10
11#[derive(Default)]
12pub struct AccessPoints {
13    accesspoint: VecDeque<SocketAddress>,
14    dealer: VecDeque<SocketAddress>,
15    spclient: VecDeque<SocketAddress>,
16}
17
18#[derive(Deserialize, Default)]
19pub struct ApResolveData {
20    accesspoint: Vec<String>,
21    dealer: Vec<String>,
22    spclient: Vec<String>,
23}
24
25impl ApResolveData {
26    // These addresses probably do some geo-location based traffic management or at least DNS-based
27    // load balancing. They are known to fail when the normal resolvers are up, so that's why they
28    // should only be used as fallback.
29    fn fallback() -> Self {
30        Self {
31            accesspoint: vec![String::from("ap.spotify.com:443")],
32            dealer: vec![String::from("dealer.spotify.com:443")],
33            spclient: vec![String::from("spclient.wg.spotify.com:443")],
34        }
35    }
36}
37
38impl AccessPoints {
39    fn is_any_empty(&self) -> bool {
40        self.accesspoint.is_empty() || self.dealer.is_empty() || self.spclient.is_empty()
41    }
42}
43
44component! {
45    ApResolver : ApResolverInner {
46        data: AccessPoints = AccessPoints::default(),
47    }
48}
49
50impl ApResolver {
51    // return a port if a proxy URL and/or a proxy port was specified. This is useful even when
52    // there is no proxy, but firewalls only allow certain ports (e.g. 443 and not 4070).
53    pub fn port_config(&self) -> Option<u16> {
54        if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() {
55            Some(self.session().config().ap_port.unwrap_or(443))
56        } else {
57            None
58        }
59    }
60
61    fn process_ap_strings(&self, data: Vec<String>) -> VecDeque<SocketAddress> {
62        let filter_port = self.port_config();
63        data.into_iter()
64            .filter_map(|ap| {
65                let mut split = ap.rsplitn(2, ':');
66                let port = split.next()?;
67                let port: u16 = port.parse().ok()?;
68                let host = split.next()?.to_owned();
69                match filter_port {
70                    Some(filter_port) if filter_port != port => None,
71                    _ => Some((host, port)),
72                }
73            })
74            .collect()
75    }
76
77    fn parse_resolve_to_access_points(&self, resolve: ApResolveData) -> AccessPoints {
78        AccessPoints {
79            accesspoint: self.process_ap_strings(resolve.accesspoint),
80            dealer: self.process_ap_strings(resolve.dealer),
81            spclient: self.process_ap_strings(resolve.spclient),
82        }
83    }
84
85    pub async fn try_apresolve(&self) -> Result<ApResolveData, Error> {
86        let req = Request::builder()
87            .method(Method::GET)
88            .uri("https://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient")
89            .body(Bytes::new())?;
90
91        let body = self.session().http_client().request_body(req).await?;
92        let data: ApResolveData = serde_json::from_slice(body.as_ref())?;
93
94        Ok(data)
95    }
96
97    async fn apresolve(&self) {
98        let result = self.try_apresolve().await;
99
100        self.lock(|inner| {
101            let (data, error) = match result {
102                Ok(data) => (data, None),
103                Err(e) => (ApResolveData::default(), Some(e)),
104            };
105
106            inner.data = self.parse_resolve_to_access_points(data);
107
108            if inner.data.is_any_empty() {
109                warn!("Failed to resolve all access points, using fallbacks");
110                if let Some(error) = error {
111                    warn!("Resolve access points error: {error}");
112                }
113
114                let fallback = self.parse_resolve_to_access_points(ApResolveData::fallback());
115                inner.data.accesspoint.extend(fallback.accesspoint);
116                inner.data.dealer.extend(fallback.dealer);
117                inner.data.spclient.extend(fallback.spclient);
118            }
119        })
120    }
121
122    fn is_any_empty(&self) -> bool {
123        self.lock(|inner| inner.data.is_any_empty())
124    }
125
126    pub async fn resolve(&self, endpoint: &str) -> Result<SocketAddress, Error> {
127        if self.is_any_empty() {
128            self.apresolve().await;
129        }
130
131        self.lock(|inner| {
132            let access_point = match endpoint {
133                // take the first position instead of the last with `pop`, because Spotify returns
134                // access points with ports 4070, 443 and 80 in order of preference from highest
135                // to lowest.
136                "accesspoint" => inner.data.accesspoint.pop_front(),
137                "dealer" => inner.data.dealer.pop_front(),
138                "spclient" => inner.data.spclient.pop_front(),
139                _ => {
140                    return Err(Error::unimplemented(format!(
141                        "No implementation to resolve access point {endpoint}"
142                    )));
143                }
144            };
145
146            let access_point = access_point.ok_or_else(|| {
147                Error::unavailable(format!("No access point available for endpoint {endpoint}"))
148            })?;
149
150            Ok(access_point)
151        })
152    }
153}