bhcli

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

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