1#![allow(dead_code)]
2
3use asession::{Session, SessionBuilder};
4use chrono::Local;
5use std::ops::Deref;
6
7macro_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 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 dp!("--check login status");
196 let body = res.text().await?;
197 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 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}