diff --git a/.gitignore b/.gitignore index 3e430f7..f565b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ target /target *.txt -*.bak \ No newline at end of file +*.bak +config.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1e734a0..36880da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -154,6 +163,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" + [[package]] name = "bytes" version = "1.10.1" @@ -279,6 +294,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "fnv" version = "1.0.7" @@ -406,11 +435,14 @@ dependencies = [ "async-stream", "backon", "clap", + "figment", "futures", "regex", "reqwest", + "serde", "serde_json", "tokio", + "toml", "url", ] @@ -721,6 +753,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "io-uring" version = "0.7.8" @@ -908,6 +946,29 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -950,6 +1011,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quote" version = "1.0.40" @@ -1176,6 +1250,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1371,6 +1454,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -1441,6 +1565,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1462,6 +1595,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1482,6 +1616,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -1777,6 +1917,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -1792,6 +1941,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 94e5aa9..c52160c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,5 +12,8 @@ regex = "1.11.1" reqwest = { version = "0.12.22", features = ["json"] } serde_json = "1.0.140" tokio = { version = "1.46", features = ["macros", "rt-multi-thread", "time"] } -url = "2.5.4" -async-stream = "0.3" \ No newline at end of file +url = { version = "2.5.4", features = ["serde"] } +async-stream = "0.3" +figment = { version = "0.10.19", features = ["env", "toml"] } +serde = { version = "1.0.219", features = ["derive"] } +toml = "0.8" diff --git a/src/adapters/input/local.rs b/src/adapters/input/local.rs new file mode 100644 index 0000000..91f1a5f --- /dev/null +++ b/src/adapters/input/local.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use std::{ + collections::HashSet, + fs, + path::Path, + str::FromStr, +}; +use crate::types::ApiKey; + +/// Load and validate API keys from a file +/// Returns a vector of unique, valid API keys +pub fn load_keys(path: &Path) -> Result> { + let keys_txt = fs::read_to_string(path)?; + // Use HashSet to automatically deduplicate keys + let unique_keys_set: HashSet<&str> = keys_txt + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect(); + + let mut keys = Vec::new(); + let mut valid_keys_for_backup = Vec::new(); + + for key_str in unique_keys_set { + match ApiKey::from_str(key_str) { + Ok(api_key) => { + keys.push(api_key.clone()); + valid_keys_for_backup.push(api_key.as_str().to_string()); + } + Err(e) => eprintln!("Skipping invalid key: {}", e), + } + } + + // Write validated keys to backup.txt + let backup_content = valid_keys_for_backup.join("\n"); + if let Err(e) = fs::write("backup.txt", backup_content) { + eprintln!("Failed to write backup file: {}", e); + } else { + println!( + "Backup file created with {} valid keys", + valid_keys_for_backup.len() + ); + } + + Ok(keys) +} \ No newline at end of file diff --git a/src/adapters/input/mod.rs b/src/adapters/input/mod.rs new file mode 100644 index 0000000..3d0df39 --- /dev/null +++ b/src/adapters/input/mod.rs @@ -0,0 +1,3 @@ +pub mod local; + +pub use local::*; diff --git a/src/adapters/mod.rs b/src/adapters/mod.rs new file mode 100644 index 0000000..b642a9c --- /dev/null +++ b/src/adapters/mod.rs @@ -0,0 +1,5 @@ +pub mod input; +pub mod output; + +pub use input::load_keys; +pub use output::write_keys_txt_file; \ No newline at end of file diff --git a/src/adapters/output/local.rs b/src/adapters/output/local.rs new file mode 100644 index 0000000..96e8bfd --- /dev/null +++ b/src/adapters/output/local.rs @@ -0,0 +1,24 @@ +use crate::types::ApiKey; +use anyhow::Result; +use std::{fs, io::Write}; +use toml::Value; + +// Write valid key to output file +pub fn write_keys_txt_file(file: &mut fs::File, key: &ApiKey) -> Result<()> { + writeln!(file, "{}", key.as_str())?; + Ok(()) +} + +// Write valid key to output file in Clewdr format +pub fn write_keys_clewdr_format(file: &mut fs::File, key: &ApiKey) -> Result<()> { + let mut table = toml::value::Table::new(); + table.insert("key".to_string(), Value::String(key.as_str().to_string())); + + let gemini_keys = Value::Array(vec![Value::Table(table)]); + let mut root = toml::value::Table::new(); + root.insert("gemini_keys".to_string(), gemini_keys); + + let toml_string = toml::to_string(&Value::Table(root))?; + write!(file, "{}", toml_string)?; + Ok(()) +} diff --git a/src/adapters/output/mod.rs b/src/adapters/output/mod.rs new file mode 100644 index 0000000..3d0df39 --- /dev/null +++ b/src/adapters/output/mod.rs @@ -0,0 +1,3 @@ +pub mod local; + +pub use local::*; diff --git a/src/config/basic_client.rs b/src/config/basic_client.rs new file mode 100644 index 0000000..b3dcf29 --- /dev/null +++ b/src/config/basic_client.rs @@ -0,0 +1,15 @@ +use std::time::Duration; + +use reqwest::Client; + +use crate::config::KeyCheckerConfig; + +pub fn client_builder(config: &KeyCheckerConfig) -> Result { + let mut builder = Client::builder().timeout(Duration::from_secs(config.timeout_sec)); + + if let Some(ref proxy_url) = config.proxy { + builder = builder.proxy(reqwest::Proxy::all(proxy_url.clone())?); + } + + builder.build() +} diff --git a/src/config/basic_config.rs b/src/config/basic_config.rs new file mode 100644 index 0000000..dd0de0a --- /dev/null +++ b/src/config/basic_config.rs @@ -0,0 +1,94 @@ +use anyhow::{Ok, Result}; +use figment::{ + Figment, + providers::{Env, Format, Toml}, +}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::sync::LazyLock; +use url::Url; + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyCheckerConfig { + // Input file path containing API keys to check. + #[serde(default)] + pub input_path: PathBuf, + + // Output file path for valid API keys. + #[serde(default)] + pub output_path: PathBuf, + + // Backup file path for all API keys. + #[serde(default)] + pub backup_path: PathBuf, + + // API host URL for key validation. + #[serde(default = "default_api_host")] + pub api_host: Url, + + // Request timeout in seconds. + #[serde(default)] + pub timeout_sec: u64, + + // Maximum number of concurrent requests. + #[serde(default)] + pub concurrency: usize, + + // Optional proxy URL for HTTP requests (e.g., --proxy http://user:pass@host:port). + #[serde(default)] + pub proxy: Option, +} + +impl Default for KeyCheckerConfig { + fn default() -> Self { + Self { + input_path: default_input_path(), + output_path: default_output_path(), + backup_path: default_backup_path(), + api_host: default_api_host(), + timeout_sec: default_timeout(), + concurrency: default_concurrency(), + proxy: None, + } + } +} +impl KeyCheckerConfig { + pub fn load_config() -> Result { + // Define the path to the configuration file + static CONFIG_PATH: LazyLock = LazyLock::new(|| "config.toml".into()); + + // Check if config.toml exists, if not create it with default values + if !CONFIG_PATH.exists() { + let default_config = Self::default(); + let toml_content = toml::to_string_pretty(&default_config)?; + fs::write(CONFIG_PATH.as_path(), toml_content)?; + } + + // Load configuration from config.toml, environment variables, and defaults + let config = Figment::new() + .merge(Toml::file(CONFIG_PATH.as_path())) + .merge(Env::prefixed("KEYCHECKER_")) + .extract()?; + Ok(config) + } +} + +fn default_input_path() -> PathBuf { + "keys.txt".into() +} +fn default_output_path() -> PathBuf { + "output_keys.txt".into() +} +fn default_backup_path() -> PathBuf { + "backup_keys.txt".into() +} +fn default_api_host() -> Url { + Url::parse("https://generativelanguage.googleapis.com/").unwrap() +} +fn default_timeout() -> u64 { + 20 +} +fn default_concurrency() -> usize { + 30 +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..4580cbf --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,5 @@ +mod basic_config; +mod basic_client; + +pub use basic_config::KeyCheckerConfig; +pub use basic_client::client_builder; diff --git a/src/key_validator.rs b/src/key_validator.rs new file mode 100644 index 0000000..b02443e --- /dev/null +++ b/src/key_validator.rs @@ -0,0 +1,80 @@ +use anyhow::Result; +use backon::{ExponentialBuilder, Retryable}; +use reqwest::{Client, StatusCode}; +use serde_json; +use tokio::time::Duration; +use url::Url; + +use crate::types::{KeyStatus, ApiKey}; + +pub async fn validate_key_with_retry(client: &Client, api_host: &Url, key: ApiKey) -> Option { + let retry_policy = ExponentialBuilder::default() + .with_max_times(3) + .with_min_delay(Duration::from_secs(3)) + .with_max_delay(Duration::from_secs(5)); + + let result = (|| async { + match keytest(&client, &api_host, &key).await { + Ok(KeyStatus::Valid) => { + println!("Key: {}... -> SUCCESS", &key.as_str()[..10]); + Ok(Some(key.clone())) + } + Ok(KeyStatus::Invalid) => { + println!("Key: {}... -> INVALID (Forbidden)", &key.as_str()[..10]); + Ok(None) + } + Ok(KeyStatus::Retryable(reason)) => { + eprintln!("Key: {}... -> RETRYABLE (Reason: {})", &key.as_str()[..10], reason); + Err(anyhow::anyhow!("Retryable error: {}", reason)) + } + Err(e) => { + eprintln!("Key: {}... -> NETWORK ERROR (Reason: {})", &key.as_str()[..10], e); + Err(e) + } + } + }) + .retry(retry_policy) + .await; + + match result { + Ok(key_result) => key_result, + Err(_) => { + eprintln!("Key: {}... -> FAILED after all retries.", &key.as_str()[..10]); + None + } + } +} + +async fn keytest(client: &Client, api_host: &Url, key: &ApiKey) -> Result { + const API_PATH: &str = "v1beta/models/gemini-2.0-flash-exp:generateContent"; + let full_url = api_host.join(API_PATH)?; + + let request_body = serde_json::json!({ + "contents": [ + { + "parts": [ + { + "text": "Hi" + } + ] + } + ] + }); + + let response = client + .post(full_url) + .header("Content-Type", "application/json") + .header("X-goog-api-key", key.as_str()) + .json(&request_body) + .send() + .await?; + + let status = response.status(); + + let key_status = match status { + StatusCode::OK => KeyStatus::Valid, + StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => KeyStatus::Invalid, + other => KeyStatus::Retryable(format!("Received status {}, will retry.", other)), + }; + Ok(key_status) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2dcabb5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod types; +pub mod key_validator; +pub mod adapters; +pub mod validation; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 15f11ce..25b2de9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,230 +1,17 @@ use anyhow::Result; -use async_stream::stream; -use backon::{ExponentialBuilder, Retryable}; -use clap::Parser; -use futures::{pin_mut, stream::StreamExt}; -use regex::Regex; -use reqwest::{Client, StatusCode}; -use std::{ - collections::HashSet, - fs, - io::Write, - path::{Path, PathBuf}, - sync::LazyLock, - thread::spawn, - time::Instant, -}; -use tokio::time::Duration; -use url::Url; - -// Regex pattern for validating Google API keys (AIzaSy followed by 33 characters) -static API_KEY_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^AIzaSy.{33}$").unwrap()); -/// Configuration structure for the key checker tool -#[derive(Parser, Debug)] -#[command(version, about = "A tool to check and backup API keys", long_about = None)] -struct KeyCheckerConfig { - /// Input file path containing API keys to check - #[arg(long, short = 'i', default_value = "keys.txt")] - input_path: PathBuf, - - /// Output file path for valid API keys - #[arg(long, short = 'o', default_value = "output_keys.txt")] - output_path: PathBuf, - - /// API host URL for key validation - #[arg(long, short = 'u', default_value = "https://generativelanguage.googleapis.com/")] - api_host: Url, - - /// Request timeout in seconds - #[arg(long, short = 't', default_value_t = 60)] - timeout_sec: u64, - - /// Maximum number of concurrent requests - #[arg(long, short = 'c', default_value_t = 30)] - concurrency: usize, - - /// Optional proxy URL for HTTP requests (supports http://user:pass@host:port) - #[arg(long, short = 'x')] - proxy: Option, -} -/// Status of API key validation -#[derive(Debug)] -enum KeyStatus { - /// Key is valid and working - Valid, - /// Key is invalid or unauthorized - Invalid, - /// Temporary error, key validation should be retried - Retryable(String), -} -/// Load and validate API keys from a file -/// Returns a vector of unique, valid API keys -fn load_keys(path: &Path) -> Result> { - let keys_txt = fs::read_to_string(path)?; - // Use HashSet to automatically deduplicate keys - let unique_keys_set: HashSet<&str> = keys_txt - .lines() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - .filter(|line| API_KEY_REGEX.is_match(line)) - .collect(); - let keys: Vec = unique_keys_set.into_iter().map(String::from).collect(); - Ok(keys) -} - -/// Validate an API key with exponential backoff retry logic -/// Returns Some(key) if valid, None if invalid or failed after all retries -async fn validate_key_with_retry(client: &Client, api_host: &Url, key: String) -> Option { - // Configure exponential backoff retry policy - let retry_policy = ExponentialBuilder::default() - .with_max_times(3) - .with_min_delay(Duration::from_secs(5)) - .with_max_delay(Duration::from_secs(10)); - - let result = (|| async { - match keytest(&client, &api_host, &key).await { - Ok(KeyStatus::Valid) => { - println!("Key: {}... -> SUCCESS", &key[..10]); - Ok(Some(key.clone())) - } - Ok(KeyStatus::Invalid) => { - println!("Key: {}... -> INVALID (Forbidden)", &key[..10]); - Ok(None) - } - Ok(KeyStatus::Retryable(reason)) => { - eprintln!("Key: {}... -> RETRYABLE (Reason: {})", &key[..10], reason); - Err(anyhow::anyhow!("Retryable error: {}", reason)) - } - Err(e) => { - eprintln!("Key: {}... -> NETWORK ERROR (Reason: {})", &key[..10], e); - Err(e) - } - } - }) - .retry(retry_policy) - .await; - - match result { - Ok(key_result) => key_result, - Err(_) => { - eprintln!("Key: {}... -> FAILED after all retries.", &key[..10]); - None - } - } -} - -/// Test a single API key by making a request to the Gemini API -/// Returns the validation status based on the HTTP response -async fn keytest(client: &Client, api_host: &Url, keys: &str) -> Result { - const API_PATH: &str = "v1beta/models/gemini-2.0-flash-exp:generateContent"; - let full_url = api_host.join(API_PATH)?; - - // Simple test request body - let request_body = serde_json::json!({ - "contents": [ - { - "parts": [ - { - "text": "Hi" - } - ] - } - ] - }); - - let response = client - .post(full_url) - .header("Content-Type", "application/json") - .header("X-goog-api-key", keys) - .json(&request_body) - .send() - .await?; - - let status = response.status(); - - let key_status = match status { - // 200 OK - Key is valid - StatusCode::OK => KeyStatus::Valid, - - // 403 & 401 - Key is invalid or unauthorized - StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => KeyStatus::Invalid, - - // Other status codes - Temporary error, retry - other => KeyStatus::Retryable(format!("Received status {}, will retry.", other)), - }; - Ok(key_status) -} - -/// Build HTTP client with optional proxy configuration -/// Returns a configured reqwest Client -fn build_client(config: &KeyCheckerConfig) -> Result { - let mut client_builder = Client::builder() - .timeout(Duration::from_secs(config.timeout_sec)); - - // Add proxy configuration if specified - if let Some(proxy_url) = &config.proxy { - let mut proxy = reqwest::Proxy::all(proxy_url.clone())?; - - // Extract username and password from URL if present - if !proxy_url.username().is_empty() { - let username = proxy_url.username(); - let password = proxy_url.password().unwrap_or(""); - proxy = proxy.basic_auth(username, password); - } - - client_builder = client_builder.proxy(proxy); - } - - client_builder.build().map_err(Into::into) -} +use gemini_keychecker::config::{KeyCheckerConfig, client_builder}; +use gemini_keychecker::adapters::load_keys; +use gemini_keychecker::validation::ValidationService; /// Main function - orchestrates the key validation process #[tokio::main] async fn main() -> Result<()> { - let start_time = Instant::now(); - let config = KeyCheckerConfig::parse(); + let config = KeyCheckerConfig::load_config().unwrap(); let keys = load_keys(&config.input_path)?; - let client = build_client(&config)?; + let client = client_builder(&config)?; - // Create channel for streaming keys from producer to consumer - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); - let stream = stream! { - while let Some(item) = rx.recv().await { - yield item; - } - }; + let validation_service = ValidationService::new(config, client); + validation_service.validate_keys(keys).await?; - // Spawn producer thread to send keys through channel - spawn(move || { - for key in keys { - if API_KEY_REGEX.is_match(&key) { - if let Err(e) = tx.send(key) { - eprintln!("Failed to send key: {}", e); - } - } else { - eprintln!("Invalid key format: {}", key); - } - } - }); - - // Create stream to validate keys concurrently - let valid_keys_stream = stream - .map(|key| validate_key_with_retry(&client, &config.api_host, key)) - .buffer_unordered(config.concurrency) - .filter_map(|r| async { r }); - pin_mut!(valid_keys_stream); - - // Open output file for writing valid keys - let mut output_file = fs::File::create(&config.output_path)?; - - // Process validated keys and write to output file - while let Some(valid_key) = valid_keys_stream.next().await { - println!("Valid key found: {}", valid_key); - if let Err(e) = writeln!(output_file, "{}", valid_key) { - eprintln!("Failed to write key to output file: {}", e); - } - } - - println!("Total Elapsed Time: {:?}", start_time.elapsed()); Ok(()) -} \ No newline at end of file +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..5557bb9 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,56 @@ +use regex::Regex; +use std::str::FromStr; +use std::sync::LazyLock; + +#[derive(Debug)] +pub enum KeyStatus { + Valid, + Invalid, + Retryable(String), +} + +#[derive(Debug, Clone)] +pub struct ApiKey { + inner: String, +} + +impl ApiKey { + pub fn as_str(&self) -> &str { + &self.inner + } +} + +#[derive(Debug)] +pub enum KeyValidationError { + InvalidFormat(String), +} + +impl std::fmt::Display for KeyValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeyValidationError::InvalidFormat(msg) => write!(f, "Invalid key format: {}", msg), + } + } +} + +impl std::error::Error for KeyValidationError {} + +impl FromStr for ApiKey { + type Err = KeyValidationError; + + fn from_str(s: &str) -> Result { + static RE: LazyLock = LazyLock::new(|| Regex::new(r"^AIzaSy.{33}$").unwrap()); + + let cleaned = s.trim(); + + if RE.is_match(cleaned) { + Ok(Self { + inner: cleaned.to_string(), + }) + } else { + Err(KeyValidationError::InvalidFormat( + "Google API key must start with 'AIzaSy' followed by 33 characters".to_string(), + )) + } + } +} diff --git a/src/validation.rs b/src/validation.rs new file mode 100644 index 0000000..65c4460 --- /dev/null +++ b/src/validation.rs @@ -0,0 +1,64 @@ +use anyhow::Result; +use async_stream::stream; +use futures::{pin_mut, stream::StreamExt}; +use reqwest::Client; +use std::{fs, time::Instant}; +use tokio::sync::mpsc; + +use crate::config::KeyCheckerConfig; +use crate::adapters::write_keys_txt_file; +use crate::key_validator::validate_key_with_retry; +use crate::types::ApiKey; + +pub struct ValidationService { + config: KeyCheckerConfig, + client: Client, +} + +impl ValidationService { + pub fn new(config: KeyCheckerConfig, client: Client) -> Self { + Self { config, client } + } + + pub async fn validate_keys(&self, keys: Vec) -> Result<()> { + let start_time = Instant::now(); + + // Create channel for streaming keys from producer to consumer + let (tx, mut rx) = mpsc::unbounded_channel::(); + let stream = stream! { + while let Some(item) = rx.recv().await { + yield item; + } + }; + + // Spawn producer task to send keys through channel + tokio::spawn(async move { + for key in keys { + if let Err(e) = tx.send(key) { + eprintln!("Failed to send key: {}", e); + } + } + }); + + // Create stream to validate keys concurrently + let valid_keys_stream = stream + .map(|key| validate_key_with_retry(&self.client, &self.config.api_host, key)) + .buffer_unordered(self.config.concurrency) + .filter_map(|r| async { r }); + pin_mut!(valid_keys_stream); + + // Open output file for writing valid keys + let mut output_file = fs::File::create(&self.config.output_path)?; + + // Process validated keys and write to output file + while let Some(valid_key) = valid_keys_stream.next().await { + println!("Valid key found: {}", valid_key.as_str()); + if let Err(e) = write_keys_txt_file(&mut output_file, &valid_key) { + eprintln!("Failed to write key to output file: {}", e); + } + } + + println!("Total Elapsed Time: {:?}", start_time.elapsed()); + Ok(()) + } +}