diff --git a/pwnagotchi/_version.py b/pwnagotchi/_version.py index c9af5365..5fb05872 100644 --- a/pwnagotchi/_version.py +++ b/pwnagotchi/_version.py @@ -1 +1 @@ -__version__ = '2.9.5.5' +__version__ = '2.9.5.4' diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 7d0410a1..6f6dbecf 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -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): diff --git a/pwnagotchi/ai/epoch.py b/pwnagotchi/ai/epoch.py index ea9581d3..2ba47212 100644 --- a/pwnagotchi/ai/epoch.py +++ b/pwnagotchi/ai/epoch.py @@ -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 diff --git a/pwnagotchi/bettercap.py b/pwnagotchi/bettercap.py index a55a1b6f..def18802 100644 --- a/pwnagotchi/bettercap.py +++ b/pwnagotchi/bettercap.py @@ -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) diff --git a/pwnagotchi/cli.py b/pwnagotchi/cli.py index 6b7c2e86..b2b1bb12 100644 --- a/pwnagotchi/cli.py +++ b/pwnagotchi/cli.py @@ -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}(? 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" diff --git a/pwnagotchi/defaults.toml b/pwnagotchi/defaults.toml index 963a26bb..13ab0312 100644 --- a/pwnagotchi/defaults.toml +++ b/pwnagotchi/defaults.toml @@ -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 \ No newline at end of file +rsync = true diff --git a/pwnagotchi/fs/__init__.py b/pwnagotchi/fs/__init__.py index e0c56801..5205ff92 100644 --- a/pwnagotchi/fs/__init__.py +++ b/pwnagotchi/fs/__init__.py @@ -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']) diff --git a/pwnagotchi/grid.py b/pwnagotchi/grid.py index 4e5aea2f..a8ea8560 100644 --- a/pwnagotchi/grid.py +++ b/pwnagotchi/grid.py @@ -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, diff --git a/pwnagotchi/identity.py b/pwnagotchi/identity.py index dbd7b0d6..f553b08d 100644 --- a/pwnagotchi/identity.py +++ b/pwnagotchi/identity.py @@ -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. diff --git a/pwnagotchi/log.py b/pwnagotchi/log.py index 1fa9bfd4..bd55ff6c 100644 --- a/pwnagotchi/log.py +++ b/pwnagotchi/log.py @@ -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 diff --git a/pwnagotchi/mesh/utils.py b/pwnagotchi/mesh/utils.py index 4d4aa2dc..7f90f2c4 100644 --- a/pwnagotchi/mesh/utils.py +++ b/pwnagotchi/mesh/utils.py @@ -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) diff --git a/pwnagotchi/plugins/default/auto-tune.py b/pwnagotchi/plugins/default/auto-tune.py index f6a42fe7..488eb373 100644 --- a/pwnagotchi/plugins/default/auto-tune.py +++ b/pwnagotchi/plugins/default/auto-tune.py @@ -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): diff --git a/pwnagotchi/plugins/default/auto-update.py b/pwnagotchi/plugins/default/auto-update.py index 97bafe4c..246cd97d 100644 --- a/pwnagotchi/plugins/default/auto-update.py +++ b/pwnagotchi/plugins/default/auto-update.py @@ -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 ) diff --git a/pwnagotchi/plugins/default/auto_backup.py b/pwnagotchi/plugins/default/auto_backup.py index 4199f717..2ed124e5 100644 --- a/pwnagotchi/plugins/default/auto_backup.py +++ b/pwnagotchi/plugins/default/auto_backup.py @@ -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 = '
Status: " - if self.backup_in_progress: - ret += "Backup in progress..." - else: - ret += "Ready" - ret += "
" - ret += '" - ret += "| Backup Location: | " - + self.options.get("backup_location", "Not set") - + " |
| Interval: | " - + str(self.interval_seconds // 60) - + " minutes |
| Max Backups: | " - + str(self.max_backups) - + " |
| Include Paths: | " - + (", ".join(self.include) if self.include else "None") - + " |
" + result["status"] + "
" - ret += 'Back' - ret += "" - 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() diff --git a/pwnagotchi/plugins/default/bt-tether.py b/pwnagotchi/plugins/default/bt-tether.py index fa5eec63..e12d4b6c 100644 --- a/pwnagotchi/plugins/default/bt-tether.py +++ b/pwnagotchi/plugins/default/bt-tether.py @@ -1,5041 +1,328 @@ -""" -Bluetooth Tether Plugin for Pwnagotchi - -Required System Packages: - sudo apt-get update - sudo apt-get install -y bluez network-manager python3-dbus python3-toml - -Features: -- Bluetooth tethering to mobile phones (iOS & Android) -- Auto-discovery of trusted devices with tethering capability -- Works with iOS randomized MAC addresses -- Auto-reconnect functionality -- Web UI for easy device pairing and management -- No manual MAC configuration needed - -Setup: -1. Install packages: sudo apt-get install -y bluez network-manager python3-dbus python3-toml -2. Enable services: - sudo systemctl enable bluetooth && sudo systemctl start bluetooth - sudo systemctl enable NetworkManager && sudo systemctl start NetworkManager -3. Access web UI at http://| Item | +Configuration | +
|---|---|
| Bluetooth | +{{bluetooth|safe}} | +
| Device | +{{device|safe}} | +
| Connection | +{{connection|safe}} | +
Plugin not ready
+ """
+ if path == "/" or not path:
try:
- import dbus
-
- bus = dbus.SystemBus()
- manager = dbus.Interface(
- bus.get_object("org.bluez", "/"),
- "org.freedesktop.DBus.ObjectManager",
- )
- objects = manager.GetManagedObjects()
- device_path = None
- for path, interfaces in objects.items():
- if "org.bluez.Device1" in interfaces:
- props = interfaces["org.bluez.Device1"]
- if props.get("Address") == mac:
- device_path = path
- break
-
- if device_path:
- device = dbus.Interface(
- bus.get_object("org.bluez", device_path), "org.bluez.Device1"
- )
- try:
- self._log("INFO", "Disconnecting NAP profile...")
- device.DisconnectProfile(self.NAP_UUID)
- time.sleep(self.DEVICE_OPERATION_DELAY)
- self._log("INFO", "NAP profile disconnected")
- except Exception as e:
- logging.debug(f"[bt-tether] NAP disconnect: {e}")
+ bluetooth = self.bluetoothctl(["info", self.mac])
+ bluetooth = bluetooth.stdout.replace("\n", "Real-time log viewer with filtering and auto-scroll capabilities
-| Time | -Level | -Message | -
|---|
| + Time + | ++ Level + | ++ Message + | + +
|---|
Real-time monitoring of WiFi capture metrics and system performance
-Edit your Pwnagotchi configuration settings
-This message is empty.
- {% endif %} -+ Pwned {{ peer.advertisement.pwnd_tot }} networks, {{ peer.encounters }} encounters. +
+ +{{ data }}
- - Redirecting to home in {{ go_back_after }} seconds... -
+ {{ message }}