openlibspot_core/
apresolve.rs

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