├── pulse.ico ├── Cargo.toml ├── README.md └── src └── main.rs /pulse.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eonian-Sharp/pulse/HEAD/pulse.ico -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pulse" 3 | version = "1.0.1" 4 | edition = "2021" 5 | 6 | build = "build.rs" 7 | 8 | [dependencies] 9 | reqwest = { version = "0.12"} 10 | tokio = { version = "1", features = ["full"]} 11 | regex = "1.5" 12 | futures = "0.3" 13 | colored = "3.0.0" 14 | csv = "1.1" 15 | structopt = "0.3.26" 16 | anyhow = "1.0.86" 17 | encoding = "0.2" 18 | encoding_rs = "0.8.34" 19 | chrono = "0.4.38" 20 | rand = "0.9.0" 21 | 22 | [build-dependencies] 23 | winres = "0.1" 24 | 25 | [profile.release] 26 | opt-level = "z" # 二进制文件大小优化 27 | strip = true # 自动从二进制文件去除符号信息. 28 | lto = true 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pulse 2 | 3 | `pulse` is a powerful web scanning and fuzzing tool written in Rust. It allows you to perform concurrent web requests, directory fuzzing, and custom regex matching on the responses. 4 | 5 | ![image](https://github.com/user-attachments/assets/4c4b9469-8ff6-49eb-b202-63f8c3fe1768) 6 | 7 | 8 | ## Features 9 | 10 | - Concurrent web requests with configurable thread count 11 | - Fuzzing with wordlists for URLs or IP addresses 12 | - Directory scanning with built-in or custom directory lists 13 | - Custom regex matching on response bodies 14 | - Filtering responses based on status codes 15 | - Silent mode for displaying only successful URLs 16 | - CSV output for easy analysis and reporting 17 | - Timeout configuration for web requests 18 | - Colored output for better visibility 19 | 20 | ## Installation 21 | 22 | To install `pulse`, you need to have Rust and Cargo installed on your system. You can install Rust by following the instructions on the official Rust website: [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) 23 | 24 | Once you have Rust and Cargo installed, you can clone the `pulse` repository and build the project: 25 | 26 | ```bash 27 | git clone https://github.com/Eonian-Sharp/pulse.git 28 | cd pulse 29 | cargo build --release 30 | ``` 31 | 32 | 33 | ### Usage 34 | 35 | ```bash 36 | pulse [OPTIONS] --input 37 | ``` 38 | 39 | Options 40 | 41 | ``` 42 | -i, --input 43 | ``` 44 | Input IP or URL with FUZZ marker 45 | ``` 46 | -t, --threads 47 | ``` 48 | 49 | Number of concurrent requests (default: 50) 50 | ``` 51 | -o, --output 52 | ``` 53 | 54 | Output CSV file name (default: output.csv) 55 | ``` 56 | -r, --regex 57 | ``` 58 | 59 | Enable regex matching 60 | ``` 61 | -w, --wordlist 62 | ``` 63 | 64 | Wordlist for fuzzing 65 | 66 | ``` 67 | -T, --timeout 68 | ``` 69 | 70 | Set timeout (default: 10 seconds) 71 | ``` 72 | -m, --custom-matches 73 | ``` 74 | 75 | Custom regex for matching in response body 76 | ``` 77 | -s, --show-code 78 | ``` 79 | 80 | Only show responses with these status codes 81 | ``` 82 | -b, --ban-code 83 | ``` 84 | 85 | Do not show responses with these status codes (default: 401,404) 86 | ``` 87 | --silent 88 | ``` 89 | 90 | Silent mode: only output successful URLs 91 | ``` 92 | -d, --dir 93 | ``` 94 | 95 | Directories to scan, comma separated 96 | ``` 97 | -D, --dir-path 98 | ``` 99 | File containing directories to scan 100 | 101 | Examples 102 | Scan a single URL: 103 | ``` 104 | pulse --input http://eoniansharp.com 105 | ``` 106 | 107 | Scan a list of IPs from a file: 108 | ``` 109 | pulse --input ips.txt 110 | ``` 111 | 112 | Perform fuzzing with a wordlist: 113 | ``` 114 | pulse --input http://eoniansharp.com/FUZZ --wordlist wordlist.txt 115 | ``` 116 | 117 | Scan with custom directories and regex matching: 118 | ``` 119 | pulse --input https://eoniansharp.com/admin --dir /admin,/secret --regex --custom-matches "password|email" 120 | ``` 121 | 122 | 123 | ## License 124 | MIT License 125 | 126 | Copyright (c) [year] [fullname] 127 | 128 | Permission is hereby granted, free of charge, to any person obtaining a copy 129 | of this software and associated documentation files (the "Software"), to deal 130 | in the Software without restriction, including without limitation the rights 131 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 132 | copies of the Software, and to permit persons to whom the Software is 133 | furnished to do so, subject to the following conditions: 134 | 135 | The above copyright notice and this permission notice shall be included in all 136 | copies or substantial portions of the Software. 137 | 138 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 139 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 140 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 141 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 142 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 143 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 144 | SOFTWARE. 145 | 146 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use anyhow::bail; 3 | use chrono::Local; 4 | use colored::Colorize; 5 | use csv::WriterBuilder; 6 | use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT,HeaderName}; 7 | use structopt::StructOpt; 8 | use tokio::fs::File; 9 | use tokio::io::{AsyncBufReadExt, BufReader}; 10 | // use tokio::io::AsyncReadExt; 11 | use tokio::sync::Semaphore; 12 | use reqwest::{ ClientBuilder, Method}; 13 | use futures::stream::{FuturesUnordered, StreamExt}; 14 | use std::time::Instant; 15 | use std::time::Duration; 16 | use std::path::PathBuf; 17 | use std::sync::Arc; 18 | use std::thread; 19 | use regex::Regex; 20 | use rand::prelude::IndexedRandom; 21 | 22 | #[derive(Debug, StructOpt)] 23 | #[structopt(name = "pulse", about = "Red Team fast and efficient target detection tool.")] 24 | struct Opt { 25 | /// version 26 | #[structopt(short, long)] 27 | version: bool, 28 | 29 | /// Input IP or URL with FUZZ marker 30 | #[structopt(short, long, value_name = "URL or File")] 31 | input: String, 32 | 33 | /// Number of concurrent requests 34 | #[structopt(short, long, default_value = "50")] 35 | threads: usize, 36 | 37 | /// Output CSV file name 38 | #[structopt(short, long, default_value = "output.csv")] 39 | output: String, 40 | 41 | /// Enable regex matching 42 | #[structopt(short, long)] 43 | regex: bool, 44 | 45 | /// Wordlist for fuzzing 46 | #[structopt(short, long, parse(from_os_str))] 47 | wordlist: Option, 48 | 49 | /// Enable debug mode 50 | #[structopt(long)] 51 | debug: bool, 52 | 53 | /// Set Timeout 54 | #[structopt(short = "T", long, default_value = "10")] 55 | timeout: u64, 56 | 57 | /// Custom regex for matching in response body 58 | #[structopt(short = "m", long)] 59 | custom_matches: Vec, 60 | 61 | /// Only show responses with these status codes 62 | #[structopt(short = "s", long, use_delimiter = true)] 63 | show_code: Vec, 64 | 65 | /// Do not show responses with these status codes 66 | #[structopt(short = "b", long, use_delimiter = true, default_value = "401,404")] 67 | ban_code: Vec, 68 | 69 | /// Silent mode: only output successful URLs 70 | #[structopt(long)] 71 | silent: bool, 72 | 73 | /// Directories to scan, comma separated 74 | #[structopt(short = "d", long, use_delimiter = true)] 75 | dir: Vec, 76 | 77 | /// File containing directories to scan 78 | #[structopt(short = "D", long, parse(from_os_str))] 79 | dir_path: Option, 80 | 81 | /// HTTP request method (GET, POST, PUT, DELETE, etc.) 82 | #[structopt(short = "M", long, default_value = "GET")] 83 | method: String, 84 | 85 | /// Custom HTTP headers to add to the request 86 | #[structopt(short = "H", long, use_delimiter = true)] 87 | custom_headers: Vec, 88 | 89 | /// Do not display responses with this length (in characters) 90 | #[structopt(short = "L", long)] 91 | filter_length: Option, 92 | 93 | /// display responses with this length (in characters) 94 | #[structopt(short = "l", long)] 95 | match_length: Option, 96 | 97 | /// Proxy URL 98 | #[structopt(short = "p", long, value_name = "URL", default_value = "")] 99 | http_proxy: String, 100 | 101 | /// User-Agent to use 102 | #[structopt(short = "u", long, default_value = "default", possible_values = &["default", "random", "android"])] 103 | user_agent: String, 104 | 105 | /// Disable SSL certificate validation 106 | #[structopt(long)] 107 | no_ssl: bool, 108 | 109 | // /// Request body content 110 | // #[structopt(short = "B", long, value_name = "BODY")] 111 | // body_content: Option, 112 | } 113 | 114 | 115 | fn logo(){ 116 | let now = Local::now(); 117 | let version = "1.0.1"; // 2024/7/29 118 | let author = "Enomothem".blue(); 119 | let pulse = "pulse".bright_red(); 120 | let live = "/".green(); 121 | let elec = "——".green(); 122 | let stime = now.format("%Y-%m-%d %H:%M:%S").to_string().white().dimmed(); 123 | 124 | // pulse 0.0.1 2024-7-20 birth 125 | let logo = format!(r#" 126 | ---- ---- 127 | /\\ ATTACK:{birth} 128 | {pulse} {version} / \\ 129 | ______________/ \\ {live}\\__{elec}_{elec}_{elec}_{elec}_{elec}_{elec}_{elec}_{elec}_{elec}_{elec}_ 130 | \\ {live} 131 | \\{live} by {author} 132 | ---- ---- 133 | "#, version = version, author = author, live = live, pulse = pulse, birth = stime, elec = elec); 134 | 135 | println!("{}", logo.red()); 136 | } 137 | 138 | fn end(){ 139 | let logo = format!(r#"__________________________________________________________"#); 140 | println!("{}", logo.red()); 141 | } 142 | 143 | fn version(){ 144 | logo(); 145 | thread::sleep(Duration::from_secs(1)); 146 | println!("{} {}","[*]".cyan(), "pulse 0.0.1 2024-7-20 2:00"); 147 | thread::sleep(Duration::from_secs(1)); 148 | println!("{} {}","[*]".cyan(), "pulse 1.0.0 2024-7-24 Github Open Source."); 149 | thread::sleep(Duration::from_secs(1)); 150 | println!("{} {}","[+]".green(), "pulse 1.0.1 2024-7-29 Add Proxy."); 151 | println!("{} {}","[*]".cyan(), "Enjoy it!"); 152 | } 153 | 154 | 155 | #[tokio::main] 156 | async fn main() -> Result<()> { 157 | 158 | 159 | let start = Instant::now(); 160 | let opt = Opt::from_args(); 161 | if opt.debug { 162 | println!("{:?}", opt); 163 | } 164 | if opt.version { 165 | version(); 166 | return Ok(()); 167 | } 168 | 169 | if !opt.silent { 170 | logo(); 171 | let separate = "|".bright_yellow(); 172 | if !opt.http_proxy.is_empty() { 173 | println!("{} 💥 {} {} ⏳ {} {} 📌 {} {} 🌐 {}", "[*]".cyan(), opt.threads, separate , opt.timeout, separate, opt.input.bright_cyan(), separate, opt.http_proxy) 174 | }else{ 175 | println!("{} 💥 {} {} ⏳ {} {} 📌 {} ", "[*]".cyan(), opt.threads, separate, opt.timeout, separate, opt.input.bright_cyan()) 176 | } 177 | } 178 | 179 | // 解析自定义请求头 180 | let mut headers = HeaderMap::new(); 181 | for header_str in &opt.custom_headers { 182 | let parts: Vec<&str> = header_str.splitn(2, ':').collect(); 183 | if parts.len() != 2 { 184 | eprintln!("Invalid custom header format: {}", header_str); 185 | continue; 186 | } 187 | let key = parts[0].trim(); 188 | let value = parts[1].trim(); 189 | let header_name = HeaderName::from_bytes(key.as_bytes()).unwrap(); 190 | headers.insert(header_name, HeaderValue::from_str(value).unwrap()); 191 | } 192 | 193 | // Create a CSV writer 194 | let wtr = Arc::new(tokio::sync::Mutex::new(WriterBuilder::new().from_path(&opt.output)?)); 195 | { 196 | let mut wtr = wtr.lock().await; 197 | wtr.write_record(&["URL/IP", "Status Code", "Length", "Title"])?; 198 | } 199 | 200 | 201 | // Create HTTP client with timeout and connection pool settings 202 | let client = if !opt.http_proxy.clone().is_empty() { 203 | let pro = reqwest::Proxy::all(opt.http_proxy).expect("Failed to create proxy"); 204 | ClientBuilder::new() 205 | .proxy(pro) 206 | .timeout(Duration::from_secs(opt.timeout)) 207 | .pool_max_idle_per_host(10) 208 | .danger_accept_invalid_certs(opt.no_ssl) 209 | .build()? 210 | } else { 211 | ClientBuilder::new() 212 | .timeout(Duration::from_secs(opt.timeout)) 213 | .pool_max_idle_per_host(10) 214 | .danger_accept_invalid_certs(opt.no_ssl) 215 | .build()? 216 | }; 217 | 218 | // Concurrent request semaphore 219 | let semaphore = Arc::new(Semaphore::new(opt.threads)); 220 | 221 | // Determine input mode and read IPs/URLs 222 | let inputs = if opt.input.contains("FUZZ") { 223 | if let Some(wordlist) = &opt.wordlist { 224 | let file = File::open(wordlist).await?; 225 | let reader = BufReader::new(file); 226 | let mut words = Vec::new(); 227 | let mut lines = reader.lines(); 228 | while let Some(line) = lines.next_line().await? { 229 | words.push(line); // 保存原始行,不添加协议头 230 | } 231 | 232 | // 在FUZZ替换之后添加协议头 233 | words.into_iter().map(|word| { 234 | let mut fuzzed_url = opt.input.replace("FUZZ", &word.trim()); 235 | if !fuzzed_url.starts_with("http://") && !fuzzed_url.starts_with("https://") { 236 | fuzzed_url = format!("http://{}", fuzzed_url); 237 | } 238 | fuzzed_url 239 | }).collect::>() 240 | } else { 241 | let mut input_url = opt.input.clone(); 242 | if !input_url.starts_with("http://") && !input_url.starts_with("https://") { 243 | input_url = format!("http://{}", input_url); 244 | } 245 | vec![input_url] 246 | } 247 | } else if let Ok(file) = File::open(&opt.input).await { 248 | let reader = BufReader::new(file); 249 | let mut ips = Vec::new(); 250 | let mut lines = reader.lines(); 251 | while let Some(line) = lines.next_line().await? { 252 | let url = line.trim(); 253 | if !url.starts_with("http://") && !url.starts_with("https://") { 254 | ips.push(format!("http://{}", url)); 255 | } else { 256 | ips.push(url.to_string()); 257 | } 258 | } 259 | ips 260 | } else { 261 | let mut input_url = opt.input.clone(); 262 | if !input_url.starts_with("http://") && !input_url.starts_with("https://") { 263 | input_url = format!("http://{}", input_url); 264 | } 265 | vec![input_url] 266 | }; 267 | 268 | // Read directories from dir and dir_path 269 | let mut directories = opt.dir.clone(); 270 | if let Some(dir_path) = &opt.dir_path { 271 | let file = File::open(dir_path).await?; 272 | let reader = BufReader::new(file); 273 | let mut lines = reader.lines(); 274 | while let Some(line) = lines.next_line().await? { 275 | directories.push(line); 276 | } 277 | } 278 | 279 | // Prepare URLs with directories 280 | let inputs_with_dirs = if directories.is_empty() { 281 | inputs.clone() 282 | } else { 283 | inputs.into_iter().flat_map(|url| { 284 | directories.iter().map(move |dir| format!("{}{}", url, dir)) 285 | }).collect::>() 286 | }; 287 | 288 | // Prepare custom regex patterns 289 | let custom_regexes: Vec<(Regex, String)> = opt.custom_matches.iter().enumerate().map(|(i, pattern)| { 290 | let regex = Regex::new(pattern).unwrap(); 291 | (regex, format!("Regex {}", i + 1)) 292 | }).collect(); 293 | 294 | let regex_results = Arc::new(tokio::sync::Mutex::new(Vec::new())); 295 | let custom_matches = Arc::new(tokio::sync::Mutex::new(Vec::new())); 296 | 297 | 298 | // Convert method string to reqwest Method 299 | let method = match opt.method.to_uppercase().as_str() { 300 | "GET" => Method::GET, 301 | "POST" => Method::POST, 302 | "PUT" => Method::PUT, 303 | "DELETE" => Method::DELETE, 304 | "OPTION" => Method::OPTIONS, 305 | "HEAD" => Method::HEAD, 306 | "PATCH" => Method::PATCH, 307 | "TRACE" => Method::TRACE, 308 | "CONNECT" => Method::CONNECT, 309 | _ => bail!("Unsupported HTTP method: {}", opt.method), 310 | }; 311 | 312 | let custom_headers = headers.clone(); 313 | let http_uas: Vec<&str> = vec![ 314 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 315 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 316 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 317 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0", 318 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 319 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 320 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0", 321 | "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 322 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 323 | ]; 324 | 325 | let android_uas: Vec<&str> = vec![ 326 | "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Mobile Safari/537.36", 327 | "Mozilla/5.0 (Linux; Android 9; SM-G960F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.136 Mobile Safari/537.36", 328 | "Mozilla/5.0 (Linux; Android 8.0.0; SM-G950F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Mobile Safari/537.36", 329 | ]; 330 | 331 | 332 | // let raw_body = if let Some(raw_path) = opt.raw.clone() { 333 | // let mut file = File::open(raw_path).await?; 334 | // let mut contents = Vec::new(); 335 | // file.read_to_end(&mut contents).await?; 336 | // Some(contents) 337 | // } else { 338 | // None 339 | // }; 340 | 341 | // let body_content = opt.body_content.unwrap_or_default(); 342 | 343 | let futures: FuturesUnordered<_> = inputs_with_dirs.into_iter().map(|url| { 344 | let client = client.clone(); 345 | let semaphore = semaphore.clone(); 346 | let wtr = wtr.clone(); 347 | let regex_results = regex_results.clone(); 348 | let custom_matches = custom_matches.clone(); 349 | let regex_enabled = opt.regex; 350 | let custom_regexes = custom_regexes.clone(); 351 | let show_code = opt.show_code.clone(); 352 | let ban_code = opt.ban_code.clone(); 353 | let silent = opt.silent; 354 | let method = method.clone(); 355 | let custom_headers = custom_headers.clone(); // 使用解析后的 headers 356 | let ua_option = opt.user_agent.clone(); 357 | let http_uas_clone = http_uas.clone(); 358 | let android_uas_clone = android_uas.clone(); 359 | // let body_content_clone = body_content.clone(); 360 | // let raw_body_clone = raw_body.clone(); 361 | 362 | tokio::spawn(async move { 363 | let permit = semaphore.acquire_owned().await; 364 | 365 | // Determine User-Agent 366 | let ua: &str = match ua_option.as_str() { 367 | "random" => { 368 | let mut rng = rand::rng(); 369 | http_uas_clone.choose(&mut rng).expect("No User-Agents available") 370 | }, 371 | "android" => { 372 | let mut rng = rand::rng(); 373 | android_uas_clone.choose(&mut rng).expect("No Android User-Agents available") 374 | }, 375 | _ => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", // default UA 376 | }; 377 | 378 | let mut request = client.request(method.clone(), &url); 379 | 380 | // Add custom headers 381 | for (key, value) in custom_headers.iter() { 382 | request = request.header(key, value); 383 | } 384 | 385 | // // Set raw body if provided 386 | // if let Some(body) = raw_body_clone.clone() { 387 | // request = request.body(body); 388 | // } 389 | 390 | let mut headers_ua = HeaderMap::new(); 391 | headers_ua.insert(USER_AGENT, HeaderValue::from_str(ua).unwrap()); 392 | request = request.headers(headers_ua); 393 | 394 | let result = async { 395 | // match request(method.clone(), &url).send().await { 396 | match request.send().await { 397 | Ok(response) => { 398 | let stat = response.status(); 399 | let stat_code = stat.as_u16(); 400 | 401 | if (!show_code.is_empty() && !show_code.contains(&stat_code)) || ban_code.contains(&stat_code) { 402 | return Ok::<(), anyhow::Error>(()); 403 | } 404 | 405 | let stat_colored = match stat_code { 406 | 200..=299 => stat_code.to_string().green(), 407 | 300..=399 => stat_code.to_string().yellow(), 408 | 400..=499 => stat_code.to_string().red(), 409 | 500..=599 => stat_code.to_string().color("blue"), 410 | _ => stat.to_string().white(), 411 | }; 412 | 413 | let text = response.text().await?; 414 | let len = text.len(); 415 | 416 | 417 | let re_title = Regex::new(r"(.*?)").unwrap(); 418 | let title = if let Some(captures) = re_title.captures(&text) { 419 | captures.get(1).unwrap().as_str().to_string() 420 | } else { 421 | "No title".to_string() 422 | }; 423 | 424 | // 长度检查 425 | if let Some(filter_length) = opt.filter_length { 426 | if len == filter_length { 427 | return Ok(()); // 不显示该长度的URL = 排除法 428 | } 429 | } 430 | if let Some(match_length) = opt.match_length { 431 | if len != match_length { 432 | return Ok(()); // 不显示除此以外的URL = 保留法 433 | } 434 | } 435 | 436 | let len = len.to_string(); 437 | if !silent { 438 | let ok = "[+]".green(); 439 | println!("{} {:55} [{}] - {:8} [{}]", ok, url.bright_magenta(), stat_colored, len.blue(), title.cyan()); 440 | } else if !show_code.is_empty() { 441 | println!("{}", url); 442 | } else if (200..=299).contains(&stat_code) { 443 | println!("{}", url); 444 | } 445 | 446 | { 447 | 448 | let mut wtr = wtr.lock().await; 449 | wtr.write_record(&[url.clone(), stat_code.to_string(), len.clone(), title.clone()]) 450 | .context("Failed to write record to CSV")?; 451 | 452 | } 453 | 454 | if regex_enabled { 455 | let regexs = vec![ 456 | (Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap(), "Email"), 457 | (Regex::new(r#"https?://[^\s"]+"#).unwrap(), "URL"), 458 | (Regex::new(r#"(?i)\b(?:/[^\s+<;>"]*)+\b"#).unwrap(), "Path"), 459 | (Regex::new(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}").unwrap(), "IP"), 460 | (Regex::new(r"\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b").unwrap(), "Domain"), 461 | (Regex::new(r"\b\w{6,}\b").unwrap(), "Token"), 462 | ]; 463 | let mut regex_results_lock = regex_results.lock().await; 464 | for (regex, label) in ®exs { 465 | for captures in regex.captures_iter(&text) { 466 | if let Some(match_str) = captures.get(1) { 467 | regex_results_lock.push(format!("URL: {}, [{}]: [{}]", url, label, match_str.as_str())); 468 | if !silent { 469 | println!(" [{}]: [{}]", label.cyan(), match_str.as_str()); 470 | } 471 | } else { 472 | regex_results_lock.push(format!("URL: {}, [{}]: [{}]", url, label, captures.get(0).unwrap().as_str())); 473 | if !silent { 474 | println!(" [{}]: [{}]", label.cyan(), captures.get(0).unwrap().as_str()); 475 | } 476 | } 477 | } 478 | } 479 | } 480 | 481 | { 482 | let mut custom_matches_lock = custom_matches.lock().await; 483 | for (regex, label) in &custom_regexes { 484 | for captures in regex.captures_iter(&text) { 485 | if let Some(match_str) = captures.get(0) { 486 | custom_matches_lock.push(format!("URL: {}, [{}]: [{}]", url, label, match_str.as_str())); 487 | if !silent { 488 | println!(" [{}]: [{}]", label.bright_red().bold(), match_str.as_str().to_string().bright_blue()); 489 | } 490 | } 491 | } 492 | } 493 | } 494 | } 495 | Err(_) => (), 496 | } 497 | Ok::<(), anyhow::Error>(()) 498 | }.await; 499 | 500 | drop(permit); 501 | result 502 | }) 503 | }).collect(); 504 | 505 | // Wait for all concurrent tasks to complete 506 | futures.for_each(|_| async {}).await; 507 | let duration = start.elapsed(); 508 | if !opt.silent { 509 | let now = Local::now(); 510 | let etime = now.format("%Y-%m-%d %H:%M:%S").to_string().white().dimmed(); 511 | end(); 512 | println!("{} 🎉 {:?}{:>23}{}", "[!]".cyan(), duration, "END:".cyan(), etime) 513 | } 514 | Ok(()) 515 | 516 | } 517 | --------------------------------------------------------------------------------