diff --git a/.editorconfig b/.editorconfig
index 272f5d7..bc53405 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,7 +1,31 @@
-# top-most EditorConfig file
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
root = true
-# Matches the exact files either package.json or .travis.yml
-[{*.yml,*.yaml,config.yml,defaults.yml}]
+[*]
indent_style = space
indent_size = 2
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[Makefile]
+indent_style = tab
+
+[*.py]
+indent_style = space
+indent_size = 4
+
+[*.json]
+insert_final_newline = ignore
+
+[*.js]
+indent_style = ignore
+insert_final_newline = ignore
+
+[*.{md,txt}]
+indent_size = 4
+trim_trailing_whitespace = false
diff --git a/bin/pwnagotchi b/bin/pwnagotchi
index 6e3ab4a..55fc22c 100755
--- a/bin/pwnagotchi
+++ b/bin/pwnagotchi
@@ -2,20 +2,14 @@
import logging
import argparse
import time
-import yaml
import signal
import sys
import toml
import pwnagotchi
-from pwnagotchi import grid
from pwnagotchi import utils
-from pwnagotchi import plugins
+from pwnagotchi.plugins import cmd as plugins_cmd
from pwnagotchi import log
-from pwnagotchi.identity import KeyPair
-from pwnagotchi.agent import Agent
-from pwnagotchi.ui import fonts
-from pwnagotchi.ui.display import Display
from pwnagotchi import restart
from pwnagotchi import fs
from pwnagotchi.utils import DottedTomlEncoder
@@ -89,11 +83,12 @@ def do_auto_mode(agent):
plugins.on('internet_available', agent)
except Exception as e:
- logging.exception("main loop exception")
+ logging.exception("main loop exception (%s)", e)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
+ parser = plugins_cmd.add_parsers(parser)
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.toml',
help='Main configuration file.')
@@ -118,15 +113,30 @@ if __name__ == '__main__':
args = parser.parse_args()
+
+ if plugins_cmd.used_plugin_cmd(args):
+ config = utils.load_config(args)
+ log.setup_logging(args, config)
+ rc = plugins_cmd.handle_cmd(args, config)
+ sys.exit(rc)
+
if args.version:
print(pwnagotchi.__version__)
sys.exit(0)
config = utils.load_config(args)
+
if args.print_config:
print(toml.dumps(config, encoder=DottedTomlEncoder()))
sys.exit(0)
+ from pwnagotchi.identity import KeyPair
+ from pwnagotchi.agent import Agent
+ from pwnagotchi.ui import fonts
+ from pwnagotchi.ui.display import Display
+ from pwnagotchi import grid
+ from pwnagotchi import plugins
+
pwnagotchi.config = config
fs.setup_mounts(config)
log.setup_logging(args, config)
diff --git a/builder/data/usr/bin/bettercap-launcher b/builder/data/usr/bin/bettercap-launcher
index 6b8502a..2598a25 100755
--- a/builder/data/usr/bin/bettercap-launcher
+++ b/builder/data/usr/bin/bettercap-launcher
@@ -1,6 +1,14 @@
#!/usr/bin/env bash
source /usr/bin/pwnlib
+# we need to decrypt something
+if is_crypted_mode; then
+ while ! is_decrypted; do
+ echo "Waiting for decryption..."
+ sleep 1
+ done
+fi
+
# start mon0
start_monitor_interface
diff --git a/builder/data/usr/bin/decryption-webserver b/builder/data/usr/bin/decryption-webserver
new file mode 100755
index 0000000..72e4016
--- /dev/null
+++ b/builder/data/usr/bin/decryption-webserver
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+from http.server import HTTPServer, BaseHTTPRequestHandler
+
+
+HTML_FORM = """
+
+
+
+ Decryption
+
+
+
+
+ Decryption
+ Some of your files are encrypted.
+ Please provide the decryption password.
+
+
+
+
+
+
+"""
+
+
+class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
+
+ def do_GET(self):
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(HTML_FORM.encode())
+
+ def do_POST(self):
+ content_length = int(self.headers['Content-Length'])
+ body = self.rfile.read(content_length)
+ self.send_response(200)
+ self.end_headers()
+ password = body.decode('UTF-8').split('=')[1]
+
+ with open('/tmp/.pwnagotchi-secret', 'wt') as pwfile:
+ pwfile.write(password)
+
+
+httpd = HTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
+httpd.serve_forever()
diff --git a/builder/data/usr/bin/pwnagotchi-launcher b/builder/data/usr/bin/pwnagotchi-launcher
index 78c37fe..a56665f 100755
--- a/builder/data/usr/bin/pwnagotchi-launcher
+++ b/builder/data/usr/bin/pwnagotchi-launcher
@@ -1,6 +1,14 @@
#!/usr/bin/env bash
source /usr/bin/pwnlib
+# we need to decrypt something
+if is_crypted_mode; then
+ while ! is_decrypted; do
+ echo "Waiting for decryption..."
+ sleep 1
+ done
+fi
+
# blink 10 times to signal ready state
blink_led 10 &
@@ -8,4 +16,4 @@ if is_auto_mode; then
/usr/local/bin/pwnagotchi
else
/usr/local/bin/pwnagotchi --manual
-fi
\ No newline at end of file
+fi
diff --git a/builder/data/usr/bin/pwnlib b/builder/data/usr/bin/pwnlib
index 4ba2556..2b7a820 100755
--- a/builder/data/usr/bin/pwnlib
+++ b/builder/data/usr/bin/pwnlib
@@ -84,4 +84,80 @@ is_auto_mode_no_delete() {
# no override, but none of the interfaces is up -> AUTO
return 0
-}
\ No newline at end of file
+}
+
+# check if we need to decrypt something
+is_crypted_mode() {
+ if [ -f /root/.pwnagotchi-crypted ]; then
+ return 0
+ fi
+ return 1
+}
+
+# decryption loop
+is_decrypted() {
+ while read -r mapping container mount; do
+ # mapping = name the device or file will be mapped to
+ # container = the luks encrypted device or file
+ # mount = the mountpoint
+
+ # fail if not mounted
+ if ! mountpoint -q "$mount" >/dev/null 2>&1; then
+ if [ -f /tmp/.pwnagotchi-secret ]; then
+ /dev/null 2>&1; then
+ echo "Container decrypted!"
+
+ fi
+ fi
+
+ if mount /dev/mapper/"$mapping" "$mount" >/dev/null 2>&1; then
+ echo "Mounted /dev/mapper/$mapping to $mount"
+ continue
+ fi
+ fi
+
+ if ! ip -4 addr show wlan0 | grep inet >/dev/null 2>&1; then
+ >/dev/null 2>&1 ip addr add 192.168.0.10/24 dev wlan0
+ fi
+
+ if ! pgrep -f decryption-webserver >/dev/null 2>&1; then
+ >/dev/null 2>&1 decryption-webserver &
+ fi
+
+ if ! pgrep wpa_supplicant >/dev/null 2>&1; then
+ >/tmp/wpa_supplicant.conf cat </dev/null 2>&1 wpa_supplicant -D nl80211 -i wlan0 -c /tmp/wpa_supplicant.conf &
+ fi
+
+ if ! pgrep dnsmasq >/dev/null 2>&1; then
+ >/dev/null 2>&1 dnsmasq -k -p 53 -h -O "6,192.168.0.10" -A "/#/192.168.0.10" -i wlan0 -K -F 192.168.0.50,192.168.0.60,255.255.255.0,24h &
+ fi
+
+ return 1
+ fi
+ done /tmp/.pwnagotchi-secret python3 -c 'print("A"*4096)'
+ sync # flush
+
+ pkill wpa_supplicant
+ pkill dnsmasq
+ kill "$(pgrep -f "decryption-webserver")"
+
+ return 0
+}
diff --git a/builder/pwnagotchi.yml b/builder/pwnagotchi.yml
index b821632..b9c21b1 100644
--- a/builder/pwnagotchi.yml
+++ b/builder/pwnagotchi.yml
@@ -101,10 +101,8 @@
- bc
- fonts-freefont-ttf
- fbi
- - python3-flask
- - python3-flask-cors
- - python3-flaskext.wtf
- fonts-ipaexfont-gothic
+ - cryptsetup
tasks:
- name: change hostname
@@ -218,6 +216,16 @@
dest: /usr/local/src/pwnagotchi
register: pwnagotchigit
+ - name: create /usr/local/share/pwnagotchi/ folder
+ file:
+ path: /usr/local/share/pwnagotchi/
+ state: directory
+
+ - name: clone pwnagotchi plugins repository
+ git:
+ repo: https://github.com/evilsocket/pwnagotchi-plugins-contrib.git
+ dest: /usr/local/share/pwnagotchi/availaible-plugins
+
- name: fetch pwnagotchi version
set_fact:
pwnagotchi_version: "{{ lookup('file', '/usr/local/src/pwnagotchi/pwnagotchi/_version.py') | regex_replace('.*__version__.*=.*''([0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*)''.*', '\\1') }}"
diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py
index 58e7702..3a46071 100644
--- a/pwnagotchi/__init__.py
+++ b/pwnagotchi/__init__.py
@@ -3,10 +3,8 @@ import logging
import time
import re
-import pwnagotchi.ui.view as view
-import pwnagotchi
-from pwnagotchi import fs
+
from pwnagotchi._version import __version__
_name = None
@@ -110,10 +108,13 @@ def temperature(celsius=True):
def shutdown():
logging.warning("syncing...")
+ from pwnagotchi import fs
for m in fs.mounts:
m.sync()
logging.warning("shutting down ...")
+
+ from pwnagotchi.ui import view
if view.ROOT:
view.ROOT.on_shutdown()
# give it some time to refresh the ui
@@ -141,6 +142,7 @@ def reboot(mode=None):
else:
logging.warning("rebooting ...")
+ from pwnagotchi.ui import view
if view.ROOT:
view.ROOT.on_rebooting()
# give it some time to refresh the ui
diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py
index 71223a4..bd88479 100644
--- a/pwnagotchi/agent.py
+++ b/pwnagotchi/agent.py
@@ -3,6 +3,7 @@ import json
import os
import re
import logging
+import asyncio
import _thread
import pwnagotchi
@@ -68,7 +69,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
for tag in self._config['bettercap']['silence']:
try:
self.run('events.ignore %s' % tag, verbose_errors=False)
- except Exception as e:
+ except Exception:
pass
def _reset_wifi_settings(self):
@@ -121,9 +122,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def _wait_bettercap(self):
while True:
try:
- s = self.session()
+ _s = self.session()
return
- except:
+ except Exception:
logging.info("waiting for bettercap API to be available ...")
time.sleep(1)
@@ -134,6 +135,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self.set_starting()
self.start_monitor_mode()
self.start_event_polling()
+ self.start_session_fetcher()
# print initial stats
self.next_epoch()
self.set_ready()
@@ -158,7 +160,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
try:
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
except Exception as e:
- logging.exception("error")
+ logging.exception("Error while setting wifi.recon.channels (%s)", e)
self.wait_for(recon_time, sleeping=False)
@@ -188,7 +190,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if self._filter_included(ap):
aps.append(ap)
except Exception as e:
- logging.exception("error")
+ logging.exception("Error while getting acces points (%s)", e)
aps.sort(key=lambda ap: ap['channel'])
return self.set_access_points(aps)
@@ -303,60 +305,67 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if not no_exceptions:
raise
- def _event_poller(self):
- self._load_recovery_data()
+ def start_session_fetcher(self):
+ _thread.start_new_thread(self._fetch_stats, ())
+
+
+ def _fetch_stats(self):
+ while True:
+ s = self.session()
+ self._update_uptime(s)
+ self._update_advertisement(s)
+ self._update_peers()
+ self._update_counters()
+ self._update_handshakes(0)
+ time.sleep(1)
+
+ async def _on_event(self, msg):
+ found_handshake = False
+ jmsg = json.loads(msg)
+
+ if jmsg['tag'] == 'wifi.client.handshake':
+ filename = jmsg['data']['file']
+ sta_mac = jmsg['data']['station']
+ ap_mac = jmsg['data']['ap']
+ key = "%s -> %s" % (sta_mac, ap_mac)
+ if key not in self._handshakes:
+ self._handshakes[key] = jmsg
+ ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
+ if ap_and_station is None:
+ logging.warning("!!! captured new handshake: %s !!!", key)
+ self._last_pwnd = ap_mac
+ plugins.on('handshake', self, filename, ap_mac, sta_mac)
+ else:
+ (ap, sta) = ap_and_station
+ self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
+ 'hostname'] != '' else ap_mac
+ logging.warning(
+ "!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
+ ap['channel'],
+ ap['rssi'],
+ sta['mac'], sta['vendor'],
+ ap['hostname'], ap['mac'], ap['vendor'])
+ plugins.on('handshake', self, filename, ap, sta)
+ found_handshake = True
+ self._update_handshakes(1 if found_handshake else 0)
+
+ def _event_poller(self, loop):
+ self._load_recovery_data()
self.run('events.clear')
while True:
- time.sleep(1)
-
- new_shakes = 0
-
logging.debug("polling events ...")
-
try:
- s = self.session()
- self._update_uptime(s)
-
- self._update_advertisement(s)
- self._update_peers()
- self._update_counters()
-
- for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
- filename = h['data']['file']
- sta_mac = h['data']['station']
- ap_mac = h['data']['ap']
- key = "%s -> %s" % (sta_mac, ap_mac)
-
- if key not in self._handshakes:
- self._handshakes[key] = h
- new_shakes += 1
- ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
- if ap_and_station is None:
- logging.warning("!!! captured new handshake: %s !!!", key)
- self._last_pwnd = ap_mac
- plugins.on('handshake', self, filename, ap_mac, sta_mac)
- else:
- (ap, sta) = ap_and_station
- self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
- 'hostname'] != '' else ap_mac
- logging.warning(
- "!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
- ap['channel'],
- ap['rssi'],
- sta['mac'], sta['vendor'],
- ap['hostname'], ap['mac'], ap['vendor'])
- plugins.on('handshake', self, filename, ap, sta)
-
- except Exception as e:
- logging.error("error: %s", e)
-
- finally:
- self._update_handshakes(new_shakes)
+ loop.create_task(self.start_websocket(self._on_event))
+ loop.run_forever()
+ except Exception as ex:
+ logging.debug("Error while polling via websocket (%s)", ex)
def start_event_polling(self):
- _thread.start_new_thread(self._event_poller, ())
+ # start a thread and pass in the mainloop
+ _thread.start_new_thread(self._event_poller, (asyncio.new_event_loop(),))
+
def is_module_running(self, module):
s = self.session()
@@ -465,4 +474,4 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
plugins.on('channel_hop', self, channel)
except Exception as e:
- logging.error("error: %s", e)
+ logging.error("Error while setting channel (%s)", e)
diff --git a/pwnagotchi/ai/__init__.py b/pwnagotchi/ai/__init__.py
index 73ca4fc..5493342 100644
--- a/pwnagotchi/ai/__init__.py
+++ b/pwnagotchi/ai/__init__.py
@@ -1,12 +1,9 @@
import os
import time
-import warnings
import logging
# https://stackoverflow.com/questions/40426502/is-there-a-way-to-suppress-the-messages-tensorflow-prints/40426709
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # or any {'0', '1', '2'}
-# https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
-warnings.simplefilter(action='ignore', category=FutureWarning)
def load(config, agent, epoch, from_disk=True):
@@ -59,7 +56,7 @@ def load(config, agent, epoch, from_disk=True):
return a2c
except Exception as e:
- logging.exception("error while starting AI")
+ logging.exception("error while starting AI (%s)", e)
logging.warning("[ai] AI not loaded!")
return False
diff --git a/pwnagotchi/ai/epoch.py b/pwnagotchi/ai/epoch.py
index 8f5380a..88d73e0 100644
--- a/pwnagotchi/ai/epoch.py
+++ b/pwnagotchi/ai/epoch.py
@@ -19,6 +19,10 @@ class Epoch(object):
self.active_for = 0
# number of epochs with no visible access points
self.blind_for = 0
+ # number of epochs in sad state
+ self.sad_for = 0
+ # number of epochs in bored state
+ self.bored_for = 0
# did deauth in this epoch in the current channel?
self.did_deauth = False
# number of deauths in this epoch
@@ -99,13 +103,13 @@ class Epoch(object):
try:
aps_per_chan[ch_idx] += 1.0
sta_per_chan[ch_idx] += len(ap['clients'])
- except IndexError as e:
+ 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 as e:
+ except IndexError:
logging.error(
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
@@ -157,6 +161,20 @@ class Epoch(object):
else:
self.active_for += 1
self.inactive_for = 0
+ self.sad_for = 0
+ self.bored_for = 0
+
+ if self.inactive_for >= self.config['personality']['sad_num_epochs']:
+ # sad > bored; cant be sad and bored
+ self.bored_for = 0
+ self.sad_for += 1
+ elif self.inactive_for >= self.config['personality']['bored_num_epochs']:
+ # sad_treshhold > inactive > bored_treshhold; cant be sad and bored
+ self.sad_for = 0
+ self.bored_for += 1
+ else:
+ self.sad_for = 0
+ self.bored_for = 0
now = time.time()
cpu = pwnagotchi.cpu_load()
@@ -172,6 +190,8 @@ class Epoch(object):
'blind_for_epochs': self.blind_for,
'inactive_for_epochs': self.inactive_for,
'active_for_epochs': self.active_for,
+ 'sad_for_epochs': self.sad_for,
+ 'bored_for_epochs': self.bored_for,
'missed_interactions': self.num_missed,
'num_hops': self.num_hops,
'num_peers': self.num_peers,
@@ -188,13 +208,15 @@ class Epoch(object):
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 inactive=%d active=%d peers=%d tot_bond=%.2f "
+ 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 reward=%s" % (
self.epoch,
utils.secs_to_hhmmss(self.epoch_duration),
utils.secs_to_hhmmss(self.num_slept),
self.blind_for,
+ self.sad_for,
+ self.bored_for,
self.inactive_for,
self.active_for,
self.num_peers,
diff --git a/pwnagotchi/ai/reward.py b/pwnagotchi/ai/reward.py
index f0e580d..daaf75f 100644
--- a/pwnagotchi/ai/reward.py
+++ b/pwnagotchi/ai/reward.py
@@ -18,4 +18,10 @@ class RewardFunction(object):
m = -.3 * (state['missed_interactions'] / tot_interactions)
i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
- return h + a + c + b + i + m
+ # include emotions if state >= 5 epochs
+ _sad = state['sad_for_epochs'] if state['sad_for_epochs'] >= 5 else 0
+ _bored = state['bored_for_epochs'] if state['bored_for_epochs'] >= 5 else 0
+ s = -.2 * (_sad / tot_epochs)
+ l = -.1 * (_bored / tot_epochs)
+
+ return h + a + c + b + i + m + s + l
diff --git a/pwnagotchi/ai/train.py b/pwnagotchi/ai/train.py
index ed644d6..4ca2638 100644
--- a/pwnagotchi/ai/train.py
+++ b/pwnagotchi/ai/train.py
@@ -176,7 +176,7 @@ class AsyncTrainer(object):
self.set_training(True, epochs_per_episode)
self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
except Exception as e:
- logging.exception("[ai] error while training")
+ logging.exception("[ai] error while training (%s)", e)
finally:
self.set_training(False)
obs = self._model.env.reset()
diff --git a/pwnagotchi/automata.py b/pwnagotchi/automata.py
index d087c47..29c7728 100644
--- a/pwnagotchi/automata.py
+++ b/pwnagotchi/automata.py
@@ -120,14 +120,14 @@ class Automata(object):
logging.warning("agent missed %d interactions -> lonely", did_miss)
self.set_lonely()
# after X times being bored, the status is set to sad or angry
- elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
+ elif self._epoch.sad_for:
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
if factor >= 2.0:
self.set_angry(factor)
else:
self.set_sad()
# after X times being inactive, the status is set to bored
- elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']:
+ elif self._epoch.bored_for:
self.set_bored()
# after X times being active, the status is set to happy / excited
elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']:
diff --git a/pwnagotchi/bettercap.py b/pwnagotchi/bettercap.py
index 05213ae..422f9cd 100644
--- a/pwnagotchi/bettercap.py
+++ b/pwnagotchi/bettercap.py
@@ -1,5 +1,8 @@
+import json
import logging
import requests
+import websockets
+
from requests.auth import HTTPBasicAuth
@@ -25,15 +28,25 @@ class Client(object):
self.username = username
self.password = password
self.url = "%s://%s:%d/api" % (scheme, hostname, port)
+ self.websocket = "ws://%s:%s@%s:%d/api" % (username, password, hostname, port)
self.auth = HTTPBasicAuth(username, password)
def session(self):
r = requests.get("%s/session" % self.url, auth=self.auth)
return decode(r)
- def events(self):
- r = requests.get("%s/events" % self.url, auth=self.auth)
- return decode(r)
+ async def start_websocket(self, consumer):
+ s = "%s/events" % self.websocket
+ while True:
+ try:
+ async with websockets.connect(s, ping_interval=60, ping_timeout=90) as ws:
+ 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:
+ logging.debug("Lost websocket connection. Reconnecting...")
def run(self, command, verbose_errors=True):
r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command})
diff --git a/pwnagotchi/defaults.toml b/pwnagotchi/defaults.toml
index 0057037..937cca1 100644
--- a/pwnagotchi/defaults.toml
+++ b/pwnagotchi/defaults.toml
@@ -1,5 +1,6 @@
main.name = ""
main.lang = "en"
+main.confd = "/etc/pwnagotchi/conf.d/"
main.custom_plugins = ""
main.iface = "mon0"
main.mon_start_cmd = "/usr/bin/monstart"
@@ -14,7 +15,6 @@ main.whitelist = [
]
main.filter = ""
-
main.plugins.grid.enabled = true
main.plugins.grid.report = false
main.plugins.grid.exclude = [
@@ -38,14 +38,17 @@ main.plugins.onlinehashcrack.enabled = false
main.plugins.onlinehashcrack.email = ""
main.plugins.onlinehashcrack.dashboard = ""
main.plugins.onlinehashcrack.single_files = false
+main.plugins.onlinehashcrack.whitelist = []
main.plugins.wpa-sec.enabled = false
main.plugins.wpa-sec.api_key = ""
main.plugins.wpa-sec.api_url = "https://wpa-sec.stanev.org"
main.plugins.wpa-sec.download_results = false
+main.plugins.wpa-sec.whitelist = []
main.plugins.wigle.enabled = false
main.plugins.wigle.api_key = ""
+main.plugins.wigle.whitelist = []
main.plugins.bt-tether.enabled = false
@@ -106,6 +109,8 @@ main.plugins.led.patterns.epoch = "oo oo oo oo oo oo oo"
main.plugins.led.patterns.peer_detected = "oo oo oo oo oo oo oo"
main.plugins.led.patterns.peer_lost = "oo oo oo oo oo oo oo"
+main.plugins.logtail.enabled = false
+
main.plugins.session-stats.enabled = true
main.plugins.session-stats.save_directory = "/var/tmp/pwnagotchi/sessions/"
diff --git a/pwnagotchi/fs/__init__.py b/pwnagotchi/fs/__init__.py
index 0abdfea..4e0fec1 100644
--- a/pwnagotchi/fs/__init__.py
+++ b/pwnagotchi/fs/__init__.py
@@ -11,6 +11,7 @@ from distutils.dir_util import copy_tree
mounts = list()
+
@contextlib.contextmanager
def ensure_write(filename, mode='w'):
path = os.path.dirname(filename)
@@ -25,18 +26,27 @@ def ensure_write(filename, mode='w'):
def size_of(path):
+ """
+ Calculate the sum of all the files in path
+ """
total = 0
- for root, dirs, files in os.walk(path):
+ for root, _, files in os.walk(path):
for f in files:
total += os.path.getsize(os.path.join(root, f))
return total
def is_mountpoint(path):
+ """
+ Checks if path is mountpoint
+ """
return os.system(f"mountpoint -q {path}") == 0
def setup_mounts(config):
+ """
+ Sets up all the configured mountpoints
+ """
global mounts
fs_cfg = config['fs']['memory']
if not fs_cfg['enabled']:
@@ -82,6 +92,7 @@ def setup_mounts(config):
mounts.append(m)
+
class MemoryFS:
@staticmethod
def zram_install():
@@ -90,11 +101,13 @@ class MemoryFS:
return os.system("modprobe zram") == 0
return True
+
@staticmethod
def zram_dev():
logging.debug("[FS] Adding zram device")
return open("/sys/class/zram-control/hot_add", "rt").read().strip("\n")
+
def __init__(self, mount, disk, size="40M",
zram=True, zram_alg="lz4", zram_disk_size="100M",
zram_fs_type="ext4", rsync=True):
@@ -109,6 +122,7 @@ class MemoryFS:
self.rsync = True
self._setup()
+
def _setup(self):
if self.zram and MemoryFS.zram_install():
# setup zram
diff --git a/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo b/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo
index 536f891..ddc462c 100644
Binary files a/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo and b/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/jp/LC_MESSAGES/voice.po b/pwnagotchi/locale/jp/LC_MESSAGES/voice.po
index efaabc4..64383a1 100644
--- a/pwnagotchi/locale/jp/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/jp/LC_MESSAGES/voice.po
@@ -228,8 +228,8 @@ msgid ""
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr ""
-"{duration}中{deauted}のAPに拒否されたけど、{associated}回チャンスがあって"
-"{handshakes}回ハンドシェイクがあったよ。。 #pownagotchi #pwnlog #pwnlife "
+"{duration}中{deauthed}のAPに拒否されたけど、{associated}回チャンスがあって"
+"{handshakes}回ハンドシェイクがあったよ。。 #pwnagotchi #pwnlog #pwnlife "
"#hacktheplanet #skynet"
msgid "hours"
diff --git a/pwnagotchi/log.py b/pwnagotchi/log.py
index 16f8293..f13e5d2 100644
--- a/pwnagotchi/log.py
+++ b/pwnagotchi/log.py
@@ -5,6 +5,7 @@ import os
import logging
import shutil
import gzip
+import warnings
from datetime import datetime
from pwnagotchi.voice import Voice
@@ -235,11 +236,18 @@ def setup_logging(args, config):
console_handler.setFormatter(formatter)
root.addHandler(console_handler)
- # https://stackoverflow.com/questions/24344045/how-can-i-completely-remove-any-logging-from-requests-module-in-python?noredirect=1&lq=1
- logging.getLogger("urllib3").propagate = False
- requests_log = logging.getLogger("requests")
- requests_log.addHandler(logging.NullHandler())
- requests_log.propagate = False
+ if not args.debug:
+ # disable scapy and tensorflow logging
+ logging.getLogger("scapy").disabled = True
+ logging.getLogger('tensorflow').disabled = True
+ # https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
+ warnings.simplefilter(action='ignore', category=FutureWarning)
+ warnings.simplefilter(action='ignore', category=DeprecationWarning)
+ # https://stackoverflow.com/questions/24344045/how-can-i-completely-remove-any-logging-from-requests-module-in-python?noredirect=1&lq=1
+ logging.getLogger("urllib3").propagate = False
+ requests_log = logging.getLogger("requests")
+ requests_log.addHandler(logging.NullHandler())
+ requests_log.prpagate = False
def log_rotation(filename, cfg):
diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py
index 6616be6..2ffd21c 100644
--- a/pwnagotchi/plugins/__init__.py
+++ b/pwnagotchi/plugins/__init__.py
@@ -4,9 +4,7 @@ import _thread
import threading
import importlib, importlib.util
import logging
-import pwnagotchi
-from pwnagotchi.ui import view
-from pwnagotchi.utils import save_config
+
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
@@ -39,6 +37,10 @@ def toggle_plugin(name, enable=True):
returns True if changed, otherwise False
"""
+ import pwnagotchi
+ from pwnagotchi.ui import view
+ from pwnagotchi.utils import save_config
+
global loaded, database
if pwnagotchi.config:
@@ -55,6 +57,8 @@ def toggle_plugin(name, enable=True):
if enable and name in database and name not in loaded:
load_from_file(database[name])
one(name, 'loaded')
+ if pwnagotchi.config:
+ one(name, 'config_changed', pwnagotchi.config)
one(name, 'ui_setup', view.ROOT)
one(name, 'ready', view.ROOT._agent)
return True
@@ -63,7 +67,7 @@ def toggle_plugin(name, enable=True):
def on(event_name, *args, **kwargs):
- for plugin_name, plugin in loaded.items():
+ for plugin_name in loaded.keys():
one(plugin_name, event_name, *args, **kwargs)
@@ -136,3 +140,4 @@ def load(config):
plugin.options = config['main']['plugins'][name]
on('loaded')
+ on('config_changed', config)
diff --git a/pwnagotchi/plugins/cmd.py b/pwnagotchi/plugins/cmd.py
new file mode 100644
index 0000000..af8226c
--- /dev/null
+++ b/pwnagotchi/plugins/cmd.py
@@ -0,0 +1,389 @@
+# Handles the commandline stuff
+
+import sys
+import os
+import logging
+import glob
+import re
+import shutil
+from fnmatch import fnmatch
+from pwnagotchi.utils import download_file, unzip, save_config, parse_version, md5
+from pwnagotchi.plugins import default_path
+
+
+REPO_URL = 'https://github.com/evilsocket/pwnagotchi-plugins-contrib/archive/master.zip'
+SAVE_DIR = '/usr/local/share/pwnagotchi/availaible-plugins/'
+DEFAULT_INSTALL_PATH = '/usr/local/share/pwnagotchi/installed-plugins/'
+
+
+def add_parsers(parser):
+ """
+ Adds the plugins subcommand to a given argparse.ArgumentParser
+ """
+ subparsers = parser.add_subparsers()
+ ## pwnagotchi plugins
+ parser_plugins = subparsers.add_parser('plugins')
+ plugin_subparsers = parser_plugins.add_subparsers(dest='plugincmd')
+
+ ## pwnagotchi plugins search
+ parser_plugins_search = plugin_subparsers.add_parser('search', help='Search for pwnagotchi plugins')
+ parser_plugins_search.add_argument('pattern', type=str, help="Search expression (wildcards allowed)")
+
+ ## pwnagotchi plugins list
+ parser_plugins_list = plugin_subparsers.add_parser('list', help='List available pwnagotchi plugins')
+ parser_plugins_list.add_argument('-i', '--installed', action='store_true', required=False, help='List also installed plugins')
+
+ ## pwnagotchi plugins update
+ parser_plugins_update = plugin_subparsers.add_parser('update', help='Updates the database')
+
+ ## pwnagotchi plugins upgrade
+ parser_plugins_upgrade = plugin_subparsers.add_parser('upgrade', help='Upgrades plugins')
+ parser_plugins_upgrade.add_argument('pattern', type=str, nargs='?', default='*', help="Filter expression (wildcards allowed)")
+
+ ## pwnagotchi plugins enable
+ parser_plugins_enable = plugin_subparsers.add_parser('enable', help='Enables a plugin')
+ parser_plugins_enable.add_argument('name', type=str, help='Name of the plugin')
+
+ ## pwnagotchi plugins disable
+ parser_plugins_disable = plugin_subparsers.add_parser('disable', help='Disables a plugin')
+ parser_plugins_disable.add_argument('name', type=str, help='Name of the plugin')
+
+ ## pwnagotchi plugins install
+ parser_plugins_install = plugin_subparsers.add_parser('install', help='Installs a plugin')
+ parser_plugins_install.add_argument('name', type=str, help='Name of the plugin')
+
+ ## pwnagotchi plugins uninstall
+ parser_plugins_uninstall = plugin_subparsers.add_parser('uninstall', help='Uninstalls a plugin')
+ parser_plugins_uninstall.add_argument('name', type=str, help='Name of the plugin')
+
+ ## pwnagotchi plugins edit
+ parser_plugins_edit = plugin_subparsers.add_parser('edit', help='Edit the options')
+ parser_plugins_edit.add_argument('name', type=str, help='Name of the plugin')
+
+ return parser
+
+
+def used_plugin_cmd(args):
+ """
+ Checks if the plugins subcommand was used
+ """
+ return hasattr(args, 'plugincmd')
+
+
+def handle_cmd(args, config):
+ """
+ Parses the arguments and does the thing the user wants
+ """
+ if args.plugincmd == 'update':
+ return update()
+ elif args.plugincmd == 'search':
+ args.installed = True # also search in installed plugins
+ return list_plugins(args, config, args.pattern)
+ elif args.plugincmd == 'install':
+ return install(args, config)
+ elif args.plugincmd == 'uninstall':
+ return uninstall(args, config)
+ elif args.plugincmd == 'list':
+ return list_plugins(args, config)
+ elif args.plugincmd == 'enable':
+ return enable(args, config)
+ elif args.plugincmd == 'disable':
+ return disable(args, config)
+ elif args.plugincmd == 'upgrade':
+ return upgrade(args, config, args.pattern)
+ elif args.plugincmd == 'edit':
+ return edit(args, config)
+
+ raise NotImplementedError()
+
+
+def edit(args, config):
+ """
+ Edit the config of the plugin
+ """
+ plugin = args.name
+ editor = os.environ.get('EDITOR', 'vim') # because vim is the best
+
+ if plugin not in config['main']['plugins']:
+ return 1
+
+ plugin_config = {'main': {'plugins': {plugin: config['main']['plugins'][plugin]}}}
+
+ import toml
+ from subprocess import call
+ from tempfile import NamedTemporaryFile
+ from pwnagotchi.utils import DottedTomlEncoder
+
+ new_plugin_config = None
+ with NamedTemporaryFile(suffix=".tmp", mode='r+t') as tmp:
+ tmp.write(toml.dumps(plugin_config, encoder=DottedTomlEncoder()))
+ tmp.flush()
+ rc = call([editor, tmp.name])
+ if rc != 0:
+ return rc
+ tmp.seek(0)
+ new_plugin_config = toml.load(tmp)
+
+ config['main']['plugins'][plugin] = new_plugin_config['main']['plugins'][plugin]
+ save_config(config, args.user_config)
+ return 0
+
+
+def enable(args, config):
+ """
+ Enables the given plugin and saves the config to disk
+ """
+ if args.name not in config['main']['plugins']:
+ config['main']['plugins'][args.name] = dict()
+ config['main']['plugins'][args.name]['enabled'] = True
+ save_config(config, args.user_config)
+ return 0
+
+
+def disable(args, config):
+ """
+ Disables the given plugin and saves the config to disk
+ """
+ if args.name not in config['main']['plugins']:
+ config['main']['plugins'][args.name] = dict()
+ config['main']['plugins'][args.name]['enabled'] = False
+ save_config(config, args.user_config)
+ return 0
+
+
+def upgrade(args, config, pattern='*'):
+ """
+ Upgrades the given plugin
+ """
+ available = _get_available()
+ installed = _get_installed(config)
+
+ for plugin, filename in installed.items():
+ if not fnmatch(plugin, pattern) or plugin not in available:
+ continue
+
+ available_version = _extract_version(available[plugin])
+ installed_version = _extract_version(filename)
+
+ if installed_version and available_version:
+ if available_version <= installed_version:
+ continue
+ else:
+ continue
+
+ logging.info('Upgrade %s from %s to %s', plugin, '.'.join(installed_version), '.'.join(available_version))
+ shutil.copyfile(available[plugin], installed[plugin])
+
+ # maybe has config
+ for conf in glob.glob(available[plugin].replace('.py', '.y?ml')):
+ dst = os.path.join(os.path.dirname(installed[plugin]), os.path.basename(conf))
+ if os.path.exists(dst) and md5(dst) != md5(conf):
+ # backup
+ logging.info('Backing up config: %s', os.path.basename(conf))
+ shutil.move(dst, dst + '.bak')
+ shutil.copyfile(conf, dst)
+
+ return 0
+
+
+def list_plugins(args, config, pattern='*'):
+ """
+ Lists the available and installed plugins
+ """
+ found = False
+
+ line = "|{name:^{width}}|{version:^9}|{enabled:^10}|{status:^15}|"
+
+ available = _get_available()
+ installed = _get_installed(config)
+
+ available_and_installed = set(list(available.keys()) + list(installed.keys()))
+ available_not_installed = set(available.keys()) - set(installed.keys())
+
+ max_len_list = available_and_installed if args.installed else available_not_installed
+ max_len = max(map(len, max_len_list))
+ header = line.format(name='Plugin', width=max_len, version='Version', enabled='Active', status='Status')
+ line_length = max(max_len, len('Plugin')) + len(header) - len('Plugin') - 12 # lol
+
+ print('-' * line_length)
+ print(header)
+ print('-' * line_length)
+
+ if args.installed:
+ # only installed (maybe update available?)
+ for plugin, filename in sorted(installed.items()):
+ if not fnmatch(plugin, pattern):
+ continue
+ found = True
+ installed_version = _extract_version(filename)
+ available_version = None
+ if plugin in available:
+ available_version = _extract_version(available[plugin])
+
+ status = "installed"
+ if installed_version and available_version:
+ if available_version > installed_version:
+ status = "installed (^)"
+
+ enabled = 'enabled' if plugin in config['main']['plugins'] and \
+ 'enabled' in config['main']['plugins'][plugin] and \
+ config['main']['plugins'][plugin]['enabled'] \
+ else 'disabled'
+
+ print(line.format(name=plugin, width=max_len, version='.'.join(installed_version), enabled=enabled, status=status))
+
+
+ for plugin in sorted(available_not_installed):
+ if not fnmatch(plugin, pattern):
+ continue
+ found = True
+ available_version = _extract_version(available[plugin])
+ print(line.format(name=plugin, width=max_len, version='.'.join(available_version), enabled='-', status='available'))
+
+ print('-' * line_length)
+
+ if not found:
+ logging.info('Maybe try: pwnagotchi plugins update')
+ return 1
+ return 0
+
+
+def _extract_version(filename):
+ """
+ Extracts the version from a python file
+ """
+ plugin_content = open(filename, 'rt').read()
+ m = re.search(r'__version__[\t ]*=[\t ]*[\'\"]([^\"\']+)', plugin_content)
+ if m:
+ return parse_version(m.groups()[0])
+ return None
+
+
+def _get_available():
+ """
+ Get all availaible plugins
+ """
+ available = dict()
+ for filename in glob.glob(os.path.join(SAVE_DIR, "*.py")):
+ plugin_name = os.path.basename(filename.replace(".py", ""))
+ available[plugin_name] = filename
+ return available
+
+
+def _get_installed(config):
+ """
+ Get all installed plugins
+ """
+ installed = dict()
+ search_dirs = [ default_path, config['main']['custom_plugins'] ]
+ for search_dir in search_dirs:
+ if search_dir:
+ for filename in glob.glob(os.path.join(search_dir, "*.py")):
+ plugin_name = os.path.basename(filename.replace(".py", ""))
+ installed[plugin_name] = filename
+ return installed
+
+
+def uninstall(args, config):
+ """
+ Uninstalls a plugin
+ """
+ plugin_name = args.name
+ installed = _get_installed(config)
+ if plugin_name not in installed:
+ logging.error('Plugin %s is not installed.', plugin_name)
+ return 1
+ os.remove(installed[plugin_name])
+ return 0
+
+
+def install(args, config):
+ """
+ Installs the given plugin
+ """
+ global DEFAULT_INSTALL_PATH
+ plugin_name = args.name
+ available = _get_available()
+ installed = _get_installed(config)
+
+ if plugin_name not in available:
+ logging.error('%s not found.', plugin_name)
+ return 1
+
+ if plugin_name in installed:
+ logging.error('%s already installed.', plugin_name)
+
+ # install into custom_plugins path
+ install_path = config['main']['custom_plugins']
+ if not install_path:
+ install_path = DEFAULT_INSTALL_PATH
+ config['main']['custom_plugins'] = install_path
+ save_config(config, args.user_config)
+
+ os.makedirs(install_path, exist_ok=True)
+
+ shutil.copyfile(available[plugin_name], os.path.join(install_path, os.path.basename(available[plugin_name])))
+
+ # maybe has config
+ for conf in glob.glob(available[plugin_name].replace('.py', '.y?ml')):
+ dst = os.path.join(install_path, os.path.basename(conf))
+ if os.path.exists(dst) and md5(dst) != md5(conf):
+ # backup
+ logging.info('Backing up config: %s', os.path.basename(conf))
+ shutil.move(dst, dst + '.bak')
+ shutil.copyfile(conf, dst)
+
+ return 0
+
+
+def _analyse_dir(path):
+ results = dict()
+ path += '*' if path.endswith('/') else '/*'
+ for filename in glob.glob(path, recursive=True):
+ if not os.path.isfile(filename):
+ continue
+ try:
+ results[filename] = md5(filename)
+ except OSError:
+ continue
+ return results
+
+
+def update():
+ """
+ Updates the database
+ """
+ global REPO_URL, SAVE_DIR
+
+ DEST = os.path.join(SAVE_DIR, 'plugins.zip')
+ logging.info('Downloading plugins to %s', DEST)
+
+ try:
+ os.makedirs(SAVE_DIR, exist_ok=True)
+ before_update = _analyse_dir(SAVE_DIR)
+
+ download_file(REPO_URL, os.path.join(SAVE_DIR, DEST))
+
+ logging.info('Unzipping...')
+ unzip(DEST, SAVE_DIR, strip_dirs=1)
+
+ after_update = _analyse_dir(SAVE_DIR)
+
+ b_len = len(before_update)
+ a_len = len(after_update)
+
+ if a_len > b_len:
+ logging.info('Found %d new file(s).', a_len - b_len)
+
+ changed = 0
+ for filename, filehash in after_update.items():
+ if filename in before_update and filehash != before_update[filename]:
+ changed += 1
+
+ if changed:
+ logging.info('%d file(s) were changed.', changed)
+
+ return 0
+ except Exception as ex:
+ logging.error('Error while updating plugins %s', ex)
+ return 1
diff --git a/pwnagotchi/plugins/default/auto-update.py b/pwnagotchi/plugins/default/auto-update.py
index 5f996c4..1723161 100644
--- a/pwnagotchi/plugins/default/auto-update.py
+++ b/pwnagotchi/plugins/default/auto-update.py
@@ -6,12 +6,11 @@ import requests
import platform
import shutil
import glob
-import pkg_resources
from threading import Lock
import pwnagotchi
import pwnagotchi.plugins as plugins
-from pwnagotchi.utils import StatusFile
+from pwnagotchi.utils import StatusFile, parse_version as version_to_tuple
def check(version, repo, native=True):
@@ -30,8 +29,8 @@ def check(version, repo, native=True):
info['available'] = latest_ver = latest['tag_name'].replace('v', '')
is_arm = info['arch'].startswith('arm')
- local = pkg_resources.parse_version(info['current'])
- remote = pkg_resources.parse_version(latest_ver)
+ local = version_to_tuple(info['current'])
+ remote = version_to_tuple(latest_ver)
if remote > local:
if not native:
info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name'])
@@ -161,6 +160,9 @@ class AutoUpdate(plugins.Plugin):
logging.info("[update] plugin loaded.")
def on_internet_available(self, agent):
+ if self.lock.locked():
+ return
+
with self.lock:
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
diff --git a/pwnagotchi/plugins/default/grid.py b/pwnagotchi/plugins/default/grid.py
index 23181ae..acf8092 100644
--- a/pwnagotchi/plugins/default/grid.py
+++ b/pwnagotchi/plugins/default/grid.py
@@ -7,6 +7,7 @@ import re
import pwnagotchi.grid as grid
import pwnagotchi.plugins as plugins
from pwnagotchi.utils import StatusFile, WifiInfo, extract_from_pcap
+from threading import Lock
def parse_pcap(filename):
@@ -54,6 +55,7 @@ class Grid(plugins.Plugin):
self.unread_messages = 0
self.total_messages = 0
+ self.lock = Lock()
def is_excluded(self, what):
for skip in self.options['exclude']:
@@ -122,21 +124,25 @@ class Grid(plugins.Plugin):
def on_internet_available(self, agent):
logging.debug("internet available")
- try:
- grid.update_data(agent.last_session)
- except Exception as e:
- logging.error("error connecting to the pwngrid-peer service: %s" % e)
- logging.debug(e, exc_info=True)
+ if self.lock.locked():
return
- try:
- self.check_inbox(agent)
- except Exception as e:
- logging.error("[grid] error while checking inbox: %s" % e)
- logging.debug(e, exc_info=True)
+ with self.lock:
+ try:
+ grid.update_data(agent.last_session)
+ except Exception as e:
+ logging.error("error connecting to the pwngrid-peer service: %s" % e)
+ logging.debug(e, exc_info=True)
+ return
- try:
- self.check_handshakes(agent)
- except Exception as e:
- logging.error("[grid] error while checking pcaps: %s" % e)
- logging.debug(e, exc_info=True)
+ try:
+ self.check_inbox(agent)
+ except Exception as e:
+ logging.error("[grid] error while checking inbox: %s" % e)
+ logging.debug(e, exc_info=True)
+
+ try:
+ self.check_handshakes(agent)
+ except Exception as e:
+ logging.error("[grid] error while checking pcaps: %s" % e)
+ logging.debug(e, exc_info=True)
diff --git a/pwnagotchi/plugins/default/logtail.py b/pwnagotchi/plugins/default/logtail.py
new file mode 100644
index 0000000..b19500e
--- /dev/null
+++ b/pwnagotchi/plugins/default/logtail.py
@@ -0,0 +1,282 @@
+import os
+import logging
+import threading
+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
+
+
+TEMPLATE = """
+{% extends "base.html" %}
+{% set active_page = "plugins" %}
+{% block title %}
+ Logtail
+{% endblock %}
+
+{% block styles %}
+ {{ super() }}
+
+{% endblock %}
+
+{% block script %}
+ var content = document.getElementById('content');
+ var filter = document.getElementById('filter');
+ var filterVal = filter.value.toUpperCase();
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', '{{ url_for('plugins') }}/logtail/stream');
+ xhr.send();
+ var position = 0;
+ var data;
+ var time;
+ var level;
+ var msg;
+ var colorClass;
+
+ function handleNewData() {
+ var messages = xhr.responseText.split('\\n');
+ filterVal = filter.value.toUpperCase();
+ messages.slice(position, -1).forEach(function(value) {
+
+ if (value.charAt(0) != '[') {
+ msg = value;
+ time = '';
+ level = '';
+ } else {
+ data = value.split(']');
+ time = data.shift() + ']';
+ level = data.shift() + ']';
+ msg = data.join(']');
+
+ switch(level) {
+ case ' [INFO]':
+ colorClass = 'info';
+ break;
+ case ' [WARNING]':
+ colorClass = 'warning';
+ break;
+ case ' [ERROR]':
+ colorClass = 'error';
+ break;
+ case ' [DEBUG]':
+ colorClass = 'debug';
+ break;
+ default:
+ colorClass = 'default';
+ break;
+ }
+ }
+
+ 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;
+ td3.textContent = msg;
+
+ tr.appendChild(td1);
+ tr.appendChild(td2);
+ tr.appendChild(td3);
+
+ tr.className = colorClass;
+
+ if (filterVal.length > 0 && value.toUpperCase().indexOf(filterVal) == -1) {
+ tr.style.visibility = "collapse";
+ }
+
+ content.appendChild(tr);
+ });
+ position = messages.length - 1;
+ }
+
+ var scrollingElement = (document.scrollingElement || document.body)
+ function scrollToBottom () {
+ scrollingElement.scrollTop = scrollingElement.scrollHeight;
+ }
+
+ var timer;
+ var scrollElm = document.getElementById('autoscroll');
+ timer = setInterval(function() {
+ handleNewData();
+ if (scrollElm.checked) {
+ scrollToBottom();
+ }
+ if (xhr.readyState == XMLHttpRequest.DONE) {
+ clearInterval(timer);
+ }
+ }, 1000);
+
+ var typingTimer;
+ var doneTypingInterval = 1000;
+
+ filter.onkeyup = function() {
+ clearTimeout(typingTimer);
+ typingTimer = setTimeout(doneTyping, doneTypingInterval);
+ }
+
+ filter.onkeydown = function() {
+ clearTimeout(typingTimer);
+ }
+
+ function doneTyping() {
+ document.body.style.cursor = 'progress';
+ var table, tr, tds, td, i, txtValue;
+ filterVal = filter.value.toUpperCase();
+ table = document.getElementById("content");
+ tr = table.getElementsByTagName("tr");
+ for (i = 0; i < tr.length; i++) {
+ tds = tr[i].getElementsByTagName("td");
+ if (tds) {
+ for (l = 0; l < tds.length; l++) {
+ td = tds[l];
+ if (td) {
+ txtValue = td.textContent || td.innerText;
+ if (txtValue.toUpperCase().indexOf(filterVal) > -1) {
+ tr[i].style.visibility = "visible";
+ break;
+ } else {
+ tr[i].style.visibility = "collapse";
+ }
+ }
+ }
+ }
+ }
+ document.body.style.cursor = 'default';
+ }
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ Time
+ |
+
+ Level
+ |
+
+ Message
+ |
+
+
+{% endblock %}
+"""
+
+
+class Logtail(plugins.Plugin):
+ __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()
+ self.options = dict()
+ self.ready = False
+
+ def on_config_changed(self, config):
+ self.config = config
+ self.ready = True
+
+ def on_loaded(self):
+ """
+ Gets called when the plugin gets loaded
+ """
+ logging.info("Logtail plugin loaded.")
+
+ def on_webhook(self, path, request):
+ if not self.ready:
+ return "Plugin not ready"
+
+ if not path or path == "/":
+ return render_template_string(TEMPLATE)
+
+ if path == 'stream':
+ def generate():
+ with open(self.config['main']['log']['path']) as f:
+ yield f.read()
+ while True:
+ yield f.readline()
+
+ return Response(generate(), mimetype='text/plain')
+
+ abort(404)
diff --git a/pwnagotchi/plugins/default/net-pos.py b/pwnagotchi/plugins/default/net-pos.py
index f4b38eb..9f7fdd0 100644
--- a/pwnagotchi/plugins/default/net-pos.py
+++ b/pwnagotchi/plugins/default/net-pos.py
@@ -49,6 +49,8 @@ class NetPos(plugins.Plugin):
saved_file.write(x + "\n")
def on_internet_available(self, agent):
+ if self.lock.locked():
+ return
with self.lock:
if self.ready:
config = agent.config()
diff --git a/pwnagotchi/plugins/default/onlinehashcrack.py b/pwnagotchi/plugins/default/onlinehashcrack.py
index 683e27a..d2fd837 100644
--- a/pwnagotchi/plugins/default/onlinehashcrack.py
+++ b/pwnagotchi/plugins/default/onlinehashcrack.py
@@ -5,7 +5,7 @@ import re
import requests
from datetime import datetime
from threading import Lock
-from pwnagotchi.utils import StatusFile
+from pwnagotchi.utils import StatusFile, remove_whitelisted
import pwnagotchi.plugins as plugins
from json.decoder import JSONDecodeError
@@ -20,7 +20,7 @@ class OnlineHashCrack(plugins.Plugin):
self.ready = False
try:
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
- except JSONDecodeError as json_err:
+ except JSONDecodeError:
os.remove('/root/.ohc_uploads')
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
self.skip = list()
@@ -35,25 +35,11 @@ class OnlineHashCrack(plugins.Plugin):
return
if 'whitelist' not in self.options:
- self.options['whitelist'] = []
-
- # remove special characters from whitelist APs to match on-disk format
- self.options['whitelist'] = set(map(lambda x: re.sub(r'[^a-zA-Z0-9]', '', x), self.options['whitelist']))
+ self.options['whitelist'] = list()
self.ready = True
logging.info("OHC: OnlineHashCrack plugin loaded.")
- def _filter_handshake_file(self, handshake_filename):
- try:
- basename = os.path.basename(handshake_filename)
- ssid, bssid = basename.split('_')
- # remove the ".pcap" from the bssid (which is really just the end of the filename)
- bssid = bssid[:-5]
- except:
- # something failed in our parsing of the filename. let the file through
- return True
-
- return ssid not in self.options['whitelist'] and bssid not in self.options['whitelist']
def _upload_to_ohc(self, path, timeout=30):
"""
@@ -96,64 +82,59 @@ class OnlineHashCrack(plugins.Plugin):
"""
Called in manual mode when there's internet connectivity
"""
+
+ if not self.ready or self.lock.locked():
+ return
+
with self.lock:
- if self.ready:
- display = agent.view()
- config = agent.config()
- reported = self.report.data_field_or('reported', default=list())
-
- handshake_dir = config['bettercap']['handshakes']
- handshake_filenames = os.listdir(handshake_dir)
- handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
- filename.endswith('.pcap')]
-
- # pull out whitelisted APs
- handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
-
- handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
-
- if handshake_new:
- logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onlinehashcrack.com")
-
- for idx, handshake in enumerate(handshake_new):
- display.set('status',
- f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
- display.update(force=True)
- try:
- self._upload_to_ohc(handshake)
- if handshake not in reported:
- reported.append(handshake)
- self.report.update(data={'reported': reported})
- logging.info(f"OHC: Successfully uploaded {handshake}")
- except requests.exceptions.RequestException as req_e:
- self.skip.append(handshake)
- logging.error("OHC: %s", req_e)
- continue
- except OSError as os_e:
- self.skip.append(handshake)
- logging.error("OHC: %s", os_e)
- continue
-
- if 'dashboard' in self.options and self.options['dashboard']:
- cracked_file = os.path.join(handshake_dir, 'onlinehashcrack.cracked')
- if os.path.exists(cracked_file):
- last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
- if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
- return
-
+ display = agent.view()
+ config = agent.config()
+ reported = self.report.data_field_or('reported', default=list())
+ handshake_dir = config['bettercap']['handshakes']
+ handshake_filenames = os.listdir(handshake_dir)
+ handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
+ filename.endswith('.pcap')]
+ # pull out whitelisted APs
+ handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist'])
+ handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
+ if handshake_new:
+ logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onlinehashcrack.com")
+ for idx, handshake in enumerate(handshake_new):
+ display.set('status',
+ f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
+ display.update(force=True)
try:
- self._download_cracked(cracked_file)
- logging.info("OHC: Downloaded cracked passwords.")
+ self._upload_to_ohc(handshake)
+ if handshake not in reported:
+ reported.append(handshake)
+ self.report.update(data={'reported': reported})
+ logging.info(f"OHC: Successfully uploaded {handshake}")
except requests.exceptions.RequestException as req_e:
- logging.debug("OHC: %s", req_e)
+ self.skip.append(handshake)
+ logging.error("OHC: %s", req_e)
+ continue
except OSError as os_e:
- logging.debug("OHC: %s", os_e)
-
- if 'single_files' in self.options and self.options['single_files']:
- with open(cracked_file, 'r') as cracked_list:
- for row in csv.DictReader(cracked_list):
- if row['password']:
- filename = re.sub(r'[^a-zA-Z0-9]', '', row['ESSID']) + '_' + row['BSSID'].replace(':','')
- if os.path.exists( os.path.join(handshake_dir, filename+'.pcap') ):
- with open(os.path.join(handshake_dir, filename+'.pcap.cracked'), 'w') as f:
- f.write(row['password'])
+ self.skip.append(handshake)
+ logging.error("OHC: %s", os_e)
+ continue
+ if 'dashboard' in self.options and self.options['dashboard']:
+ cracked_file = os.path.join(handshake_dir, 'onlinehashcrack.cracked')
+ if os.path.exists(cracked_file):
+ last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
+ if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
+ return
+ try:
+ self._download_cracked(cracked_file)
+ logging.info("OHC: Downloaded cracked passwords.")
+ except requests.exceptions.RequestException as req_e:
+ logging.debug("OHC: %s", req_e)
+ except OSError as os_e:
+ logging.debug("OHC: %s", os_e)
+ if 'single_files' in self.options and self.options['single_files']:
+ with open(cracked_file, 'r') as cracked_list:
+ for row in csv.DictReader(cracked_list):
+ if row['password']:
+ filename = re.sub(r'[^a-zA-Z0-9]', '', row['ESSID']) + '_' + row['BSSID'].replace(':','')
+ if os.path.exists( os.path.join(handshake_dir, filename+'.pcap') ):
+ with open(os.path.join(handshake_dir, filename+'.pcap.cracked'), 'w') as f:
+ f.write(row['password'])
diff --git a/pwnagotchi/plugins/default/session-stats.py b/pwnagotchi/plugins/default/session-stats.py
index 2d4a6d9..06ab168 100644
--- a/pwnagotchi/plugins/default/session-stats.py
+++ b/pwnagotchi/plugins/default/session-stats.py
@@ -16,11 +16,22 @@ TEMPLATE = """
{% endblock %}
{% block styles %}
+ {{ super() }}
+
{% endblock %}
{% block scripts %}
+ {{ super() }}
@@ -83,7 +94,6 @@ TEMPLATE = """
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
- min: 0,
tickOptions:{formatString:'%.2f'}
}
},
@@ -102,7 +112,6 @@ TEMPLATE = """
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
- min: 0,
tickOptions:{formatString:'%.2f'}
}
}
@@ -121,8 +130,9 @@ TEMPLATE = """
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/nums' + '?session=' + session, 'chart_nums', 'Wifi', true)
+ 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)
}
@@ -134,14 +144,15 @@ TEMPLATE = """
{% endblock %}
{% block content %}
-