mod.rs (8974B)
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 // Prompt the user to enter the CAPTCHA 134 print!("Please enter the CAPTCHA: "); 135 io::stdout().flush().unwrap(); 136 io::stdin().read_line(&mut captcha_input).unwrap(); 137 trim_newline(&mut captcha_input); 138 139 // Close the sxiv window 140 sxiv_process.kill().expect("Failed to close sxiv"); 141 142 println!("Captcha input: {}", captcha_input); 143 } else { 144 termage::display_image("captcha.gif", img.width(), img.height()); 145 146 // Enter captcha 147 print!("captcha: "); 148 io::stdout().flush().unwrap(); 149 io::stdin().read_line(&mut captcha_input).unwrap(); 150 trim_newline(&mut captcha_input); 151 } 152 } else { 153 captcha_input = 154 captcha::solve_b64(captcha_img).ok_or(LoginErr::CaptchaFailedSolveErr)?; 155 } 156 157 params.extend(vec![ 158 ("challenge", captcha_value.to_owned()), 159 ("captcha", captcha_input.clone()), 160 ]); 161 } 162 163 let mut resp = client.post(&login_url).form(¶ms).send()?; 164 match resp.status() { 165 StatusCode::BAD_GATEWAY => return Err(LoginErr::ServerDownErr), 166 StatusCode::INTERNAL_SERVER_ERROR => return Err(LoginErr::ServerDown500Err), 167 _ => {} 168 } 169 170 let mut refresh_header = resp 171 .headers() 172 .get("refresh") 173 .map(|v| v.to_str().unwrap()) 174 .unwrap_or(""); 175 while refresh_header != "" { 176 let rgx = Regex::new(r#"URL=(.+)"#).unwrap(); 177 let refresh_url = format!( 178 "{}{}", 179 base_url, 180 rgx.captures(&refresh_header) 181 .unwrap() 182 .get(1) 183 .unwrap() 184 .as_str() 185 ); 186 println!("waitroom enabled, wait 10sec"); 187 thread::sleep(Duration::from_secs(10)); 188 resp = client.get(refresh_url.clone()).send()?; 189 refresh_header = resp 190 .headers() 191 .get("refresh") 192 .map(|v| v.to_str().unwrap()) 193 .unwrap_or(""); 194 } 195 196 let mut resp = resp.text()?; 197 if resp.contains(CAPTCHA_USED_ERR) { 198 return Err(LoginErr::CaptchaUsedErr); 199 } else if resp.contains(CAPTCHA_WG_ERR) { 200 return Err(LoginErr::CaptchaWgErr); 201 } else if resp.contains(REG_ERR) { 202 return Err(LoginErr::RegErr); 203 } else if resp.contains(NICKNAME_ERR) { 204 return Err(LoginErr::NicknameErr); 205 } else if resp.contains(KICKED_ERR) { 206 return Err(LoginErr::KickedErr); 207 } 208 209 let mut doc = Document::from(resp.as_str()); 210 if let Some(body) = doc.find(Name("body")).next() { 211 if let Some(body_class) = body.attr("class") { 212 if body_class == "error" { 213 if let Some(h2) = doc.find(Name("h2")).next() { 214 log::error!("{}", h2.text()); 215 } 216 return Err(LoginErr::UnknownErr); 217 } else if body_class == "failednotice" { 218 log::error!("failed logins: {}", body.text()); 219 let nc = doc.find(Attr("name", "nc")).next().unwrap(); 220 let nc_value = nc.attr("value").unwrap().to_owned(); 221 let params: Vec<(&str, String)> = vec![ 222 ("lang", LANG.to_owned()), 223 ("nc", nc_value.to_owned()), 224 ("action", "login".to_owned()), 225 ]; 226 resp = client.post(&login_url).form(¶ms).send()?.text()?; 227 doc = Document::from(resp.as_str()); 228 } 229 } 230 } 231 232 let iframe = match doc.find(Attr("name", "view")).next() { 233 Some(view) => view, 234 None => { 235 fs::write("./dump_login_err.html", resp.as_str()).unwrap(); 236 panic!("failed to get view iframe"); 237 } 238 }; 239 let iframe_src = iframe.attr("src").unwrap(); 240 241 let session_captures = SESSION_RGX.captures(iframe_src).unwrap(); 242 let session = session_captures.get(1).unwrap().as_str(); 243 Ok(session.to_owned()) 244 } 245 246 pub fn logout( 247 client: &Client, 248 base_url: &str, 249 page_php: &str, 250 session: &str, 251 ) -> anyhow::Result<()> { 252 let full_url = format!("{}/{}", &base_url, &page_php); 253 let params = [("action", "logout"), ("session", &session), ("lang", LANG)]; 254 client.post(&full_url).form(¶ms).send()?; 255 Ok(()) 256 }