bhcli

"Strange's fork of n0tr1v's bhcli (onion)"
git clone https://git.dasho.dev/Strange/bhcli.git
Log | Files | Refs | README

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(&params).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(&params).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(&params).send()?;
    255     Ok(())
    256 }