Revert to 2.9.5.4

Signed-off-by: Jeroen Oudshoorn <oudshoorn.jeroen@gmail.com>
This commit is contained in:
Jeroen Oudshoorn
2026-03-25 19:01:56 +01:00
parent 214658bbcf
commit 01c6ab856d
40 changed files with 1708 additions and 8673 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = '2.9.5.5'
__version__ = '2.9.5.4'
+5 -1
View File
@@ -4,6 +4,7 @@ import os
import re
import logging
import asyncio
#import _thread
import threading
import subprocess
@@ -297,10 +298,12 @@ class Agent(Client, Automata, AsyncAdvertiser):
if delete:
logging.info("deleting %s", RECOVERY_DATA_FILE)
os.unlink(RECOVERY_DATA_FILE)
except Exception: # FIX B4: was bare except, now catches Exception only
except:
if not no_exceptions:
raise
def start_session_fetcher(self):
#_thread.start_new_thread(self._fetch_stats, ())
threading.Thread(target=self._fetch_stats, args=(), name="Session Fetcher", daemon=True).start()
def _fetch_stats(self):
@@ -384,6 +387,7 @@ class Agent(Client, Automata, AsyncAdvertiser):
def start_event_polling(self):
# start a thread and pass in the mainloop
#_thread.start_new_thread(self._event_poller, (asyncio.get_event_loop(),))
threading.Thread(target=self._event_poller, args=(asyncio.get_event_loop(),), name="Event Polling", daemon=True).start()
def is_module_running(self, module):
+51 -20
View File
@@ -4,10 +4,9 @@ import logging
import pwnagotchi
import pwnagotchi.utils as utils
import pwnagotchi.mesh.wifi as wifi
# REMOVED: from pwnagotchi.ai.reward import RewardFunction
# The RewardFunction computed a reward score that was logged but never consumed
# by any decision-making logic after AI removal. Removed to eliminate dead CPU work.
from pwnagotchi.ai.reward import RewardFunction
class Epoch(object):
@@ -45,7 +44,7 @@ class Epoch(object):
# number of peers seen during this epoch
self.num_peers = 0
# cumulative bond factor
self.tot_bond_factor = 0.0
self.tot_bond_factor = 0.0 # cum_bond_factor sounded worse ...
# average bond factor
self.avg_bond_factor = 0.0
# any activity at all during this epoch?
@@ -56,27 +55,29 @@ class Epoch(object):
self.epoch_duration = 0
# https://www.metageek.com/training/resources/why-channels-1-6-11.html
self.non_overlapping_channels = {1: 0, 6: 0, 11: 0}
# REMOVED: observation histogram vectors (_observation, _observation_ready)
# These were 14-element float arrays per channel computed every observe() call,
# feeding a neural network that no longer exists.
# observation vectors
self._observation = {
'aps_histogram': [0.0] * wifi.NumChannels,
'sta_histogram': [0.0] * wifi.NumChannels,
'peers_histogram': [0.0] * wifi.NumChannels
}
self._observation_ready = threading.Event()
self._epoch_data = {}
self._epoch_data_ready = threading.Event()
# REMOVED: self._reward = RewardFunction()
self._reward = RewardFunction()
def wait_for_epoch_data(self, with_observation=True, timeout=None):
# REMOVED: observation wait (was already commented out in original)
# if with_observation:
# self._observation_ready.wait(timeout)
# self._observation_ready.clear()
self._epoch_data_ready.wait(timeout)
self._epoch_data_ready.clear()
# REMOVED: merging histogram observation vectors - just return epoch data
return self._epoch_data
return self._epoch_data if with_observation is False else {**self._observation, **self._epoch_data}
def data(self):
return self._epoch_data
def observe(self, aps, peers):
# REMOVED: histogram computation (aps_histogram, sta_histogram, peers_histogram)
# These were normalised channel observation vectors for the neural network.
# Kept: the non-histogram peer/AP accounting the mood system depends on.
num_aps = len(aps)
if num_aps == 0:
self.blind_for += 1
@@ -91,8 +92,38 @@ class Epoch(object):
self.tot_bond_factor = sum((peer.encounters for peer in peers)) / bond_unit_scale
self.avg_bond_factor = self.tot_bond_factor / num_peers
# REMOVED: per-channel histogram normalisation loops
# REMOVED: self._observation update and self._observation_ready.set()
num_aps = len(aps) + 1e-10
num_sta = sum(len(ap['clients']) for ap in aps) + 1e-10
aps_per_chan = [0.0] * wifi.NumChannels
sta_per_chan = [0.0] * wifi.NumChannels
peers_per_chan = [0.0] * wifi.NumChannels
for ap in aps:
ch_idx = ap['channel'] - 1
try:
aps_per_chan[ch_idx] += 1.0
sta_per_chan[ch_idx] += len(ap['clients'])
except IndexError:
logging.error("got data on channel %d, we can store %d channels" % (ap['channel'], wifi.NumChannels))
for peer in peers:
try:
peers_per_chan[peer.last_channel - 1] += 1.0
except IndexError:
logging.error(
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
# normalize
aps_per_chan = [e / num_aps for e in aps_per_chan]
sta_per_chan = [e / num_sta for e in sta_per_chan]
peers_per_chan = [e / num_peers for e in peers_per_chan]
self._observation = {
'aps_histogram': aps_per_chan,
'sta_histogram': sta_per_chan,
'peers_histogram': peers_per_chan
}
self._observation_ready.set()
def track(self, deauth=False, assoc=False, handshake=False, hop=False, sleep=False, miss=False, inc=1):
if deauth:
@@ -174,13 +205,12 @@ class Epoch(object):
'temperature': temp
}
# REMOVED: self._epoch_data['reward'] = self._reward(self.epoch + 1, self._epoch_data)
self._epoch_data['reward'] = self._reward(self.epoch + 1, self._epoch_data)
self._epoch_data_ready.set()
logging.info("[epoch %d] duration=%s slept_for=%s blind=%d sad=%d bored=%d inactive=%d active=%d peers=%d tot_bond=%.2f "
"avg_bond=%.2f hops=%d missed=%d deauths=%d assocs=%d handshakes=%d cpu=%d%% mem=%d%% "
"temperature=%dC" % (
"temperature=%dC reward=%s" % (
self.epoch,
utils.secs_to_hhmmss(self.epoch_duration),
utils.secs_to_hhmmss(self.num_slept),
@@ -199,7 +229,8 @@ class Epoch(object):
self.num_shakes,
cpu * 100,
mem * 100,
temp))
temp,
self._epoch_data['reward']))
self.epoch += 1
self.epoch_started = now
+27 -37
View File
@@ -18,13 +18,6 @@ max_queue = 10000
min_sleep = 0.5
max_sleep = 5.0
# FIX B2: constants for run() retry logic
MAX_RETRIES = 10
BACKOFF_BASE = 2.0
# FIX B3: consecutive websocket OSError failures before triggering restart
MAX_WS_ERRORS = 5
def decode(r, verbose_errors=True):
try:
@@ -60,8 +53,20 @@ class Client(object):
async def start_websocket(self, consumer):
s = "%s/events" % self.websocket
# FIX B3: track consecutive OSError failures before escalating to restart
oserror_count = 0
# More modern version of the approach below
# logging.info("Creating new websocket...")
# async for ws in websockets.connect(s):
# try:
# async for msg in ws:
# try:
# await consumer(msg)
# except Exception as ex:
# logging.debug("Error while parsing event (%s)", ex)
# except websockets.exceptions.ConnectionClosedError:
# sleep_time = max_sleep*random.random()
# logging.warning('Retrying websocket connection in {} sec'.format(sleep_time))
# await asyncio.sleep(sleep_time)
# continue
# restarted every time the connection fails
while True:
@@ -69,8 +74,6 @@ class Client(object):
try:
async with websockets.connect(s, ping_interval=ping_interval, ping_timeout=ping_timeout,
max_queue=max_queue) as ws:
# reset error counter on successful connect
oserror_count = 0
# listener loop
while True:
try:
@@ -85,44 +88,31 @@ class Client(object):
await asyncio.wait_for(pong, timeout=ping_timeout)
logging.warning('[bettercap] ping OK, keeping connection alive...')
continue
except Exception:
# FIX B4: replaced bare except with except Exception
sleep_time = min_sleep + max_sleep * random.random()
except:
sleep_time = min_sleep + max_sleep*random.random()
logging.warning('[bettercap] ping error - retrying connection in {} sec'.format(sleep_time))
await asyncio.sleep(sleep_time)
break
except ConnectionRefusedError:
sleep_time = min_sleep + max_sleep * random.random()
sleep_time = min_sleep + max_sleep*random.random()
logging.warning('[bettercap] nobody seems to be listening at the bettercap endpoint...')
logging.warning('[bettercap] retrying connection in {} sec'.format(sleep_time))
await asyncio.sleep(sleep_time)
continue
except OSError:
# FIX B3: count consecutive failures, only restart after MAX_WS_ERRORS
oserror_count += 1
logging.warning('[bettercap] connection to the bettercap endpoint failed (failure %d/%d)...',
oserror_count, MAX_WS_ERRORS)
if oserror_count >= MAX_WS_ERRORS:
logging.error('[bettercap] too many consecutive websocket failures, restarting...')
pwnagotchi.restart("AUTO")
else:
sleep_time = min_sleep + max_sleep * random.random()
logging.warning('[bettercap] retrying websocket in %.1fs', sleep_time)
await asyncio.sleep(sleep_time)
continue
logging.warning('connection to the bettercap endpoint failed...')
pwnagotchi.restart("AUTO")
def run(self, command, verbose_errors=True):
# FIX B2: replace infinite while True loop with bounded retry + exponential backoff
for attempt in range(MAX_RETRIES):
while True:
try:
r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command})
return decode(r, verbose_errors=verbose_errors)
except requests.exceptions.ConnectionError:
sleep_time = min(BACKOFF_BASE ** attempt, 30)
logging.warning(
"[bettercap] can't run my request... connection failed (attempt %d/%d), retrying in %.1fs",
attempt + 1, MAX_RETRIES, sleep_time)
except requests.exceptions.ConnectionError as e:
sleep_time = min_sleep + max_sleep*random.random()
logging.warning("[bettercap] can't run my request... connection to the bettercap endpoint failed...")
logging.warning('[bettercap] retrying run in {} sec'.format(sleep_time))
sleep(sleep_time)
else:
break
logging.critical('[bettercap] unreachable after %d attempts, restarting...', MAX_RETRIES)
pwnagotchi.restart('AUTO')
return decode(r, verbose_errors=verbose_errors)
+129
View File
@@ -6,9 +6,11 @@ import sys
import tomlkit
import requests
import os
import re
import pwnagotchi
from pwnagotchi import utils
from pwnagotchi.google import cmd as google_cmd
from pwnagotchi.plugins import cmd as plugins_cmd
from pwnagotchi import log
from pwnagotchi import fs
@@ -102,6 +104,9 @@ def pwnagotchi_cli():
# Add parsers from plugins_cmd
plugins_cmd.add_parsers(subparsers)
# Add parsers from google_cmd
google_cmd.add_parsers(subparsers)
parser = argparse.ArgumentParser(prog="pwnagotchi")
# pwnagotchi --help
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.toml',
@@ -126,6 +131,8 @@ def pwnagotchi_cli():
help="Print the configuration.")
# Jayofelony added these
parser.add_argument('--wizard', dest="wizard", action="store_true", default=False,
help="Interactive installation of your personal configuration.")
parser.add_argument('--check-update', dest="check_update", action="store_true", default=False,
help="Check for updates on Pwnagotchi. And tells current version.")
parser.add_argument('--donate', dest="donate", action="store_true", default=False,
@@ -140,11 +147,133 @@ def pwnagotchi_cli():
log.setup_logging(args, config)
rc = plugins_cmd.handle_cmd(args, config)
sys.exit(rc)
if google_cmd.used_google_cmd(args):
config = utils.load_config(args)
log.setup_logging(args, config)
rc = google_cmd.handle_cmd(args)
sys.exit(rc)
if args.version:
print(pwnagotchi.__version__)
sys.exit(0)
if args.wizard:
def is_valid_hostname(hostname):
if len(hostname) > 255:
return False
if hostname[-1] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
pwn_restore = input("Do you want to restore the previous configuration?\n\n"
"[Y/N]: ")
if pwn_restore in ('y', 'yes'):
os.system("cp -f /etc/pwnagotchi/config.toml.bak /etc/pwnagotchi/config.toml")
print("Your previous configuration is restored, and I will restart in 5 seconds.")
time.sleep(5)
os.system("service pwnagotchi restart")
else:
pwn_check = input("This will create a new configuration file and overwrite your current backup, are you sure?\n\n"
"[Y/N]: ")
if pwn_check.lower() in ('y', 'yes'):
os.system("mv -f /etc/pwnagotchi/config.toml /etc/pwnagotchi/config.toml.bak")
with open("/etc/pwnagotchi/config.toml", "a+") as f:
f.write("# Do not edit this file if you do not know what you are doing!!!\n\n")
# Set pwnagotchi name
print("Welcome to the interactive installation of your personal Pwnagotchi configuration!\n"
"My name is Jayofelony, how may I call you?\n\n")
pwn_name = input("Pwnagotchi name (no spaces): ")
if pwn_name == "":
pwn_name = "Pwnagotchi"
print("I shall go by Pwnagotchi from now on!")
pwn_name = (f"[main]\n"
f"name = \"{pwn_name}\"\n")
f.write(pwn_name)
else:
if is_valid_hostname(pwn_name):
print(f"I shall go by {pwn_name} from now on!")
pwn_name = (f"[main]\n"
f"name = \"{pwn_name}\"\n")
f.write(pwn_name)
else:
print("You have chosen an invalid name. Please start over.")
exit()
pwn_whitelist = input("How many networks do you want to whitelist? "
"We will also ask a MAC for each network?\n"
"Each SSID and BSSID count as 1 network. \n\n"
"Be sure to use digits as your answer.\n\n"
"Amount of networks: ")
if int(pwn_whitelist) > 0:
f.write("whitelist = [\n")
for x in range(int(pwn_whitelist)):
ssid = input("SSID (Name): ")
bssid = input("BSSID (MAC): ")
f.write(f"\t\"{ssid}\",\n")
if bssid != "":
f.write(f"\t\"{bssid}\",\n")
f.write("]\n")
# set bluetooth tether
pwn_bluetooth = input("Do you want to enable BT-Tether?\n\n"
"[Y/N] ")
if pwn_bluetooth.lower() in ('y', 'yes'):
f.write("[main.plugins.bt-tether]\n"
"enabled = true\n\n")
pwn_bluetooth_phone_name = input("What name uses your phone, check settings?\n\n")
if pwn_bluetooth_phone_name != "":
f.write(f"phone-name = \"{pwn_bluetooth_phone_name}\"\n")
pwn_bluetooth_device = input("What device do you use? android or ios?\n\n"
"Device: ")
if pwn_bluetooth_device != "":
if pwn_bluetooth_device != "android" and pwn_bluetooth_device != "ios":
print("You have chosen an invalid device. Please start over.")
exit()
f.write(f"phone = \"{pwn_bluetooth_device.lower()}\"\n")
if pwn_bluetooth_device == "android":
f.write("ip = \"192.168.44.44\"\n")
elif pwn_bluetooth_device == "ios":
f.write("ip = \"172.20.10.6\"\n")
pwn_bluetooth_mac = input("What is the bluetooth MAC of your device?\n\n"
"MAC: ")
if pwn_bluetooth_mac != "":
f.write(f"mac = \"{pwn_bluetooth_mac}\"\n")
# set up display settings
pwn_display_enabled = input("Do you want to enable a display?\n\n"
"[Y/N]: ")
if pwn_display_enabled.lower() in ('y', 'yes'):
f.write("[ui.display]\n"
"enabled = true\n")
pwn_display_type = input("What display do you use?\n\n"
"Be sure to check for the correct display type @ \n"
"https://github.com/jayofelony/pwnagotchi/blob/master/pwnagotchi/utils.py#L240-L501\n\n"
"Display type: ")
if pwn_display_type != "":
f.write(f"type = \"{pwn_display_type}\"\n")
pwn_display_invert = input("Do you want to invert the display colors?\n"
"N = Black background\n"
"Y = White background\n\n"
"[Y/N]: ")
if pwn_display_invert.lower() in ('y', 'yes'):
f.write("[ui]\n"
"invert = true\n")
f.close()
if pwn_bluetooth.lower() in ('y', 'yes'):
if pwn_bluetooth_device.lower == "android":
print("To visit the webui when connected with your phone, visit: http://192.168.44.44:8080\n"
"Be sure to run `sudo bluetoothctl` to set-up the bluetooth connection for the first time. And read the wiki step 4.\n"
"Your configuration is done, and I will restart in 5 seconds.")
elif pwn_bluetooth_device.lower == "ios":
print("To visit the webui when connected with your phone, visit: http://172.20.10.6:8080\n"
"Your configuration is done, and I will restart in 5 seconds.")
else:
print("Your configuration is done, and I will restart in 5 seconds.")
time.sleep(5)
os.system("service pwnagotchi restart")
else:
print("Ok, doing nothing.")
sys.exit(0)
if args.donate:
print("Donations can be made @ \n "
"https://github.com/sponsors/jayofelony \n\n"
+43 -35
View File
@@ -21,14 +21,34 @@ custom_plugin_repos = [
"https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip",
"https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip"
]
custom_plugins = "/etc/pwnagotchi/custom-plugins/"
custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/"
[main.plugins.auto-tune]
enabled = true
[main.plugins.auto_backup] #More options availble https://github.com/wpa-2/Pwnagotchi-Plugins/blob/main/auto_backup_config.md
enabled = true
backup_location = "/etc/pwnagotchi/backups"
[main.plugins.auto_backup]
enabled = false
interval = "daily" # or "hourly", or a number (minutes)
max_tries = 0
backup_location = "/home/pi/"
files = [
"/root/settings.yaml",
"/root/client_secrets.json",
"/root/.api-report.json",
"/root/.ssh",
"/root/.bashrc",
"/root/.profile",
"/home/pi/handshakes",
"/root/peers",
"/etc/pwnagotchi/",
"/usr/local/share/pwnagotchi/custom-plugins",
"/etc/ssh/",
"/home/pi/.bashrc",
"/home/pi/.profile",
"/home/pi/.wpa_sec_uploads"
]
exclude = [ "/etc/pwnagotchi/logs/*"]
commands = [ "tar cf {backup_file} {files}"]
[main.plugins.auto-update]
enabled = true
@@ -37,21 +57,13 @@ interval = 1
token = "" # Create a personal access token (classic) with scope set to public_repo to use the GitHub API
[main.plugins.bt-tether]
enabled = false # Enable the plugin
# Display Settings
show_on_screen = true # Master switch: enable/disable all on-screen display (default: true)
show_mini_status = true # Show compact mini status indicator (single letter) (default: true)
mini_status_position = [110, 0] # Position [x, y] for mini status (default: [110, 0])
show_detailed_status = true # Show detailed status line with IP (default: true)
detailed_status_position = [0, 82] # Position for detailed status (default: [0, 82])
# Auto-Reconnect Settings
auto_reconnect = true # Automatically reconnect when connection drops (default: true)
reconnect_interval = 60 # Check connection every N seconds (default: 60)
reconnect_failure_cooldown = 300 # Cooldown after max failures in seconds (default: 300 = 5 minutes)
# Discord Notifications (Optional)
discord_webhook_url = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL" # Send IP notifications to Discord (optional)
enabled = false
phone-name = "" # name as shown on the phone i.e. "Pwnagotchi's Phone"
mac = ""
phone = "" # android or ios
ip = "" # optional, default : 192.168.44.2 if android or 172.20.10.2 if ios
gateway = "" #optional, default : 192.168.44.1 if android or 172.20.10.2 if ios
dns = "8.8.8.8 1.1.1.1" # optional, default (google): "8.8.8.8 1.1.1.1". Consider using anonymous DNS like OpenNic :-)
[main.plugins.fix_services]
enabled = true
@@ -59,6 +71,12 @@ enabled = true
[main.plugins.cache]
enabled = true
[main.plugins.gdrivesync]
enabled = false
backupfiles = [""]
backup_folder = "PwnagotchiBackups"
interval = 1
[main.plugins.gpio_buttons]
enabled = false
@@ -124,9 +142,6 @@ shutdown = 2
[main.plugins.webcfg]
enabled = true
[main.plugins.pwnstore_ui]
enabled = true
[main.plugins.webgpsmap]
enabled = false
@@ -198,18 +213,17 @@ cool = ["(⌐■_■)", "(단__단)"]
happy = ["(•‿‿•)", "(^‿‿^)", "(^◡◡^)"]
excited = ["(ᵔ◡◡ᵔ)", "(✜‿‿✜)"]
grateful = ["(^‿‿^)"]
motivated = ["(☼‿‿☼)", "(★‿★)", "(•̀ᴗ•́)"]
demotivated = ["(≖__≖)", "( ̄ヘ ̄)", "(¬、¬)"]
motivated = ["(☼‿‿☼)"]
demotivated = ["(≖__≖)"]
smart = ["(✜‿‿✜)"]
lonely = ["(ب__ب)", "(。•́︿•̀。)", "(︶︹︺)"]
sad = ["(╥☁╥ )", "(╥﹏╥)", "(ಥ﹏ಥ)"]
lonely = ["(ب__ب)"]
sad = ["(╥☁╥ )"]
angry = ["(-_-')", "(⇀__⇀)", "(`___´)"]
friend = ["(♥‿‿♥)", "(♡‿‿♡)", "(♥‿♥ )", "(♥ω♥ )"]
broken = ["(☓‿‿☓)"]
debug = ["(#__#)"]
upload = ["(1__0)", "(1__1)", "(0__1)"]
png = false
scale = 1
position_x = 0
position_y = 34
@@ -223,19 +237,13 @@ origin = ""
port = 8080
on_frame = ""
[ui.web.theme]
# Theme customization for the web interface. Use RGB values for the accent color.
accent_r = 76
accent_g = 175
accent_b = 80
[ui.display]
enabled = false
rotation = 180
type = "waveshare_4"
[bettercap]
handshakes = "/etc/pwnagotchi/handshakes"
handshakes = "/home/pi/handshakes"
silence = [
"ble.device.new",
"ble.device.lost",
@@ -269,4 +277,4 @@ mount = "/var/tmp/pwnagotchi"
size = "10M"
sync = 3600
zram = true
rsync = true
rsync = true
+2
View File
@@ -3,6 +3,7 @@ import re
import tempfile
import contextlib
import shutil
#import _thread
import threading
import logging
@@ -86,6 +87,7 @@ def setup_mounts(config):
logging.debug("[FS] Starting thread to sync %s (interval: %d)",
options['mount'], interval)
threading.Thread(target=m.daemonize, args=(interval,),name="File Sys", daemon=True).start()
#_thread.start_new_thread(m.daemonize, (interval,))
else:
logging.debug("[FS] Not syncing %s, because interval is 0",
options['mount'])
+13 -5
View File
@@ -1,5 +1,6 @@
import subprocess
import requests
import json
import logging
import pwnagotchi
@@ -16,7 +17,7 @@ def is_connected():
r = requests.get(host, headers=headers, timeout=(30.0, 60.0))
if r.json().get('isUp'):
return True
except Exception:
except:
pass
return False
@@ -36,8 +37,7 @@ def call(path, obj=None):
def advertise(enabled=True):
# FIX B1: parentheses around ternary ensure correct string interpolation
return call("/mesh/%s" % ('true' if enabled else 'false'))
return call("/mesh/%s" % 'true' if enabled else 'false')
def set_advertisement_data(data):
@@ -62,8 +62,12 @@ def closest_peer():
def update_data(last_session):
# REMOVED: brain.json loading - file is never created by the noai fork
# REMOVED: AI session fields (train_epochs, avg_reward, min_reward, max_reward) - always zero without AI
brain = {}
try:
with open('/root/brain.json') as fp:
brain = json.load(fp)
except:
pass
enabled = [name for name, options in pwnagotchi.config['main']['plugins'].items() if
'enabled' in options and options['enabled']]
language = pwnagotchi.config['main']['lang']
@@ -73,6 +77,10 @@ def update_data(last_session):
'session': {
'duration': last_session.duration,
'epochs': last_session.epochs,
'train_epochs': last_session.train_epochs,
'avg_reward': last_session.avg_reward,
'min_reward': last_session.min_reward,
'max_reward': last_session.max_reward,
'deauthed': last_session.deauthed,
'associated': last_session.associated,
'handshakes': last_session.handshakes,
+1 -1
View File
@@ -56,7 +56,7 @@ class KeyPair(object):
try:
os.remove(self.priv_path)
os.remove(self.pub_path)
except Exception: # FIX B4: was bare except
except:
pass
# no exception, keys loaded correctly.
+1 -1
View File
@@ -54,7 +54,7 @@ class LastSession(object):
try:
with open(LAST_SESSION_FILE, 'rt') as fp:
saved = fp.read().strip()
except Exception: # FIX B4: was bare except, swallowed KeyboardInterrupt
except:
saved = ''
return saved
+2
View File
@@ -1,3 +1,4 @@
#import _thread
import threading
import logging
import time
@@ -41,6 +42,7 @@ class AsyncAdvertiser(object):
def start_advertising(self):
if self._config['personality']['advertise']:
#_thread.start_new_thread(self._adv_poller, ())
threading.Thread(target=self._adv_poller,args=(), name="Grid", daemon=True).start()
grid.set_advertisement_data(self._advertisement)
+1 -1
View File
@@ -57,7 +57,7 @@ class auto_tune(plugins.Plugin):
"sta_ttl": "Clients older than this will ignored",
}
self.options = dict()
self.presets_dir = os.path.expanduser("/etc/pwnagotchi/auto-tune-presets")
self.presets_dir = os.path.expanduser("~/auto-tune-presets")
self._ensure_presets_dir()
def _ensure_presets_dir(self):
+2 -2
View File
@@ -72,7 +72,7 @@ def check(version, repo, native=True, token=""):
def make_path_for(name):
path = os.path.join("/opt/", name)
path = os.path.join("/home/pi/", name)
if os.path.exists(path):
logging.debug("[update] deleting %s" % path)
shutil.rmtree(path, ignore_errors=True, onerror=None)
@@ -155,7 +155,7 @@ def install(display, update):
try:
# Activate the virtual environment and install the package
subprocess.run(
["bash", "-c", f"source /opt/.pwn/bin/activate && pip install {source_path}"],
["bash", "-c", f"source /home/pi/.pwn/bin/activate && pip install {source_path}"],
check=True
)
+112 -347
View File
@@ -5,383 +5,148 @@ import os
import subprocess
import time
import socket
import threading
import glob
from flask import render_template_string
class AutoBackup(plugins.Plugin):
__author__ = "WPA2"
__version__ = "2.2"
__license__ = "GPL3"
__description__ = (
"Backs up Pwnagotchi configuration and data, keeping recent backups."
)
# Hardcoded defaults for Pwnagotchi
DEFAULT_FILES = [
"/root/.api-report.json",
"/root/.ssh",
"/root/.bashrc",
"/root/.profile",
"/root/peers",
"/etc/pwnagotchi/",
"/etc/ssh/",
]
DEFAULT_INTERVAL_SECONDS = 60 * 60 # 60 minutes
DEFAULT_MAX_BACKUPS = 3
DEFAULT_EXCLUDE = [
"/etc/pwnagotchi/logs/*",
"*.bak",
"*.tmp",
]
__author__ = 'WPA2'
__version__ = '1.1.3'
__license__ = 'GPL3'
__description__ = 'Backs up files when internet is available, with support for excludes.'
def __init__(self):
self.ready = False
self.tries = 0
# Used to throttle repeated log messages for "backup not due yet"
self.last_not_due_logged = 0
self.status_file = "/root/.auto-backup"
# Store the status file path separately.
self.status_file = '/root/.auto-backup'
self.status = StatusFile(self.status_file)
self.lock = threading.Lock()
self.backup_in_progress = False
self.hostname = socket.gethostname()
self._agent = None
def on_loaded(self):
"""Validate only required option: backup_location"""
if (
"backup_location" not in self.options
or self.options["backup_location"] is None
):
logging.error("AUTO-BACKUP: Option 'backup_location' is not set.")
return
self.hostname = socket.gethostname()
# Read config with internal defaults - DO NOT modify self.options
self.files = self.options.get("files", self.DEFAULT_FILES)
self.interval_seconds = self.options.get(
"interval_seconds", self.DEFAULT_INTERVAL_SECONDS
)
self.max_backups = self.options.get(
"max_backups_to_keep", self.DEFAULT_MAX_BACKUPS
)
self.exclude = self.options.get("exclude", self.DEFAULT_EXCLUDE)
self.include = self.options.get("include", [])
# Handle commands: if old format, use correct default internally
commands = self.options.get("commands", ["tar", "czf"])
if isinstance(commands, str) or (
isinstance(commands, list)
and len(commands) == 1
and isinstance(commands[0], str)
and "{" in str(commands)
):
logging.warning(
"AUTO-BACKUP: Old command format detected in config, using default: tar czf"
)
self.commands = ["tar", "czf"]
elif not commands:
self.commands = ["tar", "czf"]
else:
self.commands = commands
# Validate include paths if specified
if self.include:
if not isinstance(self.include, list):
self.include = [self.include]
for path in self.include:
if not os.path.exists(path):
logging.warning(
f"AUTO-BACKUP: include path '{path}' does not exist, will skip if still missing at backup time"
)
required_options = ['files', 'interval', 'backup_location', 'max_tries']
for opt in required_options:
if opt not in self.options or self.options[opt] is None:
logging.error(f"AUTO-BACKUP: Option '{opt}' is not set.")
return
# If no custom command(s) are provided, use the default plain tar command.
# The command includes a placeholder for {excludes} so that if no excludes are set, it will be empty.
if 'commands' not in self.options or not self.options['commands']:
self.options['commands'] = ["tar cf {backup_file} {excludes} {files}"]
self.ready = True
include_msg = (
f", includes: {len(self.include)} additional path(s)"
if self.include
else ""
)
logging.info(
f"AUTO-BACKUP: Plugin loaded for host '{self.hostname}'. Interval: 60min, Backups kept: {self.max_backups}{include_msg}"
)
logging.info("AUTO-BACKUP: Successfully loaded.")
def get_interval_seconds(self):
"""
Convert the interval option into seconds.
Supports:
- "daily" for 24 hours,
- "hourly" for 60 minutes,
- or a numeric value (interpreted as minutes).
"""
interval = self.options['interval']
if isinstance(interval, str):
if interval.lower() == "daily":
return 24 * 60 * 60
elif interval.lower() == "hourly":
return 60 * 60
else:
try:
minutes = float(interval)
return minutes * 60
except ValueError:
logging.error("AUTO-BACKUP: Invalid interval format. Defaulting to daily interval.")
return 24 * 60 * 60
elif isinstance(interval, (int, float)):
return float(interval) * 60
else:
logging.error("AUTO-BACKUP: Unrecognized type for interval. Defaulting to daily interval.")
return 24 * 60 * 60
def is_backup_due(self):
"""Check if backup is due based on interval."""
"""
Determines if enough time has passed since the last backup.
If the status file does not exist, a backup is due.
"""
interval_sec = self.get_interval_seconds()
try:
last_backup = os.path.getmtime(self.status_file)
except OSError:
# Status file doesn't exist—backup is due.
return True
return (time.time() - last_backup) >= self.interval_seconds
now = time.time()
return (now - last_backup) >= interval_sec
def _cleanup_old_backups(self):
"""Deletes the oldest backups if we exceed the limit."""
try:
backup_dir = self.options["backup_location"]
max_keep = self.max_backups
# Filter by this device's hostname
search_pattern = os.path.join(
backup_dir, f"{self.hostname}-backup-*.tar.gz"
)
files = glob.glob(search_pattern)
if not files:
logging.debug("AUTO-BACKUP: No backup files found for cleanup")
return
# Sort files by modification time (oldest first)
files.sort(key=os.path.getmtime)
# Calculate how many to delete
if len(files) > max_keep:
num_to_delete = len(files) - max_keep
logging.info(
f"AUTO-BACKUP: Found {len(files)} backups, keeping {max_keep}, deleting {num_to_delete} old backup(s)..."
)
for old_file in files[:num_to_delete]:
try:
os.remove(old_file)
logging.info(
f"AUTO-BACKUP: Deleted: {os.path.basename(old_file)}"
)
except OSError as e:
logging.error(f"AUTO-BACKUP: Failed to delete {old_file}: {e}")
except Exception as e:
logging.error(f"AUTO-BACKUP: Cleanup error: {e}")
def _run_backup_thread(self, agent, existing_files):
"""Execute backup in separate thread."""
try:
backup_location = self.options["backup_location"]
# Create backup directory if it doesn't exist
if not os.path.exists(backup_location):
try:
os.makedirs(backup_location)
logging.info(
f"AUTO-BACKUP: Created backup directory: {backup_location}"
)
except OSError as e:
logging.error(
f"AUTO-BACKUP: Failed to create backup directory: {e}"
)
return
# Add timestamp to filename
timestamp = time.strftime("%Y%m%d-%H%M%S")
backup_file = os.path.join(
backup_location, f"{self.hostname}-backup-{timestamp}.tar.gz"
)
# Try to update display if agent is available
if agent:
try:
display = agent.view()
display.set("status", "Backing up...")
display.update()
except:
pass
logging.info(f"AUTO-BACKUP: Starting backup to {backup_file}...")
# Build command
command_list = list(self.commands)
command_list.append(backup_file)
# Add exclusions
for pattern in self.exclude:
command_list.append(f"--exclude={pattern}")
# Add files to backup
command_list.extend(existing_files)
# Execute backup command
process = subprocess.Popen(
command_list,
shell=False,
stdin=None,
stdout=open("/dev/null", "w"),
stderr=subprocess.PIPE,
)
_, stderr_output = process.communicate()
if process.returncode != 0:
raise OSError(
f"Backup command failed with code {process.returncode}: {stderr_output.decode('utf-8').strip()}"
)
logging.info(f"AUTO-BACKUP: Backup successful: {backup_file}")
# Run cleanup after successful backup
self._cleanup_old_backups()
# Try to update display if agent is available
if agent:
try:
display = agent.view()
display.set("status", "Backup done!")
display.update()
except:
pass
# Update status file timestamp
self.status.update()
# Reset try counter on success
self.tries = 0
except Exception as e:
self.tries += 1
logging.error(f"AUTO-BACKUP: Backup error (attempt {self.tries}): {e}")
finally:
self.backup_in_progress = False
def on_ready(self, agent):
"""Called when Pwnagotchi is ready. Set up backup scheduler."""
def on_internet_available(self, agent):
if not self.ready:
return
self._agent = agent
# Start background scheduler thread
scheduler_thread = threading.Thread(
target=self._backup_scheduler_loop, daemon=True, name="AutoBackupScheduler"
)
scheduler_thread.start()
logging.info("AUTO-BACKUP: Periodic backup scheduler started")
def on_webhook(self, path, request):
"""Handle web UI requests."""
if request.method == "GET":
if path == "/" or not path:
action_path = (
request.path
if request.path.endswith("/backup")
else "%s/backup" % request.path
)
ret = '<html><head><title>AUTO Backup</title><meta name="csrf_token" content="{{ csrf_token() }}"></head><body>'
ret += "<h1>AUTO Backup</h1>"
ret += "<p>Status: "
if self.backup_in_progress:
ret += "<b>Backup in progress...</b>"
else:
ret += "<b>Ready</b>"
ret += "</p>"
ret += '<form method="POST" action="%s">' % action_path
ret += '<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token() }}">'
ret += '<input type="submit" value="Start Manual Backup" class="btn primary">'
ret += "</form>"
ret += "<hr>"
ret += "<h2>Configuration</h2>"
ret += '<table border="1" cellpadding="5">'
ret += (
"<tr><td><b>Backup Location:</b></td><td>"
+ self.options.get("backup_location", "Not set")
+ "</td></tr>"
)
ret += (
"<tr><td><b>Interval:</b></td><td>"
+ str(self.interval_seconds // 60)
+ " minutes</b></td></tr>"
)
ret += (
"<tr><td><b>Max Backups:</b></td><td>"
+ str(self.max_backups)
+ "</td></tr>"
)
ret += (
"<tr><td><b>Include Paths:</b></td><td>"
+ (", ".join(self.include) if self.include else "None")
+ "</td></tr>"
)
ret += "</table>"
ret += "</body></html>"
return render_template_string(ret)
elif request.method == "POST":
if path == "backup" or path == "/backup":
result = self.manual_backup(self._agent)
ret = '<html><head><title>AUTO Backup</title><meta name="csrf_token" content="{{ csrf_token() }}"></head><body>'
ret += "<h1>AUTO Backup</h1>"
ret += "<p><b>" + result["status"] + "</b></p>"
ret += '<a href="/plugins/auto_backup/">Back</a>'
ret += "</body></html>"
return render_template_string(ret)
return "Not found"
def _backup_scheduler_loop(self):
"""Background thread that checks if backup is due every minute."""
while True:
try:
if self.ready:
agent = getattr(self, "_agent", None)
self._periodic_backup_check(agent)
time.sleep(60)
except Exception as e:
logging.error(f"AUTO-BACKUP: Scheduler error: {e}")
def _get_backup_files(self):
"""Collect all files to backup."""
existing_files = list(filter(os.path.exists, self.files))
if self.include:
for path in self.include:
if os.path.exists(path):
existing_files.append(path)
logging.debug(f"AUTO-BACKUP: Added include path: {path}")
return existing_files
def _periodic_backup_check(self, agent=None):
"""Periodic backup check."""
if agent is None:
agent = getattr(self, "_agent", None)
if not self.ready or self.backup_in_progress:
return
if self.tries >= 3:
if self.options['max_tries'] and self.tries >= self.options['max_tries']:
logging.info("AUTO-BACKUP: Maximum tries reached, skipping backup.")
return
if not self.is_backup_due():
now = time.time()
# Log "backup not due" only once every 600 seconds.
if now - self.last_not_due_logged > 600:
logging.info("AUTO-BACKUP: Backup not due yet based on the interval.")
self.last_not_due_logged = now
return
existing_files = self._get_backup_files()
# Only include files/directories that exist to prevent errors.
existing_files = list(filter(lambda f: os.path.exists(f), self.options['files']))
if not existing_files:
logging.warning("AUTO-BACKUP: No files to backup exist")
logging.warning("AUTO-BACKUP: No files found to backup.")
return
files_to_backup = " ".join(existing_files)
self.backup_in_progress = True
backup_thread = threading.Thread(
target=self._run_backup_thread,
args=(agent, existing_files),
daemon=True,
name="AutoBackupThread",
)
backup_thread.start()
logging.debug("AUTO-BACKUP: Backup thread started")
# Build excludes string if configured.
# Use get() so that if 'exclude' is missing or empty, we default to an empty list.
excludes = ""
exclude_list = self.options.get('exclude', [])
if exclude_list:
for pattern in exclude_list:
excludes += f" --exclude='{pattern}'"
def manual_backup(self, agent):
"""Manually trigger a backup."""
if self.backup_in_progress:
return {"status": "Backup already in progress"}
# Get the backup location from config.
backup_location = self.options['backup_location']
existing_files = self._get_backup_files()
if not existing_files:
return {"status": "No files to backup"}
# Retrieve the global config from agent. If agent.config is callable, call it.
global_config = getattr(agent, 'config', None)
if callable(global_config):
global_config = global_config()
if global_config is None:
global_config = {}
pwnagotchi_name = global_config.get('main', {}).get('name', socket.gethostname())
backup_file = os.path.join(backup_location, f"{pwnagotchi_name}-backup.tar")
self.backup_in_progress = True
backup_thread = threading.Thread(
target=self._run_backup_thread,
args=(agent, existing_files),
daemon=True,
name="AutoBackupThread",
)
backup_thread.start()
logging.info("AUTO-BACKUP: Manual backup triggered")
return {"status": "Backup started - check logs for details"}
try:
display = agent.view()
logging.info("AUTO-BACKUP: Starting backup process...")
display.set('status', 'Backing up ...')
display.update()
# Execute each backup command.
for cmd in self.options['commands']:
formatted_cmd = cmd.format(backup_file=backup_file, files=files_to_backup, excludes=excludes)
logging.info(f"AUTO-BACKUP: Running command: {formatted_cmd}")
process = subprocess.Popen(
formatted_cmd,
shell=True,
stdin=None,
stdout=open("/dev/null", "w"),
stderr=subprocess.STDOUT,
executable="/bin/bash"
)
process.wait()
if process.returncode > 0:
raise OSError(f"Command failed with return code: {process.returncode}")
logging.info(f"AUTO-BACKUP: Backup completed successfully. File created at {backup_file}")
display.set('status', 'Backup done!')
display.update()
self.status.update()
except OSError as os_e:
self.tries += 1
logging.error(f"AUTO-BACKUP: Backup error: {os_e}")
display.set('status', 'Backup failed!')
display.update()
File diff suppressed because it is too large Load Diff
+42 -42
View File
@@ -126,35 +126,25 @@ class FixServices(plugins.Plugin):
def on_bcap_sys_log(self, agent, event):
if self.is_disabled:
return
message = event.get('data', {}).get('Message', '')
if not isinstance(message, str):
return
if 'wifi error while hopping to channel' not in message:
return
# Cooldown: don't spam recon flips when bettercap is unstable
if time.time() - self.LASTTRY < 30:
return
logging.debug("[Fix_Services]SYSLOG MATCH: %s" % message)
logging.debug("[Fix_Services]**** restarting wifi.recon")
try:
result = agent.run("wifi.recon off; wifi.recon on")
if result.get("success"):
logging.debug("[Fix_Services] wifi.recon flip: success!")
self.LASTTRY = time.time()
if hasattr(agent, 'view'):
display = agent.view()
if display:
display.update(force=True, new_data={"status": "Wifi recon flipped!", "face": faces.COOL})
if re.search('wifi error while hopping to channel', event['data']['Message']):
logging.debug("[Fix_Services]SYSLOG MATCH: %s" % event['data']['Message'])
logging.debug("[Fix_Services]**** restarting wifi.recon")
try:
result = agent.run("wifi.recon off; wifi.recon on")
if result["success"]:
logging.debug("[Fix_Services] wifi.recon flip: success!")
if hasattr(agent, 'view'):
display = agent.view()
if display:
display.update(force=True, new_data={"status": "Wifi recon flipped!", "face": faces.COOL})
else:
print("Wifi recon flipped")
else:
logging.debug("Wifi recon flipped")
else:
logging.warning("[Fix_Services] wifi.recon flip: FAILED: %s" % repr(result))
self.LASTTRY = time.time()
logging.warning("[Fix_Services] wifi.recon flip: FAILED: %s" % repr(result))
self._tryTurningItOffAndOnAgain(agent)
except Exception as err:
logging.error("[Fix_Services]SYSLOG wifi.recon flip fail: %s" % err)
self._tryTurningItOffAndOnAgain(agent)
except Exception as err:
logging.error("[Fix_Services]SYSLOG wifi.recon flip fail: %s" % err)
self.LASTTRY = time.time()
self._tryTurningItOffAndOnAgain(agent)
def on_epoch(self, agent, epoch, epoch_data):
if self.is_disabled:
@@ -190,13 +180,13 @@ class FixServices(plugins.Plugin):
try:
result = agent.run("wifi.recon off; wifi.recon on")
if result.get("success"):
if result["success"]:
logging.debug("[Fix_Services] wifi.recon flip: success!")
if display:
display.update(force=True, new_data={"status": "Wifi recon flipped!",
"face": faces.COOL})
else:
logging.debug("Wifi recon flipped")
print("Wifi recon flipped\nthat was easy!")
else:
logging.warning("[Fix_Services] wifi.recon flip: FAILED: %s" % repr(result))
@@ -235,7 +225,7 @@ class FixServices(plugins.Plugin):
if hasattr(agent, 'view'):
display.set('status', 'Restarting pwnagotchi!')
display.update(force=True)
subprocess.run(["systemctl", "restart", "bettercap"], timeout=30)
os.system("systemctl restart bettercap")
pwnagotchi.restart("AUTO")
# Look for pattern 6
@@ -244,7 +234,7 @@ class FixServices(plugins.Plugin):
if hasattr(agent, 'view'):
display.set('status', 'Restarting pwnagotchi!')
display.update(force=True)
subprocess.run(["systemctl", "restart", "bettercap"], timeout=30)
os.system("systemctl restart bettercap")
pwnagotchi.restart("AUTO")
# Look for pattern 7
@@ -252,20 +242,20 @@ class FixServices(plugins.Plugin):
logging.debug("[Fix_Services] Monitor mode failed!")
try:
result = agent.run("wifi.recon off; wifi.recon on")
if result.get("success"):
if result["success"]:
logging.debug("[Fix_Services] wifi.recon flip: success!")
if display:
display.update(force=True, new_data={"status": "Wifi recon flipped!",
"face": faces.COOL})
else:
logging.debug("Wifi recon flipped")
print("Wifi recon flipped\nthat was easy!")
else:
logging.warning("[Fix_Services] wifi.recon flip: FAILED: %s" % repr(result))
except Exception as err:
logging.error("[Fix_Services wifi.recon flip] %s" % repr(err))
else:
logging.debug("[Fix_Services] logs look good")
print("logs look good")
def logPrintView(self, level, message, ui=None, displayData=None, force=True):
try:
@@ -281,7 +271,9 @@ class FixServices(plugins.Plugin):
if ui:
ui.update(force=force, new_data=displayData)
elif displayData and "status" in displayData:
logging.debug(displayData["status"])
print(displayData["status"])
else:
print("[%s] %s" % (level, message))
except Exception as err:
logging.error("[logPrintView] ERROR %s" % repr(err))
@@ -328,7 +320,7 @@ class FixServices(plugins.Plugin):
try:
result = connection.run("wifi.recon off")
if result.get("success"):
if "success" in result:
self.logPrintView("info", "[Fix_Services] wifi.recon off: %s!" % repr(result),
display, {"status": "Wifi recon paused!", "face": faces.COOL})
time.sleep(2)
@@ -378,28 +370,36 @@ class FixServices(plugins.Plugin):
try:
# try accessing mon0 in bettercap
result = connection.run("set wifi.interface wlan0mon")
if result.get("success"):
if "success" in result:
logging.debug("[Fix_Services set wifi.interface wlan0mon worked!")
# stop looping and get back to recon
break
else:
logging.debug(
"[Fix_Services set wifi.interface wlan0mon] failed? %s" % repr(result))
"[Fix_Services set wifi.interfaceface wlan0mon] failed? %s" % repr(result))
except Exception as err:
logging.debug(
"[Fix_Services set wifi.interface wlan0mon] except: %s" % repr(err))
except Exception as cerr: #
logging.error("failed loading wlan0mon attempt #%s: %s" % (tries, repr(cerr)))
if not display:
print("failed loading wlan0mon attempt #%s: %s" % (tries, repr(cerr)))
except Exception as err: # from modprobe
if not display:
print("Failed reloading brcmfmac")
logging.error("[Fix_Services] Failed reloading brcmfmac %s" % repr(err))
except Exception as nope: # from modprobe -r
# fails if already unloaded, so probably fine
logging.error("[Fix_Services #%s modprobe -r] %s" % (tries, repr(nope)))
if not display:
print("[Fix_Services #%s modprobe -r] %s" % (tries, repr(nope)))
pass
tries = tries + 1
if tries < 3:
logging.debug("[Fix_Services] wlan0mon didn't make it. trying again")
if not display:
print(" wlan0mon didn't make it. trying again")
else:
logging.debug("[Fix_Services] wlan0mon loading failed, no choice but to reboot ..")
pwnagotchi.reboot()
@@ -410,7 +410,7 @@ class FixServices(plugins.Plugin):
display.update(force=True, new_data={"status": "And back on again...",
"face": faces.INTENSE})
else:
logging.debug("And back on again...")
print("And back on again...")
logging.debug("[Fix_Services] wlan0mon back up")
else:
self.LASTTRY = time.time()
@@ -422,12 +422,12 @@ class FixServices(plugins.Plugin):
try:
result = connection.run("wifi.clear; wifi.recon on")
if result.get("success"):
if "success" in result: # and result["success"] is True:
if display:
display.update(force=True, new_data={"status": "I can see again! (probably)",
"face": faces.HAPPY})
else:
logging.debug("I can see again")
print("I can see again")
logging.debug("[Fix_Services] wifi.recon on")
self.LASTTRY = time.time() + 120 # 2-minute pause until next time.
else:
+117 -303
View File
@@ -1,13 +1,18 @@
import os
import logging
import threading
from itertools import islice
from time import sleep
from datetime import datetime,timedelta
from pwnagotchi import plugins
from pwnagotchi.utils import StatusFile
from flask import render_template_string
from flask import jsonify
from flask import abort
from flask import Response
INDEX = """
TEMPLATE = """
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
@@ -15,267 +20,83 @@ INDEX = """
{% endblock %}
{% block styles %}
{{ super() }}
<style>
/* Logtail-specific styles - plugin header */
.logtail-header {
margin-bottom: 2rem;
padding: 1.5rem 0;
border-bottom: 1px solid var(--border-color);
}
/* Search/Control Bar */
#divTop {
position: -webkit-sticky;
position: sticky;
top: 0;
display: flex;
gap: 0.5rem;
align-items: center;
width: 100%;
padding: 1rem;
margin-bottom: 1.5rem;
font-size: 0.95rem;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
z-index: 100;
}
#filter {
flex: 1;
min-width: 200px;
}
/* Autoscroll Toggle Wrapper */
#divTop > span {
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
#autoscroll {
width: auto;
height: auto;
margin: 0;
padding: 0;
cursor: pointer;
accent-color: var(--accent);
}
label[for="autoscroll"] {
display: inline;
font-size: 0.85rem;
color: var(--text-main);
font-weight: 400;
margin: 0;
font-family: var(--font-main);
cursor: pointer;
}
/* Table Container */
.table-container {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-md);
margin-bottom: 2rem;
}
table {
table-layout: auto;
width: 100%;
border-collapse: collapse;
background-color: var(--card-bg);
}
thead {
background-color: var(--card-bg);
}
th {
padding: 14px 16px;
text-align: left;
color: var(--accent);
font-weight: 600;
font-family: var(--font-pixel);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.85rem;
border-bottom: 2px solid var(--border-color);
}
td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
color: var(--text-body);
font-size: 0.9rem;
}
tbody tr:hover {
background-color: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.05);
transition: background-color 0.2s ease;
}
tbody tr:last-child td {
border-bottom: none;
}
/* Time column */
td:nth-child(1) {
width: 130px;
font-family: var(--font-pixel);
color: var(--text-muted);
font-size: 0.85rem;
}
/* Level column */
td:nth-child(2) {
width: 80px;
text-align: center;
font-weight: 600;
font-family: var(--font-pixel);
}
/* Message column */
td:nth-child(3) {
flex: 1;
word-break: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* Log Level Coloring */
tr.default td:nth-child(2) {
color: var(--text-main);
}
tr.info td:nth-child(2) {
color: var(--info);
}
tr.warning td:nth-child(2) {
color: #ffa500;
}
tr.error td:nth-child(2) {
color: var(--danger);
}
tr.debug td:nth-child(2) {
color: #b39ddb;
}
/* Responsive Design */
@media screen and (max-width: 768px) {
#divTop {
flex-direction: column;
align-items: stretch;
{{ super() }}
<style>
* {
box-sizing: border-box;
}
#filter {
min-width: 100%;
width: 100%;
font-size: 16px;
padding: 12px 20px 12px 40px;
border: 1px solid #ddd;
margin-bottom: 12px;
}
th, td {
padding: 10px 12px;
font-size: 0.85rem;
}
td:nth-child(1) {
width: 100px;
}
td:nth-child(2) {
width: 65px;
}
}
@media screen and (max-width: 480px) {
#divTop {
padding: 0.75rem;
margin-bottom: 1rem;
}
th, td {
padding: 8px 10px;
font-size: 0.8rem;
}
th {
font-size: 0.75rem;
}
td:nth-child(1) {
width: 75px;
}
td:nth-child(2) {
width: 55px;
}
.table-container {
margin-bottom: 2rem;
}
/* Mobile table display */
table, tr, td {
padding: 0;
border: none;
}
table {
border: none;
border-collapse: collapse;
width: 100%;
border: 1px solid #ddd;
}
tr:first-child, thead, th {
display: none;
border: none;
th, td {
text-align: left;
padding: 12px;
width: 1px;
white-space: nowrap;
}
td:nth-child(2) {
text-align: center;
}
thead, tr:hover {
background-color: #f1f1f1;
}
tr {
float: left;
border-bottom: 1px solid #ddd;
}
div.sticky {
position: -webkit-sticky;
position: sticky;
top: 0;
display: table;
width: 100%;
margin-bottom: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--card-bg);
padding: 0.75rem;
}
td {
float: left;
div.sticky > * {
display: table-cell;
}
div.sticky > span {
width: 1%;
}
div.sticky > input {
width: 100%;
padding: 0.5rem 0;
margin-bottom: 0.25rem;
border: none;
}
td::before {
content: attr(data-label);
display: block;
color: var(--accent);
font-weight: 600;
font-family: var(--font-pixel);
font-size: 0.8rem;
text-transform: uppercase;
margin-bottom: 0.25rem;
letter-spacing: 0.5px;
tr.default {
color: black;
}
}
</style>
tr.info {
color: black;
}
tr.warning {
color: darkorange;
}
tr.error {
color: crimson;
}
tr.debug {
color: blueviolet;
}
.ui-mobile .ui-page-active {
overflow: visible;
overflow-x: visible;
}
</style>
{% endblock %}
{% block script %}
var table = document.getElementById("table").querySelector("tbody");
var filter = document.getElementById("filter");
var table = document.getElementById('table');
var filter = document.getElementById('filter');
var filterVal = filter.value.toUpperCase();
var xhr = new XMLHttpRequest();
xhr.open("GET", "logtail/stream");
xhr.open('GET', '{{ url_for('plugins') }}/logtail/stream');
xhr.send();
var position = 0;
var data;
@@ -289,39 +110,39 @@ INDEX = """
filterVal = filter.value.toUpperCase();
messages.slice(position, -1).forEach(function(value) {
if (value.charAt(0) != "[") {
if (value.charAt(0) != '[') {
msg = value;
time = "";
level = "";
time = '';
level = '';
} else {
data = value.split("]");
time = data.shift() + "]";
level = data.shift() + "]";
msg = data.join("]");
data = value.split(']');
time = data.shift() + ']';
level = data.shift() + ']';
msg = data.join(']');
switch(level) {
case " [INFO]":
colorClass = "info";
case ' [INFO]':
colorClass = 'info';
break;
case " [WARNING]":
colorClass = "warning";
case ' [WARNING]':
colorClass = 'warning';
break;
case " [ERROR]":
colorClass = "error";
case ' [ERROR]':
colorClass = 'error';
break;
case " [DEBUG]":
colorClass = "debug";
case ' [DEBUG]':
colorClass = 'debug';
break;
default:
colorClass = "default";
colorClass = 'default';
break;
}
}
var tr = document.createElement("tr");
var td1 = document.createElement("td");
var td2 = document.createElement("td");
var td3 = document.createElement("td");
var tr = document.createElement('tr');
var td1 = document.createElement('td');
var td2 = document.createElement('td');
var td3 = document.createElement('td');
td1.textContent = time;
td2.textContent = level;
@@ -348,7 +169,7 @@ INDEX = """
}
var timer;
var scrollElm = document.getElementById("autoscroll");
var scrollElm = document.getElementById('autoscroll');
timer = setInterval(function() {
handleNewData();
if (scrollElm.checked) {
@@ -360,7 +181,7 @@ INDEX = """
}, 1000);
var typingTimer;
var doneTypingInterval = 500;
var doneTypingInterval = 1000;
filter.onkeyup = function() {
clearTimeout(typingTimer);
@@ -372,56 +193,50 @@ INDEX = """
}
function doneTyping() {
document.body.style.cursor = 'progress';
var tr, tds, td, i, txtValue;
filterVal = filter.value.toUpperCase();
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
for (i = 1; i < tr.length; i++) {
txtValue = tr[i].textContent || tr[i].innerText;
if (filterVal.length === 0 || txtValue.toUpperCase().indexOf(filterVal) > -1) {
if (txtValue.toUpperCase().indexOf(filterVal) > -1) {
tr[i].style.display = "table-row";
} else {
tr[i].style.display = "none";
}
}
document.body.style.cursor = 'default';
}
{% endblock %}
{% block content %}
<div class="logtail-header">
<h2>System Log</h2>
<p>Real-time log viewer with filtering and auto-scroll capabilities</p>
</div>
<div id="divTop">
<input type="text" id="filter" placeholder="Filter logs..." title="Type to filter log messages">
<span>
<input type="checkbox" id="autoscroll" checked>
<label for="autoscroll">Auto-scroll</label>
</span>
</div>
<div class="table-container">
<table id="table">
<thead>
<tr>
<th>Time</th>
<th>Level</th>
<th>Message</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="sticky">
<input type="text" id="filter" placeholder="Search for ..." title="Type in a filter">
<span><input checked type="checkbox" id="autoscroll"></span>
<span><label for="autoscroll"> Autoscroll to bottom</label><br></span>
</div>
<table id="table">
<thead>
<th>
Time
</th>
<th>
Level
</th>
<th>
Message
</th>
</thead>
</table>
{% endblock %}
"""
class Logtail(plugins.Plugin):
__author__ = "33197631+dadav@users.noreply.github.com"
__version__ = "0.1.0"
__license__ = "GPL3"
__description__ = "This plugin tails the logfile."
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '0.1.0'
__license__ = 'GPL3'
__description__ = 'This plugin tails the logfile.'
def __init__(self):
self.lock = threading.Lock()
@@ -443,16 +258,15 @@ class Logtail(plugins.Plugin):
return "Plugin not ready"
if not path or path == "/":
return render_template_string(INDEX)
if path == "stream":
return render_template_string(TEMPLATE)
if path == 'stream':
def generate():
with open(self.config["main"]["log"]["path"]) as f:
yield "".join(f.readlines()[-self.options.get("max-lines", 4096) :])
with open(self.config['main']['log']['path']) as f:
yield ''.join(f.readlines()[-self.options.get('max-lines', 4096):])
while True:
yield f.readline()
return Response(generate(), mimetype="text/plain")
return Response(generate(), mimetype='text/plain')
abort(404)
+199 -675
View File
@@ -2,7 +2,7 @@ import os
import logging
import threading
from time import sleep
from datetime import datetime, timedelta
from datetime import datetime,timedelta
from pwnagotchi import plugins
from pwnagotchi.utils import StatusFile
from flask import render_template_string
@@ -12,728 +12,252 @@ TEMPLATE = """
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
Session Stats
Session stats
{% endblock %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="/css/jquery.jqplot.min.css"/>
<link rel="stylesheet" href="/css/jquery.jqplot.css"/>
<style>
/* Session Stats Header */
.stats-header {
margin-bottom: 2rem;
padding: 1.5rem 0;
border-bottom: 1px solid var(--border-color);
}
/* Session Selector */
.session-selector {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
background-color: var(--card-bg);
padding: 1rem;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.session-selector label {
display: inline;
font-size: 0.9rem;
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
margin: 0;
font-family: var(--font-pixel);
}
#session {
flex: 1;
min-width: 200px;
}
/* Stats Container */
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 3rem;
}
.stat-card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
box-shadow: var(--shadow-md);
}
.stat-card:hover {
border-color: var(--accent);
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.1);
}
.stat-label {
font-size: 0.85rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
font-family: var(--font-pixel);
font-weight: 600;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2.2rem;
font-weight: bold;
color: var(--accent);
font-family: var(--font-pixel);
line-height: 1;
letter-spacing: 1px;
}
/* Charts Container */
.charts-section {
margin-top: 3rem;
}
.charts-section h3 {
margin: 0 0 2rem 0;
color: var(--accent);
font-family: var(--font-pixel);
font-size: 1.4rem;
text-transform: uppercase;
letter-spacing: 1px;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 2rem;
}
div.chart {
height: 300px;
height: 400px;
width: 100%;
position: relative;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
box-shadow: var(--shadow-md);
transition: all 0.3s ease;
overflow-x: auto;
overflow-y: hidden;
}
div.chart:hover {
border-color: var(--accent);
box-shadow: 0 8px 25px rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.1);
}
div.chart canvas {
max-height: 250px;
display: block;
min-width: 100%;
}
.chart-hint {
font-size: 0.75rem;
color: var(--text-muted);
text-align: center;
margin-top: 0.5rem;
font-family: var(--font-main);
}
/* Responsive Design */
@media (max-width: 768px) {
.stats-container {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.stat-card {
padding: 1rem;
}
.stat-value {
font-size: 1.8rem;
}
.stat-label {
font-size: 0.8rem;
}
.charts-grid {
grid-template-columns: 1fr;
}
div.chart {
height: 250px;
}
}
@media (max-width: 480px) {
.session-selector {
flex-direction: column;
align-items: stretch;
}
.session-selector label {
display: block;
margin-bottom: 0.5rem;
}
#session {
width: 100%;
}
.stats-container {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.stat-card {
padding: 0.75rem;
}
.stat-value {
font-size: 1.5rem;
}
.stat-label {
font-size: 0.75rem;
}
.charts-grid {
gap: 1rem;
}
div.chart {
height: 200px;
padding: 0.75rem;
}
div#session {
width: 100%;
}
</style>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="/js/plugins/chart.min.js"></script>
<script type="text/javascript" src="/js/jquery.jqplot.min.js"></script>
<script type="text/javascript" src="/js/jquery.jqplot.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.mobile.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.json2.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.dateAxisRenderer.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.highlighter.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.cursor.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.enhancedLegendRenderer.js"></script>
{% endblock %}
{% block script %}
const charts = {};
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error(`Failed to fetch ${url}:`, error);
return { values: [], labels: [] };
}
}
function getTransparentColor(color) {
// Convert rgb() to rgba() with 0.2 opacity, or append hex opacity
if (color.startsWith('rgb(')) {
return color.replace('rgb(', 'rgba(').replace(')', ', 0.2)');
}
return color + '33'; // hex format
}
function createChart(elementId, title, data) {
const container = document.getElementById(elementId);
if (!container || !data.values || data.values.length === 0) return;
if (charts[elementId]) charts[elementId].destroy();
const allLabels = new Set();
data.values.forEach(values => {
values.forEach(([ts]) => allLabels.add(ts));
});
const labels = Array.from(allLabels).sort();
const datasets = data.values.map((values, index) => {
const color = getChartColor(index);
const valueMap = Object.fromEntries(values);
const chartData = labels.map(ts => valueMap[ts] ?? null);
return {
label: data.labels[index],
data: chartData,
borderColor: color,
backgroundColor: getTransparentColor(color),
borderWidth: 2,
fill: true,
tension: 0.1,
pointRadius: 1,
pointHoverRadius: 4
};
});
let canvas = container.querySelector('canvas');
if (!canvas) {
canvas = document.createElement('canvas');
container.appendChild(canvas);
}
// Calculate required width based on number of data points
const dataPointCount = labels.length;
const minPixelsPerPoint = 50; // minimum pixels for each data point
const calculatedWidth = Math.max(container.clientWidth, dataPointCount * minPixelsPerPoint);
// Set canvas dimensions explicitly
canvas.width = calculatedWidth;
canvas.height = 250;
charts[elementId] = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
responsive: false,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: title,
font: { size: 16, family: 'var(--font-pixel)', weight: 'bold' },
color: '#fff',
padding: 20
},
legend: {
display: true,
position: 'bottom',
labels: {
color: '#fff',
font: { family: 'var(--font-main)', size: 12, weight: 'bold' },
padding: 15,
boxHeight: 4
}
},
tooltip: {
backgroundColor: '#000',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'var(--accent)',
borderWidth: 1
}
},
scales: {
x: {
grid: {
color: '#333',
display: true
},
ticks: {
color: '#fff',
font: { family: 'var(--font-main)', size: 11, weight: 'bold' },
maxTicksLimit: 8
}
},
y: {
grid: {
color: '#333',
display: true
},
ticks: {
color: '#fff',
font: { family: 'var(--font-main)', size: 11, weight: 'bold' }
}
}
}
$(document).ready(function(){
var ajaxDataRenderer = function(url, plot, options) {
var ret = null;
$.ajax({
async: false,
url: url,
dataType:"json",
success: function(data) {
ret = data;
}
});
return ret;
};
// Add hint text if it doesn't exist
if (!container.querySelector('.chart-hint')) {
const hint = document.createElement('div');
hint.className = 'chart-hint';
hint.textContent = 'Scroll left/right to view more data';
container.appendChild(hint);
}
}
function getChartColor(index) {
// Get accent color from CSS root variables
const root = document.documentElement;
const r = getComputedStyle(root).getPropertyValue('--accent-r').trim();
const g = getComputedStyle(root).getPropertyValue('--accent-g').trim();
const b = getComputedStyle(root).getPropertyValue('--accent-b').trim();
const accentColor = `rgb(${r},${g},${b})`;
// Use accent color as first chart color, then secondary colors
const colors = [accentColor, '#ff9800', '#2196f3', '#f44336', '#9c27b0', '#00bcd4'];
return colors[index % colors.length];
}
async function updateStats() {
const sessionSelect = document.getElementById("session");
const session = sessionSelect?.options[sessionSelect.selectedIndex]?.text || 'Current';
const params = session === 'Current' ? '' : '?session=' + encodeURIComponent(session);
// Fetch summary stats
const summary = await fetchData('/plugins/session-stats/summary' + params);
if (summary.networks !== undefined) {
document.getElementById('stat_networks').textContent = summary.networks;
document.getElementById('stat_handshakes').textContent = summary.handshakes;
document.getElementById('stat_deauths').textContent = summary.deauths;
document.getElementById('stat_duration').textContent = summary.duration;
document.getElementById('stat_temp').textContent = summary.temp || '0°C';
document.getElementById('stat_mem').textContent = summary.mem || '0%';
document.getElementById('stat_cpu').textContent = summary.cpu || '0%';
}
// Fetch chart data
const chartConfigs = [
{ endpoint: 'networks', id: 'chart_networks', title: 'Networks Captured' },
{ endpoint: 'handshakes', id: 'chart_handshakes', title: 'Handshakes Captured' },
{ endpoint: 'deauths', id: 'chart_deauths', title: 'Deauthentications Sent' },
{ endpoint: 'temp', id: 'chart_temp', title: 'Temperature (°C)' },
{ endpoint: 'mem', id: 'chart_mem', title: 'Memory Usage (%)' },
{ endpoint: 'cpu', id: 'chart_cpu', title: 'CPU Load (%)' }
];
for (const config of chartConfigs) {
const data = await fetchData('/plugins/session-stats/' + config.endpoint + params);
createChart(config.id, config.title, data);
}
}
async function loadSessionFiles() {
const data = await fetchData('/plugins/session-stats/sessions');
const select = document.getElementById("session");
data.files?.forEach(file => {
const option = document.createElement("option");
option.text = file;
select.appendChild(option);
function loadFiles(url, elm) {
var data = ajaxDataRenderer(url);
var x = document.getElementById(elm);
$.each(data['files'], function( index, value ) {
var option = document.createElement("option");
option.text = value;
x.add(option);
});
select.addEventListener('change', updateStats);
}
document.addEventListener('DOMContentLoaded', () => {
loadSessionFiles();
updateStats();
setInterval(updateStats, 30000);
function loadData(url, elm, title, fill) {
var data = ajaxDataRenderer(url);
var plot_os = $.jqplot(elm, data.values,{
title: title,
stackSeries: fill,
seriesDefaults: {
showMarker: !fill,
fill: fill,
fillAndStroke: fill
},
legend: {
show: true,
renderer: $.jqplot.EnhancedLegendRenderer,
placement: 'outsideGrid',
labels: data.labels,
location: 's',
rendererOptions: {
numberRows: '2',
},
rowSpacing: '0px'
},
axes:{
xaxis:{
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
tickOptions:{formatString:'%.2f'}
}
},
highlighter: {
show: true,
sizeAdjust: 7.5
},
cursor:{
show: true,
tooltipLocation:'sw'
}
}).replot({
axes:{
xaxis:{
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
tickOptions:{formatString:'%.2f'}
}
}
});
}
function loadSessionFiles() {
loadFiles('/plugins/session-stats/session', 'session');
$("#session").change(function() {
loadSessionData();
});
}
function loadSessionData() {
var x = document.getElementById("session");
var session = x.options[x.selectedIndex].text;
loadData('/plugins/session-stats/os' + '?session=' + session, 'chart_os', 'OS', false)
loadData('/plugins/session-stats/temp' + '?session=' + session, 'chart_temp', 'Temp', false)
loadData('/plugins/session-stats/wifi' + '?session=' + session, 'chart_wifi', 'Wifi', true)
loadData('/plugins/session-stats/duration' + '?session=' + session, 'chart_duration', 'Sleeping', true)
loadData('/plugins/session-stats/reward' + '?session=' + session, 'chart_reward', 'Reward', false)
loadData('/plugins/session-stats/epoch' + '?session=' + session, 'chart_epoch', 'Epochs', false)
}
loadSessionFiles();
loadSessionData();
setInterval(loadSessionData, 60000);
});
{% endblock %}
{% block content %}
<div class="stats-header">
<h2>Session Statistics</h2>
<p>Real-time monitoring of WiFi capture metrics and system performance</p>
</div>
<div class="session-selector">
<label for="session">Session:</label>
<select id="session">
<option selected>Current</option>
</select>
</div>
<div class="stats-container">
<div class="stat-card">
<div class="stat-label">Networks Captured</div>
<div class="stat-value" id="stat_networks">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Handshakes</div>
<div class="stat-value" id="stat_handshakes">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Deauths Sent</div>
<div class="stat-value" id="stat_deauths">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Session Duration</div>
<div class="stat-value" id="stat_duration">0s</div>
</div>
<div class="stat-card">
<div class="stat-label">Temperature</div>
<div class="stat-value" id="stat_temp">0°C</div>
</div>
<div class="stat-card">
<div class="stat-label">Memory Usage</div>
<div class="stat-value" id="stat_mem">0%</div>
</div>
<div class="stat-card">
<div class="stat-label">CPU Load</div>
<div class="stat-value" id="stat_cpu">0%</div>
</div>
</div>
<div class="charts-section">
<h3>Trend Charts</h3>
<div class="charts-grid">
<div id="chart_networks" class="chart"><canvas></canvas></div>
<div id="chart_handshakes" class="chart"><canvas></canvas></div>
<div id="chart_deauths" class="chart"><canvas></canvas></div>
<div id="chart_temp" class="chart"><canvas></canvas></div>
<div id="chart_mem" class="chart"><canvas></canvas></div>
<div id="chart_cpu" class="chart"><canvas></canvas></div>
</div>
</div>
<select id="session">
<option selected>Current</option>
</select>
<div id="chart_os" class="chart"></div>
<div id="chart_temp" class="chart"></div>
<div id="chart_wifi" class="chart"></div>
<div id="chart_duration" class="chart"></div>
<div id="chart_reward" class="chart"></div>
<div id="chart_epoch" class="chart"></div>
{% endblock %}
"""
class GhettoClock:
def __init__(self):
self.lock = threading.Lock()
self._track = datetime.now()
self._counter_thread = threading.Thread(target=self.counter)
self._counter_thread.daemon = True
self._counter_thread.start()
def counter(self):
while True:
with self.lock:
self._track += timedelta(seconds=1)
sleep(1)
def now(self):
with self.lock:
return self._track
class SessionStats(plugins.Plugin):
__author__ = "33197631+dadav@users.noreply.github.com modified by wsvdmeer"
__version__ = "0.2.0"
__license__ = "GPL3"
__description__ = (
"Displays WiFi capture stats including networks, handshakes, and deauths."
)
DEFAULT_UPDATE_INTERVAL = 15 # RPi-friendly: 15 sec = 4 disk writes/min
DEFAULT_SAVE_PATH = (
"/etc/pwnagotchi/sessions/" # Standard location for user data
)
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '0.1.0'
__license__ = 'GPL3'
__description__ = 'This plugin displays stats of the current session.'
def __init__(self):
self.lock = threading.Lock()
self.options = dict()
self.stats = dict()
self.initialized = False
self.running = False
self.agent = None
self.realtime_thread = None
self.clock = GhettoClock()
def on_loaded(self):
# Use default save path if not configured
save_dir = self.options.get("save_directory", self.DEFAULT_SAVE_PATH)
os.makedirs(save_dir, exist_ok=True)
self.session_name = "stats_{}.json".format(
datetime.now().strftime("%Y_%m_%d_%H_%M")
)
self.session = StatusFile(
os.path.join(save_dir, self.session_name),
data_format="json",
)
logging.info(f"Session-stats plugin loaded. Saving to: {save_dir}")
# Try to load historical data from the most recent previous session
try:
session_files = sorted(
[
f
for f in os.listdir(save_dir)
if f.startswith("stats_") and f.endswith(".json")
]
)
if len(session_files) > 1: # More than just the current session
last_session_file = session_files[
-2
] # Second to last is the previous session
last_session_path = os.path.join(save_dir, last_session_file)
last_session = StatusFile(last_session_path, data_format="json")
historical_data = last_session.data_field_or("data", default=dict())
if historical_data:
self.stats.update(historical_data)
logging.info(
f"Loaded {len(historical_data)} historical data points from {last_session_file}"
)
except Exception as e:
logging.warning(f"Could not load historical session data: {e}")
self.running = True
self.realtime_thread = threading.Thread(
target=self._realtime_loop, daemon=True, name="session-stats-realtime"
)
self.realtime_thread.start()
logging.info("Session-stats realtime collection thread started.")
def on_ready(self, agent):
"""Called when the agent is ready - store reference for realtime stats"""
self.agent = agent
logging.debug("Session-stats agent reference captured")
def on_ui_setup(self, ui):
"""Get agent reference from UI when UI is set up"""
if hasattr(ui, "_agent") and not self.agent:
self.agent = ui._agent
logging.debug("Session-stats agent reference captured from UI")
def on_unload(self):
self.running = False
if self.realtime_thread and self.realtime_thread.is_alive():
self.realtime_thread.join(timeout=5)
logging.info("Session-stats plugin unloaded.")
def _collect_stats(self):
"""Collect current stats from agent (called both from realtime loop and epochs)"""
if not self.agent:
return None
try:
networks = len(self.agent._access_points)
handshakes = len(self.agent._handshakes)
stats_entry = {
"num_peers": networks,
"num_handshakes": handshakes,
"num_deauths": 0, # Will be updated if on_epoch is called
"temperature": 0, # Will be updated from system or epoch data
"mem_usage": 0,
"cpu_load": 0,
}
return stats_entry
except Exception as e:
logging.warning(f"Could not collect stats: {e}")
return None
def _realtime_loop(self):
"""Background thread that collects stats periodically without waiting for epochs"""
update_interval = self.options.get(
"update_interval", self.DEFAULT_UPDATE_INTERVAL
)
agent_acquired = False
while self.running:
try:
sleep(update_interval)
if not self.agent:
if not agent_acquired:
logging.debug(
"Session-stats realtime loop: waiting for agent reference..."
)
continue
if not agent_acquired:
logging.info(
"Session-stats realtime loop: agent acquired, starting stats collection"
)
agent_acquired = True
with self.lock:
stats_entry = self._collect_stats()
if stats_entry:
# Use high-resolution timestamp
current_time = datetime.now()
timestamp = current_time.strftime("%H:%M:%S.%f")[:-3]
# Only update if this is new data or initialized
if not self.initialized:
self.stats[timestamp] = stats_entry
self.initialized = True
self.session.update(data={"data": self.stats})
logging.info(
f"Session-stats initialized (realtime): {stats_entry['num_peers']} networks, "
f"{stats_entry['num_handshakes']} handshakes"
)
else:
# Add to stats if data changed
last_stats = (
list(self.stats.values())[-1] if self.stats else None
)
if last_stats and (
stats_entry["num_peers"]
!= last_stats.get("num_peers", 0)
or stats_entry["num_handshakes"]
!= last_stats.get("num_handshakes", 0)
):
self.stats[timestamp] = stats_entry
self.session.update(data={"data": self.stats})
except Exception as e:
logging.warning(f"Error in realtime stats loop: {e}")
"""
Gets called when the plugin gets loaded
"""
# this has to happen in "loaded" because the options are not yet
# available in the __init__
os.makedirs(self.options['save_directory'], exist_ok=True)
self.session_name = "stats_{}.json".format(self.clock.now().strftime("%Y_%m_%d_%H_%M"))
self.session = StatusFile(os.path.join(self.options['save_directory'],
self.session_name),
data_format='json')
logging.info("Session-stats plugin loaded.")
def on_epoch(self, agent, epoch, epoch_data):
# Store agent reference if not already set
if not self.agent:
self.agent = agent
logging.debug("Session-stats agent reference captured from epoch callback")
else:
self.agent = agent
"""
Save the epoch_data to self.stats
"""
with self.lock:
# Collect epoch-specific system metrics
stats_entry = {
"num_peers": len(agent._access_points),
"num_handshakes": len(agent._handshakes),
"num_deauths": epoch_data.get("num_deauths", 0),
"temperature": epoch_data.get("temperature", 0),
"mem_usage": epoch_data.get("mem_usage", 0),
"cpu_load": epoch_data.get("cpu_load", 0),
}
self.stats[self.clock.now().strftime("%H:%M:%S")] = epoch_data
self.session.update(data={'data': self.stats})
# Add epoch data with high-resolution timestamp
current_time = datetime.now()
timestamp = current_time.strftime("%H:%M:%S.%f")[:-3]
self.stats[timestamp] = stats_entry
self.session.update(data={"data": self.stats})
if not self.initialized:
self.initialized = True
logging.info(
f"Session-stats epoch update: {len(agent._access_points)} networks, "
f"{len(agent._handshakes)} handshakes"
)
@staticmethod
def extract_key_values(data, subkeys):
result = dict()
result['values'] = list()
result['labels'] = subkeys
for plot_key in subkeys:
v = [ [ts,d[plot_key]] for ts, d in data.items()]
result['values'].append(v)
return result
def on_webhook(self, path, request):
if not path or path == "/":
return render_template_string(TEMPLATE)
session_param = request.args.get("session")
save_dir = self.options.get("save_directory", self.DEFAULT_SAVE_PATH)
session_param = request.args.get('session')
if path == "os":
extract_keys = ['cpu_load','mem_usage',]
elif path == "temp":
extract_keys = ['temperature']
elif path == "wifi":
extract_keys = [
'missed_interactions',
'num_hops',
'num_peers',
'tot_bond',
'avg_bond',
'num_deauths',
'num_associations',
'num_handshakes',
]
elif path == "duration":
extract_keys = [
'duration_secs',
'slept_for_secs',
]
elif path == "reward":
extract_keys = [
'reward',
]
elif path == "epoch":
extract_keys = [
'active_for_epochs',
]
elif path == "session":
return jsonify({'files': os.listdir(self.options['save_directory'])})
with self.lock:
data = self.stats
if session_param and session_param != "Current":
file_stats = StatusFile(
os.path.join(save_dir, session_param),
data_format="json",
)
data = file_stats.data_field_or("data", default=dict())
if path == "summary":
total_networks = len(set(d.get("num_peers", 0) for d in data.values()))
total_handshakes = sum(d.get("num_handshakes", 0) for d in data.values())
total_deauths = sum(d.get("num_deauths", 0) for d in data.values())
duration = len(data) if data else 0
temp = max([d.get("temperature", 0) for d in data.values()], default=0)
mem = max([d.get("mem_usage", 0) for d in data.values()], default=0)
cpu = max([d.get("cpu_load", 0) for d in data.values()], default=0)
return jsonify(
{
"networks": total_networks,
"handshakes": total_handshakes,
"deauths": total_deauths,
"duration": f"{duration}s",
"temp": f"{temp:.1f}°C",
"mem": f"{mem:.1f}%",
"cpu": f"{cpu:.1f}%",
}
)
elif path == "networks":
return jsonify(self._extract_key_values(data, ["num_peers"]))
elif path == "handshakes":
return jsonify(self._extract_key_values(data, ["num_handshakes"]))
elif path == "deauths":
return jsonify(self._extract_key_values(data, ["num_deauths"]))
elif path == "temp":
return jsonify(self._extract_key_values(data, ["temperature"]))
elif path == "mem":
return jsonify(self._extract_key_values(data, ["mem_usage"]))
elif path == "cpu":
return jsonify(self._extract_key_values(data, ["cpu_load"]))
elif path == "sessions":
return jsonify({"files": os.listdir(save_dir)})
return jsonify({"error": "Unknown path"})
@staticmethod
def _extract_key_values(data, subkeys):
result = {"values": [], "labels": subkeys}
for plot_key in subkeys:
v = [[ts, d.get(plot_key, 0)] for ts, d in data.items()]
result["values"].append(v)
return result
if session_param and session_param != 'Current':
file_stats = StatusFile(os.path.join(self.options['save_directory'], session_param), data_format='json')
data = file_stats.data_field_or('data', default=dict())
return jsonify(SessionStats.extract_key_values(data, extract_keys))
+111 -261
View File
@@ -1,7 +1,7 @@
import logging
import json
import toml
import threading # FIX B5: replaced _thread with threading
import _thread
import pwnagotchi
from pwnagotchi import restart, plugins
from pwnagotchi.utils import save_config, merge_config
@@ -18,315 +18,174 @@ INDEX = """
{% block meta %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=0" />
{% endblock %}{% block styles %}
{% endblock %}
{% block styles %}
{{ super() }}
<style>
/* Webcfg-specific styles - plugin header */
.webcfg-header {
margin-bottom: 2rem;
padding: 1.5rem 0;
border-bottom: 1px solid var(--border-color);
}
/* Search/Control Bar */
#divTop {
position: -webkit-sticky;
position: sticky;
top: 0;
display: flex;
gap: 0.5rem;
align-items: center;
top: 0px;
width: 100%;
padding: 1rem;
margin-bottom: 1.5rem;
font-size: 0.95rem;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
z-index: 100;
font-size: 16px;
padding: 5px;
border: 1px solid #ddd;
margin-bottom: 5px;
}
#searchText {
flex: 1;
min-width: 200px;
}
/* Select Box for Add Type */
#selAddType {
min-width: 120px;
cursor: pointer;
}
/* Wrapper spans */
#divTop > span {
display: flex;
align-items: center;
}
/* Table Container */
.table-container {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-md);
margin-bottom: 2rem;
width: 100%;
}
table {
table-layout: auto;
width: 100%;
border-collapse: collapse;
background-color: var(--card-bg);
}
thead {
background-color: var(--card-bg);
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
th {
padding: 14px 16px;
text-align: left;
color: var(--accent);
font-weight: 600;
font-family: var(--font-pixel);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.85rem;
border-bottom: 2px solid var(--border-color);
th, td {
padding: 15px;
text-align: left;
}
td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
color: var(--text-body);
font-size: 0.9rem;
table tr:nth-child(even) {
background-color: #eee;
}
tbody tr:hover {
background-color: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.05);
transition: background-color 0.2s ease;
table tr:nth-child(odd) {
background-color: #fff;
}
tbody tr:last-child td {
border-bottom: none;
table th {
background-color: black;
color: white;
}
/* Remove Button Column */
td:nth-child(1) {
width: 50px;
padding: 12px 8px;
text-align: center;
}
td:nth-child(1) .del_btn_wrapper {
display: flex;
justify-content: center;
}
/* Remove Button - Compact Icon Style */
.remove {
background-color: var(--danger);
color: transparent;
border: none;
padding: 6px 6px;
border-radius: 4px;
font-size: 0.7rem;
font-family: var(--font-pixel);
font-weight: 600;
background-color: #f44336;
color: white;
border: 2px solid #f44336;
padding: 4px 8px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 4px rgba(255, 85, 85, 0.2);
white-space: nowrap;
letter-spacing: 0px;
min-width: 32px;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 6 5 6 21 6'%3E%3C/polyline%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'%3E%3C/path%3E%3Cline x1='10' y1='11' x2='10' y2='17'%3E%3C/line%3E%3Cline x1='14' y1='11' x2='14' y2='17'%3E%3C/line%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
background-size: 18px;
}
.remove:hover {
background-color: var(--danger-hover);
box-shadow: 0 2px 6px rgba(255, 85, 85, 0.3);
transform: scale(1.05);
background-color: white;
color: black;
}
.remove:active {
transform: scale(0.95);
}
/* Save Button Group */
#divSaveTop {
#btnSave {
position: -webkit-sticky;
position: sticky;
bottom: 0;
display: flex;
gap: 1rem;
padding: 1rem;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
flex-wrap: wrap;
z-index: 100;
margin-top: 2rem;
bottom: 0px;
width: 100%;
background-color: #0061b0;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
float: right;
}
#divSaveTop .btn {
flex: 1;
min-width: 150px;
#divTop {
display: table;
width: 100%;
}
#divTop > * {
display: table-cell;
}
#divTop > span {
width: 1%;
}
#divTop > input {
width: 100%;
}
/* Responsive Design */
@media screen and (max-width: 768px) {
#divTop {
flex-direction: column;
align-items: stretch;
}
#searchText {
min-width: 100%;
}
th, td {
padding: 10px 12px;
font-size: 0.85rem;
}
td:nth-child(1) {
width: 50px;
padding: 10px 4px;
}
.remove {
min-width: 30px;
min-height: 30px;
padding: 5px 5px;
}
#divSaveTop {
flex-direction: column;
gap: 0.75rem;
}
}
@media screen and (max-width: 480px) {
#divTop {
padding: 0.75rem;
margin-bottom: 1rem;
}
th, td {
padding: 8px 10px;
font-size: 0.8rem;
}
th {
font-size: 0.75rem;
}
td:nth-child(1) {
width: 50px;
padding: 8px 4px;
}
.remove {
min-width: 28px;
min-height: 28px;
padding: 4px 4px;
}
.table-container {
margin-bottom: 2rem;
}
#divSaveTop {
flex-direction: column;
gap: 0.75rem;
margin-bottom: 70px;
}
/* Mobile table display */
@media screen and (max-width:700px) {
table, tr, td {
padding: 0;
border: none;
padding:0;
border:1px solid black;
}
table {
border: none;
border:none;
}
tr:first-child, thead, th {
display: none;
border: none;
display:none;
border:none;
}
tr {
float: left;
width: 100%;
margin-bottom: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--card-bg);
padding: 0.75rem;
margin-bottom: 2em;
}
table tr:nth-child(odd) {
background-color: #eee;
}
td {
float: left;
width: 100%;
padding: 0.5rem 0;
margin-bottom: 0.25rem;
border: none;
padding:1em;
}
td::before {
content: attr(data-label);
display: block;
color: var(--accent);
font-weight: 600;
font-family: var(--font-pixel);
font-size: 0.8rem;
text-transform: uppercase;
margin-bottom: 0.25rem;
letter-spacing: 0.5px;
content:attr(data-label);
word-wrap: break-word;
background: #eee;
border-right:2px solid black;
width: 20%;
float:left;
padding:1em;
font-weight: bold;
margin:-1em 1em -1em -1em;
}
td[data-label=""] {
padding: 0 !important;
margin-bottom: 0 !important;
}
td[data-label=""] .del_btn_wrapper {
text-align: right;
margin-bottom: 0.75rem;
.del_btn_wrapper {
content:attr(data-label);
word-wrap: break-word;
background: #eee;
border-right:2px solid black;
width: 20%;
float:left;
padding:1em;
font-weight: bold;
margin:-1em 1em -1em -1em;
}
}
</style>
{% endblock %}
{% block content %}
<div class="webcfg-header">
<h2>Configuration Manager</h2>
<p>Edit your Pwnagotchi configuration settings</p>
</div>
<div id="divTop">
<input type="text" id="searchText" placeholder="Search for options ..." title="Type an option name">
<span><select id="selAddType"><option value="text">Text</option><option value="number">Number</option></select></span>
<span><button class="btn primary" type="button" onclick="addOption()">+</button></span>
<span><button id="btnAdd" type="button" onclick="addOption()">+</button></span>
</div>
<div class="table-container" id="content"></div>
<div id="divSaveTop">
<button class="btn primary" type="button" onclick="saveConfig()">Save and restart</button>
<button class="btn danger" type="button" onclick="saveConfigNoRestart()">Merge and Save (CAUTION)</button>
<button id="btnSave" type="button" onclick="saveConfig()">Save and restart</button>
<button id="btnSave" type="button" onclick="saveConfigNoRestart()">Merge and Save (CAUTION)</button>
</div>
<div id="content"></div>
{% endblock %}
{% block script %}
@@ -345,7 +204,7 @@ INDEX = """
td = document.createElement("td");
td.setAttribute("data-label", "");
btnDel = document.createElement("Button");
btnDel.innerHTML = "";
btnDel.innerHTML = "X";
btnDel.onclick = function(){ delRow(this);};
btnDel.className = "remove";
divDelBtn.appendChild(btnDel);
@@ -531,7 +390,7 @@ INDEX = """
td = document.createElement("td");
td.setAttribute("data-label", "");
btnDel = document.createElement("Button");
btnDel.innerHTML = "";
btnDel.innerHTML = "X";
btnDel.onclick = function(){ delRow(this);};
btnDel.className = "remove";
divDelBtn.appendChild(btnDel);
@@ -568,8 +427,7 @@ INDEX = """
input.type = 'text';
input.value = '[]';
}else{
var valType = typeof(json[key]);
input.type = valType === 'string' ? 'text' : valType;
input.type = typeof(json[key]);
input.value = json[key];
}
td.appendChild(input);
@@ -629,14 +487,14 @@ def serializer(obj):
class WebConfig(plugins.Plugin):
__author__ = "33197631+dadav@users.noreply.github.com modified by wsvdmeer"
__version__ = "1.0.0"
__license__ = "GPL3"
__description__ = "This plugin allows the user to make runtime changes."
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'This plugin allows the user to make runtime changes.'
def __init__(self):
self.ready = False
self.mode = "MANU"
self.mode = 'MANU'
self._agent = None
def on_config_changed(self, config):
@@ -645,11 +503,11 @@ class WebConfig(plugins.Plugin):
def on_ready(self, agent):
self._agent = agent
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'
def on_internet_available(self, agent):
self._agent = agent
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'
def on_loaded(self):
"""
@@ -676,7 +534,7 @@ class WebConfig(plugins.Plugin):
if path == "save-config":
try:
save_config(request.get_json(), '/etc/pwnagotchi/config.toml') # test
threading.Thread(target=restart, args=(self.mode,), daemon=True).start() # FIX B5
_thread.start_new_thread(restart, (self.mode,))
return "success"
except Exception as ex:
logging.error(ex)
@@ -684,21 +542,13 @@ class WebConfig(plugins.Plugin):
elif path == "merge-save-config":
try:
self.config = merge_config(request.get_json(), self.config)
pwnagotchi.config = merge_config(
request.get_json(), pwnagotchi.config
)
pwnagotchi.config = merge_config(request.get_json(), pwnagotchi.config)
logging.debug("PWNAGOTCHI CONFIG:\n%s" % repr(pwnagotchi.config))
if self._agent:
self._agent._config = merge_config(
request.get_json(), self._agent._config
)
logging.debug(
" Agent CONFIG:\n%s" % repr(self._agent._config)
)
self._agent._config = merge_config(request.get_json(), self._agent._config)
logging.debug(" Agent CONFIG:\n%s" % repr(self._agent._config))
logging.debug(" Updated CONFIG:\n%s" % request.get_json())
save_config(
request.get_json(), "/etc/pwnagotchi/config.toml"
) # test
save_config(request.get_json(), '/etc/pwnagotchi/config.toml') # test
return "success"
except Exception as ex:
logging.error("[webcfg mergesave] %s" % ex)
+4 -4
View File
@@ -34,7 +34,7 @@ class WpaSec(plugins.Plugin):
self._init_db()
def _init_db(self):
db_conn = sqlite3.connect('/etc/pwnagotchi/.wpa_sec_db')
db_conn = sqlite3.connect('/home/pi/.wpa_sec_db')
db_conn.execute('pragma journal_mode=wal')
with db_conn:
db_conn.execute('''
@@ -72,7 +72,7 @@ class WpaSec(plugins.Plugin):
if not remove_whitelisted([filename], config['main']['whitelist']):
return
db_conn = sqlite3.connect('/etc/pwnagotchi/.wpa_sec_db')
db_conn = sqlite3.connect('/home/pi/.wpa_sec_db')
with db_conn:
db_conn.execute('''
INSERT INTO handshakes (path, status)
@@ -93,7 +93,7 @@ class WpaSec(plugins.Plugin):
display = agent.view()
try:
db_conn = sqlite3.connect('/etc/pwnagotchi/.wpa_sec_db')
db_conn = sqlite3.connect('/home/pi/.wpa_sec_db')
cursor = db_conn.cursor()
cursor.execute('SELECT path FROM handshakes WHERE status = ?', (self.Status.TOUPLOAD.value,))
@@ -264,7 +264,7 @@ class WpaSec(plugins.Plugin):
def on_ui_update(self, ui):
if 'show_pwd' in self.options and self.options['show_pwd'] and 'download_results' in self.options and self.options['download_results']:
file_path = '/etc/pwnagotchi/handshakes/wpa-sec.cracked.potfile'
file_path = '/home/pi/handshakes/wpa-sec.cracked.potfile'
try:
with open(file_path, 'r') as file:
# Read all lines and extract the required fields
+6 -21
View File
@@ -44,7 +44,7 @@ class FilledRect(Widget):
class Text(Widget):
def __init__(self, value="", position=(0, 0), font=None, color=0, wrap=False, max_length=0, png=False, scale=1):
def __init__(self, value="", position=(0, 0), font=None, color=0, wrap=False, max_length=0, png=False):
super().__init__(position, color)
self.value = value
self.font = font
@@ -52,7 +52,6 @@ class Text(Widget):
self.max_length = max_length
self.wrapper = TextWrapper(width=self.max_length, replace_whitespace=False) if wrap else None
self.png = png
self.scale = scale
def draw(self, canvas, drawer):
if self.value is not None:
@@ -68,30 +67,16 @@ class Text(Widget):
self.pixels = self.image.load()
for y in range(self.image.size[1]):
for x in range(self.image.size[0]):
if self.pixels[x, y][3] < 255:
self.pixels[x, y] = (255, 255, 255, 255)
if self.pixels[x,y][3] < 255: # check alpha
self.pixels[x,y] = (255, 255, 255, 255)
if self.color == 255:
self._image = ImageOps.colorize(self.image.convert('L'), black="white", white="black")
self._image = ImageOps.colorize(self.image.convert('L'), black = "white", white = "black")
else:
self._image = self.image
if self.scale != 1:
width, height = self._image.size
new_width = int(width * self.scale)
new_height = int(height * self.scale)
scaled_image = Image.new('RGBA', (new_width, new_height))
original_pixels = self._image.load()
scaled_pixels = scaled_image.load()
for y in range(new_height):
for x in range(new_width):
original_x = x // self.scale
original_y = y // self.scale
scaled_pixels[x, y] = original_pixels[original_x, original_y]
self.image = scaled_image
else:
self.image = self._image
self.image = self.image.convert('1')
self.image = self._image.convert('1')
canvas.paste(self.image, self.xy)
class LabeledValue(Widget):
def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0, label_spacing=5):
super().__init__(position, color)
-3
View File
@@ -26,9 +26,6 @@ class Display(View):
)
self._render_thread_instance.start()
def is_whisplay(self):
return self._implementation.name == 'whisplay'
def is_lcdhat(self):
return self._implementation.name == 'lcdhat'
-1
View File
@@ -23,7 +23,6 @@ DEBUG = '(#__#)'
UPLOAD = '(1__0)'
UPLOAD1 = '(1__1)'
UPLOAD2 = '(0__1)'
SCALE = 1
PNG = False
POSITION_X = 0
POSITION_Y = 40
-4
View File
@@ -4,10 +4,6 @@ def display_for(config):
from pwnagotchi.ui.hw.inky import Inky
return Inky(config)
elif config['ui']['display']['type'] == 'whisplay':
from pwnagotchi.ui.hw.whisplay import Whisplay
return Whisplay(config)
elif config['ui']['display']['type'] == 'inkyv2':
from pwnagotchi.ui.hw.inkyv2 import InkyV2
return InkyV2(config)
+14 -16
View File
@@ -10,22 +10,22 @@ class Waveshare2in13bV3(DisplayImpl):
super(Waveshare2in13bV3, self).__init__(config, 'waveshare2in13b_v3')
def layout(self):
fonts.setup(10, 8, 10, 25, 25, 9)
self._layout['width'] = 212
self._layout['height'] = 104
self._layout['face'] = (0, 26)
self._layout['name'] = (5, 15)
fonts.setup(10, 9, 10, 35, 25, 9)
self._layout['width'] = 250
self._layout['height'] = 122
self._layout['face'] = (0, 40)
self._layout['name'] = (5, 20)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (28, 0)
self._layout['uptime'] = (147, 0)
self._layout['line1'] = [0, 12, 212, 12]
self._layout['line2'] = [0, 92, 212, 92]
self._layout['friend_face'] = (0, 76)
self._layout['friend_name'] = (40, 78)
self._layout['shakes'] = (0, 93)
self._layout['mode'] = (187, 93)
self._layout['uptime'] = (185, 0)
self._layout['line1'] = [0, 14, 250, 14]
self._layout['line2'] = [0, 108, 250, 108]
self._layout['friend_face'] = (0, 92)
self._layout['friend_name'] = (40, 94)
self._layout['shakes'] = (0, 109)
self._layout['mode'] = (225, 109)
self._layout['status'] = {
'pos': (91, 15),
'pos': (125, 20),
'font': fonts.status_font(fonts.Medium),
'max': 20
}
@@ -39,10 +39,8 @@ class Waveshare2in13bV3(DisplayImpl):
self._display.Clear()
def render(self, canvasBlack=None, canvasRed=None):
# Match V4 behavior: if only one canvas is passed, show it on black channel
buffer = self._display.getbuffer
# Create blank image matching the display dimensions
image = Image.new('1', (self._layout['height'], self._layout['width']), 255)
image = Image.new('1', (self._layout['height'], self._layout['width']))
imageBlack = image if canvasBlack is None else canvasBlack
imageRed = image if canvasRed is None else canvasRed
self._display.display(buffer(imageBlack), buffer(imageRed))
+13 -15
View File
@@ -11,21 +11,21 @@ class Waveshare213bV4(DisplayImpl):
def layout(self):
fonts.setup(10, 9, 10, 35, 25, 9)
self._layout['width'] = 212
self._layout['height'] = 104
self._layout['face'] = (0, 26)
self._layout['name'] = (5, 15)
self._layout['width'] = 250
self._layout['height'] = 122
self._layout['face'] = (0, 40)
self._layout['name'] = (5, 20)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (28, 0)
self._layout['uptime'] = (147, 0)
self._layout['line1'] = [0, 12, 212, 12]
self._layout['line2'] = [0, 92, 212, 92]
self._layout['friend_face'] = (0, 76)
self._layout['friend_name'] = (40, 78)
self._layout['shakes'] = (0, 93)
self._layout['mode'] = (187, 93)
self._layout['uptime'] = (185, 0)
self._layout['line1'] = [0, 14, 250, 14]
self._layout['line2'] = [0, 108, 250, 108]
self._layout['friend_face'] = (0, 92)
self._layout['friend_name'] = (40, 94)
self._layout['shakes'] = (0, 109)
self._layout['mode'] = (225, 109)
self._layout['status'] = {
'pos': (91, 15),
'pos': (125, 20),
'font': fonts.status_font(fonts.Medium),
'max': 20
}
@@ -39,10 +39,8 @@ class Waveshare213bV4(DisplayImpl):
self._display.Clear()
def render(self, canvasBlack=None, canvasRed=None):
# Match V4 behavior: if only one canvas is passed, show it on black channel
buffer = self._display.getbuffer
# Create blank image matching the display dimensions
image = Image.new('1', (self._layout['height'], self._layout['width']), 255)
image = Image.new('1', (self._layout['height'], self._layout['width']))
imageBlack = image if canvasBlack is None else canvasBlack
imageRed = image if canvasRed is None else canvasRed
self._display.display(buffer(imageBlack), buffer(imageRed))
+3 -5
View File
@@ -1,3 +1,4 @@
# import _thread
import threading
import logging
import random
@@ -72,11 +73,8 @@ class View(object):
'line2': Line(self._layout['line2'], color=BLACK),
'face': Text(value=faces.SLEEP,
position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']),
color=BLACK, font=fonts.Huge,
png=config['ui']['faces']['png'],
scale = config['ui']['faces'].get('scale', 1)
),
position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']),
color=BLACK, font=fonts.Huge, png=config['ui']['faces']['png']),
# 'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=BLACK),
'friend_name': Text(value=None, position=self._layout['friend_face'], font=fonts.BoldSmall, color=BLACK),
+80 -160
View File
@@ -1,7 +1,7 @@
import logging
import os
import base64
import threading # FIX B5: replaced _thread with threading
import _thread
import secrets
import json
from functools import wraps
@@ -9,8 +9,8 @@ from functools import wraps
import flask
# https://stackoverflow.com/questions/14888799/disable-console-messages-in-flask-server
logging.getLogger("werkzeug").setLevel(logging.ERROR)
os.environ["WERKZEUG_RUN_MAIN"] = "false"
logging.getLogger('werkzeug').setLevel(logging.ERROR)
os.environ['WERKZEUG_RUN_MAIN'] = 'false'
import pwnagotchi
import pwnagotchi.grid as grid
@@ -32,45 +32,21 @@ class Handler:
self._agent = agent
self._app = app
# Dynamic theme CSS route
self._app.add_url_rule("/css/theme.css", "dynamic_theme", self.dynamic_theme)
self._app.add_url_rule('/', 'index', self.with_auth(self.index))
self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
self._app.add_url_rule("/", "index", self.with_auth(self.index))
self._app.add_url_rule("/ui", "ui", self.with_auth(self.ui))
self._app.add_url_rule(
"/shutdown", "shutdown", self.with_auth(self.shutdown), methods=["POST"]
)
self._app.add_url_rule(
"/reboot", "reboot", self.with_auth(self.reboot), methods=["POST"]
)
self._app.add_url_rule(
"/restart", "restart", self.with_auth(self.restart), methods=["POST"]
)
self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
self._app.add_url_rule('/reboot', 'reboot', self.with_auth(self.reboot), methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
# inbox
self._app.add_url_rule("/inbox", "inbox", self.with_auth(self.inbox))
self._app.add_url_rule(
"/inbox/profile", "inbox_profile", self.with_auth(self.inbox_profile)
)
self._app.add_url_rule(
"/inbox/peers", "inbox_peers", self.with_auth(self.inbox_peers)
)
self._app.add_url_rule(
"/inbox/<id>", "show_message", self.with_auth(self.show_message)
)
self._app.add_url_rule(
"/inbox/<id>/<mark>", "mark_message", self.with_auth(self.mark_message)
)
self._app.add_url_rule(
"/inbox/new", "new_message", self.with_auth(self.new_message)
)
self._app.add_url_rule(
"/inbox/send",
"send_message",
self.with_auth(self.send_message),
methods=["POST"],
)
self._app.add_url_rule('/inbox', 'inbox', self.with_auth(self.inbox))
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.with_auth(self.inbox_profile))
self._app.add_url_rule('/inbox/peers', 'inbox_peers', self.with_auth(self.inbox_peers))
self._app.add_url_rule('/inbox/<id>', 'show_message', self.with_auth(self.show_message))
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.with_auth(self.mark_message))
self._app.add_url_rule('/inbox/new', 'new_message', self.with_auth(self.new_message))
self._app.add_url_rule('/inbox/send', 'send_message', self.with_auth(self.send_message), methods=['POST'])
# plugins
plugins_with_auth = self.with_auth(self.plugins)
@@ -82,57 +58,52 @@ class Handler:
def _check_creds(self, u, p):
# trying to be timing attack safe
return secrets.compare_digest(
u, self._config["username"]
) and secrets.compare_digest(p, self._config["password"])
return secrets.compare_digest(u, self._config['username']) and \
secrets.compare_digest(p, self._config['password'])
def with_auth(self, f):
@wraps(f)
def wrapper(*args, **kwargs):
if not self._config["auth"]:
if not self._config['auth']:
return f(*args, **kwargs)
else:
auth = request.authorization
if (
not auth
or not auth.username
or not auth.password
or not self._check_creds(auth.username, auth.password)
):
return Response(
"Unauthorized",
401,
{"WWW-Authenticate": 'Basic realm="Unauthorized"'},
)
if not auth or not auth.username or not auth.password or not self._check_creds(auth.username,
auth.password):
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'})
return f(*args, **kwargs)
return wrapper
def index(self):
return render_template(
"index.html",
title=pwnagotchi.name(),
other_mode="AUTO" if self._agent.mode == "manual" else "MANU",
fingerprint=self._agent.fingerprint(),
)
return render_template('index.html',
title=pwnagotchi.name(),
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU',
fingerprint=self._agent.fingerprint())
def inbox(self):
page = request.args.get("p", default=1, type=int)
inbox = {"pages": 1, "records": 0, "messages": []}
inbox = {
"pages": 1,
"records": 0,
"messages": []
}
error = None
try:
if not grid.is_connected():
raise Exception("not connected")
raise Exception('not connected')
inbox = grid.inbox(page, with_pager=True)
except Exception as e:
logging.exception("error while reading pwnmail inbox")
logging.exception('error while reading pwnmail inbox')
error = str(e)
return render_template(
"inbox.html", name=pwnagotchi.name(), page=page, error=error, inbox=inbox
)
return render_template('inbox.html',
name=pwnagotchi.name(),
page=page,
error=error,
inbox=inbox)
def inbox_profile(self):
data = {}
@@ -141,16 +112,14 @@ class Handler:
try:
data = grid.get_advertisement_data()
except Exception as e:
logging.exception("error while reading pwngrid data")
logging.exception('error while reading pwngrid data')
error = str(e)
return render_template(
"profile.html",
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=json.dumps(data, indent=2),
error=error,
)
return render_template('profile.html',
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=json.dumps(data, indent=2),
error=error)
def inbox_peers(self):
peers = {}
@@ -159,12 +128,13 @@ class Handler:
try:
peers = grid.memory()
except Exception as e:
logging.exception("error while reading pwngrid peers")
logging.exception('error while reading pwngrid peers')
error = str(e)
return render_template(
"peers.html", name=pwnagotchi.name(), peers=peers, error=error
)
return render_template('peers.html',
name=pwnagotchi.name(),
peers=peers,
error=error)
def show_message(self, id):
message = {}
@@ -172,22 +142,23 @@ class Handler:
try:
if not grid.is_connected():
raise Exception("not connected")
raise Exception('not connected')
message = grid.inbox_message(id)
if message["data"]:
message["data"] = base64.b64decode(message["data"]).decode("utf-8")
if message['data']:
message['data'] = base64.b64decode(message['data']).decode("utf-8")
except Exception as e:
logging.exception("error while reading pwnmail message %d" % int(id))
logging.exception('error while reading pwnmail message %d' % int(id))
error = str(e)
return render_template(
"message.html", name=pwnagotchi.name(), error=error, message=message
)
return render_template('message.html',
name=pwnagotchi.name(),
error=error,
message=message)
def new_message(self):
to = request.args.get("to", default="")
return render_template("new_message.html", to=to)
return render_template('new_message.html', to=to)
def send_message(self):
to = request.form["to"]
@@ -196,7 +167,7 @@ class Handler:
try:
if not grid.is_connected():
raise Exception("not connected")
raise Exception('not connected')
grid.send_message(to, message)
except Exception as e:
@@ -214,41 +185,18 @@ class Handler:
def plugins(self, name, subpath):
if name is None:
# Determine which plugins are from the default folder
default_plugins = set()
default_path = os.path.join(
os.path.dirname(os.path.realpath(plugins.__file__)), "default"
)
for plugin_name, plugin_path in plugins.database.items():
if plugin_path.startswith(default_path):
default_plugins.add(plugin_name)
return render_template(
"plugins.html",
loaded=plugins.loaded,
database=plugins.database,
default_plugins=default_plugins,
)
return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database)
if name == "toggle" and request.method == "POST":
checked = True if "enabled" in request.form else False
return (
"success"
if plugins.toggle_plugin(request.form["plugin"], checked)
else "failed"
)
if name == 'toggle' and request.method == 'POST':
checked = True if 'enabled' in request.form else False
return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed'
if name == "upgrade" and request.method == "POST":
if name == 'upgrade' and request.method == 'POST':
logging.info(f"Upgrading plugin: {request.form['plugin']}")
os.system(
f"pwnagotchi plugins update && pwnagotchi plugins upgrade {request.form['plugin']}"
)
os.system(f"pwnagotchi plugins update && pwnagotchi plugins upgrade {request.form['plugin']}")
return redirect("/plugins")
if (
name in plugins.loaded
and plugins.loaded[name] is not None
and hasattr(plugins.loaded[name], "on_webhook")
):
if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'):
try:
return plugins.loaded[name].on_webhook(subpath, request)
except Exception:
@@ -259,60 +207,32 @@ class Handler:
# serve a message and shuts down the unit
def shutdown(self):
try:
return render_template(
"status.html",
title=pwnagotchi.name(),
go_back_after=60,
message="Shutting down ...",
)
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
message='Shutting down ...')
finally:
# FIX B5: replaced _thread.start_new_thread with threading.Thread
threading.Thread(target=pwnagotchi.shutdown, daemon=True).start()
_thread.start_new_thread(pwnagotchi.shutdown, ())
# serve a message and reboot the unit
def reboot(self):
try:
return render_template(
"status.html",
title=pwnagotchi.name(),
go_back_after=60,
message="Rebooting ...",
)
finally:
# FIX B5: replaced _thread.start_new_thread with threading.Thread
threading.Thread(target=pwnagotchi.reboot, daemon=True).start()
try:
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
message='Rebooting ...')
finally:
_thread.start_new_thread(pwnagotchi.reboot, ())
# serve a message and restart the unit in the other mode
def restart(self):
mode = request.form["mode"]
if mode not in ("AUTO", "MANU"):
mode = "MANU"
mode = request.form['mode']
if mode not in ('AUTO', 'MANU'):
mode = 'MANU'
try:
return render_template(
"status.html",
title=pwnagotchi.name(),
go_back_after=30,
message="Restarting in %s mode ..." % mode,
)
return render_template('status.html', title=pwnagotchi.name(), go_back_after=30,
message='Restarting in %s mode ...' % mode)
finally:
# FIX B5: replaced _thread.start_new_thread with threading.Thread
threading.Thread(
target=pwnagotchi.restart, args=(mode,), daemon=True
).start()
# serve dynamic CSS with accent color from config
def dynamic_theme(self):
"""Generate CSS accent RGB variables from config [ui.web.theme] section"""
# Get RGB values from already-loaded config, fallback to default green
r = self._config.get("theme", {}).get("accent_r", 76)
g = self._config.get("theme", {}).get("accent_g", 175)
b = self._config.get("theme", {}).get("accent_b", 80)
css = f":root {{\n --accent: rgb({r}, {g}, {b});\n --accent-r: {r};\n --accent-g: {g};\n --accent-b: {b};\n}}"
return Response(css, mimetype="text/css")
_thread.start_new_thread(pwnagotchi.restart, (mode,))
# serve the PNG file with the display image
def ui(self):
with web.frame_lock:
return send_file(web.frame_path, mimetype="image/png")
return send_file(web.frame_path, mimetype='image/png')
+1
View File
@@ -1,3 +1,4 @@
#import _thread
import threading
import secrets
import logging
File diff suppressed because it is too large Load Diff
+72 -59
View File
@@ -1,73 +1,86 @@
<!doctype html>
<html lang="en">
{% block head %}
<head>
<!DOCTYPE html>
<html>
{% block head %}
<head>
{% block meta %}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#007bff" />
<meta name="description" content="Pwnagotchi Web Interface" />
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
<title>{% block title %}{% endblock %}</title>
<title>
{% block title %}
{% endblock %}
</title>
{% block styles %}
<link rel="stylesheet" type="text/css" href="/css/style.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
{% if active_page == 'profile' %}
<link rel="stylesheet" type="text/css" href="/css/profile.css" />
{% elif active_page in ['inbox', 'new'] %}
<link rel="stylesheet" type="text/css" href="/css/inbox.css" />
{% elif active_page == 'plugins' %}
<link rel="stylesheet" type="text/css" href="/css/plugins.css" />
{% endif %}
<link rel="apple-touch-icon" href="/images/pwnagotchi.png" />
<link rel="icon" type="image/png" href="/images/pwnagotchi.png" />
<link rel="stylesheet" href="/js/jquery.mobile/jquery.mobile-1.4.5.min.css"/>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
<link rel="apple-touch-icon" href="/images/pwnagotchi.png">
<link rel="icon" type="image/png" href="/images/pwnagotchi.png">
{% endblock %}
</head>
{% endblock %} {% block body %}
<body>
<div class="page-container">
{% if error %}
<script>
document.addEventListener("DOMContentLoaded", () => {
showToast("{{ error }}", 5000, "error");
</head>
{% endblock %}
{% block body %}
<body>
<div data-role="page">
{% if error %}
<div id="error" class="error ui-content" data-role="popup" data-overlay-theme="a" data-theme="b">
<p>{{ error }}</p>
</div>
<script>
$(function(){
$("#error").popup("open");
});
</script>
{% endif %} {% set navigation = [ ( '/', 'home', 'Home' ), ( '/inbox',
'inbox', 'Inbox' ), ( '/inbox/new', 'new', 'New' ), ( '/inbox/profile',
'profile', 'Profile' ), ( '/inbox/peers', 'peers', 'Peers' ), (
'/plugins', 'plugins', 'Plugins' ), ] %} {% set active_page =
active_page|default('inbox') %}
</script>
{% endif %}
<div class="page-content">{% block content %} {% endblock %}</div>
{% set navigation = [
( '/', 'home', 'eye', 'Home' ),
( '/inbox', 'inbox', 'bars', 'Inbox' ),
( '/inbox/new', 'new', 'mail', 'New' ),
( '/inbox/profile', 'profile', 'info', 'Profile' ),
( '/inbox/peers', 'peers', 'user', 'Peers' ),
( '/plugins', 'plugins', 'grid', 'Plugins' ),
] %}
{% set active_page = active_page|default('inbox') %}
<footer class="page-footer">
<nav class="navbar">
{% for href, id, caption in navigation %}
<li class="navbar-item">
<a
href="{{ href }}"
id="{{ id }}"
class="{% if active_page == id %}active{% endif %}"
>
<span class="navbar-icon"></span>
<span>{{ caption }}</span>
</a>
</li>
{% endfor %}
</nav>
</footer>
<div data-role="footer">
<div data-role="navbar" data-iconpos="left">
<ul>
{% for href, id, icon, caption in navigation %}
<li class="navitem">
<a href="{{ href }}" id="{{ id }}" data-icon="{{ icon }}" class="{{ 'ui-btn-active' if active_page == id }}">{{ caption }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% block scripts %}
<script src="/js/kjua.min.js"></script>
<script src="/js/app.js"></script>
<script>
{% block script %}{% endblock %}
</script>
{% block content %}
{% endblock %}
</body>
{% endblock %}
</div>
{% block scripts %}
<script type="text/javascript" src="/js/jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="/js/jquery.mobile/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="/js/jquery.timeago.js"></script>
<script type="text/javascript" src="/js/kjua-0.9.0.min.js"></script>
<script type="text/javascript">
$.mobile.ajaxEnabled = false;
$.mobile.pushStateEnabled = false;
jQuery(document).ready(function() {
jQuery("time.timeago").timeago();
});
{% block script %}
{% endblock %}
</script>
{% endblock %}
</body>
{% endblock %}
</html>
+41 -50
View File
@@ -1,55 +1,46 @@
{% extends "base.html" %} {% set active_page = "inbox" %} {% block title %} {{
name }} Inbox {% endblock %} {% block script %} autoRefresh(15000);
filterList('.search-box input', '.inbox li'); {% endblock %} {% block content %}
<div class="search-box">
<input type="text" placeholder="Search inbox..." />
</div>
{% extends "base.html" %}
{% set active_page = "inbox" %}
<ul class="inbox">
{% for message in inbox.messages %}
<li class="message {{ 'unread' if not message.seen_at else 'read' }}">
<a href="/inbox/{{ message.id }}">
<div class="list-item-primary">
{{ message.sender_name }}@{{ message.sender }}
</div>
<div class="list-item-secondary">
Received
<time class="timeago" datetime="{{ message.created_at }}"
>{{ message.created_at }}</time
>
{% if message.seen_at %}, seen
<time class="timeago" datetime="{{ message.seen_at }}"
>{{ message.seen_at }}</time
>{% endif %}
</div>
</a>
</li>
{% endfor %}
{% block title %}
{{ name }} Inbox
{% endblock %}
{% block script %}
$(function() {
setTimeout(function(){
window.location.reload(true);
}, 15000);
});
{% endblock %}
{% block content %}
<ul class="inbox" data-role="listview" data-filter="true" data-filter-placeholder="Search inbox..." data-inset="true">
{% for message in inbox.messages %}
<li class="message">
<a href="/inbox/{{ message.id }}" class="{{ 'unread' if not message.seen_at else 'read' }}">
<h2>{{ message.sender_name }}@{{ message.sender }}</h2>
<p>
Received
<time class="timeago" datetime="{{ message.created_at }}">{{ message.created_at }}</time>
{% if message.seen_at %}, seen
<time class="timeago" datetime="{{ message.seen_at }}">{{ message.seen_at }}</time>
{% endif %}.
</p>
</a>
</li>
{% endfor %}
</ul>
{% if inbox.pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<div class="page-item">
<a href="/inbox?p={{ page - 1 }}" class="page-link">Previous</a>
</div>
{% else %}
<div class="page-item disabled">
<span class="page-link">← Previous</span>
</div>
{% endif %}
<div
class="page-item"
style="padding: 0 1rem; display: flex; align-items: center"
>
Page {{ page }} of {{ inbox.pages }}
</div>
{% if page < inbox.pages %}
<div class="page-item">
<a href="/inbox?p={{ page + 1 }}" class="page-link">Next →</a>
</div>
{% else %}
<div class="page-item disabled"><span class="page-link">Next →</span></div>
{% endif %}
<div data-role="navbar">
<ul>
{% if page > 1 %}
<li><a href="/inbox?p={{ page - 1 }}" class="ui-btn">Prev</a></li>
{% endif %}
{% if page < inbox.pages %}
<li><a href="/inbox?p={{ page + 1 }}" class="ui-btn">Next</a></li>
{% endif %}
</ul>
</div>
{% endif %} {% endblock %}
{% endif %}
{% endblock %}
+44 -37
View File
@@ -1,39 +1,46 @@
{% extends "base.html" %} {% set active_page = "home" %} {% block title %} {{
title }} {% endblock %} {% block script %} pollResource("#ui", (el) =>
el.src.split("?")[0] + "?" + new Date().getTime(), 1000); {% endblock %} {%
block content %}
<img class="ui-image pixelated" src="/ui" id="ui" alt="Device UI" />
<div class="card">
<div class="card-body">
<div class="control-buttons">
<form
method="post"
action="/shutdown"
onsubmit="return confirm('This will halt the unit, continue?');"
>
<button type="submit" class="btn danger">Shutdown</button>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
<form
method="post"
action="/reboot"
onsubmit="return confirm('This will reboot the unit, continue?');"
>
<button type="submit" class="btn secondary">Reboot</button>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
<form
method="post"
action="/restart"
onsubmit="return confirm('This will restart the service in {{ other_mode }} mode, continue?');"
>
<button type="submit" class="btn primary">
Restart {{ other_mode }}
</button>
<input type="hidden" name="mode" value="{{ other_mode }}" />
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
</div>
</div>
{% extends "base.html" %}
{% set active_page = "home" %}
{% block title %}
{{ title }}
{% endblock %}
{% block script %}
window.onload = function() {
var image = document.getElementById("ui");
function updateImage() {
image.src = image.src.split("?")[0] + "?" + new Date().getTime();
}
setInterval(updateImage, 1000);
}
{% endblock %}
{% block content %}
<img class="ui-image pixelated" src="/ui" id="ui"/>
<div data-role="navbar">
<ul>
<li>
<form class="action" method="post" action="/shutdown"
onsubmit="return confirm('this will halt the unit, continue?');">
<input type="submit" class="button ui-btn ui-corner-all" value="Shutdown"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
</li>
<li>
<form class="action" method="post" action="/reboot"
onsubmit="return confirm('this will reboot the unit, continue?');">
<input type="submit" class="button ui-btn ui-corner-all" value="Reboot"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
</li>
<li>
<form class="action" method="post" action="/restart"
onsubmit="return confirm('This will restart the service in {{ other_mode }} mode, continue?');">
<input type="submit" class="button ui-btn ui-corner-all" value="Restart in {{ other_mode }} mode"/>
<input type="hidden" name="mode" value="{{ other_mode }}"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
</li>
</ul>
</div>
{% endblock %}
+36 -38
View File
@@ -1,39 +1,37 @@
{% extends "base.html" %} {% set active_page = "" %} {% block title %} Message
from {{ message.sender_name }} {% endblock %} {% block script %} // Mark message
as read fetch('/inbox/{{ message.id }}/seen') .then(res => res.text())
.then(data => console.log('Message marked as read')); {% endblock %} {% block
content %}
<div class="card">
<div class="card-header">
<div class="messagebody-meta">
Message from
<strong>{{ message.sender_name }}@{{ message.sender }}</strong>, sent
<time class="timeago" datetime="{{ message.created_at }}"
>{{ message.created_at }}</time
>
{% if message.seen_at %}, seen
<time class="timeago" datetime="{{ message.seen_at }}"
>{{ message.seen_at }}</time
>{% endif %}
</div>
</div>
<div class="card-body">
{% if message.data %}
<p class="messagebody">{{ message.data }}</p>
{% else %}
<p class="text-muted"><em>This message is empty.</em></p>
{% endif %}
</div>
<div class="card-footer">
<a href="/inbox/new?to={{ message.sender }}" class="btn btn-primary"
>Reply</a
>
<a
href="/inbox/{{ message.id }}/deleted"
class="btn danger"
onclick="return confirm('Are you sure?');"
>Delete</a
>
</div>
</div>
{% extends "base.html" %}
{% set active_page = "" %}
{% block title %}
Message from {{ message.sender_name }}
{% endblock %}
{% block script %}
jQuery(document).ready(function() {
// mark the message as read
jQuery.get("/inbox/{{ message.id }}/seen", function(res){ console.log(res); });
});
{% endblock %}
{% block content %}
<p class="messagebody">
<span style="color: #888">
Message from {{ message.sender_name }}@{{ message.sender }}, sent
<time class="timeago" datetime="{{ message.created_at }}">{{ message.created_at }}</time>
{% if message.seen_at %}, seen
<time class="timeago" datetime="{{ message.seen_at }}">{{ message.seen_at }}</time>{% endif %}.</span>
<br/>
<br/>
{% if message.data %}
<span style="white-space: pre-line">{{ message.data }}</span>
{% else %}
<small>This message is empty.</small>
{% endif %}
<br/>
<br/>
<a href="/inbox/new?to={{ message.sender }}" class="ui-btn ui-icon-comment ui-btn-icon-left ui-shadow-icon">Reply</a>
<a href="/inbox/{{ message.id }}/deleted" class="ui-btn ui-icon-delete ui-btn-icon-left ui-shadow-icon" onclick="return confirm('Are you sure?')">Delete</a>
</p>
{% endblock %}
+49 -22
View File
@@ -1,28 +1,55 @@
{% extends "base.html" %} {% set active_page = "new" %} {% block title %} {% if
to %}Reply to {{ to }}{% else %}New Message{% endif %} {% endblock %} {% block
script %} handleFormSubmit('#message_form', { onSuccess: function() {
alert('Message sent!'); window.location.href = '/inbox'; }, onError: function()
{ alert('Failed to send message.'); } }); {% endblock %} {% block content %}
<div class="card">
<div class="card-body">
{% extends "base.html" %}
{% set active_page = "new" %}
{% block title %}
{% if to %}
Reply to {{ to }}
{% else %}
New Message
{% endif %}
{% endblock %}
{% block script %}
$(function(){
$("#message_form").submit(function(e) {
e.preventDefault();
var form = $(this);
var url = form.attr('action');
$.ajax({
type: "POST",
url: url,
data: form.serialize(),
success: function(data) {
if( data.error ) {
if( data.error.indexOf('404') != -1 )
alert('Fingerprint not found.');
else if( data.error.indexOf('aborted') != -1 )
alert('Empty or invalid message.');
else
alert(data.error);
return;
}
alert("Message sent!");
document.location.href = "/inbox";
}
});
});
});
{% endblock %}
{% block content %}
<div style="padding: 1em">
<form id="message_form" method="POST" action="/inbox/send">
<div>
<label for="to">To (Fingerprint):</label>
<input type="text" name="to" id="to" value="{{ to }}" required />
</div>
<label for="to">To:</label>
<input type="text" name="to" id="to" value="{{ to }}">
<div>
<label for="message">Message:</label>
<textarea
name="message"
id="message"
placeholder="Enter your message here..."
></textarea>
</div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit" class="btn btn-primary">Send Message</button>
<textarea cols="40" rows="8" name="message" id="message"></textarea>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="submit" class="button" value="Send"/>
</form>
</div>
</div>
{% endblock %}
+18 -21
View File
@@ -1,24 +1,21 @@
{% extends "base.html" %} {% set active_page = "peers" %} {% block title %} {{
name }} Peers {% endblock %} {% block script %} filterList('.search-box input',
'.peers li'); {% endblock %} {% block content %}
<div class="search-box">
<input type="text" placeholder="Search peers..." />
</div>
{% extends "base.html" %}
{% set active_page = "peers" %}
<ul class="peers">
{% for peer in peers %}
<li class="peer">
<a href="/inbox/new?to={{ peer.fingerprint }}">
<div class="list-item-primary">
{{ peer.advertisement.face }} {{ peer.advertisement.name }}@{{
peer.fingerprint }}
</div>
<div class="list-item-secondary">
Pwned {{ peer.advertisement.pwnd_tot }} networks, {{ peer.encounters }}
encounters
</div>
</a>
</li>
{% endfor %}
{% block title %}
{{ name }} Friends
{% endblock %}
{% block content %}
<ul class="peers" data-role="listview" data-filter="true" data-filter-placeholder="Search peers..." data-inset="true">
{% for peer in peers %}
<li class="peer">
<a href="/inbox/new?to={{ peer.fingerprint }}">
<h2>{{ peer.advertisement.face }} {{ peer.advertisement.name }}@{{ peer.fingerprint }}</h2>
<p>
Pwned {{ peer.advertisement.pwnd_tot }} networks, {{ peer.encounters }} encounters.
</p>
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
+102 -73
View File
@@ -1,74 +1,103 @@
{% extends "base.html" %} {% set active_page = "plugins" %} {% block
title%}Plugins{% endblock %} {% block styles %}{{ super() }}{% endblock %} {%
block script %}{% endblock %} {% block content %}
<div id="container">
{% for name in database.keys() | sort %} {% set has_info = name in loaded and
loaded[name].__description__ is defined %}
<div class="plugins-box">
<div class="tooltip">
<div class="plugin-header">
<h4>
{% if name in loaded and loaded[name].on_webhook is defined %}
<a href="/plugins/{{ name | urlencode }}">{{ name }}</a>
{% else %}{{ name }}{% endif %}
</h4>
{% if name in loaded and loaded[name].__version__ is defined %}
<span class="badge version">{{ loaded[name].__version__ }}</span>
{% endif %}
</div>
{% if name in loaded %} {% if has_info %}
<span class="tooltiptext">{{ loaded[name].__description__ }}</span>
{% else %}
<span class="tooltiptext">Description unable to load yet.</span>
{% endif %} {% endif %}
</div>
<div class="author">
{% if name in loaded and loaded[name].__author__ is defined %} {{
loaded[name].__author__ }} {% endif %}
</div>
<div class="plugin-badges">
{% if name in default_plugins %}
<span class="badge default">DEFAULT</span>
{% endif %}
</div>
<div>
<form method="POST" action="/plugins/toggle" class="plugin-toggle">
<label class="switch">
<input
type="checkbox"
name="enabled"
id="flip-checkbox-{{name}}"
{%
if
name
in
loaded
%}
checked
{%
endif
%}
/>
<span class="slider"></span>
</label>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="plugin" value="{{ name }}" />
</form>
<form method="POST" action="/plugins/upgrade">
<button
type="submit"
name="upgrade"
class="btn primary plugin-upgrade-btn"
>
Upgrade Plugin
</button>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="plugin" value="{{ name }}" />
</form>
</div>
</div>
{% endfor %}
</div>
{% endblock %} {% block scripts %}{{ super() }}
<script src="/js/plugins.js"></script>
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
Plugins
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: #3388cc;
color: #fff;
text-align: center;
border-radius: 10px;
border: 2px solid black;
padding: 20px 0;
position: absolute;
z-index: 1;
top: 100%;
left: 50%;
margin-left: -100px;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
</style>
{% endblock %}
{% block script %}
$(function(){
$('input[type=checkbox]').change(function(e) {
var checkbox = $(this);
var form = checkbox.closest('form');
var url = form.attr('action');
$.ajax({
type: 'POST',
url: url,
data: form.serialize(),
success: function(data) {
if( data.indexOf('failed') != -1 ) {
alert('Could not be toggled.');
}
}
});
});
$('input[type=submit]').change(function(e) {
var button = $(this);
var form = button.closest('form');
var url = form.attr('action');
$.ajax({
type: 'POST',
url: url,
data: form.serialize(),
success: function(data) {
if( data.indexOf('failed') != -1 ) {
alert('Could not be upgraded.');
}
}
});
});
});
{% endblock %}
{% block content %}
<div id="container">
{% for name in database.keys() | sort %}
{% set has_info = name in loaded and loaded[name].__description__ is defined %}
<div class="plugins-box">
<div class="tooltip">
<h4>
<a {% if name in loaded and loaded[name].on_webhook is defined %} href="/plugins/{{name}}" {% endif %}>{{name}}</a>
</h4>
{% if has_info %}
<span class="tooltiptext">{{ loaded[name].__description__ }}</span>
{% else %}
<span class="tooltiptext">Description can't be loaded yet.</span>
{% endif %}
</div>
<form method="POST" action="/plugins/toggle">
<input type="checkbox" data-role="flipswitch" name="enabled" id="flip-checkbox-{{name}}" data-on-text="Enabled" data-off-text="Disabled" data-wrapper-class="custom-size-flipswitch" {% if name in loaded %} checked {% endif %}>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="plugin" value="{{ name }}"/>
</form>
<form method="POST" action="/plugins/upgrade">
<input type="submit" name="upgrade" value="Upgrade">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="plugin" value="{{ name }}"/>
</form>
</div>
{% endfor %}
</div>
{% endblock %}
+31 -29
View File
@@ -1,34 +1,36 @@
{% extends "base.html" %} {% set active_page = "profile" %} {% block title
%}Profile{% endblock %} {% block script %}
document.addEventListener('DOMContentLoaded', function() { if (typeof
generateQRCode === 'function') { generateQRCode('qrcode',
'https://opwngrid.xyz/search/{{ fingerprint }}', { size: 300 }); } else {
console.error('generateQRCode function not available'); } }); {% endblock %} {%
block content %}
<div class="card">
<div class="card-body">
<div>
<label>Name</label>
<h4>{{ name }}</h4>
</div>
{% extends "base.html" %}
{% set active_page = "profile" %}
<div class="mt-2">
<label>Fingerprint</label>
<h4>
<a href="https://opwngrid.xyz/search/{{ fingerprint }}" target="_blank"
>{{ fingerprint }}</a
>
</h4>
</div>
{% block title %}
Profile
{% endblock %}
<div class="mt-2 text-center">
<div id="qrcode" style="display: inline-block"></div>
</div>
{% block script %}
$(function(){
$('#qrcode').kjua({
text: 'https://opwngrid.xyz/search/{{ fingerprint }}',
render: 'svg',
mode: 0,
size: 400,
fontname: 'sans',
fontcolor: '#000'
});
});
{% endblock %}
<div class="mt-3">
<label>Data</label>
<pre><code>{{ data }}</code></pre>
</div>
</div>
{% block content %}
<div style="padding: 1em">
<label for="name">Name</label>
<h4 id="name">{{ name }}</h4>
<label for="fingerprint">Fingerprint</label>
<h4 id="fingerprint">
<a href="https://opwngrid.xyz/search/{{ fingerprint }}" target="_blank">{{ fingerprint }}</a>
</h4>
<div id="qrcode"></div>
<br/>
<label for="data">Data</label>
<pre><code id="data">{{ data }}</code></pre>
</div>
{% endblock %}
+5 -8
View File
@@ -1,15 +1,12 @@
<html>
<head>
<title>{{ title }}</title>
<meta http-equiv="refresh" content="{{ go_back_after }};URL=/" />
<link rel="stylesheet" type="text/css" href="/css/style.css" />
<title>{{ title }}</title>
<meta http-equiv="refresh" content="{{ go_back_after }};URL=/">
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
</head>
<body>
<div class="status">
<h2>{{ message }}</h2>
<p style="font-size: 0.9rem">
Redirecting to home in {{ go_back_after }} seconds...
</p>
{{ message }}
</div>
</body>
</html>
</html>
-2
View File
@@ -218,8 +218,6 @@ def load_config(args):
config['ui']['display']['type'] = 'dummydisplay'
# NON E-INK DISPLAYS---------------------------------------------------------------
elif config['ui']['display']['type'] in 'whisplay':
config['ui']['display']['type'] = 'whisplay'
elif config['ui']['display']['type'] in ('wavesharelcd0in96', 'wslcd0in96'):
config['ui']['display']['type'] = 'wavesharelcd0in96'