vk_auth/
lib.rs

1use anyhow::{Error, Result};
2use reqwest::Client;
3use scraper::{Html, Selector};
4use std::collections::HashMap;
5use std::fmt::Formatter;
6use std::time::Duration;
7use url::Url;
8
9const START_PATTERN: &str = "location.href='";
10const END_PATTERN: &str = "';</script>";
11
12#[derive(Debug, Clone)]
13pub enum ParseError {
14    NoLoginForm,
15    NoFormAction,
16    InvalidFormInputField,
17    InvalidRedirectData,
18    AuthorizationFailed,
19}
20
21impl std::fmt::Display for ParseError {
22    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23        match self {
24            ParseError::NoLoginForm => {
25                write!(f, "NoLoginForm")
26            }
27            ParseError::NoFormAction => {
28                write!(f, "NoFormAction")
29            }
30            ParseError::InvalidFormInputField => {
31                write!(f, "InvalidFormInputField")
32            }
33            ParseError::InvalidRedirectData => {
34                write!(f, "InvalidRedirectData: expected data not found on page")
35            }
36            ParseError::AuthorizationFailed => {
37                write!(f, "AuthorizationFailed: invalid authorization data")
38            }
39        }?;
40        Ok(())
41    }
42}
43
44impl std::error::Error for ParseError {}
45
46#[derive(Debug, Clone)]
47pub struct AccessToken {
48    access_token: String,
49    expires_in: Duration,
50    user_id: String,
51}
52
53impl AccessToken {
54    pub fn access_token(&self) -> &str {
55        &self.access_token
56    }
57    pub fn expires_in(&self) -> Duration {
58        self.expires_in
59    }
60    pub fn user_id(&self) -> &str {
61        &self.user_id
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct Authorizer {
67    client: Client,
68}
69
70#[derive(Debug, Clone)]
71pub struct AuthorizerBuilder {
72    client: Option<Client>,
73}
74
75impl AuthorizerBuilder {
76    pub fn new() -> Self {
77        Self { client: None }
78    }
79
80    pub fn with_client(mut self, client: Client) -> Self {
81        self.client = Some(client);
82        self
83    }
84
85    pub fn build(self) -> Result<Authorizer> {
86        let client = self
87            .client
88            .unwrap_or(reqwest::Client::builder().cookie_store(true).build()?);
89        Ok(Authorizer { client })
90    }
91}
92
93impl Default for AuthorizerBuilder {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl Authorizer {
100    pub fn builder() -> AuthorizerBuilder {
101        AuthorizerBuilder::new()
102    }
103
104    pub async fn get_token(
105        &self,
106        api_id: &str,
107        email_or_phone: &str,
108        password: &str,
109    ) -> Result<AccessToken> {
110        let initial_resp = self
111            .client
112            .get(format!(
113                "https://oauth.vk.com/oauth/authorize?client_id={}&scope=0&response_type=token",
114                api_id
115            ))
116            .send()
117            .await?
118            .text()
119            .await?;
120        let doc = Html::parse_document(initial_resp.as_str());
121        let form = doc
122            .select(&Selector::parse("form").unwrap())
123            .next()
124            .ok_or(ParseError::NoLoginForm)?;
125        let url = form
126            .value()
127            .attr("action")
128            .ok_or(ParseError::NoFormAction)?;
129        let mut data = HashMap::new();
130        data.insert("email", email_or_phone);
131        data.insert("pass", password);
132        data.insert("expire", "0");
133        for node in form.children() {
134            let val = node.value();
135            if !val.is_element() {
136                continue;
137            }
138            let inp = node.value().as_element().unwrap();
139            if inp.name() != "input" {
140                continue;
141            }
142            if inp.attr("type").unwrap() == "hidden" {
143                data.insert(
144                    inp.attr("name").ok_or(ParseError::InvalidFormInputField)?,
145                    inp.attr("value").ok_or(ParseError::InvalidFormInputField)?,
146                );
147            }
148        }
149        let resp = self
150            .client
151            .post(url)
152            .form(&data)
153            .send()
154            .await?
155            .text()
156            .await?;
157        get_token_from_page(resp.as_str()).map_err(|e| match e.downcast_ref::<ParseError>() {
158            Some(parse_err) => match parse_err {
159                ParseError::NoLoginForm => e,
160                ParseError::NoFormAction => e,
161                ParseError::InvalidFormInputField => e,
162                ParseError::InvalidRedirectData => {
163                    // if we on login page again, so the auth data was wrong
164                    if doc
165                        .select(
166                            &Selector::parse(r#"form[action*="https://login.vk.com"]"#).unwrap(),
167                        )
168                        .next()
169                        .is_some()
170                    {
171                        Error::from(ParseError::AuthorizationFailed)
172                    } else {
173                        e
174                    }
175                }
176                ParseError::AuthorizationFailed => e,
177            },
178            None => e,
179        })
180    }
181}
182
183fn get_token_from_page(resp: &str) -> Result<AccessToken> {
184    let sfound = resp
185        .find(START_PATTERN)
186        .ok_or(ParseError::InvalidRedirectData)?;
187    let efound = resp
188        .rfind(END_PATTERN)
189        .ok_or(ParseError::InvalidRedirectData)?;
190    let redirect_url = resp[sfound + START_PATTERN.len()..efound].into();
191    let query: HashMap<_, _> = form_urlencoded::parse(
192        Url::parse(redirect_url)?
193            .fragment()
194            .ok_or(ParseError::InvalidRedirectData)?
195            .as_bytes(),
196    )
197    .into_owned()
198    .collect();
199    Ok(AccessToken {
200        access_token: query
201            .get("access_token")
202            .ok_or(ParseError::InvalidRedirectData)?
203            .into(),
204        expires_in: Duration::from_secs(
205            query
206                .get("expires_in")
207                .ok_or(ParseError::InvalidRedirectData)?
208                .parse()?,
209        ),
210        user_id: query
211            .get("user_id")
212            .ok_or(ParseError::InvalidRedirectData)?
213            .into(),
214    })
215}