authku/
lib.rs

1#![allow(dead_code)]
2
3use asession::{Session, SessionBuilder};
4use chrono::Local;
5use std::ops::Deref;
6
7// print only when debug mode is on
8macro_rules! dp {
9    ($e:expr) => {
10        if cfg!(debug_assertions) {
11            dbg!($e);
12        }
13    };
14}
15
16pub struct Client {
17    session: Session,
18    status: Option<String>,
19}
20
21impl Client {
22    pub fn new() -> Self {
23        let client = SessionBuilder::new()
24            .cookies_store_into("cookies".into())
25            .build()
26            .unwrap();
27
28        Self {
29            session: client,
30            status: None,
31        }
32    }
33
34    pub async fn get_ticket(
35        &self,
36        uid: &str,
37        password: &str,
38    ) -> Result<String, Box<dyn std::error::Error>> {
39        let res = self
40            .session
41            .post("https://hkuportal.hku.hk/cas/servlet/edu.yale.its.tp.cas.servlet.Login")
42            .form(&[("keyid", ""), ("username", uid), ("password", password)])
43            .send()
44            .await
45            .unwrap();
46
47        // get ticket and url
48        let body = res.text().await.unwrap();
49        let url = regex::Regex::new(r#"Click <a href="(.*)">here</a>"#)
50            .unwrap()
51            .captures(&body)
52            .unwrap()
53            .get(1)
54            .unwrap()
55            .as_str();
56
57        Ok(url.split("=").last().unwrap().to_string())
58    }
59
60    pub async fn login_portal(
61        &self,
62        uid: &str,
63        password: &str,
64    ) -> Result<&Self, Box<dyn std::error::Error>> {
65        dp!("-start login to portal");
66        dp!("--passing uid and password");
67        let res = self
68            .session
69            .post("https://hkuportal.hku.hk/cas/servlet/edu.yale.its.tp.cas.servlet.Login")
70            .form(&[("keyid", ""), ("username", uid), ("password", password)])
71            .send()
72            .await?;
73
74        dp!("--get ticket and url");
75        let body = res.text().await?;
76        let url = regex::Regex::new(r#"Click <a href="(.*)">here</a>"#)
77            .unwrap()
78            .captures(&body)
79            .unwrap()
80            .get(1)
81            .unwrap()
82            .as_str();
83        let ticket = url.split("=").last().unwrap();
84        dp!(ticket);
85
86        dp!("--verify ticket");
87        self.session.get(url).send().await?;
88
89        dp!("--login to sis");
90        let res = self
91            .session
92            .post("https://sis-eportal.hku.hk/psp/ptlprod/?cmd=login&languageCd=ENG")
93            .form(&[
94                ("ticket", ticket),
95                ("userid", "hku_dummy"),
96                ("pwd", "d"),
97                ("timezoneOffset", "0"),
98            ])
99            .send()
100            .await?;
101
102        dp!("--checking login status");
103        let body = res.text().await?;
104        if body.contains("PSPAGE homePageHdr") {
105            dp!("!-login to portal success");
106            Ok(&self)
107        } else {
108            dp!("!-login portal may failed");
109            Ok(&self)
110        }
111    }
112
113    pub async fn login_lib(
114        &self,
115        uid: &str,
116        password: &str,
117    ) -> Result<&Self, Box<dyn std::error::Error>> {
118        dp!("-start login to library");
119        dp!("--get lib login page");
120        self.session
121            .get("https://booking.lib.hku.hk/Secure/FacilityStatusDate.aspx")
122            .send()
123            .await?;
124
125        let res = self.session.get("https://lib.hku.hk/hkulauth/legacy/authMain?uri=https://booking.lib.hku.hk/getpatron.aspx")
126            .send().await?;
127
128        dp!("--get scope and saml url");
129        let body = res.text().await?;
130        let scope = regex::Regex::new(r#"scope = "(.*)""#)
131            .unwrap()
132            .captures(&body)
133            .unwrap()
134            .get(1)
135            .unwrap()
136            .as_str();
137        let saml_url =
138            regex::Regex::new(r#"<script src="(https://ids.hku.hk/idp/profile/SAML2.*)""#)
139                .unwrap()
140                .captures(&body)
141                .unwrap()
142                .get(1)
143                .unwrap()
144                .as_str();
145
146        dp!(scope);
147        dp!(saml_url);
148
149        dp!("--get saml page");
150        self.session.get(saml_url).send().await?;
151
152        let login_data = [
153            ("conversation", "e1s1"),
154            ("scope", scope),
155            ("userid", uid),
156            ("password", password),
157            ("submit", "Submit"),
158        ];
159
160        dp!("--send login data");
161        let res = self
162            .session
163            .post("https://ids.hku.hk/idp/ProcessAuthnLib")
164            .form(&login_data)
165            .send()
166            .await?;
167
168
169        dp!("--get saml data");
170        let body = res.text().await?;
171        let saml_response =
172            regex::Regex::new(r#"<input type="hidden" name="SAMLResponse" value="(.*)"/>"#)
173                .unwrap()
174                .captures(&body)
175                .unwrap()
176                .get(1)
177                .unwrap()
178                .as_str();
179        let saml_data = [("SAMLResponse", saml_response), ("RelayState", scope)];
180
181        dp!("--handle saml");
182        let res = self
183            .session
184            .post("https://lib.hku.hk/hkulauth/handleSAML")
185            .form(&saml_data)
186            .send()
187            .await?;
188
189        // let res = self
190        //     .session
191        //     .get("https://booking.lib.hku.hk/Secure/FacilityStatusDate.aspx")
192        //     .send()
193        //     .await?;
194
195        dp!("--check login status");
196        let body = res.text().await?;
197        // dp!(&body);
198        if body.contains("By making a booking / application, you are deemed to accept the relevant")
199        {
200            Ok(&self)
201        } else {
202            dp!("Warning: login may failed");
203            Ok(&self)
204        }
205    }
206
207    pub async fn login_moodle(
208        &self,
209        uid: &str,
210        password: &str,
211    ) -> Result<&Self, Box<dyn std::error::Error>> {
212        // TODO: login moodle
213        dp!("-start login to moodle");
214
215        dp!("--get login page");
216        self.session.get("https://moodle.hku.hk/my/").send().await?;
217
218        dp!("--goto hku login page");
219        self.session
220            .get("https://moodle.hku.hk/login/index.php?authCAS=CAS")
221            .send()
222            .await?;
223
224        dp!("--generate keyid");
225        let keyid = Local::now()
226            .format("%Y%m%d%H%M%S")
227            .to_string();
228        dp!(&keyid);
229
230        dp!("--send login data");
231        let res = self.session
232            .post("https://hkuportal.hku.hk/cas/servlet/edu.yale.its.tp.cas.servlet.Login")
233            .form(&[
234                ("keyid", keyid.as_str()),
235                (
236                    "service",
237                    "https://moodle.hku.hk/login/index.php?authCAS=CAS",
238                ),
239                ("username", uid),
240                ("password", password),
241            ])
242            .send()
243            .await?;
244        
245        dp!("--get ticket");
246        let body = res.text().await?;
247        let url = regex::Regex::new(r#"Click <a href="(.*)">here</a>"#)
248            .unwrap()
249            .captures(&body)
250            .unwrap()
251            .get(1)
252            .unwrap()
253            .as_str();
254        let ticket = url.split("=").last().unwrap();
255        dp!(ticket);
256
257        dp!("--verify ticket");
258        let res = self.session.get(url).send().await?;
259
260        dp!("--check login status");
261        let body = res.text().await?;
262        if body.contains("My courses") {
263            dp!("!-login to moodle success");
264            Ok(&self)
265        } else {
266            dp!("!-login may failed");
267            Ok(&self)
268        }
269    }
270}
271
272impl Deref for Client {
273    type Target = Session;
274    fn deref(&self) -> &Session {
275        &self.session
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    macro_rules! aw {
284        ($e:expr) => {
285            tokio_test::block_on($e)
286        };
287    }
288
289    #[test]
290    fn test_portal_login() {
291        let uid = std::env::var("HKU_UID").unwrap_or_else(|_e| panic!("HKU_UID not set"));
292        let pwd = std::env::var("HKU_PWD").unwrap_or_else(|_e| panic!("HKU_PWD not set"));
293        let client = Client::new();
294        aw!(client.login_portal(&uid, &pwd)).unwrap();
295    }
296
297    #[test]
298    fn test_lib_login() {
299        let uid = std::env::var("HKU_UID").unwrap_or_else(|_e| panic!("HKU_UID not set"));
300        let pwd = std::env::var("HKU_PWD").unwrap_or_else(|_e| panic!("HKU_PWD not set"));
301        let client = Client::new();
302        aw!(client.login_lib(&uid, &pwd)).unwrap();
303    }
304
305    #[test]
306    fn test_moodle_login() {
307        let uid = std::env::var("HKU_UID").unwrap_or_else(|_e| panic!("HKU_UID not set"));
308        let pwd = std::env::var("HKU_PWD").unwrap_or_else(|_e| panic!("HKU_PWD not set"));
309        let client = Client::new();
310        aw!(client.login_moodle(&uid, &pwd)).unwrap();
311    }
312
313    #[test]
314    fn list_env_vars() {
315        for (key, value) in std::env::vars() {
316            println!("{}: {}", key, value);
317        }
318    }
319}