1use std::{
2 borrow::BorrowMut,
3 io::{BufRead, Write},
4 sync::Arc,
5};
6
7use cookie_store::serde::json::{load_all, save_incl_expired_and_nonpersistent};
8use reqwest::{
9 Client,
10 cookie::{CookieStore, Jar},
11 header::{COOKIE, HOST, HeaderValue, SET_COOKIE},
12};
13use reqwest_cookie_store::CookieStoreRwLock;
14use url::Url;
15use wdpe::error::{ClientError, WebDynproError};
16
17use crate::{
18 error::{RusaintError, SsuSsoError},
19 utils::{DEFAULT_USER_AGENT, default_header},
20};
21
22const SSU_USAINT_PORTAL_URL: &str = "https://saint.ssu.ac.kr/irj/portal";
23const SSU_USAINT_SSO_URL: &str = "https://saint.ssu.ac.kr/webSSO/sso.jsp";
24const SMARTID_LOGIN_URL: &str = "https://smartid.ssu.ac.kr/Symtra_sso/smln.asp";
25const SMARTID_LOGIN_FORM_REQUEST_URL: &str = "https://smartid.ssu.ac.kr/Symtra_sso/smln_pcs.asp";
26
27#[derive(Debug, Default)]
29pub struct USaintSession(CookieStoreRwLock);
30
31impl CookieStore for USaintSession {
32 fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &Url) {
33 self.0.set_cookies(cookie_headers, url)
34 }
35
36 fn cookies(&self, url: &Url) -> Option<HeaderValue> {
37 self.0.cookies(url)
38 }
39}
40
41impl USaintSession {
42 pub fn anonymous() -> USaintSession {
44 USaintSession(CookieStoreRwLock::default())
45 }
46
47 pub async fn with_token(id: &str, token: &str) -> Result<USaintSession, RusaintError> {
49 let session_store = Self::anonymous();
50 let client = Client::builder()
51 .user_agent(DEFAULT_USER_AGENT)
52 .build()
53 .unwrap();
54 let portal = client
56 .get(SSU_USAINT_PORTAL_URL)
57 .headers(default_header())
58 .header(HOST, "saint.ssu.ac.kr".parse::<HeaderValue>().unwrap())
59 .send()
60 .await
61 .map_err(|e| {
62 WebDynproError::from(ClientError::FailedRequest(format!(
63 "failed to send request: {e}"
64 )))
65 })?;
66 let waf = portal.cookies().find(|cookie| cookie.name() == "WAF");
67
68 session_store.set_cookies(
69 portal
70 .headers()
71 .iter()
72 .filter_map(|header| {
73 if header.0 == SET_COOKIE {
74 Some(header.1)
75 } else {
76 None
77 }
78 })
79 .borrow_mut(),
80 portal.url(),
81 );
82
83 if let Some(waf) = waf {
84 let waf_cookie_str = format!("WAF={}; domain=saint.ssu.ac.kr; path=/;", waf.value());
85 session_store
86 .0
87 .write()
88 .unwrap()
89 .parse(
90 &waf_cookie_str,
91 &Url::parse("https://saint.ssu.ac.kr").unwrap(),
92 )
93 .unwrap();
94 } else {
95 tracing::warn!("WAF cookie not found in portal response");
96 }
97 let token_cookie_str = format!("sToken={token}; domain=.ssu.ac.kr; path=/; secure");
98 let req = client
99 .get(format!("{SSU_USAINT_SSO_URL}?sToken={token}&sIdno={id}"))
100 .query(&[("sToken", token), ("sIdno", id)])
101 .headers(default_header())
102 .header(
103 COOKIE,
104 session_store
105 .cookies(&Url::parse("https://saint.ssu.ac.kr").unwrap())
106 .unwrap(),
107 )
108 .header(COOKIE, token_cookie_str.parse::<HeaderValue>().unwrap())
109 .header(HOST, "saint.ssu.ac.kr".parse::<HeaderValue>().unwrap())
110 .build()
111 .map_err(|e| {
112 WebDynproError::from(ClientError::FailedRequest(format!(
113 "failed to build request: {e}"
114 )))
115 })?;
116 let res = client.execute(req).await.map_err(|e| {
117 WebDynproError::from(ClientError::FailedRequest(format!(
118 "failed to send request: {e}"
119 )))
120 })?;
121 let mut new_cookies = res.headers().iter().filter_map(|header| {
122 if header.0 == SET_COOKIE {
123 Some(header.1)
124 } else {
125 None
126 }
127 });
128 session_store.set_cookies(&mut new_cookies, res.url());
129 if let Some(sapsso_cookies) = session_store.cookies(res.url()) {
130 let str = sapsso_cookies
131 .to_str()
132 .or(Err(ClientError::NoCookies(res.url().to_string())))
133 .map_err(WebDynproError::from)?;
134 if str.contains("MYSAPSSO2") {
135 Ok(session_store)
136 } else {
137 Err(WebDynproError::from(ClientError::NoSuchCookie(
138 "MYSAPSSO2".to_string(),
139 )))?
140 }
141 } else {
142 Err(WebDynproError::from(ClientError::NoCookies(
143 res.url().to_string(),
144 )))?
145 }
146 }
147
148 pub async fn with_password(id: &str, password: &str) -> Result<USaintSession, RusaintError> {
150 let token = obtain_ssu_sso_token(id, password).await?;
151 Self::with_token(id, &token).await
152 }
153
154 pub fn save_to_json<W: Write>(&self, writer: &mut W) -> Result<(), RusaintError> {
156 let store = self.0.read().unwrap();
157 save_incl_expired_and_nonpersistent(&store, writer).map_err(|_| {
158 WebDynproError::from(ClientError::NoCookies("Failed to save cookies".to_string()))
159 })?;
160
161 Ok(())
162 }
163
164 pub fn from_json<R: BufRead>(reader: R) -> Result<USaintSession, RusaintError> {
166 let store = load_all(reader).map_err(|_| {
167 WebDynproError::from(ClientError::NoCookies("Failed to load cookies".to_string()))
168 })?;
169 let store = CookieStoreRwLock::new(store);
170 Ok(USaintSession(store))
171 }
172}
173
174pub async fn obtain_ssu_sso_token(id: &str, password: &str) -> Result<String, SsuSsoError> {
176 let jar: Arc<Jar> = Arc::new(Jar::default());
177 let client = Client::builder()
178 .cookie_provider(jar)
179 .cookie_store(true)
180 .user_agent(DEFAULT_USER_AGENT)
181 .build()?;
182 let body = client
183 .get(SMARTID_LOGIN_URL)
184 .headers(default_header())
185 .send()
186 .await?
187 .text()
188 .await?;
189 let (in_tp_bit, rqst_caus_cd) = parse_login_form(&body)?;
190 let params = [
191 ("in_tp_bit", in_tp_bit.as_str()),
192 ("rqst_caus_cd", rqst_caus_cd.as_str()),
193 ("userid", id),
194 ("pwd", password),
195 ];
196 let res = client
197 .post(SMARTID_LOGIN_FORM_REQUEST_URL)
198 .headers(default_header())
199 .form(¶ms)
200 .send()
201 .await?;
202 let cookie_token = {
203 res.cookies()
204 .find(|cookie| cookie.name() == "sToken" && !cookie.value().is_empty())
205 .map(|cookie| cookie.value().to_string())
206 };
207 let message = if cookie_token.is_none() {
208 let mut content = res.text().await?;
209 let start = content.find("alert(\"").unwrap_or(0);
210 let end = content.find("\");").unwrap_or(content.len());
211 content.truncate(end);
212 let message = content.split_off(start + 7);
213 Some(message)
214 } else {
215 None
216 };
217 cookie_token.ok_or(SsuSsoError::CantFindToken(
218 message.unwrap_or("Internal Error".to_string()),
219 ))
220}
221
222fn parse_login_form(body: &str) -> Result<(String, String), SsuSsoError> {
223 let document = scraper::Html::parse_document(body);
224 let in_tp_bit_selector = scraper::Selector::parse(r#"input[name="in_tp_bit"]"#).unwrap();
225 let rqst_caus_cd_selector = scraper::Selector::parse(r#"input[name="rqst_caus_cd"]"#).unwrap();
226 let in_tp_bit = document
227 .select(&in_tp_bit_selector)
228 .next()
229 .ok_or(SsuSsoError::CantLoadForm)?
230 .value()
231 .attr("value")
232 .ok_or(SsuSsoError::CantLoadForm)?;
233 let rqst_caus_cd = document
234 .select(&rqst_caus_cd_selector)
235 .next()
236 .ok_or(SsuSsoError::CantLoadForm)?
237 .value()
238 .attr("value")
239 .ok_or(SsuSsoError::CantLoadForm)?;
240 Ok((in_tp_bit.to_owned(), rqst_caus_cd.to_owned()))
241}