├── src ├── scan │ ├── mod.rs │ └── http_scan.rs ├── utils │ ├── mod.rs │ └── favicon.rs ├── cli │ ├── mod.rs │ └── cli_options.rs ├── runner │ ├── mod.rs │ └── executor.rs ├── input │ ├── mod.rs │ ├── cidr.rs │ └── url.rs ├── http │ ├── mod.rs │ ├── request.rs │ ├── client.rs │ └── response.rs ├── fingerprint │ ├── mod.rs │ ├── loader.rs │ ├── model.rs │ └── matcher.rs ├── lib.rs ├── output │ ├── json.rs │ ├── csv.rs │ └── mod.rs └── main.rs ├── .gitignore ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── release.yml └── README.md /src/scan/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http_scan; 2 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod favicon; 2 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cli_options; 2 | -------------------------------------------------------------------------------- /src/runner/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod executor; 2 | -------------------------------------------------------------------------------- /src/input/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cidr; 2 | pub mod url; 3 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod request; 3 | pub mod response; 4 | -------------------------------------------------------------------------------- /src/fingerprint/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod loader; 2 | pub mod matcher; 3 | pub mod model; 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // 调用的模块 2 | pub mod cli; 3 | pub mod fingerprint; 4 | pub mod http; 5 | pub mod input; 6 | pub mod output; 7 | pub mod runner; 8 | pub mod scan; 9 | pub mod utils; 10 | -------------------------------------------------------------------------------- /src/fingerprint/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::fingerprint::model::Fingerprint; 2 | use std::error::Error; 3 | 4 | pub fn load_fingerprints_from_str(json: &str) -> Result, Box> { 5 | let configs: Vec = serde_json::from_str(json)?; 6 | 7 | let mut result = Vec::with_capacity(configs.len()); 8 | 9 | for cfg in configs { 10 | let fp = 11 | Fingerprint::try_from(cfg).map_err(|e| format!("fingerprint parse error: {}", e))?; 12 | result.push(fp); 13 | } 14 | 15 | Ok(result) 16 | } 17 | -------------------------------------------------------------------------------- /src/output/json.rs: -------------------------------------------------------------------------------- 1 | use crate::output::{Output, SaveInfo}; 2 | use std::fs::File; 3 | 4 | pub struct JsonOutput { 5 | pub file_path: String, 6 | } 7 | impl JsonOutput { 8 | pub fn new(file_path: String) -> Self { 9 | JsonOutput { file_path } 10 | } 11 | } 12 | 13 | impl Output for JsonOutput { 14 | fn write(&self, results: Vec) { 15 | let file = File::create(&self.file_path).expect("Failed to create JSON file"); 16 | serde_json::to_writer_pretty(file, &results).expect("Failed to write JSON data"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/fingerprint/model.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Clone, Copy, Deserialize)] 4 | #[serde(rename_all = "lowercase")] 5 | pub enum MatchMethod { 6 | Keyword, 7 | Faviconhash, 8 | } 9 | #[derive(Debug, Clone, Copy, Deserialize)] 10 | #[serde(rename_all = "lowercase")] 11 | pub enum MatchLocation { 12 | Header, 13 | Body, 14 | } 15 | 16 | #[derive(Debug, Deserialize)] 17 | pub struct Fingerprint { 18 | pub cms: String, 19 | pub method: MatchMethod, 20 | pub location: MatchLocation, 21 | pub keyword: Vec, 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | 21 | 22 | # Added by cargo 23 | # 24 | # already existing elements were commented out 25 | 26 | #/target 27 | -------------------------------------------------------------------------------- /src/http/request.rs: -------------------------------------------------------------------------------- 1 | use crate::http::response::ResponseInfo; 2 | use crate::utils::favicon::fetch_favicon_hash; 3 | use reqwest::Client; 4 | 5 | pub async fn fetch( 6 | client: &Client, 7 | original_url: &str, 8 | status_code: &[u16], 9 | ) -> Result> { 10 | let resp = client.get(original_url).send().await?; 11 | 12 | // 这块过滤状态码 13 | if status_code.contains(&resp.status().as_u16()) || status_code.is_empty() { 14 | let favicon_hash = fetch_favicon_hash(client, original_url).await; 15 | ResponseInfo::from_response(original_url, resp, favicon_hash).await 16 | } else { 17 | Err("filtered by status code".into()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "windfire" 3 | version = "0.4.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | clap = { version = "4.5.48", features = ["derive"] } 8 | reqwest = { version = "0.12.23", features = ["socks"] } 9 | serde = { version = "1.0.228", features = ["derive"] } 10 | tokio = { version = "1.47.1", features = ["full"] } 11 | regex = "1.11.3" 12 | base64 = "0.22.1" 13 | murmur3 = "0.5.2" 14 | serde_json = "1.0.145" 15 | once_cell = "1.21.3" 16 | futures = "0.3.31" 17 | csv = "1.3.1" 18 | url = "2.5.7" 19 | 20 | [profile.release] 21 | lto = true # 启用链路时间优化 22 | opt-level = "z" # 针对规模进行优化 23 | codegen-units = 1 # 减少并行代码生成单元以提高优化 24 | debug = false # 禁用调试信息生成,从而减小最终二进制文件的大小 25 | strip = true # 删除编译生成的二进制文件中的调试信息和符号 26 | panic = "abort" 27 | -------------------------------------------------------------------------------- /src/fingerprint/matcher.rs: -------------------------------------------------------------------------------- 1 | use crate::fingerprint::model::{Fingerprint, MatchLocation, MatchMethod}; 2 | use crate::http::response::ResponseInfo; 3 | 4 | impl Fingerprint { 5 | // 指纹匹配 6 | pub fn matches(&self, resp: &ResponseInfo) -> bool { 7 | match self.method { 8 | MatchMethod::Faviconhash => resp 9 | .favicon_hash 10 | .as_ref() 11 | .is_some_and(|hash| self.keyword.iter().all(|k| k == hash)), 12 | 13 | MatchMethod::Keyword => { 14 | let target = match self.location { 15 | MatchLocation::Header => &resp.headers, 16 | MatchLocation::Body => &resp.body, 17 | }; 18 | self.keyword.iter().all(|k| target.contains(k)) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/output/csv.rs: -------------------------------------------------------------------------------- 1 | use crate::output::{Output, SaveInfo}; 2 | use csv::WriterBuilder; 3 | use std::fs::File; 4 | use std::io::Write; 5 | 6 | pub struct CsvOutput { 7 | pub file_path: String, 8 | } 9 | impl CsvOutput { 10 | pub fn new(file_path: String) -> CsvOutput { 11 | CsvOutput { file_path } 12 | } 13 | } 14 | impl Output for CsvOutput { 15 | fn write(&self, results: Vec) { 16 | let mut file = File::create(&self.file_path).expect("Failed to create CSV file"); 17 | file.write_all(b"\xEF\xBB\xBF") 18 | .expect("Failed to write BOM"); 19 | 20 | let mut wtr = WriterBuilder::new().from_writer(file); 21 | 22 | for record in results { 23 | if let Err(e) = wtr.serialize(record) { 24 | panic!("Error writing record: {}", e); 25 | } 26 | } 27 | 28 | wtr.flush().expect("Failed to flush CSV writer"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/output/mod.rs: -------------------------------------------------------------------------------- 1 | mod csv; 2 | mod json; 3 | 4 | use crate::output::csv::CsvOutput; 5 | use crate::output::json::JsonOutput; 6 | use serde::Serialize; 7 | 8 | #[derive(Debug, Serialize, Clone)] 9 | pub struct SaveInfo { 10 | pub url: String, 11 | pub status: u16, 12 | pub title: String, 13 | pub server: String, 14 | pub jump_url: String, // 跳转后的url 15 | pub content_length: usize, 16 | pub cms: String, 17 | } 18 | 19 | pub trait Output { 20 | fn write(&self, results: Vec); 21 | } 22 | 23 | pub fn output_results(results: Vec, output: Option) { 24 | if let Some(output) = output { 25 | if output.ends_with("csv") { 26 | let csv_output = CsvOutput::new(output); 27 | csv_output.write(results); 28 | } else { 29 | let json_output = JsonOutput::new(output); 30 | json_output.write(results); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/input/cidr.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | use std::str::FromStr; 3 | 4 | pub fn is_cidr(s: &str) -> bool { 5 | let (ip, prefix) = match s.split_once('/') { 6 | Some(pair) => pair, 7 | None => return false, 8 | }; 9 | 10 | let ip_ok = Ipv4Addr::from_str(ip).is_ok(); 11 | let prefix_ok = prefix.parse::().map(|p| p <= 32).unwrap_or(false); 12 | 13 | ip_ok && prefix_ok 14 | } 15 | 16 | pub fn expand_cidr(cidr: &str) -> Vec { 17 | let (base_ip, prefix) = cidr.split_once('/').unwrap(); 18 | let prefix: u32 = prefix.parse().unwrap(); 19 | 20 | let base_ip: Ipv4Addr = base_ip.parse().unwrap(); 21 | let base_u32 = u32::from(base_ip); 22 | 23 | let mask = if prefix == 0 { 24 | 0 25 | } else { 26 | u32::MAX << (32 - prefix) 27 | }; 28 | 29 | let network = base_u32 & mask; 30 | let broadcast = network | !mask; 31 | 32 | (network..=broadcast) 33 | .map(|ip| Ipv4Addr::from(ip).to_string()) 34 | .collect() 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 muddlelife 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::sync::Arc; 3 | use windfire::cli::cli_options::Args; 4 | use windfire::fingerprint::loader::load_fingerprints_from_str; 5 | use windfire::http::client::create_http_client; 6 | use windfire::input::url::build_urls; 7 | use windfire::output::output_results; 8 | use windfire::runner::executor::run; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<(), Box> { 12 | let args = Args::parse(); 13 | 14 | // 生成url列表 15 | let urls = build_urls(&args)?; 16 | if urls.is_empty() { 17 | println!("No URLs found."); 18 | return Ok(()); 19 | } 20 | // 加载指纹库 21 | let finger_json = include_str!("finger.json"); 22 | let fingerprints = load_fingerprints_from_str(finger_json)?; 23 | let fingerprints = Arc::new(fingerprints); 24 | 25 | // 创建http client 26 | let client = create_http_client(args.timeout, args.proxy)?; 27 | 28 | let status_code = Arc::new(args.status_code); 29 | 30 | let results = run(client, urls, fingerprints, args.thread, status_code).await; 31 | 32 | output_results(results, args.output); 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/favicon.rs: -------------------------------------------------------------------------------- 1 | use base64::Engine; 2 | use base64::engine::general_purpose::STANDARD; 3 | use murmur3::murmur3_32; 4 | use reqwest::{Client, Url}; 5 | use std::io::Cursor; 6 | 7 | pub async fn fetch_favicon_hash(client: &Client, url: &str) -> Option { 8 | let url = Url::parse(url).ok()?; 9 | url.join("favicon.ico").ok().map(|u| u.to_string()); 10 | download_and_hash(client, url.as_ref()).await 11 | } 12 | 13 | // 计算hash,私有函数 14 | async fn download_and_hash(client: &Client, url: &str) -> Option { 15 | let resp = client.get(url).send().await.ok()?; 16 | 17 | if !resp.status().is_success() { 18 | return None; 19 | } 20 | 21 | let bytes = resp.bytes().await.ok()?; 22 | let base64 = STANDARD.encode(&bytes); 23 | 24 | let with_newlines: String = base64 25 | .as_bytes() 26 | .chunks(76) // 每 76 个字符分为一组 27 | .map(|chunk| String::from_utf8_lossy(chunk)) // 转为字符串 28 | .collect::>() // 收集到 Vec 29 | .join("\n"); // 在每组之间插入换行符 30 | 31 | // 然后对进行 mmnh编码 32 | let mut cursor = Cursor::new(with_newlines + "\n"); 33 | match murmur3_32(&mut cursor, 0) { 34 | Ok(hash) => Some(hash.to_string()), 35 | Err(_) => None, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | publish: 13 | name: Publish for ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | include: 18 | - os: ubuntu-latest 19 | artifact_name: windfire 20 | asset_name: windfire-linux-amd64 21 | - os: windows-latest 22 | artifact_name: windfire.exe 23 | asset_name: windfire-windows-amd64.exe 24 | - os: macos-latest 25 | artifact_name: windfire 26 | asset_name: windfire-macos-arm64 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Build 31 | run: cargo build --release 32 | - name: Upload binaries to release 33 | uses: svenstaro/upload-release-action@v2 34 | with: 35 | repo_token: ${{ secrets.GITHUB_TOKEN }} 36 | file: target/release/${{ matrix.artifact_name }} 37 | asset_name: ${{ matrix.asset_name }} 38 | tag: ${{ github.ref }} 39 | body: | 40 | ${{ steps.read_release.outputs.RELEASE_BODY }} 41 | EXTRA_FILES: "README.md LICENSE" -------------------------------------------------------------------------------- /src/scan/http_scan.rs: -------------------------------------------------------------------------------- 1 | use crate::fingerprint::model::Fingerprint; 2 | use crate::http::request; 3 | use crate::output::SaveInfo; 4 | use reqwest::Client; 5 | use serde::Serialize; 6 | 7 | #[derive(Debug, Clone, Serialize)] 8 | pub struct ScanResult { 9 | pub url: String, 10 | pub status: u16, 11 | pub title: String, 12 | pub server: String, 13 | pub content_length: usize, 14 | pub jump_url: String, 15 | pub cms: Vec, 16 | } 17 | 18 | pub async fn scan_one( 19 | client: &Client, 20 | url: &str, 21 | fingerprints: &[Fingerprint], 22 | status_code: &[u16], 23 | ) -> Result> { 24 | let resp = request::fetch(client, url, status_code).await?; 25 | let mut matched: Vec = Vec::new(); 26 | 27 | for fp in fingerprints { 28 | if fp.matches(&resp) { 29 | matched.push(fp.cms.clone()); 30 | } 31 | } 32 | matched.sort(); 33 | matched.dedup(); 34 | 35 | Ok(SaveInfo { 36 | url: resp.url, 37 | status: resp.status, 38 | title: resp.title, 39 | server: resp.server, 40 | content_length: resp.content_length, 41 | jump_url: resp.jump_url, 42 | cms: matched.join("||"), 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/runner/executor.rs: -------------------------------------------------------------------------------- 1 | use crate::fingerprint::model::Fingerprint; 2 | use crate::output::SaveInfo; 3 | use crate::scan::http_scan::scan_one; 4 | use futures::StreamExt; 5 | use reqwest::Client; 6 | use std::sync::Arc; 7 | use tokio::sync::mpsc; 8 | 9 | pub async fn run( 10 | client: Client, 11 | urls: Vec, 12 | fingerprint: Arc>, 13 | threads: usize, 14 | status_code: Arc>, 15 | ) -> Vec { 16 | let (tx, mut rx) = mpsc::unbounded_channel(); 17 | let threads = threads.max(1); 18 | let mut results = Vec::new(); 19 | let value = tx.clone(); 20 | let scan_handle = tokio::spawn(async move { 21 | futures::stream::iter(urls) 22 | .map(|url| { 23 | let client = client.clone(); 24 | let fps = Arc::clone(&fingerprint); 25 | let status_code = Arc::clone(&status_code); 26 | let tx = value.clone(); 27 | 28 | async move { 29 | match scan_one(&client, &url, &fps, &status_code).await { 30 | Ok(result) => { 31 | let _ = tx.send(result.clone()); 32 | Some(result) 33 | } 34 | Err(_) => None, 35 | } 36 | } 37 | }) 38 | .buffer_unordered(threads) 39 | .collect::>() 40 | .await 41 | }); 42 | 43 | drop(tx); 44 | 45 | while let Some(save_info) = rx.recv().await { 46 | println!( 47 | "{} [{}] [{}] [{}] [{}] [{}]", 48 | save_info.url, 49 | save_info.status, 50 | save_info.title, 51 | save_info.server, 52 | save_info.content_length, 53 | save_info.cms, 54 | ); 55 | results.push(save_info); 56 | } 57 | 58 | let _ = scan_handle.await; 59 | results 60 | } 61 | -------------------------------------------------------------------------------- /src/http/client.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::HeaderMap; 2 | use reqwest::{Client, Proxy, header}; 3 | use std::time::Duration; 4 | 5 | // 创建 http header头 6 | fn create_http_header() -> HeaderMap { 7 | let mut headers = HeaderMap::new(); 8 | 9 | headers.insert( 10 | header::USER_AGENT, 11 | header::HeaderValue::from_static( 12 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0", 13 | ), 14 | ); 15 | 16 | headers.insert( 17 | header::ACCEPT, 18 | header::HeaderValue::from_static( 19 | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 20 | ), 21 | ); 22 | headers.insert( 23 | header::CACHE_CONTROL, 24 | header::HeaderValue::from_static("max-age=0"), 25 | ); 26 | headers.insert(header::DNT, header::HeaderValue::from_static("1")); 27 | headers.insert( 28 | header::UPGRADE_INSECURE_REQUESTS, 29 | header::HeaderValue::from_static("1"), 30 | ); 31 | headers.insert( 32 | header::CONNECTION, 33 | header::HeaderValue::from_static("close"), 34 | ); 35 | headers.insert( 36 | header::ACCEPT_LANGUAGE, 37 | header::HeaderValue::from_static("en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"), 38 | ); 39 | // shiro 40 | headers.insert( 41 | header::COOKIE, 42 | header::HeaderValue::from_static("rememberMe=yyds"), 43 | ); 44 | 45 | headers 46 | } 47 | 48 | // 创建 http 客户端 49 | pub fn create_http_client( 50 | timeout: u64, 51 | proxy: Option, 52 | ) -> Result> { 53 | let mut builder = Client::builder() 54 | .timeout(Duration::from_secs(timeout)) 55 | .danger_accept_invalid_certs(true) 56 | .default_headers(create_http_header()); 57 | 58 | // 只有当 proxy 有值时才添加配置 59 | if let Some(proxy_url) = proxy { 60 | let p = Proxy::all(proxy_url)?; 61 | builder = builder.proxy(p); 62 | } 63 | Ok(builder.build()?) 64 | } 65 | -------------------------------------------------------------------------------- /src/http/response.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use regex::Regex; 3 | use reqwest::Response; 4 | 5 | static TITLE_REGEX: Lazy = 6 | Lazy::new(|| Regex::new(r"(?is)]*>(.*?)").unwrap()); 7 | #[derive(Debug, Clone)] 8 | pub struct ResponseInfo { 9 | pub url: String, 10 | pub status: u16, 11 | pub title: String, 12 | pub server: String, 13 | pub content_length: usize, 14 | pub headers: String, 15 | pub jump_url: String, 16 | pub body: String, 17 | pub favicon_hash: Option, 18 | } 19 | impl ResponseInfo { 20 | pub async fn from_response( 21 | original_url: &str, 22 | resp: Response, 23 | favicon_hash: Option, 24 | ) -> Result> { 25 | let status = resp.status().as_u16(); 26 | let url = original_url.to_string(); 27 | let jump_url = resp.url().to_string(); 28 | 29 | let server = resp 30 | .headers() 31 | .get("Server") 32 | .and_then(|v| v.to_str().ok()) 33 | .unwrap_or("") 34 | .to_string(); 35 | 36 | let header = resp 37 | .headers() 38 | .iter() 39 | .map(|(key, value)| format!("{}: {}", key, value.to_str().unwrap_or(""))) 40 | .collect::>() 41 | .join("\n"); 42 | 43 | let body = resp.text().await.unwrap_or_default(); 44 | let title = extract_title(&body); 45 | let content_length = body.len(); 46 | 47 | Ok(ResponseInfo { 48 | url, 49 | status, 50 | title, 51 | server, 52 | content_length, 53 | headers: header, 54 | jump_url, 55 | body, 56 | favicon_hash, 57 | }) 58 | } 59 | } 60 | 61 | // 获取title 62 | fn extract_title(body: &str) -> String { 63 | TITLE_REGEX 64 | .captures(body) 65 | .and_then(|cap| cap.get(1)) 66 | .map(|m| m.as_str().trim().to_string()) 67 | .unwrap_or_default() 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 利用Rust编写的高效URL测活工具,主要特点快速、批量、轻量,支持异步。 3 | 4 | # 功能介绍 5 | | 功能 | 描述 | 6 | |-----|-------------------------------| 7 | | 资产测活 |对目标资产(域名、IP、URL) 进行测活 | 8 | | 支持代理 |支持多种代理,包括 HTTP、HTTPS 和 SOCKS 代理 | 9 | | 状态码过滤|对 HTTP 响应的状态码进行过滤,默认只保留200 | 10 | | 指定路径 |支持指定路径进行测活,默认为根目录 | 11 | | 指纹识别 |采用开源指纹库识别网站CMS | 12 | | 高并发 |支持高并发请求、支持异步 | 13 | 14 | 15 | # 用法 16 | ## 帮助信息 17 | ```text 18 | An efficient and fast url survival detection tool 19 | 20 | Usage: windfire [OPTIONS] 21 | 22 | Options: 23 | An efficient and fast url survival detection tool 24 | 25 | Usage: windfire.exe [OPTIONS] <--url |--file > 26 | 27 | Options: 28 | -t, --thread Setting the number of threads [default: 50] 29 | -u, --url Enter a target 30 | -f, --file Enter a file path 31 | -s, --timeout The http request timeout [default: 10] 32 | -c, --status-code Display the specified status code 33 | -p, --path Designated path scan [default: ] 34 | -x, --proxy Supported Proxy socks5, http, and https, Example: -x socks5://127.0.0.1:1080 35 | -o, --output Output can be a CSV or JSON file. Example: -o result.csv or -o result.json 36 | -h, --help Print help (see more with '--help') 37 | -V, --version Print version 38 | ``` 39 | ## 参数说明 40 | * -t --thread 设置线程数量,默认50 41 | * -u --url 输入一个目标,支持单个IP地址、IP段、域名、URL、host:port形式,默认扫描80和443端口 42 | * -f --file 输入一个文件路径,文件内每行一个目标,txt文本 43 | * -s --timeout 设置http请求超时时间,默认10秒 44 | * -c --status-code 显示指定的状态码,默认全部打印;加上参数则进行筛选,可以输入多个,用逗号隔开,如200,403 45 | * -p --path 指定扫描路径,默认为空,不指定,如 -p admin 46 | * -x --proxy 支持代理,目前支持socks5,http,https,如:-x socks5://127.0.0.1:1080 47 | * -o --output 支持将扫描结果导出为csv文件或者json文件,如:-o result.csv 或者 -o result.json 48 | * -h --help 显示帮助信息 49 | * -V --version 显示版本信息 50 | 51 | ## 使用 52 | 1. 单个目标指定 53 | ```shell 54 | windfire -u https://www.baidu.com 55 | ``` 56 | 2. 批量执行目标 57 | ```shell 58 | windfire -f urls.txt 59 | ``` 60 | 3. 指定路径测活 61 | ```shell 62 | windfire -f urls.txt -p admin -c 200 63 | ``` 64 | 4. 批量执行目标,结果导出 65 | ```shell 66 | windfire -f urls.txt > result.txt 67 | ``` 68 | 5. 指定代理 69 | ```shell 70 | windfire -f urls.txt -x socks5://127.0.0.1:1080 71 | ``` 72 | 6. 批量执行,可保存为csv文件或者json格式 73 | ```shell 74 | windfire -f urls.txt -o result.csv 75 | ``` 76 | ## 默认打印信息 77 | ```shell 78 | https://www.baidu.com [200] [百度一下,你就知道] [BWS/1.1] [414219] ["CMS"] 79 | ``` 80 | 包括:起始地址(url)、状态码(status_code)、标题(title)、服务器(server)、响应页面大小(content_length)、指纹信息 -------------------------------------------------------------------------------- /src/cli/cli_options.rs: -------------------------------------------------------------------------------- 1 | // 主要做解析参数 2 | use url::Url; 3 | 4 | use clap::{ArgGroup, Parser}; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command( 8 | version = "0.0.4", 9 | about = "An efficient and fast url survival detection tool", 10 | long_about = "Efficient URL activity tester written in Rust. Fast, batch, and lightweight", 11 | group( 12 | ArgGroup::new("target") 13 | .required(true) 14 | .args(["url", "file"]), 15 | ) 16 | )] 17 | pub struct Args { 18 | /// Setting the number of threads 19 | #[arg(short, long, default_value = "50",value_parser= validate_thread_range)] 20 | pub thread: usize, 21 | 22 | /// Enter a url 23 | #[arg(short = 'u', long)] 24 | pub url: Option, 25 | 26 | /// Enter a file path 27 | #[arg(short = 'f', long)] 28 | pub file: Option, 29 | 30 | /// The http request timeout 31 | #[arg(short = 's', long, default_value = "10",value_parser = clap::value_parser!(u64).range(1..=60))] 32 | pub timeout: u64, //改为 u64 以适配 Duration::from_secs 33 | 34 | /// Display the specified status code 35 | #[arg(short = 'c', long, value_delimiter = ',')] 36 | pub status_code: Vec, // 优化状态码参数 37 | 38 | /// Designated path scan 39 | #[arg(short = 'p', long, default_value = "")] 40 | pub path: Option, 41 | 42 | /// Supported Proxy socks5, http, and https, Example: -x socks5://127.0.0.1:1080 43 | #[arg(short = 'x', long, value_parser = validate_proxy_url)] 44 | pub proxy: Option, 45 | 46 | /// Output can be a CSV or JSON file. Example: -o result.csv or -o result.json 47 | #[arg(short = 'o', long, value_parser = validate_output_format)] 48 | pub output: Option, 49 | } 50 | 51 | // 验证线程参数,不超过5000 52 | fn validate_thread_range(s: &str) -> Result { 53 | s.parse() 54 | .map_err(|_| "Invalid number of threads".to_string()) 55 | .and_then(|n| { 56 | if (1..=5000).contains(&n) { 57 | Ok(n) 58 | } else { 59 | Err("Thread count must be between 1 and 5000".to_string()) 60 | } 61 | }) 62 | } 63 | // 验证代理参数 64 | fn validate_proxy_url(s: &str) -> Result { 65 | let url = Url::parse(s).map_err(|_| format!("'{}' is not a valid URL format", s))?; 66 | 67 | match url.scheme() { 68 | "http" | "https" | "socks5" => Ok(s.to_string()), 69 | _ => Err( 70 | "Invalid proxy protocol. Supported protocols are http, https and socks5".to_string(), 71 | ), 72 | } 73 | } 74 | 75 | // 验证输出参数 76 | fn validate_output_format(s: &str) -> Result { 77 | if s.ends_with(".csv") || s.ends_with(".json") { 78 | Ok(s.to_string()) 79 | } else { 80 | Err("Invalid output format".to_string()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/input/url.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::cli_options::Args; 2 | use crate::input::cidr::{expand_cidr, is_cidr}; 3 | use std::error::Error; 4 | use std::fs::File; 5 | use std::io::{BufRead, BufReader}; 6 | 7 | pub fn build_urls(args: &Args) -> Result, Box> { 8 | let mut urls = Vec::new(); 9 | 10 | // 单个url 11 | if let Some(url) = &args.url { 12 | // 开头有http协议 13 | if url.starts_with("http://") || url.starts_with("https://") { 14 | urls.push(url.clone()); 15 | } else if is_cidr(url) { 16 | // 针对ip地址和网段 17 | let ip_vec: Vec = expand_cidr(url); 18 | for ip in ip_vec { 19 | urls.push(format!("http://{}", ip)); 20 | urls.push(format!("https://{}", ip)); 21 | } 22 | } else { 23 | // 针对ip地址加端口以及域名加端口,还得判断下是否为80端口 24 | urls.extend(expand_ip_or_domain(url)) 25 | }; 26 | } 27 | 28 | // 文件url 29 | if let Some(file) = &args.file { 30 | // let mut file_urls 31 | let file_urls = read_urls_from_file(file)?; 32 | urls.extend(file_urls); 33 | } 34 | 35 | // 添加path 36 | if let Some(path) = &args.path { 37 | urls = normalize_urls(urls, path); 38 | } 39 | // 去重复 40 | urls.sort(); 41 | urls.dedup(); 42 | Ok(urls) 43 | } 44 | 45 | // 读取文件 46 | fn read_urls_from_file(path: &str) -> Result, Box> { 47 | let file = File::open(path)?; 48 | let reader = BufReader::new(file); 49 | 50 | let mut result = Vec::new(); 51 | 52 | for line in reader.lines() { 53 | let line = line?; 54 | let line = line.trim(); 55 | 56 | if line.is_empty() || line.starts_with('#') { 57 | continue; 58 | } 59 | let expanded = expand_line(line); 60 | result.extend(expanded); 61 | } 62 | 63 | Ok(result) 64 | } 65 | 66 | // 解析http 67 | fn expand_line(line: &str) -> Vec { 68 | if line.starts_with("http://") || line.starts_with("https://") { 69 | return vec![line.to_string()]; 70 | } 71 | 72 | if is_cidr(line) { 73 | let mut ip_url = Vec::new(); 74 | let ip_vec = expand_cidr(line); 75 | 76 | // 包含 http 和 https 77 | for ip in ip_vec { 78 | ip_url.extend(expand_ip_or_domain(&ip)); 79 | } 80 | return ip_url; 81 | } 82 | 83 | expand_ip_or_domain(line) 84 | } 85 | 86 | // 协议规则 87 | fn expand_ip_or_domain(input: &str) -> Vec { 88 | if let Some((_, port)) = input.split_once(':') { 89 | match port { 90 | "443" => vec![format!("https://{}", input)], 91 | "80" => vec![format!("http://{}", input)], 92 | _ => vec![format!("http://{}", input), format!("https://{}", input)], 93 | } 94 | } else { 95 | vec![format!("http://{}", input), format!("https://{}", input)] 96 | } 97 | } 98 | 99 | fn normalize_urls(urls: Vec, path: &str) -> Vec { 100 | let mut result = Vec::new(); 101 | 102 | let path = path.trim_start_matches('/'); 103 | 104 | for url in urls { 105 | if url.ends_with('/') { 106 | result.push(format!("{}{}", url, path)); 107 | } else { 108 | result.push(format!("{}/{}", url, path)); 109 | } 110 | } 111 | result 112 | } 113 | --------------------------------------------------------------------------------