mod.rs (7161B)
1 use std::{error, fs, io, thread}; 2 use std::fmt::{Display, Formatter}; 3 use std::io::Write; 4 use std::time::Duration; 5 use base64::Engine; 6 use base64::engine::general_purpose; 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 crate::{CAPTCHA_FAILED_SOLVE_ERR, CAPTCHA_USED_ERR, CAPTCHA_WG_ERR, KICKED_ERR, LANG, NICKNAME_ERR, REG_ERR, SERVER_DOWN_500_ERR, SERVER_DOWN_ERR, SESSION_RGX, trim_newline, UNKNOWN_ERR}; 13 14 pub mod captcha; 15 16 #[derive(Debug)] 17 pub enum LoginErr { 18 ServerDownErr, 19 ServerDown500Err, 20 CaptchaFailedSolveErr, // When auto-solver failed to solve the lechatphp built-in captcha 21 CaptchaUsedErr, 22 CaptchaWgErr, 23 RegErr, 24 NicknameErr, 25 KickedErr, 26 UnknownErr, 27 Reqwest(reqwest::Error), 28 } 29 30 impl From<reqwest::Error> for LoginErr { 31 fn from(value: reqwest::Error) -> Self { 32 LoginErr::Reqwest(value) 33 } 34 } 35 36 impl Display for LoginErr { 37 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 38 let s = match self { 39 LoginErr::ServerDownErr => SERVER_DOWN_ERR.to_owned(), 40 LoginErr::ServerDown500Err => SERVER_DOWN_500_ERR.to_owned(), 41 LoginErr::CaptchaFailedSolveErr => CAPTCHA_FAILED_SOLVE_ERR.to_owned(), 42 LoginErr::CaptchaUsedErr => CAPTCHA_USED_ERR.to_owned(), 43 LoginErr::CaptchaWgErr => CAPTCHA_WG_ERR.to_owned(), 44 LoginErr::RegErr => REG_ERR.to_owned(), 45 LoginErr::NicknameErr => NICKNAME_ERR.to_owned(), 46 LoginErr::KickedErr => KICKED_ERR.to_owned(), 47 LoginErr::UnknownErr => UNKNOWN_ERR.to_owned(), 48 LoginErr::Reqwest(e) => e.to_string(), 49 }; 50 write!(f, "{}", s) 51 } 52 } 53 54 impl error::Error for LoginErr {} 55 56 pub fn login( 57 client: &Client, 58 base_url: &str, 59 page_php: &str, 60 username: &str, 61 password: &str, 62 color: &str, 63 manual_captcha: bool) -> Result<String, LoginErr> 64 { 65 // Get login page 66 let login_url = format!("{}/{}", &base_url, &page_php); 67 let resp = client.get(&login_url).send()?; 68 if resp.status() == StatusCode::BAD_GATEWAY { 69 return Err(LoginErr::ServerDownErr); 70 } 71 let resp = resp.text()?; 72 let doc = Document::from(resp.as_str()); 73 74 // Post login form 75 let mut params = vec![ 76 ("action", "login".to_owned()), 77 ("lang", LANG.to_owned()), 78 ("nick", username.to_owned()), 79 ("pass", password.to_owned()), 80 ("colour", color.to_owned()), 81 ]; 82 83 if let Some(captcha_node) = doc.find(And(Name("input"), Attr("name", "challenge"))).next() { 84 let captcha_value = captcha_node.attr("value").unwrap(); 85 let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); 86 87 let mut captcha_input = String::new(); 88 if manual_captcha { 89 // Otherwise, save the captcha on disk and prompt user for answer 90 let img_decoded = general_purpose::STANDARD.decode(captcha_img.strip_prefix("data:image/png;base64,").unwrap()).unwrap(); 91 let img = image::load_from_memory(&img_decoded).unwrap(); 92 let img_buf = image::imageops::resize( 93 &img, 94 img.width() * 4, 95 img.height() * 4, 96 image::imageops::FilterType::Nearest, 97 ); 98 // Save captcha as file on disk 99 img_buf.save("captcha.gif").unwrap(); 100 101 termage::display_image("captcha.gif", img.width(), img.height()); 102 103 // Enter captcha 104 print!("captcha: "); 105 io::stdout().flush().unwrap(); 106 io::stdin().read_line(&mut captcha_input).unwrap(); 107 trim_newline(&mut captcha_input); 108 } else { 109 captcha_input = captcha::solve_b64(captcha_img).ok_or(LoginErr::CaptchaFailedSolveErr)?; 110 } 111 112 params.extend(vec![ 113 ("challenge", captcha_value.to_owned()), 114 ("captcha", captcha_input.clone()), 115 ]); 116 } 117 118 let mut resp = client.post(&login_url).form(¶ms).send()?; 119 match resp.status() { 120 StatusCode::BAD_GATEWAY => return Err(LoginErr::ServerDownErr), 121 StatusCode::INTERNAL_SERVER_ERROR => return Err(LoginErr::ServerDown500Err), 122 _ => {} 123 } 124 125 let mut refresh_header = resp.headers().get("refresh").map(|v| v.to_str().unwrap()).unwrap_or(""); 126 while refresh_header != "" { 127 let rgx = Regex::new(r#"URL=(.+)"#).unwrap(); 128 let refresh_url = format!("{}{}", base_url, rgx.captures(&refresh_header).unwrap().get(1).unwrap().as_str()); 129 println!("waitroom enabled, wait 10sec"); 130 thread::sleep(Duration::from_secs(10)); 131 resp = client.get(refresh_url.clone()).send()?; 132 refresh_header = resp.headers().get("refresh").map(|v| v.to_str().unwrap()).unwrap_or(""); 133 } 134 135 let mut resp = resp.text()?; 136 if resp.contains(CAPTCHA_USED_ERR) { 137 return Err(LoginErr::CaptchaUsedErr); 138 } else if resp.contains(CAPTCHA_WG_ERR) { 139 return Err(LoginErr::CaptchaWgErr); 140 } else if resp.contains(REG_ERR) { 141 return Err(LoginErr::RegErr); 142 } else if resp.contains(NICKNAME_ERR) { 143 return Err(LoginErr::NicknameErr); 144 } else if resp.contains(KICKED_ERR) { 145 return Err(LoginErr::KickedErr); 146 } 147 148 let mut doc = Document::from(resp.as_str()); 149 if let Some(body) = doc.find(Name("body")).next() { 150 if let Some(body_class) = body.attr("class") { 151 if body_class == "error" { 152 if let Some(h2) = doc.find(Name("h2")).next() { 153 log::error!("{}", h2.text()); 154 } 155 return Err(LoginErr::UnknownErr); 156 } else if body_class == "failednotice" { 157 log::error!("failed logins: {}", body.text()); 158 let nc = doc.find(Attr("name", "nc")).next().unwrap(); 159 let nc_value = nc.attr("value").unwrap().to_owned(); 160 let params: Vec<(&str, String)> = vec![ 161 ("lang", LANG.to_owned()), 162 ("nc", nc_value.to_owned()), 163 ("action", "login".to_owned()), 164 ]; 165 resp = client.post(&login_url).form(¶ms).send()?.text()?; 166 doc = Document::from(resp.as_str()); 167 } 168 } 169 } 170 171 let iframe = match doc.find(Attr("name", "view")).next() { 172 Some(view) => view, 173 None => { 174 fs::write("./dump_login_err.html", resp.as_str()).unwrap(); 175 panic!("failed to get view iframe"); 176 } 177 }; 178 let iframe_src = iframe.attr("src").unwrap(); 179 180 let session_captures = SESSION_RGX.captures(iframe_src).unwrap(); 181 let session = session_captures.get(1).unwrap().as_str(); 182 Ok(session.to_owned()) 183 } 184 185 pub fn logout( 186 client: &Client, 187 base_url: &str, 188 page_php: &str, 189 session: &str) -> anyhow::Result<()> 190 { 191 let full_url = format!("{}/{}", &base_url, &page_php); 192 let params = [ 193 ("action", "logout"), 194 ("session", &session), 195 ("lang", LANG), 196 ]; 197 client.post(&full_url).form(¶ms).send()?; 198 Ok(()) 199 }