bhcli

A TUI for chatting on LE PHP Chats
git clone https://git.dasho.dev/bhcli.git
Log | Files | Refs | README

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