bhcli-simple

A simplified version of bhcli
git clone https://git.dasho.dev/bhcli-simple.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 }