4#error "PoliticianStorage.h requires the Arduino framework. Use ESP-IDF VFS and nvs_flash APIs directly."
9#include <Preferences.h>
11#ifndef POLITICIAN_NO_NETWORK_LOGGER
13#include <WiFiClient.h>
14#if __has_include(<WiFiUdp.h>)
16#elif __has_include(<WiFiUDP.h>)
28#ifndef POLITICIAN_NO_STD_FUNCTION
40 const char *ts = cb ? cb() :
nullptr;
41 return ts ? ts :
"1970-01-01 00:00:00";
50 if (out < maxLen - 1) output[out++] =
'"';
51 for (
size_t i = 0; input[i] && out < maxLen - 2; i++) {
52 if (input[i] ==
'"' && out < maxLen - 2) output[out++] =
'"';
53 output[out++] = input[i];
55 if (out < maxLen - 1) output[out++] =
'"';
106 bool open(fs::FS &fs,
const char *path, uint32_t maxBytes = 0) {
109 _maxBytes = maxBytes;
112 strncpy(_basePath, path,
sizeof(_basePath) - 1);
113 _basePath[
sizeof(_basePath) - 1] =
'\0';
118 bool isNew = !fs.exists(path);
120 fs::File check = fs.open(path, FILE_READ);
121 if (check) { isNew = (check.size() == 0); check.close(); }
124 _file = fs.open(path, FILE_APPEND);
125 if (!_file) { _fs =
nullptr;
return false; }
130 _file.write(hdr, hl);
141 if (!_open)
return false;
143 if (!_open)
return false;
146 if (len > 0) { _file.write(buf, len); _file.flush(); }
154 bool writePacket(
const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec) {
155 if (!_open)
return false;
157 if (!_open)
return false;
160 if (wlen > 0) { _file.write(buf, wlen); _file.flush(); }
166 if (_open) { _file.close(); _open =
false; _fs =
nullptr; }
180 return logger.
open(fs, path) && logger.
write(rec);
187 static bool appendPacket(fs::FS &fs,
const char *path,
const uint8_t *payload, uint16_t len,
188 int8_t rssi, uint8_t channel, uint32_t ts_usec) {
190 return logger.
open(fs, path) && logger.
writePacket(payload, len, rssi, channel, ts_usec);
194 void _checkRotate() {
195 if (!_open || _maxBytes == 0)
return;
196 if ((uint32_t)_file.size() < _maxBytes)
return;
200 const char *dot = strrchr(_basePath,
'.');
202 size_t base_len = (size_t)(dot - _basePath);
203 snprintf(rotPath,
sizeof(rotPath),
"%.*s_%02u%s",
204 (
int)base_len, _basePath, _fileIdx, dot);
206 snprintf(rotPath,
sizeof(rotPath),
"%s_%02u", _basePath, _fileIdx);
208 _file = _fs->open(rotPath, FILE_WRITE);
212 _file.write(hdr, hl);
221 uint32_t _maxBytes = 0;
222 char _basePath[64] = {};
223 uint8_t _fileIdx = 0;
241 bool open(fs::FS &fs,
const char *path) {
243 _file = fs.open(path, FILE_APPEND);
244 if (!_file)
return false;
254 if (!_open)
return false;
256 if (!str.empty()) { _file.println(str.c_str()); _file.flush(); }
262 if (_open) { _file.close(); _open =
false; }
274 return logger.
open(fs, path) && logger.
write(rec);
303 float lat,
float lon,
float alt = 0.0,
float acc = 10.0,
304 const char* timestamp =
nullptr) {
305 fs::File file = _openWithHeader(fs, path);
306 if (!file)
return false;
308 char ssidEscaped[72];
312 snprintf(line,
sizeof(line),
"%02X:%02X:%02X:%02X:%02X:%02X,%s,%s,%s,%d,%d,%.6f,%.6f,%.1f,%.1f,WIFI",
337 float lat,
float lon,
float alt = 0.0,
float acc = 10.0,
338 const char* timestamp =
nullptr) {
339 fs::File file = _openWithHeader(fs, path);
340 if (!file)
return false;
342 char ssidEscaped[72];
346 snprintf(line,
sizeof(line),
"%02X:%02X:%02X:%02X:%02X:%02X,%s,%s,%s,%d,%d,%.6f,%.6f,%.1f,%.1f,WIFI",
358 static const char* _authStr(uint8_t enc) {
360 case 1:
return "[WEP][ESS]";
361 case 2:
return "[WPA-PSK-TKIP][ESS]";
362 case 3:
return "[WPA2-PSK-CCMP][ESS]";
363 case 4:
return "[WPA2-EAP-CCMP][ESS]";
364 default:
return "[ESS]";
368 static fs::File _openWithHeader(fs::FS &fs,
const char* path) {
369 bool isNew = !fs.exists(path);
371 fs::File check = fs.open(path, FILE_READ);
372 if (check) { isNew = (check.size() == 0); check.close(); }
374 fs::File file = fs.open(path, FILE_APPEND);
375 if (!file)
return file;
377 file.println(
"WigleWifi-1.4,appRelease=1.0,model=Politician,release=1.0,device=ESP32,display=1.0,board=ESP32,brand=Espressif");
378 file.println(
"MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type");
391 const char *timestamp =
nullptr) {
392 bool isNew = !fs.exists(path);
394 fs::File file = fs.open(path, FILE_APPEND);
395 if (!file)
return false;
398 file.println(
"Enterprise BSSID,Client MAC,Plaintext Identity,EAP Method,FirstSeen,Channel,RSSI");
401 char identityEscaped[136];
405 snprintf(line,
sizeof(line),
"%02X:%02X:%02X:%02X:%02X:%02X,%02X:%02X:%02X:%02X:%02X:%02X,%s,%u,%s,%d,%d",
426 static const int MAX_STORED = 128;
427 uint8_t _cache[MAX_STORED][6];
434 Serial.println(
"[NvsBssidCache] WARNING: namespace name exceeds 15 chars and will be truncated");
435 strncpy(_ns, ns,
sizeof(_ns) - 1);
436 _ns[
sizeof(_ns) - 1] =
'\0';
437 memset(_cache, 0,
sizeof(_cache));
444 _prefs.begin(_ns,
false);
445 size_t bytes = _prefs.getBytes(
"bssids", _cache,
sizeof(_cache));
447 if (_count > MAX_STORED) _count = MAX_STORED;
456 for (
size_t i = 0; i < _count; i++) {
457 engine.markCaptured(_cache[i]);
467 bool add(
const uint8_t* bssid) {
468 for (
size_t i = 0; i < _count; i++) {
469 if (memcmp(_cache[i], bssid, 6) == 0)
return false;
471 if (_count >= MAX_STORED)
return false;
473 memcpy(_cache[_count], bssid, 6);
485 if (!_dirty)
return false;
486 _prefs.putBytes(
"bssids", _cache, _count * 6);
497 size_t count()
const {
return _count; }
505 _prefs.remove(
"bssids");
510#ifndef POLITICIAN_NO_NETWORK_LOGGER
519class TcpStreamLogger {
521 TcpStreamLogger() : _client(nullptr), _open(false) {}
523 bool connect(WiFiClient &client) {
524 if (!client.connected())
return false;
527 size_t hl = format::writePcapngGlobalHeader(hdr);
528 _client->write(hdr, hl);
533 bool write(
const HandshakeRecord &rec) {
534 if (!_open || !_client || !_client->connected()) { _open =
false;
return false; }
536 size_t len = format::writePcapngRecord(rec, buf,
sizeof(buf));
537 if (len > 0) { _client->write(buf, len);
return true; }
542 if (_open && _client) { _client->stop(); }
547 bool isConnected()
const {
return _open && _client && _client->connected(); }
561class UdpStreamLogger {
563 UdpStreamLogger() : _udp(nullptr), _host(nullptr), _port(0), _open(false) {}
565 bool begin(WiFiUDP &udp,
const char *host, uint16_t port) {
573 bool write(
const HandshakeRecord &rec) {
574 if (!_open || !_udp)
return false;
576 size_t len = format::writePcapngRecord(rec, buf,
sizeof(buf));
577 if (len == 0 || len > 1472)
return false;
578 _udp->beginPacket(_host, _port);
579 _udp->write(buf, len);
580 return _udp->endPacket() != 0;
588 bool isOpen()
const {
return _open; }
The core WiFi handshake capturing engine.
Helper for logging harvested 802.1X Enterprise Credentials.
static bool append(fs::FS &fs, const char *path, const EapIdentityRecord &rec, const char *timestamp=nullptr)
Helper for writing HandshakeRecords to an HC22000 text file.
bool open(fs::FS &fs, const char *path)
Opens the HC22000 file for streaming writes.
void close()
Closes the underlying file handle.
bool isOpen() const
Returns true if the file is currently open for streaming.
bool write(const HandshakeRecord &rec)
Writes a HandshakeRecord to the open file.
static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec)
Appends a HandshakeRecord to a file as an HC22000 string (opens and closes per call).
Helper for persistently storing captured BSSIDs in NVS memory.
NvsBssidCache(const char *ns="wardrive")
bool isDirty() const
Returns true if there are in-RAM changes not yet written to NVS.
bool add(const uint8_t *bssid)
Adds a newly captured BSSID to the in-RAM cache.
bool flush()
Writes any pending in-RAM changes to NVS.
size_t count() const
Returns the number of BSSIDs currently stored.
void loadInto(Politician &engine)
Feeds the loaded BSSIDs into the Politician engine so it knows to ignore them.
void begin()
Initializes the NVS memory and loads the cached BSSIDs into RAM.
void clear()
Clears the entire cache from both RAM and NVS immediately.
Helper for writing HandshakeRecords and raw packets to a standard PCAPNG file.
static bool appendPacket(fs::FS &fs, const char *path, const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec)
Appends a raw 802.11 sniffer frame to a PCAPNG file (opens and closes per call).
bool isOpen() const
Returns true if the file is currently open for streaming.
void close()
Closes the underlying file handle.
bool open(fs::FS &fs, const char *path, uint32_t maxBytes=0)
Opens the PCAPNG file for streaming writes.
static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec)
Appends a HandshakeRecord to a file as PCAPNG (opens and closes per call).
bool write(const HandshakeRecord &rec)
Writes a HandshakeRecord to the open file.
bool writePacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec)
Writes a raw 802.11 sniffer frame to the open file.
Helper for writing precise GPS location coordinates to a Wigle.net compatible CSV file.
static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec, float lat, float lon, float alt=0.0, float acc=10.0, const char *timestamp=nullptr)
Appends a HandshakeRecord's details alongside GPS coordinates to a Wigle CSV.
static bool appendAp(fs::FS &fs, const char *path, const ApRecord &ap, float lat, float lon, float alt=0.0, float acc=10.0, const char *timestamp=nullptr)
Appends any discovered ApRecord alongside GPS coordinates to a Wigle CSV.
std::function< const char *()> TimestampCb
void escapeCsvField(const char *input, char *output, size_t maxLen)
Writes a CSV-safe quoted field into output (RFC 4180).
const char * _timestamp()
TimestampCb & _timestampCb()
void setTimestampProvider(detail::TimestampCb cb)
Sets a global timestamp provider called by all CSV loggers when no explicit timestamp string is suppl...
uint8_t eap_method
EAP method negotiated by the AP (EAP_METHOD_* constant); 0 if not yet observed.
Snapshot of a discovered Access Point from the internal cache.
A harvested 802.1X Enterprise plaintext identity, delivered to the IdentityCb callback.
A captured handshake or PMKID record delivered to the EapolCb callback.