commit 318fdcfbf22367520483db7a94f10e0debe4583e
parent c6aed740498e276bd280dc4aa3051d2bfb6ff538
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Thu, 6 Apr 2023 20:11:33 -0700
cleanup
Diffstat:
4 files changed, 4 insertions(+), 311 deletions(-)
diff --git a/src/bhc/mod.rs b/src/bhc/mod.rs
diff --git a/src/lechatphp/mod.rs b/src/lechatphp/captcha.rs
diff --git a/src/lechatphp/mod.rs b/src/lechatphp/mod.rs
@@ -1,309 +1 @@
-use std::collections::{HashMap, HashSet};
-use std::fmt::{Display, Formatter};
-use std::hash::Hash;
-use base64::{engine::general_purpose, Engine as _};
-use bresenham::Bresenham;
-use image::{DynamicImage, GenericImageView, Rgba};
-use lazy_static::lazy_static;
-
-const B64_PREFIX: &'static str = "R0lGODlhCAAOAIAAAAAAAAAAACH5BAgAAAAALAAAAAAIAA4AgAQCBPz+/AI";
-// list of letters that contains other letters: (h, n) (I, l) (y, u) (Q, O) (B, 3) (E, L) (R, P)
-// So our alphabet needs to have "I" before "l" since "l" is contained by "I".
-const ALPHABET1: &'static str = "abdcefgh1ijkImnpoqrstyQuvwxzABCDEGJKMNHLORPFSTlUVWXYZ023456789";
-const LETTER_WIDTH: u32 = 8;
-const LETTER_HEIGHT: u32 = 14;
-const NB_CHARS: u32 = 5;
-const LEFT_PADDING: u32 = 5; // left padding for difficulty 1 and 2
-const TOP_PADDING: u32 = 7; // top padding for difficulty 1 and 2
-
-lazy_static! {
- static ref B64_MAP: HashMap<char, &'static str> = HashMap::from([
- ('0', "VhI8Qkbv3FIvGMeiQ1fPSzSXiSAIFADs="),
- ('1', "UhI8QkcvnHlpJSXgNbdnO2FViVQAAOw=="),
- ('2', "UhI+hcWruGkMgSmrfvGnrtVDiKBYAOw=="),
- ('3', "UhH+hatyBEkTuzVjpldWtHIUiUgAAOw=="),
- ('4', "VhI8XkcvqIFiGTmbvdRFl2TzJSCYFADs="),
- ('5', "WhG+hG6CYGnwrygedRIw3jGlhRpZGAQA7"),
- ('6', "XhI+hcWruGoiJrRcha5fPTS0bQpamUQAAOw=="),
- ('7', "ThG+hq5jhQEPz1OeuhJT3CIZiAQA7"),
- ('8', "XhI+hcWruGosRLPYwxLnaqXEXQpYmUgAAOw=="),
- ('9', "VhI+hcWru2kosTjAjxduydyHiSBoFADs="),
- ('A', "VhI8Qkbv3FIvGMeiQ3RlT+X3JSBoFADs="),
- ('B', "VhG+hm3EK4GMtLimtntlmeHXISJYFADs="),
- ('C', "VhI+hatyLAIwuhSgv1edWt1TYSAIFADs="),
- ('D', "WhG+hm3EK4GNTNhvpdZXnvHjISJZAAQA7"),
- ('E', "UhG+hG6CYGnyTSYrw0RE6K3niaBQAOw=="),
- ('F', "ThG+hy5jhgIpsSugs0oe/CIZiAQA7"),
- ('G', "VhI+hatyLXgQuhbMqhfhWl13TSB4FADs="),
- ('H', "UhG8RqMr93Gm0xjVhlkl3BIaiUQAAOw=="),
- ('I', "UhH+hi4rmXmzhgZmq1JQuboUiUAAAOw=="),
- ('J', "UhI8QG6mdlpMRInqhpRI/64TiUQAAOw=="),
- ('K', "XhG8RqHruQIQrNXbfirLO2oUaQpamUQAAOw=="),
- ('L', "UhG8RmMvKAHwOTVvP1bmpH4UiUgAAOw=="),
- ('M', "WhG8RqJ0NI1DTUVgrPZg7vz3ISJZGAQA7"),
- ('N', "WhG8RqH3tAFTJUSgXzpvTGlXISJZGAQA7"),
- ('O', "VhI+hcWru2kpTxlfxBZBx/23ISJYFADs="),
- ('P', "ThG+hG6DI4JJsPuQavhJnD4ZiAQA7"),
- ('Q', "WhI+hcWru2kpTxjdhXSxeDjDISJZAAQA7"),
- ('R', "WhG+hG6DI4JJs1vNc07jlNHXISJZGAQA7"),
- ('S', "UhH+hC4obkHGywVjpw1tbC1XiiBQAOw=="),
- ('T', "UhG+hq9cIHpIuwGghTXn2eoXiVQAAOw=="),
- ('U', "UhG8RqMr93GlrQivT1UBmioTiiBQAOw=="),
- ('V', "VhG8RqMr9gJOLpjon1bsefiHiSIoFADs="),
- ('W', "UhG8RqMr93GnLUWBhplzyh4TiaBQAOw=="),
- ('X', "UhG8RqMrrQJQNUXvfjG5S72GiWAAAOw=="),
- ('Y', "UhG8RqMrrQJQNUXvtPDst/WHiiBQAOw=="),
- ('Z', "UhG+hG5jtVIRHIlvpcwBnpnHiWAAAOw=="),
- ('a', "ThI+pq+FuYAyNvitnfuB2yoRKAQA7"),
- ('b', "WhG8RmMvKAFSONukaPDRji4VgRJZHAQA7"),
- ('c', "RhI+pq+FuYHwt1CWBfJn5VAAAOw=="),
- ('d', "VhI8YkbD93JtMrmoutpvmeEnNSB4FADs="),
- ('e', "RhI+pq+FxnJEyvntXBRWzzxQAOw=="),
- ('f', "VhI8QG7f2VJNwoliZpQm7XSXiSBoFADs="),
- ('g', "VhI+pm+EO3nnQwBqDpXvRq03aFy4FADs="),
- ('h', "VhG8RmMvKAFSONukaPDTjrXlgRJYFADs="),
- ('i', "ThI8Qkcrd1kMrzlNTpldKCIZAAQA7"),
- ('j', "UhI8XkbANF0uPTlTxVSw/P0kZVAAAOw=="),
- ('k', "VhH8RaMrgWJwrQrUSRs7Sll3PSB4FADs="),
- ('l', "RhI+haMueAgPw1CZfjvrODxYAOw=="),
- ('m', "ThI+piwHh4ItUWkjn1Rl3x4RKAQA7"),
- ('n', "UhI+pixHgHnSG0hNljY1jvzHiUgAAOw=="),
- ('o', "ThI+pq+FxnJEyPvSgBbXyy4RMAQA7"),
- ('p', "VhI+pixHgHnSG0hNljS3vOUlXxygFADs="),
- ('q', "VhI+pq+HwHnTS0IBuxpLaiCXVMSIFADs="),
- ('r', "ShI+pixHgnInzyXTbw1uzDxoFADs="),
- ('s', "RhI+pm+EPHHphUanorLeyvxQAOw=="),
- ('t', "UhI95EcrIYlsTVuqueYD3qIQiUgAAOw=="),
- ('u', "ThI+pixHt3onSUOggyJvHzoRLAQA7"),
- ('v', "ThI+pixHt3nGAVmnt1VtOz4RLAQA7"),
- ('w', "UhI+pixHt3onPAXprzJliyzHiUgAAOw=="),
- ('x', "ThI+pixH9nJHTPZjsBVTSyoRLAQA7"),
- ('y', "VhI+pixHt3onSUOggyJvHXlkPxS0FADs="),
- ('z', "PhI+pm+GvXAuzIjkfZXwVADs="),
- ]);
- static ref RED_COLOR: Rgba<u8> = Rgba::from([204, 2, 4, 255]);
- static ref ON_COLOR: Rgba<u8> = Rgba::from([252, 254, 252, 255]);
-}
-
-fn get_letter_img(letter: char) -> DynamicImage {
- let b64_suffix = B64_MAP.get(&letter).expect(format!("letter image not found for {}", letter).as_str());
- let img_dec = general_purpose::STANDARD.decode(format!("{}{}", B64_PREFIX, b64_suffix)).unwrap();
- image::load_from_memory(&img_dec).unwrap()
-}
-
-pub fn solve_b64(b64_str: &str) -> Option<String> {
- let img_dec = general_purpose::STANDARD.decode(b64_str.strip_prefix("data:image/gif;base64,")?).ok()?;
- let img = image::load_from_memory(&img_dec).ok()?;
- if img.width() > 60 {
- return match solve_difficulty3(&img) {
- Ok(answer) => Some(answer),
- Err(e) => {
- println!("{:?}", e);
- None
- },
- };
- }
- solve_difficulty2(&img)
-}
-
-// This function can solve both difficulty 1 and 2.
-fn solve_difficulty2(img: &DynamicImage) -> Option<String> {
- let mut answer = String::new();
- for i in 0..NB_CHARS {
- let sub_img = img.crop_imm(LEFT_PADDING + ((LETTER_WIDTH +1)*i), TOP_PADDING, LETTER_WIDTH, LETTER_HEIGHT);
- for c in ALPHABET1.chars() {
- if img_contains_letter(&sub_img, c) {
- answer.push(c);
- break;
- }
- }
- }
- Some(answer)
-}
-
-#[derive(Debug, PartialEq, Eq, Hash)]
-struct Letter {
- offset: Point,
- character: char,
-}
-
-impl Letter {
- fn new(offset: Point, character: char) -> Self {
- Self { offset, character }
- }
-
- fn offset(&self) -> Point {
- self.offset.clone()
- }
-
- fn center(&self) -> Point {
- let offset = self.offset();
- Point::new(offset.x + LETTER_WIDTH/2, offset.y + LETTER_HEIGHT/2 - 1)
- }
-}
-
-#[derive(Debug)]
-struct CaptchaErr(String);
-
-impl Display for CaptchaErr {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.0)
- }
-}
-
-impl std::error::Error for CaptchaErr {}
-
-// SolveDifficulty3 solve captcha for difficulty 3
-// For each pixel, verify if a match is found. If we do have a match,
-// verify that we have some "red" in it.
-//
-// Red circle is 17x17 (initial point)
-fn solve_difficulty3(img: &DynamicImage) -> Result<String, CaptchaErr> {
- //img.save(format!("captcha.gif")).unwrap();
-
- // Step1: Find all letters with red on the center
- let letters_set = find_letters(&img)?;
-
- // Step2: Find the starting letter
- let starting = get_starting_letter(&img, &letters_set)
- .ok_or(CaptchaErr("could not find starting letter".to_owned()))?;
-
- // Step3: Solve path
- let answer = solve_path(starting, &letters_set, &img);
- Ok(answer)
-}
-
-fn solve_path(starting: &Letter, letters_set: &HashSet<Letter>, img: &DynamicImage) -> String {
- let mut answer = String::new();
- let mut remaining: HashSet<_> = letters_set.iter().collect();
- let mut letter = remaining.take(&starting).unwrap();
- for _ in 0..NB_CHARS {
- answer.push(letter.character);
- let mut dest_count = HashMap::<&Letter, usize>::new();
- for dest in remaining.iter() {
- let red = Bresenham::new(letter.center().into(), dest.center().into())
- .filter(|(x, y)| is_red(img.get_pixel(*x as u32, *y as u32)))
- .count();
- dest_count.insert(dest, red);
- }
- if let Some((dest_max, _)) = dest_count.into_iter().max_by_key(|e| e.1) {
- letter = remaining.take(dest_max).unwrap();
- }
- }
- answer
-}
-
-fn find_letters(img: &DynamicImage) -> Result<HashSet<Letter>, CaptchaErr> {
- const IMAGE_WIDTH: u32 = 150;
- const IMAGE_HEIGHT: u32 = 200;
- const MIN_PX_FOR_LETTER: usize = 21;
- let mut letters_set = HashSet::new();
- for y in 0..IMAGE_HEIGHT-LETTER_HEIGHT {
- for x in 0..IMAGE_WIDTH-LETTER_WIDTH {
- let letter_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT);
- // We know that minimum amount of pixels on to form a letter is 21
- // We can skip squares that do not have this prerequisite
- // Check middle pixels for red, if no red pixels, we can ignore that square
- if count_px_on(&letter_img) < MIN_PX_FOR_LETTER || !has_red_in_center_area(&letter_img) {
- continue;
- }
- 'alphabet_loop: for c in ALPHABET1.chars() {
- if !img_contains_letter(&letter_img, c) {
- continue;
- }
- // "w" fits in "W". So if we find "W" 1 px bellow, discard "w"
- for (a, b, x, y) in vec![('w', 'W', x, y+1), ('k', 'K', x+1, y+1)] {
- if c == a {
- let one_px_down_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT);
- if img_contains_letter(&one_px_down_img, b) {
- continue 'alphabet_loop;
- }
- }
- }
- letters_set.insert(Letter::new(Point::new(x, y), c));
- break;
- }
- }
- }
- if letters_set.len() != NB_CHARS as usize {
- return Err(CaptchaErr(format!("did not find exactly 5 letters {}", letters_set.len())));
- }
- Ok(letters_set)
-}
-
-fn get_starting_letter<'a>(img: &DynamicImage, letters_set: &'a HashSet<Letter>) -> Option<&'a Letter> {
- const MIN_STARTING_PT_RED_PX: usize = 50;
- for letter in letters_set.iter() {
- let square = img.crop_imm(letter.offset.x-5, letter.offset.y-3, LETTER_WIDTH+5+6, LETTER_HEIGHT+3+2);
- let count_red = count_red_px(&square);
- if count_red > MIN_STARTING_PT_RED_PX {
- return Some(letter);
- }
- }
- None
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-struct Point {
- x: u32,
- y: u32,
-}
-
-impl Point {
- fn new(x: u32, y: u32) -> Self {
- Self{x, y}
- }
-}
-
-impl From<Point> for bresenham::Point {
- fn from(value: Point) -> Self {
- (value.x as isize, value.y as isize)
- }
-}
-
-// give an image and a valid letter image, return either or not the letter is in that image.
-fn img_contains_letter(img: &DynamicImage, c: char) -> bool {
- let letter_img = get_letter_img(c);
- if letter_img.dimensions() != img.dimensions() {
- return false;
- }
- for y in 0..LETTER_HEIGHT {
- for x in 0..LETTER_WIDTH {
- let good_letter_color = letter_img.get_pixel(x, y);
- let letter_img_color = img.get_pixel(x, y);
- // If we find an Off pixel where it's supposed to be On, skip that letter
- if is_on(good_letter_color) && !is_on(letter_img_color) {
- return false;
- }
- }
- }
- true
-}
-
-fn is_on(c: Rgba<u8>) -> bool {
- c == *ON_COLOR || c == *RED_COLOR
-}
-
-fn is_red(c: Rgba<u8>) -> bool {
- c == *RED_COLOR
-}
-
-fn has_red_in_center_area(letter_img: &DynamicImage) -> bool {
- letter_img.view(LETTER_WIDTH/2 - 1, LETTER_HEIGHT/2 - 1, 2, 2)
- .pixels()
- .any(|(_, _, c)| is_red(c))
-}
-
-// Count pixels that are On (either white or red)
-fn count_px_on(img: &DynamicImage) -> usize {
- img.pixels()
- .filter(|(_, _, c)| is_on(*c))
- .count()
-}
-
-// Count pixels that are red
-fn count_red_px(img: &DynamicImage) -> usize {
- img.pixels()
- .filter(|(_, _, c)| is_red(*c))
- .count()
-}
-\ No newline at end of file
+pub mod captcha;
+\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
@@ -1,5 +1,6 @@
mod util;
mod lechatphp;
+mod bhc;
use log;
use log::LevelFilter;
@@ -849,7 +850,7 @@ impl LeChatPHPClient {
io::stdin().read_line(&mut captcha_input).unwrap();
trim_newline(&mut captcha_input);
} else {
- match lechatphp::solve_b64(captcha_img) {
+ match lechatphp::captcha::solve_b64(captcha_img) {
Some(answer) => captcha_input = answer,
None => return Err(LoginErr::CaptchaFailedSolveErr),
}