steam_connect/
verify.rs

1use regex::Regex;
2
3use crate::Error;
4
5#[derive(Debug, Serialize, Deserialize)]
6struct LoginData {
7    #[serde(rename = "openid.ns")]
8    ns: String,
9    #[serde(rename = "openid.mode")]
10    mode: String,
11    #[serde(rename = "openid.op_endpoint")]
12    op_endpoint: String,
13    #[serde(rename = "openid.claimed_id")]
14    claimed_id: String,
15    #[serde(rename = "openid.identity")]
16    identity: String,
17    #[serde(rename = "openid.return_to")]
18    return_to: String,
19    #[serde(rename = "openid.response_nonce")]
20    response_nonce: String,
21    #[serde(rename = "openid.invalidate_handle")]
22    invalidate_handle: Option<String>,
23    #[serde(rename = "openid.assoc_handle")]
24    assoc_handle: String,
25    #[serde(rename = "openid.signed")]
26    signed: String,
27    #[serde(rename = "openid.sig")]
28    sig: String,
29}
30
31impl LoginData {
32    pub fn claim_id(&self) -> Result<u64, Error> {
33        lazy_static! {
34            static ref RE_STEAM_ID64: Regex =
35                Regex::new("^(http|https)://steamcommunity.com/openid/id/([0-9]{17}$)").unwrap();
36        }
37
38        RE_STEAM_ID64
39            .captures(self.claimed_id.as_str())
40            .ok_or(Error::ParseSteamID("Invalid claimed url".to_owned()))?
41            .get(2)
42            .ok_or(Error::ParseSteamID(
43                "Failed to retrieve SteamID64".to_owned(),
44            ))?
45            .as_str()
46            .parse::<u64>()
47            .map_err(|e| Error::ParseSteamID(e.to_string()))
48    }
49}
50
51#[allow(dead_code)]
52#[derive(Debug, Clone, Deserialize)]
53pub struct PlayerSummaries {
54    pub steamid: String,
55    pub personaname: String,
56    pub profileurl: String,
57    pub avatar: String,
58    pub avatarmedium: String,
59    pub avatarfull: String,
60    pub avatarhash: Option<String>,
61    pub personastate: Option<i32>,
62    pub personastateflags: Option<i32>,
63    pub communityvisibilitystate: u32,
64    pub profilestate: Option<i32>,
65    pub lastlogoff: Option<i64>,
66    pub commentpermission: Option<u32>,
67    pub realname: Option<String>,
68    pub primaryclanid: Option<String>,
69    pub timecreated: Option<u64>,
70    pub gameid: Option<String>,
71    pub gameserverip: Option<String>,
72    pub gameextrainfo: Option<String>,
73    pub cityid: Option<i64>,
74    pub loccountrycode: Option<String>,
75    pub locstatecode: Option<String>,
76    pub loccityid: Option<i64>,
77}
78
79#[derive(Deserialize)]
80struct SummariesPlayers {
81    players: Vec<PlayerSummaries>,
82}
83
84#[derive(Deserialize)]
85struct SummariesResponse {
86    response: SummariesPlayers,
87}
88
89#[derive(Debug)]
90pub struct Verify {
91    claimed_id: u64,
92}
93
94impl Verify {
95    async fn is_valid(&self, data: &LoginData) -> Result<bool, Error> {
96        let form = serde_qs::to_string(&data).map_err(|e| Error::ParseSteamID(e.to_string()))?;
97
98        let client = reqwest::Client::new();
99        let response = client
100            .post("https://steamcommunity.com/openid/login")
101            .header("Content-Type", "application/x-www-form-urlencoded")
102            .body(form)
103            .send()
104            .await
105            .map_err(|e| Error::ParseSteamID(e.to_string()))?
106            .text()
107            .await
108            .map_err(|e| Error::ParseSteamID(e.to_string()))?;
109
110        let is_valid = response
111            .split("\n")
112            .filter_map(|line| {
113                let mut pair = line.splitn(2, ":");
114                Some((pair.next()?, pair.next()?))
115            })
116            .any(|(k, v)| k == "is_valid" && v == "true");
117
118        Ok(is_valid)
119    }
120
121    /// Checks query string for validity and retrieves SteamID64. Call this function in the handler on the callback page
122    pub async fn verify_request(query_string: &str) -> Result<Self, Error> {
123        let mut data = serde_qs::from_str::<LoginData>(query_string).map_err(Error::Deserialize)?;
124        data.mode = "check_authentication".to_owned();
125
126        let verify = Self {
127            claimed_id: data.claim_id()?,
128        };
129
130        if !verify.is_valid(&data).await? {
131            return Err(Error::ParseSteamID("Invalid data".to_string()));
132        }
133
134        Ok(verify)
135    }
136
137    /// Query the Steam API to get a player profile
138    pub async fn get_summaries(&self, apikey: &str) -> Result<PlayerSummaries, Error> {
139        let steamid = self.claimed_id.to_string();
140
141        let client = reqwest::Client::new();
142        let response = client
143            .get("https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/")
144            .query(&[("key", apikey), ("steamids", steamid.as_str())])
145            .send()
146            .await
147            .map_err(|e| Error::GetSummaries(e.to_string()))?
148            .json::<SummariesResponse>()
149            .await
150            .map_err(|e| Error::GetSummaries(e.to_string()))?;
151
152        let player = response
153            .response
154            .players
155            .first()
156            .ok_or(Error::GetSummaries("Failed to find player".to_owned()))?;
157
158        Ok(player.clone())
159    }
160
161    /// SteamID64
162    pub fn claim_id(&self) -> u64 {
163        self.claimed_id
164    }
165}