steam_vent/
serverlist.rs

1use rand::prelude::*;
2use rand::rng;
3use reqwest::{Client, Error};
4use serde::Deserialize;
5use std::iter::Cycle;
6use std::net::SocketAddr;
7use std::sync::{Arc, Mutex};
8use std::vec::IntoIter;
9use thiserror::Error;
10use tracing::debug;
11
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum ServerDiscoveryError {
15    #[error("Failed send discovery request: {0:#}")]
16    Network(reqwest::Error),
17    #[error("steam returned an empty server list")]
18    NoServers,
19    #[error("steam returned an empty websocket server list")]
20    NoWsServers,
21}
22
23impl From<reqwest::Error> for ServerDiscoveryError {
24    fn from(value: Error) -> Self {
25        ServerDiscoveryError::Network(value)
26    }
27}
28
29/// Options to use for discovering steam api servers
30#[derive(Default, Clone, Debug)]
31pub struct DiscoverOptions {
32    web_client: Option<Client>,
33    // todo: some smart cell based routing based on
34    // https://raw.githubusercontent.com/SteamDatabase/SteamTracking/6d23ebb0070998ae851278cfae5f38832f4ac28d/ClientExtracted/steam/cached/CellMap.vdf
35    cell: u8,
36}
37
38impl DiscoverOptions {
39    /// Set the request client to use to make requests to the web-api
40    pub fn with_web_client(self, web_client: Client) -> Self {
41        DiscoverOptions {
42            web_client: Some(web_client),
43            ..self
44        }
45    }
46
47    /// Specify the steam cell ID to request servers for.
48    pub fn with_cell(self, cell: u8) -> Self {
49        DiscoverOptions { cell, ..self }
50    }
51}
52
53/// A list of tcp and websocket servers to use for connecting
54#[derive(Debug, Clone)]
55pub struct ServerList {
56    tcp_count: usize,
57    tcp_servers: Arc<Mutex<Cycle<IntoIter<SocketAddr>>>>,
58    ws_count: usize,
59    ws_servers: Arc<Mutex<Cycle<IntoIter<String>>>>,
60}
61
62impl ServerList {
63    /// Create a server list from the provided servers
64    pub fn new(
65        tcp_servers: Vec<SocketAddr>,
66        ws_servers: Vec<String>,
67    ) -> Result<Self, ServerDiscoveryError> {
68        if tcp_servers.is_empty() {
69            return Err(ServerDiscoveryError::NoServers);
70        }
71        if ws_servers.is_empty() {
72            return Err(ServerDiscoveryError::NoWsServers);
73        }
74
75        Ok(ServerList {
76            tcp_count: tcp_servers.len(),
77            ws_count: ws_servers.len(),
78            tcp_servers: Arc::new(Mutex::new(tcp_servers.into_iter().cycle())),
79            ws_servers: Arc::new(Mutex::new(ws_servers.into_iter().cycle())),
80        })
81    }
82
83    /// Discover the server list from the steam web-api with default options
84    pub async fn discover() -> Result<ServerList, ServerDiscoveryError> {
85        Self::discover_with(DiscoverOptions::default()).await
86    }
87
88    /// Discover the server list from the steam web-api with custom options
89    pub async fn discover_with(
90        options: DiscoverOptions,
91    ) -> Result<ServerList, ServerDiscoveryError> {
92        let client = options.web_client.unwrap_or_default();
93        let cell = options.cell;
94
95        let response: ServerListResponse = client
96            .get(format!(
97                "https://api.steampowered.com/ISteamDirectory/GetCMList/v1/?cellid={cell}"
98            ))
99            .send()
100            .await?
101            .json()
102            .await?;
103        response.try_into()
104    }
105
106    /// Pick a server from the server list, rotating them in a round-robin way for reconnects.
107    ///
108    /// # Returns
109    /// The selected `SocketAddr`
110    pub fn pick(&self) -> SocketAddr {
111        // SAFETY:
112        // `lock` cannot panic as we cannot lock again within the same thread.
113        // `unwrap` is safe as `discover_with` already checks for servers being present.
114        let addr = self.tcp_servers.lock().unwrap().next().unwrap();
115        debug!(addr = ?addr, "picked server from list");
116        addr
117    }
118
119    /// Pick a WebSocket server from the server list, rotating them in a round-robin way for reconnects.
120    ///
121    /// # Returns
122    /// A WebSocket URL to connect to, if the server list contains any servers.
123    pub fn pick_ws(&self) -> String {
124        // SAFETY: Same as for `pick`.
125        let addr = self.ws_servers.lock().unwrap().next().unwrap();
126        debug!(addr = ?addr, "picked websocket server from list");
127        format!("wss://{addr}/cmsocket/")
128    }
129
130    pub fn tcp_servers(&self) -> Vec<SocketAddr> {
131        let mut iter = self.tcp_servers.lock().unwrap();
132        take_from_iter(&mut *iter, self.tcp_count)
133    }
134
135    pub fn ws_servers(&self) -> Vec<String> {
136        let mut iter = self.ws_servers.lock().unwrap();
137        take_from_iter(&mut *iter, self.ws_count)
138    }
139}
140
141fn take_from_iter<T, I: Iterator<Item = T>>(iter: &mut I, count: usize) -> Vec<T> {
142    let mut result = Vec::with_capacity(count);
143    for _ in 0..count {
144        if let Some(item) = iter.next() {
145            result.push(item)
146        }
147    }
148    result
149}
150
151#[test]
152fn test_save_servers() {
153    use std::net::{IpAddr, Ipv4Addr};
154
155    let socket1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234);
156    let socket2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 2345);
157
158    let ws1 = String::from("server1:1234");
159    let ws2 = String::from("server2");
160    let ws3 = String::from("server3");
161
162    let list = ServerList::new(
163        vec![socket1, socket2],
164        vec![ws1.clone(), ws2.clone(), ws3.clone()],
165    )
166    .unwrap();
167
168    assert_eq!(vec![socket1, socket2], list.tcp_servers());
169    assert_eq!(
170        vec![ws1.clone(), ws2.clone(), ws3.clone()],
171        list.ws_servers()
172    );
173
174    let _ = list.pick();
175    let _ = list.pick_ws();
176    let _ = list.pick_ws();
177    let _ = list.pick_ws();
178
179    assert_eq!(vec![socket2, socket1], list.tcp_servers());
180    assert_eq!(
181        vec![ws1.clone(), ws2.clone(), ws3.clone()],
182        list.ws_servers()
183    );
184}
185
186impl TryFrom<ServerListResponse> for ServerList {
187    type Error = ServerDiscoveryError;
188
189    fn try_from(value: ServerListResponse) -> Result<Self, Self::Error> {
190        let (mut servers, mut ws_servers) = (
191            value.response.server_list,
192            value.response.server_list_websockets,
193        );
194        servers.shuffle(&mut rng());
195        ws_servers.shuffle(&mut rng());
196
197        ServerList::new(servers, ws_servers)
198    }
199}
200
201#[derive(Debug, Deserialize)]
202struct ServerListResponse {
203    response: ServerListResponseInner,
204}
205
206#[derive(Debug, Deserialize)]
207struct ServerListResponseInner {
208    #[serde(rename = "serverlist")]
209    server_list: Vec<SocketAddr>,
210    #[serde(rename = "serverlist_websockets")]
211    server_list_websockets: Vec<String>,
212}