mod.rs (9088B)
1 use crate::{ 2 trim_newline, CAPTCHA_FAILED_SOLVE_ERR, CAPTCHA_USED_ERR, CAPTCHA_WG_ERR, KICKED_ERR, LANG, 3 NICKNAME_ERR, REG_ERR, SERVER_DOWN_500_ERR, SERVER_DOWN_ERR, SESSION_RGX, UNKNOWN_ERR, 4 }; 5 use base64::engine::general_purpose; 6 use base64::Engine; 7 use http::StatusCode; 8 use regex::Regex; 9 use reqwest::blocking::Client; 10 use select::document::Document; 11 use select::predicate::{And, Attr, Name}; 12 use std::fmt::{Display, Formatter}; 13 use std::io::Write; 14 use std::process::{Command, Stdio}; 15 use std::time::Duration; 16 use std::{error, fs, io, thread}; 17 18 pub mod captcha; 19 20 #[derive(Debug)] 21 pub enum LoginErr { 22 ServerDownErr, 23 ServerDown500Err, 24 CaptchaFailedSolveErr, // When auto-solver failed to solve the lechatphp built-in captcha 25 CaptchaUsedErr, 26 CaptchaWgErr, 27 RegErr, 28 NicknameErr, 29 KickedErr, 30 UnknownErr, 31 Reqwest(reqwest::Error), 32 } 33 34 impl From<reqwest::Error> for LoginErr { 35 fn from(value: reqwest::Error) -> Self { 36 LoginErr::Reqwest(value) 37 } 38 } 39 40 impl Display for LoginErr { 41 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 42 let s = match self { 43 LoginErr::ServerDownErr => SERVER_DOWN_ERR.to_owned(), 44 LoginErr::ServerDown500Err => SERVER_DOWN_500_ERR.to_owned(), 45 LoginErr::CaptchaFailedSolveErr => CAPTCHA_FAILED_SOLVE_ERR.to_owned(), 46 LoginErr::CaptchaUsedErr => CAPTCHA_USED_ERR.to_owned(), 47 LoginErr::CaptchaWgErr => CAPTCHA_WG_ERR.to_owned(), 48 LoginErr::RegErr => REG_ERR.to_owned(), 49 LoginErr::NicknameErr => NICKNAME_ERR.to_owned(), 50 LoginErr::KickedErr => KICKED_ERR.to_owned(), 51 LoginErr::UnknownErr => UNKNOWN_ERR.to_owned(), 52 LoginErr::Reqwest(e) => e.to_string(), 53 }; 54 write!(f, "{}", s) 55 } 56 } 57 58 impl error::Error for LoginErr {} 59 60 pub fn login( 61 client: &Client, 62 base_url: &str, 63 page_php: &str, 64 username: &str, 65 password: &str, 66 color: &str, 67 manual_captcha: bool, 68 sxiv: bool, 69 ) -> Result<String, LoginErr> { 70 // Get login page 71 let login_url = format!("{}/{}", &base_url, &page_php); 72 let resp = client.get(&login_url).send()?; 73 if resp.status() == StatusCode::BAD_GATEWAY { 74 return Err(LoginErr::ServerDownErr); 75 } 76 let resp = resp.text()?; 77 let doc = Document::from(resp.as_str()); 78 79 // Post login form 80 let mut params = vec![ 81 ("action", "login".to_owned()), 82 ("lang", LANG.to_owned()), 83 ("nick", username.to_owned()), 84 ("pass", password.to_owned()), 85 ("colour", color.to_owned()), 86 ]; 87 88 if let Some(captcha_node) = doc 89 .find(And(Name("input"), Attr("name", "challenge"))) 90 .next() 91 { 92 let captcha_value = captcha_node.attr("value").unwrap(); 93 let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); 94 95 let mut captcha_input = String::new(); 96 if manual_captcha { 97 // Otherwise, save the captcha on disk and prompt user for answer 98 // println!("Captcha image source: {}", captcha_img); 99 // let img_decoded = general_purpose::STANDARD.decode(captcha_img.strip_prefix("data:image/gif;base64,").unwrap()).unwrap(); 100 // 101 // Attempt to strip the appropriate prefix based on the MIME type 102 let base64_str = 103 if let Some(base64) = captcha_img.strip_prefix("data:image/png;base64,") { 104 base64 105 } else if let Some(base64) = captcha_img.strip_prefix("data:image/gif;base64,") { 106 base64 107 } else { 108 panic!("Unexpected captcha image format. Expected PNG or GIF."); 109 }; 110 111 // Decode the base64 string into binary image data 112 let img_decoded = general_purpose::STANDARD.decode(base64_str).unwrap(); 113 114 // 115 let img = image::load_from_memory(&img_decoded).unwrap(); 116 let img_buf = image::imageops::resize( 117 &img, 118 img.width() * 4, 119 img.height() * 4, 120 image::imageops::FilterType::Nearest, 121 ); 122 // Save captcha as file on disk 123 img_buf.save("captcha.gif").unwrap(); 124 125 if sxiv { 126 let mut sxiv_process = Command::new("sxiv") 127 .arg("captcha.gif") 128 .stdout(Stdio::null()) 129 .stderr(Stdio::null()) 130 .spawn() 131 .expect("Failed to open image with sxiv"); 132 133 // Wait for the process to prevent zombie processes 134 let _ = sxiv_process.wait(); 135 136 // Prompt the user to enter the CAPTCHA 137 print!("Please enter the CAPTCHA: "); 138 io::stdout().flush().unwrap(); 139 io::stdin().read_line(&mut captcha_input).unwrap(); 140 trim_newline(&mut captcha_input); 141 142 // Close the sxiv window 143 sxiv_process.kill().expect("Failed to close sxiv"); 144 145 println!("Captcha input: {}", captcha_input); 146 } else { 147 termage::display_image("captcha.gif", img.width(), img.height()); 148 149 // Enter captcha 150 print!("captcha: "); 151 io::stdout().flush().unwrap(); 152 io::stdin().read_line(&mut captcha_input).unwrap(); 153 trim_newline(&mut captcha_input); 154 } 155 } else { 156 captcha_input = 157 captcha::solve_b64(captcha_img).ok_or(LoginErr::CaptchaFailedSolveErr)?; 158 } 159 160 params.extend(vec![ 161 ("challenge", captcha_value.to_owned()), 162 ("captcha", captcha_input.clone()), 163 ]); 164 } 165 166 let mut resp = client.post(&login_url).form(¶ms).send()?; 167 match resp.status() { 168 StatusCode::BAD_GATEWAY => return Err(LoginErr::ServerDownErr), 169 StatusCode::INTERNAL_SERVER_ERROR => return Err(LoginErr::ServerDown500Err), 170 _ => {} 171 } 172 173 let mut refresh_header = resp 174 .headers() 175 .get("refresh") 176 .map(|v| v.to_str().unwrap()) 177 .unwrap_or(""); 178 while refresh_header != "" { 179 let rgx = Regex::new(r#"URL=(.+)"#).unwrap(); 180 let refresh_url = format!( 181 "{}{}", 182 base_url, 183 rgx.captures(&refresh_header) 184 .unwrap() 185 .get(1) 186 .unwrap() 187 .as_str() 188 ); 189 println!("waitroom enabled, wait 10sec"); 190 thread::sleep(Duration::from_secs(10)); 191 resp = client.get(refresh_url.clone()).send()?; 192 refresh_header = resp 193 .headers() 194 .get("refresh") 195 .map(|v| v.to_str().unwrap()) 196 .unwrap_or(""); 197 } 198 199 let mut resp = resp.text()?; 200 if resp.contains(CAPTCHA_USED_ERR) { 201 return Err(LoginErr::CaptchaUsedErr); 202 } else if resp.contains(CAPTCHA_WG_ERR) { 203 return Err(LoginErr::CaptchaWgErr); 204 } else if resp.contains(REG_ERR) { 205 return Err(LoginErr::RegErr); 206 } else if resp.contains(NICKNAME_ERR) { 207 return Err(LoginErr::NicknameErr); 208 } else if resp.contains(KICKED_ERR) { 209 return Err(LoginErr::KickedErr); 210 } 211 212 let mut doc = Document::from(resp.as_str()); 213 if let Some(body) = doc.find(Name("body")).next() { 214 if let Some(body_class) = body.attr("class") { 215 if body_class == "error" { 216 if let Some(h2) = doc.find(Name("h2")).next() { 217 log::error!("{}", h2.text()); 218 } 219 return Err(LoginErr::UnknownErr); 220 } else if body_class == "failednotice" { 221 log::error!("failed logins: {}", body.text()); 222 let nc = doc.find(Attr("name", "nc")).next().unwrap(); 223 let nc_value = nc.attr("value").unwrap().to_owned(); 224 let params: Vec<(&str, String)> = vec![ 225 ("lang", LANG.to_owned()), 226 ("nc", nc_value.to_owned()), 227 ("action", "login".to_owned()), 228 ]; 229 resp = client.post(&login_url).form(¶ms).send()?.text()?; 230 doc = Document::from(resp.as_str()); 231 } 232 } 233 } 234 235 let iframe = match doc.find(Attr("name", "view")).next() { 236 Some(view) => view, 237 None => { 238 fs::write("./dump_login_err.html", resp.as_str()).unwrap(); 239 panic!("failed to get view iframe"); 240 } 241 }; 242 let iframe_src = iframe.attr("src").unwrap(); 243 244 let session_captures = SESSION_RGX.captures(iframe_src).unwrap(); 245 let session = session_captures.get(1).unwrap().as_str(); 246 Ok(session.to_owned()) 247 } 248 249 pub fn logout( 250 client: &Client, 251 base_url: &str, 252 page_php: &str, 253 session: &str, 254 ) -> anyhow::Result<()> { 255 let full_url = format!("{}/{}", &base_url, &page_php); 256 let params = [("action", "logout"), ("session", &session), ("lang", LANG)]; 257 client.post(&full_url).form(¶ms).send()?; 258 Ok(()) 259 }