Merge pull request from evilsocket/master

pull
This commit is contained in:
hmax42 2019-11-18 17:19:52 +01:00 committed by GitHub
commit b1ad247e11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
265 changed files with 42492 additions and 340 deletions
.editorconfig.gitignore
bin
pwnagotchi
__init__.pyagent.py
ai
defaults.ymlgrid.py
locale/spa/LC_MESSAGES
mesh
plugins
ui

7
.editorconfig Normal file

@ -0,0 +1,7 @@
# top-most EditorConfig file
root = true
# Matches the exact files either package.json or .travis.yml
[{*.yml,*.yaml,config.yml,defaults.yml}]
indent_style = space
indent_size = 2

2
.gitignore vendored

@ -15,3 +15,5 @@ output-pwnagotchi
build
dist
pwnagotchi.egg-info
*backup*.tgz
*backup*.gz

@ -1,19 +1,91 @@
#!/usr/bin/python3
import logging
import argparse
import time
import yaml
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
def do_clear(display):
logging.info("clearing the display ...")
display.clear()
exit(0)
def do_manual_mode(agent):
logging.info("entering manual mode ...")
agent.mode = 'manual'
agent.last_session.parse(agent.view(), args.skip_session)
if not args.skip_session:
logging.info(
"the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
agent.last_session.duration_human,
agent.last_session.epochs,
agent.last_session.train_epochs,
agent.last_session.avg_reward,
agent.last_session.min_reward,
agent.last_session.max_reward))
while True:
display.on_manual_mode(agent.last_session)
time.sleep(5)
if grid.is_connected():
plugins.on('internet_available', agent)
def do_auto_mode(agent):
logging.info("entering auto mode ...")
agent.mode = 'auto'
agent.start()
while True:
try:
# recon on all channels
agent.recon()
# get nearby access points grouped by channel
channels = agent.get_access_points_by_channel()
# for each channel
for ch, aps in channels:
agent.set_channel(ch)
if not agent.is_stale() and agent.any_activity():
logging.info("%d access points on channel %d" % (len(aps), ch))
# for each ap on this channel
for ap in aps:
# send an association frame in order to get for a PMKID
agent.associate(ap)
# deauth all client stations in order to get a full handshake
for sta in ap['clients']:
agent.deauth(ap, sta)
# An interesting effect of this:
#
# From Pwnagotchi's perspective, the more new access points
# and / or client stations nearby, the longer one epoch of
# its relative time will take ... basically, in Pwnagotchi's universe,
# WiFi electromagnetic fields affect time like gravitational fields
# affect ours ... neat ^_^
agent.next_epoch()
if grid.is_connected():
plugins.on('internet_available', agent)
except Exception as e:
logging.exception("main loop exception")
if __name__ == '__main__':
import argparse
import time
import logging
import yaml
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
parser = argparse.ArgumentParser()
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.yml',
@ -32,7 +104,10 @@ if __name__ == '__main__':
help="Enable debug logs.")
parser.add_argument('--version', dest="version", action="store_true", default=False,
help="Prints the version.")
help="Print the version.")
parser.add_argument('--print-config', dest="print_config", action="store_true", default=False,
help="Print the configuration.")
args = parser.parse_args()
@ -41,6 +116,10 @@ if __name__ == '__main__':
exit(0)
config = utils.load_config(args)
if args.print_config:
print(yaml.dump(config, default_flow_style=False))
exit(0)
utils.setup_logging(args, config)
pwnagotchi.set_name(config['main']['name'])
@ -48,79 +127,14 @@ if __name__ == '__main__':
plugins.load(config)
display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
keypair = KeyPair(view=display)
agent = Agent(view=display, config=config, keypair=keypair)
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent.fingerprint(), pwnagotchi.version))
logging.debug("effective configuration:\n\n%s\n\n" % yaml.dump(config, default_flow_style=False))
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s" % (plugin.__class__.__name__, plugin.__version__))
if args.do_clear:
logging.info("clearing the display ...")
display.clear()
do_clear(display)
exit(0)
elif args.do_manual:
logging.info("entering manual mode ...")
agent.mode = 'manual'
agent.last_session.parse(agent.view(), args.skip_session)
if not args.skip_session:
logging.info(
"the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
agent.last_session.duration_human,
agent.last_session.epochs,
agent.last_session.train_epochs,
agent.last_session.avg_reward,
agent.last_session.min_reward,
agent.last_session.max_reward))
while True:
display.on_manual_mode(agent.last_session)
time.sleep(5)
if grid.is_connected():
plugins.on('internet_available', agent)
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
if args.do_manual:
do_manual_mode(agent)
else:
logging.info("entering auto mode ...")
agent.mode = 'auto'
agent.start()
while True:
try:
# recon on all channels
agent.recon()
# get nearby access points grouped by channel
channels = agent.get_access_points_by_channel()
# for each channel
for ch, aps in channels:
agent.set_channel(ch)
if not agent.is_stale() and agent.any_activity():
logging.info("%d access points on channel %d" % (len(aps), ch))
# for each ap on this channel
for ap in aps:
# send an association frame in order to get for a PMKID
agent.associate(ap)
# deauth all client stations in order to get a full handshake
for sta in ap['clients']:
agent.deauth(ap, sta)
# An interesting effect of this:
#
# From Pwnagotchi's perspective, the more new access points
# and / or client stations nearby, the longer one epoch of
# its relative time will take ... basically, in Pwnagotchi's universe,
# WiFi electromagnetic fields affect time like gravitational fields
# affect ours ... neat ^_^
agent.next_epoch()
if grid.is_connected():
plugins.on('internet_available', agent)
except Exception as e:
logging.exception("main loop exception")
do_auto_mode(agent)

@ -6,7 +6,7 @@ import re
import pwnagotchi.ui.view as view
import pwnagotchi
version = '1.2.1'
version = '1.3.0'
_name = None

@ -32,10 +32,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._started_at = time.time()
self._filter = None if config['main']['filter'] is None else re.compile(config['main']['filter'])
self._current_channel = 0
self._tot_aps = 0
self._aps_on_channel = 0
self._supported_channels = utils.iface_channels(config['main']['iface'])
self._view = view
self._view.set_agent(self)
self._web_ui = Server(self, self._config['ui']['display'])
self._web_ui = Server(self, config['ui'])
self._access_points = []
self._last_pwnd = None
@ -47,6 +49,10 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if not os.path.exists(config['bettercap']['handshakes']):
os.makedirs(config['bettercap']['handshakes'])
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), self.fingerprint(), pwnagotchi.version))
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s" % (plugin.__class__.__name__, plugin.__version__))
def config(self):
return self._config
@ -176,7 +182,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
for ap in s['wifi']['aps']:
if ap['encryption'] == '' or ap['encryption'] == 'OPEN':
continue
elif ap['hostname'] not in whitelist:
elif ap['hostname'] not in whitelist \
and ap['mac'].lower() not in whitelist \
and ap['mac'][:8].lower() not in whitelist:
if self._filter_included(ap):
aps.append(ap)
except Exception as e:
@ -185,6 +193,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
aps.sort(key=lambda ap: ap['channel'])
return self.set_access_points(aps)
def get_total_aps(self):
return self._tot_aps
def get_aps_on_channel(self):
return self._aps_on_channel
def get_current_channel(self):
return self._current_channel
def get_access_points_by_channel(self):
aps = self.get_access_points()
channels = self._config['personality']['channels']
@ -221,16 +238,16 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
# self._view.set('epoch', '%04d' % self._epoch.epoch)
def _update_counters(self):
tot_aps = len(self._access_points)
self._tot_aps = len(self._access_points)
tot_stas = sum(len(ap['clients']) for ap in self._access_points)
if self._current_channel == 0:
self._view.set('aps', '%d' % tot_aps)
self._view.set('aps', '%d' % self._tot_aps)
self._view.set('sta', '%d' % tot_stas)
else:
aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
self._aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
stas_on_channel = sum(
[len(ap['clients']) for ap in self._access_points if ap['channel'] == self._current_channel])
self._view.set('aps', '%d (%d)' % (aps_on_channel, tot_aps))
self._view.set('aps', '%d (%d)' % (self._aps_on_channel, self._tot_aps))
self._view.set('sta', '%d (%d)' % (stas_on_channel, tot_stas))
def _update_handshakes(self, new_shakes=0):
@ -324,10 +341,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
(ap, sta) = ap_and_station
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
'hostname'] != '<hidden>' else ap_mac
logging.warning("!!! captured new handshake on channel %d: %s (%s) -> %s [%s (%s)] !!!" % ( \
ap['channel'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor']))
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:
@ -380,8 +399,8 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._view.on_assoc(ap)
try:
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients]..." % ( \
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients'])))
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm..." % ( \
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi']))
self.run('wifi.assoc %s' % ap['mac'])
self._epoch.track(assoc=True)
except Exception as e:
@ -401,8 +420,8 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._view.on_deauth(sta)
try:
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d ..." % (
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel']))
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ..." % (
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi']))
self.run('wifi.deauth %s' % sta['mac'])
self._epoch.track(deauth=True)
except Exception as e:

@ -4,31 +4,37 @@ import pwnagotchi.mesh.wifi as wifi
MAX_EPOCH_DURATION = 1024
histogram_size = wifi.NumChannels
shape = (1,
# aps per channel
histogram_size +
# clients per channel
histogram_size +
# peers per channel
histogram_size +
# duration
1 +
# inactive
1 +
# active
1 +
# missed
1 +
# hops
1 +
# deauths
1 +
# assocs
1 +
# handshakes
1)
def describe(extended=False):
if not extended:
histogram_size = wifi.NumChannels
else:
# see https://github.com/evilsocket/pwnagotchi/issues/583
histogram_size = wifi.NumChannelsExt
return histogram_size, (1,
# aps per channel
histogram_size +
# clients per channel
histogram_size +
# peers per channel
histogram_size +
# duration
1 +
# inactive
1 +
# active
1 +
# missed
1 +
# hops
1 +
# deauths
1 +
# assocs
1 +
# handshakes
1)
def featurize(state, step):

@ -34,10 +34,14 @@ class Environment(gym.Env):
self._epoch_num = 0
self._last_render = None
channels = agent.supported_channels()
# see https://github.com/evilsocket/pwnagotchi/issues/583
self._supported_channels = agent.supported_channels()
self._extended_spectrum = any(ch > 140 for ch in self._supported_channels)
self._histogram_size, self._observation_shape = featurizer.describe(self._extended_spectrum)
Environment.params += [
Parameter('_channel_%d' % ch, min_value=0, max_value=1, meta=ch + 1) for ch in
range(featurizer.histogram_size) if ch + 1 in channels
range(self._histogram_size) if ch + 1 in self._supported_channels
]
self.last = {
@ -50,7 +54,7 @@ class Environment(gym.Env):
}
self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.params if p.trainable])
self.observation_space = spaces.Box(low=0, high=1, shape=featurizer.shape, dtype=np.float32)
self.observation_space = spaces.Box(low=0, high=1, shape=self._observation_shape, dtype=np.float32)
self.reward_range = reward.range
@staticmethod
@ -118,7 +122,7 @@ class Environment(gym.Env):
return self.last['state_v']
def _render_histogram(self, hist):
for ch in range(featurizer.histogram_size):
for ch in range(self._histogram_size):
if hist[ch]:
logging.info(" CH %d: %s" % (ch + 1, hist[ch]))

@ -30,6 +30,8 @@ main:
enabled: false
speed: 19200
device: /dev/ttyUSB0
webgpsmap:
enabled: false
onlinehashcrack:
enabled: false
email: ~
@ -41,7 +43,7 @@ main:
enabled: false
api_key: ~
bt-tether:
enabled: false # if you want to use this, set ui.display.video.address to 0.0.0.0
enabled: false # if you want to use this, set ui.display.web.address to 0.0.0.0
devices:
android-phone:
enabled: false
@ -67,8 +69,9 @@ main:
priority: 999 # routing priority
memtemp: # Display memory usage, cpu load and cpu temperature on screen
enabled: false
scale: celsius
orientation: horizontal # horizontal/vertical
pawgps:
paw-gps:
enabled: false
#The IP Address of your phone with Paw Server running, default (option is empty) is 192.168.44.1
ip: ''
@ -76,8 +79,8 @@ main:
enabled: false
#The following is a list of the GPIO number for your button, and the command you want to run when it is pressed
gpios:
20: 'touch /root/.pwnagotchi-auto && systemctl restart pwnagotchi'
21: 'shutdown -h now'
#20: 'touch /root/.pwnagotchi-auto && systemctl restart pwnagotchi'
#21: 'shutdown -h now'
led:
enabled: true
# for /sys/class/leds/led0/brightness
@ -119,10 +122,12 @@ main:
mon_max_blind_epochs: 50
# if true, will not restart the wifi module
no_restart: false
# access points to ignore
# access points to ignore. Could be the ssid, bssid or the vendor part of bssid.
whitelist:
- EXAMPLE_NETWORK
- ANOTHER_EXAMPLE_NETWORK
- fo:od:ba:be:fo:od # BSSID
- fo:od:ba # Vendor BSSID
# if not null, filter access points by this regular expression
filter: null
# logging
@ -289,6 +294,19 @@ ui:
# preserve your display over time, you should set this value to 0.0 so that the display will be refreshed only
# if any of the important data fields changed (the uptime and blinking cursor won't trigger a refresh).
fps: 0.0
# web ui
web:
enabled: true
address: '0.0.0.0'
username: changeme # !!! CHANGE THIS !!!
password: changeme # !!! CHANGE THIS !!!
origin: null
port: 8080
# command to be executed when a new png frame is available
# for instance, to use with framebuffer based displays:
# on_frame: 'fbi --noverbose -a -d /dev/fb1 -T 1 /root/pwnagotchi.png > /dev/null 2>&1'
on_frame: ''
# hardware display
display:
enabled: true
rotation: 180
@ -298,16 +316,6 @@ ui:
# Waveshare tri-color 2.13in display can be over-driven with color set as 'fastAndFurious'
# THIS IS POTENTIALLY DANGEROUS. DO NOT USE UNLESS YOU UNDERSTAND THAT IT COULD KILL YOUR DISPLAY
color: 'black'
video:
enabled: true
address: '0.0.0.0'
origin: null
port: 8080
# command to be executed when a new png frame is available
# for instance, to use with framebuffer based displays:
# on_frame: 'fbi --noverbose -a -d /dev/fb1 -T 1 /root/pwnagotchi.png > /dev/null 2>&1'
on_frame: ''
# bettercap rest api configuration
bettercap:

@ -12,9 +12,13 @@ API_ADDRESS = "http://127.0.0.1:8666/api/v1"
def is_connected():
try:
socket.create_connection(("www.google.com", 80))
return True
except OSError:
# check DNS
host = socket.gethostbyname('api.pwnagotchi.ai')
if host:
# check connectivity itself
socket.create_connection((host, 443), timeout=30)
return True
except:
pass
return False
@ -22,9 +26,11 @@ def is_connected():
def call(path, obj=None):
url = '%s%s' % (API_ADDRESS, path)
if obj is None:
r = requests.get(url, headers=None)
r = requests.get(url, headers=None, timeout=(30.0, 60.0))
elif isinstance(obj, dict):
r = requests.post(url, headers=None, json=obj, timeout=(30.0, 60.0))
else:
r = requests.post(url, headers=None, json=obj)
r = requests.post(url, headers=None, data=obj, timeout=(30.0, 60.0))
if r.status_code != 200:
raise Exception("(status %d) %s" % (r.status_code, r.text))
@ -39,6 +45,14 @@ def set_advertisement_data(data):
return call("/mesh/data", obj=data)
def get_advertisement_data():
return call("/mesh/data")
def memory():
return call("/mesh/memory")
def peers():
return call("/mesh/peers")
@ -95,3 +109,15 @@ def report_ap(essid, bssid):
def inbox(page=1, with_pager=False):
obj = call("/inbox?p=%d" % page)
return obj["messages"] if not with_pager else obj
def inbox_message(id):
return call("/inbox/%d" % int(id))
def mark_message(id, mark):
return call("/inbox/%d/%s" % (int(id), str(mark)))
def send_message(to, message):
return call("/unit/%s/inbox" % to, message.encode('utf-8'))

@ -0,0 +1,248 @@
# Interfaz en español para pwnagotchi
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Angel Hernandez Segura, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Angel Hernandez Segura <ahsec.7@gmail.com>\n"
"Language-Team: Español <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "ZzzzZZzzzzZzzz"
msgstr "ZzzzZZzzzzZzzz"
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Hola, Soy Pwnagotchi! Iniciando..."
msgid "New day, new hunt, new pwns!"
msgstr "Un nuevo dia, nuevos objetivos, nuevos pwns"
msgid "Hack the Planet!"
msgstr "Hack the Planet!"
msgid "AI ready."
msgstr "IA lista"
msgid "The neural network is ready."
msgstr "La red neuronal esta lista"
msgid "Generating keys, do not turn off ..."
msgstr "Generando llaves, no apagar"
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, canal {channel} esta libre! Tu AP te lo agredecera"
msgid "Reading last session logs ..."
msgstr "Leyendo logs de la ultima sesion"
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "He leido {lines_so_far} lineas de los logs hasta ahora "
msgid "I'm bored ..."
msgstr "Estoy aburrido"
msgid "Let's go for a walk!"
msgstr "Vamos a caminar!"
msgid "This is the best day of my life!"
msgstr "Este es el mejor dia de mi vida"
msgid "Shitty day :/"
msgstr "Dia de mierda :/"
msgid "I'm extremely bored ..."
msgstr "Estoy extremadamente aburrido ..."
msgid "I'm very sad ..."
msgstr "Estoy mut triste"
msgid "I'm sad"
msgstr "Estoy triste"
msgid "Leave me alone ..."
msgstr "Dejame solo ..."
msgid "I'm mad at you!"
msgstr "Estoy enojado contigo!"
msgid "I'm living the life!"
msgstr "Estoy disfrutando la vida!"
msgid "I pwn therefore I am."
msgstr "Yo pwn, por lo tanto existo"
msgid "So many networks!!!"
msgstr "Tantas redes!!!"
msgid "I'm having so much fun!"
msgstr "Me estoy divirtiendo mucho!"
msgid "My crime is that of curiosity ..."
msgstr "Mi crimen es la curiosidad ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Hola {name}! Mucho gusto."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name}! Que hay?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {name} como te va?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Unit {name} esta cerca!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ... adios {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} se fue ..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Whoops ... {name} se fue"
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} se ha perdido!"
msgid "Missed!"
msgstr "Perdido!"
msgid "Good friends are a blessing!"
msgstr "Los buenos amigos son una bendicion"
msgid "I love my friends!"
msgstr "Amo a mis amigos!"
msgid "Nobody wants to play with me ..."
msgstr "Nadie quiere jugar conmigo ..."
msgid "I feel so alone ..."
msgstr "Me siento muy solo ..."
msgid "Where's everybody?!"
msgstr "Donde estan todos?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Tomando una siesta por {secs}s ..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s) "
msgid "Good night."
msgstr "Buenas noches."
msgid "Zzz"
msgstr "Zzz"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Esperando por {secs}s ..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Mirando alrededor ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Hey {what} vamos a ser amigos!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Asociandose a {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Yo {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Acabo de decidir que {mac} no necesita WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "De-autenticando {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Vetando {mac}!"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Bien, obtuvimos {num} nuevos handshake{plural}!"
#, python-brace-format
msgid "You have {count} new message{plural}!"
msgstr "Tienes {count} nuevos mensajes{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Oops, algo salio mal ... Reiniciando ..."
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Bloquee {num} staciones\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Hice {num} nuevos amigos\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Obtuve {num} handshakes\n"
msgid "Met 1 peer"
msgstr "Conoci a 1 unidad"
#, python-brace-format
msgid "Met {num} peers"
msgstr "conoci {num} unidades"
#, python-brace-format
msgid ""
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr "He estado hackeando por {duration} y de-autenticando {deauthed} "
"clientes! Tambien conoci {associated} nuevos amigos y comi {handshakes} "
"handshakes! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "horas"
msgid "minutes"
msgstr "minutos"
msgid "seconds"
msgstr "segundos"
msgid "hour"
msgstr "hora"
msgid "minute"
msgstr "minuto"
msgid "second"
msgstr "segundo"

@ -1,4 +1,5 @@
NumChannels = 140
NumChannelsExt = 165 # see https://github.com/evilsocket/pwnagotchi/issues/583
def freq_to_channel(freq):

@ -1,5 +1,6 @@
import os
import glob
import _thread
import importlib, importlib.util
import logging
@ -31,7 +32,7 @@ def one(plugin_name, event_name, *args, **kwargs):
callback = getattr(plugin, cb_name, None)
if callback is not None and callable(callback):
try:
callback(*args, **kwargs)
_thread.start_new_thread(callback, (*args, *kwargs))
except Exception as e:
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
logging.error(e, exc_info=True)

@ -1,14 +1,15 @@
import os
import time
import re
import logging
import os
import subprocess
import time
import dbus
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.utils import StatusFile
import pwnagotchi.plugins as plugins
class BTError(Exception):
@ -382,7 +383,7 @@ class IfaceWrapper:
class Device:
def __init__(self, name, share_internet, mac, ip, netmask, interval, priority=10, scantime=15, search_order=0, max_tries=0, **kwargs):
def __init__(self, name, share_internet, mac, ip, netmask, interval, gateway=None, priority=10, scantime=15, search_order=0, max_tries=0, **kwargs):
self.name = name
self.status = StatusFile(f'/root/.bt-tether-{name}')
self.status.update()
@ -394,6 +395,7 @@ class Device:
self.share_internet = share_internet
self.ip = ip
self.netmask = netmask
self.gateway = gateway
self.interval = interval
self.mac = mac
self.scantime = scantime
@ -461,7 +463,7 @@ class BTTether(plugins.Plugin):
logging.error("BT-TETHER: Can't start bluetooth.service")
return
logging.info("BT-TETHER: Sussessfully loaded ...")
logging.info("BT-TETHER: Successfully loaded ...")
self.ready = True
def on_ui_setup(self, ui):
@ -543,7 +545,10 @@ class BTTether(plugins.Plugin):
continue
addr = f"{device.ip}/{device.netmask}"
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
if device.gateway:
gateway = device.gateway
else:
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
wrapped_interface = IfaceWrapper(interface)
logging.debug('BT-TETHER: Add ip to %s', interface)

@ -34,5 +34,5 @@ class GPIOButtons(plugins.Plugin):
for gpio, command in gpios.items():
self.ports[gpio] = command
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=250)
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)
logging.info("Added command: %s to GPIO #%d", command, gpio)

@ -1,42 +1,116 @@
import logging
import json
import logging
import os
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
class GPS(plugins.Plugin):
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'Save GPS coordinates whenever an handshake is captured.'
__author__ = "evilsocket@gmail.com"
__version__ = "1.0.0"
__license__ = "GPL3"
__description__ = "Save GPS coordinates whenever an handshake is captured."
def __init__(self):
self.running = False
self.coordinates = None
def on_loaded(self):
logging.info("gps plugin loaded for %s" % self.options['device'])
logging.info(f"gps plugin loaded for {self.options['device']}")
def on_ready(self, agent):
if os.path.exists(self.options['device']):
logging.info("enabling gps bettercap's module for %s" % self.options['device'])
if os.path.exists(self.options["device"]):
logging.info(
f"enabling bettercap's gps module for {self.options['device']}"
)
try:
agent.run('gps off')
except:
agent.run("gps off")
except Exception:
pass
agent.run('set gps.device %s' % self.options['device'])
agent.run('set gps.speed %d' % self.options['speed'])
agent.run('gps on')
running = True
agent.run(f"set gps.device {self.options['device']}")
agent.run(f"set gps.baudrate {self.options['speed']}")
agent.run("gps on")
self.running = True
else:
logging.warning("no GPS detected")
def on_handshake(self, agent, filename, access_point, client_station):
if self.running:
info = agent.session()
gps = info['gps']
gps_filename = filename.replace('.pcap', '.gps.json')
self.coordinates = info["gps"]
gps_filename = filename.replace(".pcap", ".gps.json")
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
with open(gps_filename, 'w+t') as fp:
json.dump(gps, fp)
logging.info(f"saving GPS to {gps_filename} ({self.coordinates})")
with open(gps_filename, "w+t") as fp:
json.dump(self.coordinates, fp)
def on_ui_setup(self, ui):
# add coordinates for other displays
if ui.is_waveshare_v2():
lat_pos = (127, 75)
lon_pos = (122, 84)
alt_pos = (127, 94)
elif ui.is_inky():
# guessed values, add tested ones if you can
lat_pos = (112, 30)
lon_pos = (112, 49)
alt_pos = (87, 63)
else:
# guessed values, add tested ones if you can
lat_pos = (127, 51)
lon_pos = (127, 56)
alt_pos = (102, 71)
label_spacing = 0
ui.add_element(
"latitude",
LabeledValue(
color=BLACK,
label="lat:",
value="-",
position=lat_pos,
label_font=fonts.Small,
text_font=fonts.Small,
label_spacing=label_spacing,
),
)
ui.add_element(
"longitude",
LabeledValue(
color=BLACK,
label="long:",
value="-",
position=lon_pos,
label_font=fonts.Small,
text_font=fonts.Small,
label_spacing=label_spacing,
),
)
ui.add_element(
"altitude",
LabeledValue(
color=BLACK,
label="alt:",
value="-",
position=alt_pos,
label_font=fonts.Small,
text_font=fonts.Small,
label_spacing=label_spacing,
),
)
def on_ui_update(self, ui):
if self.coordinates and all([
# avoid 0.000... measurements
self.coordinates["Latitude"], self.coordinates["Longitude"]
]):
# last char is sometimes not completely drawn ¯\_(ツ)_/¯
# using an ending-whitespace as workaround on each line
ui.set("latitude", f"{self.coordinates['Latitude']:.4f} ")
ui.set("longitude", f" {self.coordinates['Longitude']:.4f} ")
ui.set("altitude", f" {self.coordinates['Altitude']:.1f}m ")

@ -44,12 +44,12 @@ class Led(plugins.Plugin):
logging.debug("[led] using pattern '%s' ..." % pattern)
for c in pattern:
if c == ' ':
self._led(0)
else:
self._led(1)
else:
self._led(0)
time.sleep(self._delay / 1000.0)
# reset
self._led(1)
self._led(0)
def _worker(self):
while True:

@ -44,30 +44,39 @@ class MemTemp(plugins.Plugin):
if ui.is_waveshare_v2():
h_pos = (180, 80)
v_pos = (180, 61)
elif ui.is_inky():
h_pos = (140, 68)
v_pos = (165, 54)
else:
h_pos = (155, 76)
v_pos = (180, 61)
if self.options['orientation'] == "horizontal":
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -',
position=h_pos,
label_font=fonts.Small, text_font=fonts.Small))
elif self.options['orientation'] == "vertical":
if self.options['orientation'] == "vertical":
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value=' mem:-\n cpu:-\ntemp:-',
position=v_pos,
label_font=fonts.Small, text_font=fonts.Small))
else:
# default to horizontal
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -',
position=h_pos,
label_font=fonts.Small, text_font=fonts.Small))
def on_ui_update(self, ui):
if self.options['scale'] == "fahrenheit":
temp = (pwnagotchi.temperature() * 9 / 5) + 32
elif self.options['scale'] == "celsius":
temp = pwnagotchi.temperature()
symbol = "f"
elif self.options['scale'] == "kelvin":
temp = pwnagotchi.temperature() + 273.15
if self.options['orientation'] == "horizontal":
ui.set('memtemp',
" mem cpu temp\n %s%% %s%% %sc" % (self.mem_usage(), self.cpu_load(), temp))
symbol = "k"
else:
# default to celsius
temp = pwnagotchi.temperature()
symbol = "c"
elif self.options['orientation'] == "vertical":
if self.options['orientation'] == "vertical":
ui.set('memtemp',
" mem:%s%%\n cpu:%s%%\ntemp:%sc" % (self.mem_usage(), self.cpu_load(), temp))
" mem:%s%%\n cpu:%s%%\ntemp:%s%s" % (self.mem_usage(), self.cpu_load(), temp, symbol))
else:
# default to horizontal
ui.set('memtemp',
" mem cpu temp\n %s%% %s%% %s%s" % (self.mem_usage(), self.cpu_load(), temp, symbol))

@ -3,7 +3,10 @@ import requests
import pwnagotchi.plugins as plugins
'''
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemic:
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
NEW BETTER GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
Old guide here, (not recommended if you plan on using it with the webgpsmap plugin)
https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
'''
@ -22,7 +25,9 @@ class PawGPS(plugins.Plugin):
def on_handshake(self, agent, filename, access_point, client_station):
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
ip = "192.168.44.1"
ip = "192.168.44.1:8080"
else:
ip = self.options['ip']
gps = requests.get('http://' + ip + '/gps.xhtml')
gps_filename = filename.replace('.pcap', '.gps.json')

@ -55,8 +55,8 @@ class UPSLite(plugins.Plugin):
self.ups = UPS()
def on_ui_setup(self, ui):
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
def on_ui_update(self, ui):
ui.set('ups', "%4.2fV/%2i%%" % (self.ups.voltage(), self.ups.capacity()))
ui.set('ups', "%2i%%" % self.ups.capacity())

@ -0,0 +1,218 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/xml; charset=utf-8" />
<title>GPS MAP</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"/>
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" />
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" />
<script type='text/javascript' src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js'></script>
<style type="text/css">
/* for map */
html, body, #mapdiv { height: 100%; width: 100%; margin:0; background-color:#000;}
.pwnAPPin path {
fill: #ce7575;
}
.pwnAPPinOpen path {
fill: #76ce75;
}
/* animated ap marker */
.pwnAPPin .ring_outer, .pwnAPPinOpen .ring_outer {
animation: opacityPulse 2s cubic-bezier(1, 0.14, 1, 1);
animation-iteration-count: infinite;
opacity: .5;
}
.pwnAPPin .ring_inner, .pwnAPPinOpen .ring_inner {
animation: opacityPulse 2s cubic-bezier(0.4, 0.74, 0.56, 0.82);
animation-iteration-count: infinite;
opacity: .8;
}
@keyframes opacityPulse {
0% {
opacity: 0.1;
}
50% {
opacity: 1.0;
}
100% {
opacity: 0.1;
}
}
@keyframes bounceInDown {
from, 60%, 75%, 90%, to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, 5px, 0);
}
75% {
transform: translate3d(0, -3px, 0);
}
90% {
transform: translate3d(0, 5px, 0);
}
to {
transform: none;
}
}
.bounceInDown {
animation-name: bounceInDown;
animation-duration: 2s;
animation-fill-mode: both;
}
/* animated radar */
.radar {
animation: pulsate 1s ease-out;
-webkit-animation: pulsate 1s ease-out;
-webkit-animation-iteration-count: infinite;
/* opacity: 0.0 */
}
#loading {
top: 50%;
left: 50%;
position: fixed;
background-color: rgba(255, 255, 255, 0.9);
border: 0.5vw #ff0000 solid;
border-radius: 2vw;
padding: 5vw;
transform: translateX(-50%) translateY(-50%);
text-align:center;
display: none;
}
#loading .face { font-size:8vw; }
#loading .text {position:absolute;bottom:0;text-align:center; font-size: 1vw;color:#a0a0a0;}
</style>
</head>
<body>
<div id="mapdiv"></div>
<div id="loading"><div class="face"><nobr>(⌐■&nbsp;<span id="loading_ap_img"></span>&nbsp;■)</nobr></div><div class="text" id="loading_infotext">loading positions...</div></div>
<script type="text/javascript">
function loadJSON(url, callback) {
document.getElementById("loading").style.display = "flex";
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', url, true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
};
xobj.send(null);
}
function escapeHtml(unsafe) {
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function formatMacAddress(macAddress) {
if (macAddress !== null) {
macAddress = macAddress.toUpperCase();
if (macAddress.length >= 3 && macAddress.length <= 16) {
macAddress = macAddress.replace(/\W/ig, '');
macAddress = macAddress.replace(/(.{2})/g, "$1:");
macAddress = macAddress.replace(/:+$/,'');
}
}
return macAddress;
}
// select your map theme from https://leaflet-extras.github.io/leaflet-providers/preview/
// use 2 layers with alpha for a nice dark style
var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});
var CartoDB_DarkMatter = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
opacity:0.8,
maxZoom: 19
});
var mymap = L.map('mapdiv');
Esri_WorldImagery.addTo(mymap);
CartoDB_DarkMatter.addTo(mymap);
var svg = '<svg class="pwnAPPin" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#931F1F" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#931F1F" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#931F1F" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#931F1F" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
var svgOpen = '<svg class="pwnAPPinOpen" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#1f9321" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#1f9321" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#1f9321" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#1f9321" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
document.getElementById('loading_ap_img').innerHTML = svg;
var myIcon = L.divIcon({
className: "leaflet-data-marker",
html: svg.replace('#','%23'),
iconAnchor : [40, 30],
iconSize : [80, 60],
popupAnchor : [0, -30],
});
var myIconOpen = L.divIcon({
className: "leaflet-data-marker",
html: svgOpen.replace('#','%23'),
iconAnchor : [40, 30],
iconSize : [80, 60],
popupAnchor : [0, -30],
});
var accuracys = [];
var markers = [];
var marker_pos = [];
var markerClusters = L.markerClusterGroup();
loadJSON("/plugins/webgpsmap/all", function(response) {
var positions = JSON.parse(response);
count = 0;
Object.keys(positions).forEach(function(key) {
count++;
if(positions[key].lng){
new_marker_pos = [positions[key].lat, positions[key].lng];
if (positions[key].acc) {
radius = Math.round(Math.min(positions[key].acc, 500));
markerColor = 'red';
markerColorCode = '#f03';
fillOpacity = 0.002;
if (positions[key].pass) {
markerColor = 'green';
markerColorCode = '#1aff00';
fillOpacity = 0.1;
}
accuracys.push(
L.circle(new_marker_pos, {
color: markerColor,
fillColor: markerColorCode,
fillOpacity: fillOpacity,
weight: 1,
opacity: 0.1,
radius: Math.min(positions[key].acc, 500),
}).setStyle({'className': 'radar'}).addTo(mymap)
);
}
if (positions[key].pass) {
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
} else {
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
}
passInfo = '';
if (positions[key].pass) {
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
}
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
markers.push(newMarker);
marker_pos.push(new_marker_pos);
markerClusters.addLayer( newMarker );
}
});
if (count > 0) {
mymap.addLayer( markerClusters );
var bounds = new L.LatLngBounds(marker_pos);
mymap.fitBounds(bounds);
document.getElementById("loading").style.display = "none";
} else {
document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
}
});
</script>
</body></html>

@ -0,0 +1,369 @@
import pwnagotchi.plugins as plugins
import logging
import os
import json
import re
import datetime
from flask import Response
from functools import lru_cache
'''
2do:
- make the cache handling multiple clients
- cleanup the javascript in a class and handle "/newest" additions
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
https://gis.stackexchange.com/questions/312737/filtering-interactive-leaflet-map-with-dropdown-menu
https://blogs.kent.ac.uk/websolutions/2015/01/29/filtering-map-markers-with-leaflet-js-a-brief-technical-overview/
http://www.digital-geography.com/filter-leaflet-maps-slider/
http://bl.ocks.org/zross/47760925fcb1643b4225
-
'''
class Webgpsmap(plugins.Plugin):
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
__version__ = '1.2.2'
__name__ = 'webgpsmap'
__license__ = 'GPL3'
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
__help__ = """
- install: copy "webgpsmap.py" and "webgpsmap.html" to your configured "custom_plugins" directory
- add webgpsmap.yml to your config
- connect your PC/Smartphone/* with USB, BT or other to your pwnagotchi and browse to http://pwnagotchi.local:8080/plugins/webgpsmap/
(change pwnagotchi.local to your pwnagotchis IP, if needed)
"""
ALREADY_SENT = list()
SKIP = list()
def __init__(self):
self.ready = False
def on_ready(self, agent):
self.config = agent.config()
self.ready = True
def on_loaded(self):
"""
Plugin got loaded
"""
logging.info("webgpsmap plugin loaded")
def on_webhook(self, path, request):
"""
Returns ewquested data
"""
# defaults:
response_header_contenttype = None
response_mimetype = "application/xhtml+xml"
if not self.ready:
try:
response_data = bytes('''<html>
<head>
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>Not ready yet</body>
</html>''', "utf-8")
response_status = 500
response_mimetype = "application/xhtml+xml"
response_header_contenttype = 'text/html'
except Exception as ex:
logging.error(ex)
return
else:
if request.method == "GET":
if path == '/' or not path:
# returns the html template
self.ALREADY_SENT = list()
try:
response_data = bytes(self.get_html(), "utf-8")
except Exception as ex:
logging.error(ex)
return
response_status = 200
response_mimetype = "application/xhtml+xml"
response_header_contenttype = 'text/html'
elif path.startswith('all'):
# returns all positions
try:
self.ALREADY_SENT = list()
response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes'])), "utf-8")
response_status = 200
response_mimetype = "application/json"
response_header_contenttype = 'application/json'
except Exception as ex:
logging.error(ex)
return
# elif path.startswith('/newest'):
# # returns all positions newer then timestamp
# response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes']), newest_only=True), "utf-8")
# response_status = 200
# response_mimetype = "application/json"
# response_header_contenttype = 'application/json'
else:
# unknown GET path
response_data = bytes('''<html>
<head>
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>4😋4</body>
</html>''', "utf-8")
response_status = 404
else:
# unknown request.method
response_data = bytes('''<html>
<head>
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>4😋4</body>
</html>''', "utf-8")
response_status = 404
try:
r = Response(response=response_data, status=response_status, mimetype=response_mimetype)
if response_header_contenttype is not None:
r.headers["Content-Type"] = response_header_contenttype
return r
except Exception as ex:
logging.error(ex)
return
# cache 1024 items
@lru_cache(maxsize=1024, typed=False)
def _get_pos_from_file(self, path):
return PositionFile(path)
def load_gps_from_dir(self, gpsdir, newest_only=False):
"""
Parses the gps-data from disk
"""
handshake_dir = gpsdir
gps_data = dict()
logging.info("webgpsmap: scanning %s", handshake_dir)
all_files = os.listdir(handshake_dir)
#print(all_files)
all_pcap_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.pcap')
]
all_geo_or_gps_files = []
for filename_pcap in all_pcap_files:
filename_base = filename_pcap[:-5] # remove ".pcap"
logging.debug("webgpsmap: found: " + filename_base)
filename_position = None
check_for = os.path.basename(filename_base) + ".gps.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
check_for = os.path.basename(filename_base) + ".geo.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
if filename_position is not None:
# logging.debug("webgpsmap: -- found: %s %d" % (check_for, len(all_geo_or_gps_files)) )
all_geo_or_gps_files.append(filename_position)
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skiped networks? No!
if newest_only:
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
logging.info("webgpsmap: Found %d .(geo|gps).json files from %d handshakes. Fetching positions ...",
len(all_geo_or_gps_files), len(all_pcap_files))
for pos_file in all_geo_or_gps_files:
try:
pos = self._get_pos_from_file(pos_file)
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO:
continue
ssid, mac = pos.ssid(), pos.mac()
ssid = "unknown" if not ssid else ssid
# invalid mac is strange and should abort; ssid is ok
if not mac:
raise ValueError("Mac can't be parsed from filename")
gps_data[ssid+"_"+mac] = {
'ssid': ssid,
'mac': mac,
'type': 'gps' if pos.type() == PositionFile.GPS else 'geo',
'lng': pos.lng(),
'lat': pos.lat(),
'acc': pos.accuracy(),
'ts_first': pos.timestamp_first(),
'ts_last': pos.timestamp_last(),
}
check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
if check_for in all_files:
gps_data[ssid + "_" + mac]["pass"] = pos.password()
self.ALREADY_SENT += pos_file
except json.JSONDecodeError as js_e:
self.SKIP += pos_file
logging.error(js_e)
continue
except ValueError as v_e:
self.SKIP += pos_file
logging.error(v_e)
continue
except OSError as os_e:
self.SKIP += pos_file
logging.error(os_e)
continue
logging.info("webgpsmap loaded %d positions", len(gps_data))
return gps_data
def get_html(self):
"""
Returns the html page
"""
try:
template_file = os.path.dirname(os.path.realpath(__file__))+"/"+"webgpsmap.html"
html_data = open(template_file, "r").read()
except Exception as ex:
logging.error("error loading template file: %s", template_file)
logging.error(ex)
return html_data
class PositionFile:
"""
Wraps gps / net-pos files
"""
GPS = 0
GEO = 1
def __init__(self, path):
self._file = path
self._filename = os.path.basename(path)
try:
with open(path, 'r') as json_file:
self._json = json.load(json_file)
except json.JSONDecodeError as js_e:
raise js_e
def mac(self):
"""
Returns the mac from filename
"""
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo)\.json', self._filename)
if parsed_mac:
mac = parsed_mac.groups()[0]
return mac
return None
def ssid(self):
"""
Returns the ssid from filename
"""
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo)\.json', self._filename)
if parsed_ssid:
return parsed_ssid.groups()[0]
return None
def json(self):
"""
returns the parsed json
"""
return self._json
def timestamp_first(self):
"""
returns the timestamp of AP first seen
"""
# use file timestamp creation time of the pcap file
return int("%.0f" % os.path.getctime(self._file))
def timestamp_last(self):
"""
returns the timestamp of AP last seen
"""
return_ts = None
if 'ts' in self._json:
return_ts = self._json['ts']
elif 'Updated' in self._json:
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
date_iso_formated = self._json['Updated']
# fill milliseconds to 6 numbers
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
part2 = part2.ljust(6, '0')
date_iso_formated = part1 + "." + part2 + "+" + part3
dateObj = datetime.datetime.fromisoformat(date_iso_formated)
return_ts = int("%.0f" % dateObj.timestamp())
else:
# use file timestamp last modification of the pcap file
return_ts = int("%.0f" % os.path.getmtime(self._file))
return return_ts
def password(self):
"""
returns the password from file.pcap.cracked od None
"""
return_pass = None
password_file_path = self._file[:-9] + ".pcap.cracked"
if os.path.isfile(password_file_path):
try:
password_file = open(password_file_path, 'r')
return_pass = password_file.read()
password_file.close()
except OSError as err:
print("OS error: {0}".format(err))
except:
print("Unexpected error:", sys.exc_info()[0])
raise
return return_pass
def type(self):
"""
returns the type of the file
"""
if self._file.endswith('.gps.json'):
return PositionFile.GPS
if self._file.endswith('.geo.json'):
return PositionFile.GEO
return None
def lat(self):
try:
if self.type() == PositionFile.GPS:
lat = self._json['Latitude']
if self.type() == PositionFile.GEO:
lat = self._json['location']['lat']
if lat != 0:
return lat
raise ValueError("Lat is 0")
except KeyError:
pass
return None
def lng(self):
try:
if self.type() == PositionFile.GPS:
lng = self._json['Longitude']
if self.type() == PositionFile.GEO:
lng = self._json['location']['lng']
if lng != 0:
return lng
raise ValueError("Lng is 0")
except KeyError:
pass
return None
def accuracy(self):
if self.type() == PositionFile.GPS:
return 50.0
if self.type() == PositionFile.GEO:
try:
return self._json['accuracy']
except KeyError:
pass
return None

@ -58,12 +58,13 @@ class Text(Widget):
class LabeledValue(Widget):
def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0):
def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0, label_spacing=5):
super().__init__(position, color)
self.label = label
self.value = value
self.label_font = label_font
self.text_font = text_font
self.label_spacing = label_spacing
def draw(self, canvas, drawer):
if self.label is None:
@ -71,4 +72,4 @@ class LabeledValue(Widget):
else:
pos = self.xy
drawer.text(pos, self.label, font=self.label_font, fill=self.color)
drawer.text((pos[0] + 5 + 5 * len(self.label), pos[1]), self.value, font=self.text_font, fill=self.color)
drawer.text((pos[0] + self.label_spacing + 5 * len(self.label), pos[1]), self.value, font=self.text_font, fill=self.color)

@ -25,9 +25,6 @@ class Display(View):
)
self._render_thread_instance.start()
def set_ready(self):
self._webui.start()
def is_inky(self):
return self._implementation.name == 'inky'
@ -61,6 +58,9 @@ class Display(View):
def is_waveshare213d(self):
return self._implementation.name == 'waveshare213d'
def is_spotpear24inch(self):
return self._implementation.name == 'spotpear24inch'
def is_waveshare_any(self):
return self.is_waveshare_v1() or self.is_waveshare_v2()
@ -91,8 +91,8 @@ class Display(View):
def _on_view_rendered(self, img):
try:
if self._config['ui']['display']['video']['on_frame'] != '':
os.system(self._config['ui']['display']['video']['on_frame'])
if self._config['ui']['web']['on_frame'] != '':
os.system(self._config['ui']['web']['on_frame'])
except Exception as e:
logging.error("%s" % e)

@ -9,7 +9,7 @@ from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
def display_for(config):
# config has been normalized already in utils.load_config
@ -44,4 +44,7 @@ def display_for(config):
return Waveshare154inch(config)
elif config['ui']['display']['type'] == 'waveshare213d':
return Waveshare213d(config)
return Waveshare213d(config)
elif config['ui']['display']['type'] == 'spotpear24inch':
return Spotpear24inch(config)

@ -0,0 +1,144 @@
FBIOGET_VSCREENINFO=0x4600
FBIOPUT_VSCREENINFO=0x4601
FBIOGET_FSCREENINFO=0x4602
FBIOGETCMAP=0x4604
FBIOPUTCMAP=0x4605
FBIOPAN_DISPLAY=0x4606
FBIOGET_CON2FBMAP=0x460F
FBIOPUT_CON2FBMAP=0x4610
FBIOBLANK=0x4611
FBIO_ALLOC=0x4613
FBIO_FREE=0x4614
FBIOGET_GLYPH=0x4615
FBIOGET_HWCINFO=0x4616
FBIOPUT_MODEINFO=0x4617
FBIOGET_DISPINFO=0x4618
from mmap import mmap
from fcntl import ioctl
import struct
mm = None
bpp, w, h = 0, 0, 0 # framebuffer bpp and size
bytepp = 0
vx, vy, vw, vh = 0, 0, 0, 0 #virtual window offset and size
vi, fi = None, None
_fb_cmap = 'IIPPPP' # start, len, r, g, b, a
RGB = False
_verbose = False
msize_kb = 0
def report_fb(i=0, layer=0):
with open('/dev/fb'+str(i), 'r+b')as f:
vi = ioctl(f, FBIOGET_VSCREENINFO, bytes(160))
vi = list(struct.unpack('I'*40, vi))
ffm = 'c'*16+'L'+'I'*4+'H'*3+'ILIIHHH'
fic = struct.calcsize(ffm)
fi = struct.unpack(ffm, ioctl(f, FBIOGET_FSCREENINFO, bytes(fic)))
def ready_fb(_bpp=None, i=0, layer=0, _win=None):
global mm, bpp, w, h, vi, fi, RGB, msize_kb, vx, vy, vw, vh, bytepp
if mm and bpp == _bpp: return mm, w, h, bpp
with open('/dev/fb'+str(i), 'r+b')as f:
vi = ioctl(f, FBIOGET_VSCREENINFO, bytes(160))
vi = list(struct.unpack('I'*40, vi))
bpp = vi[6]
bytepp = bpp//8
if _bpp:
vi[6] = _bpp # 24 bit = BGR 888 mode
try:
vi = ioctl(f, FBIOPUT_VSCREENINFO, struct.pack('I'*40, *vi)) # fb_var_screeninfo
vi = struct.unpack('I'*40,vi)
bpp = vi[6]
bytepp = bpp//8
except:
pass
if vi[8] == 0 : RGB = True
ffm = 'c'*16+'L'+'I'*4+'H'*3+'ILIIHHH'
fic = struct.calcsize(ffm)
fi = struct.unpack(ffm, ioctl(f, FBIOGET_FSCREENINFO, bytes(fic)))
msize = fi[17] # = w*h*bpp//8
ll, start = fi[-7:-5]
w, h = ll//bytepp, vi[1] # when screen is vertical, width becomes wrong. ll//3 is more accurate at such time.
if _win and len(_win)==4: # virtual window settings
vx, vy, vw, vh = _win
if vw == 'w': vw = w
if vh == 'h': vh = h
vx, vy, vw, vh = map(int, (vx, vy, vw, vh))
if vx>=w: vx = 0
if vy>=h: vy = 0
if vx>w: vw = w - vx
else: vw -= vx
if vy>h: vh = h - vy
else: vh -= vy
else:
vx, vy, vw, vh = 0,0,w,h
msize_kb = vw*vh*bytepp//1024 # more accurate FB memory size in kb
mm = mmap(f.fileno(), msize, offset=start)
return mm, w, h, bpp#ll//(bpp//8), h
def fill_scr(r,g,b):
if bpp == 32:
seed = struct.pack('BBBB', b, g, r, 255)
elif bpp == 24:
seed = struct.pack('BBB', b, g, r)
elif bpp == 16:
seed = struct.pack('H', r>>3<<11 | g>>2<<5 | b>>3)
mm.seek(0)
show_img(seed * vw * vh)
def black_scr():
fill_scr(0,0,0)
def white_scr():
fill_scr(255,255,255)
def mmseekto(x,y):
mm.seek((x + y*w) * bytepp)
def dot(x, y, r, g, b):
mmseekto(x,y)
mm.write(struct.pack('BBB',*((r,g,b) if RGB else (b,g,r))))
def get_pixel(x,y):
mmseekto(x,y)
return mm.read(bytepp)
def _888_to_565(bt):
b = b''
for i in range(0, len(bt),3):
b += int.to_bytes(bt[i]>>3<<11|bt[i+1]>>2<<5|bt[i+2]>>3, 2, 'little')
return b
def numpy_888_565(bt):
import numpy as np
arr = np.fromstring(bt, dtype=np.uint32)
return (((0xF80000 & arr)>>8)|((0xFC00 & arr)>>5)|((0xF8 & arr)>>3)).astype(np.uint16).tostring()
def show_img(img):
if not type(img) is bytes:
if not RGB:
if bpp == 24: # for RPI
img = img.tobytes('raw', 'BGR')
else:
img = img.convert('RGBA').tobytes('raw', 'BGRA')
if bpp == 16:
img = numpy_888_565(img)
else:
if bpp == 24:
img = img.tobytes()
else:
img = img.convert('RGBA').tobytes()
if bpp == 16:
img = numpy_888_565(img)
from io import BytesIO
b = BytesIO(img)
s = vw*bytepp
for y in range(vh): # virtual window drawing
mmseekto(vx,vy+y)
mm.write(b.read(s))

@ -0,0 +1,52 @@
import logging
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.hw.base import DisplayImpl
import os,time
class Spotpear24inch(DisplayImpl):
def __init__(self, config):
super(Spotpear24inch, self).__init__(config, 'spotpear24inch')
self._display = None
def layout(self):
fonts.setup(12, 10, 12, 70)
self._layout['width'] = 320
self._layout['height'] = 240
self._layout['face'] = (35, 50)
self._layout['name'] = (5, 20)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (40, 0)
self._layout['uptime'] = (240, 0)
self._layout['line1'] = [0, 14, 320, 14]
self._layout['line2'] = [0, 220, 320, 220]
self._layout['friend_face'] = (0, 130)
self._layout['friend_name'] = (40, 135)
self._layout['shakes'] = (0, 220)
self._layout['mode'] = (280, 220)
self._layout['status'] = {
'pos': (80, 160),
'font': fonts.Medium,
'max': 20
}
return self._layout
def refresh(self):
time.sleep(0.1)
def initialize(self):
from pwnagotchi.ui.hw.libs.fb import fb
self._display = fb
logging.info("initializing spotpear 24inch lcd display")
self._display.ready_fb(i=1)
self._display.black_scr()
def render(self, canvas):
self._display.show_img(canvas.rotate(180))
self.refresh()
def clear(self):
self._display.black_scr()
self.refresh()

@ -119,12 +119,14 @@ class View(object):
def _refresh_handler(self):
delay = 1.0 / self._config['ui']['fps']
# logging.info("view refresh handler started with period of %.2fs" % delay)
while True:
name = self._state.get('name')
self.set('name', name.rstrip('').strip() if '' in name else (name + ''))
self.update()
try:
name = self._state.get('name')
self.set('name', name.rstrip('').strip() if '' in name else (name + ''))
self.update()
except Exception as e:
logging.warning("non fatal error while updating view: %s" % e)
time.sleep(delay)
def set(self, key, value):
@ -360,14 +362,15 @@ class View(object):
if self._frozen:
return
changes = self._state.changes(ignore=self._ignore_changes)
state = self._state
changes = state.changes(ignore=self._ignore_changes)
if force or len(changes):
self._canvas = Image.new('1', (self._width, self._height), WHITE)
drawer = ImageDraw.Draw(self._canvas)
plugins.on('ui_update', self)
for key, lv in self._state.items():
for key, lv in state.items():
lv.draw(self._canvas, drawer)
web.update_frame(self._canvas)

@ -1,39 +1,181 @@
import logging
import os
import base64
import _thread
import secrets
import json
from functools import wraps
# https://stackoverflow.com/questions/14888799/disable-console-messages-in-flask-server
logging.getLogger('werkzeug').setLevel(logging.ERROR)
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.ui.web as web
from pwnagotchi import plugins
from flask import send_file
from flask import Response
from flask import request
from flask import jsonify
from flask import abort
from flask import redirect
from flask import render_template, render_template_string
class Handler:
def __init__(self, agent, app):
def __init__(self, config, agent, app):
self._config = config
self._agent = agent
self._app = app
self._app.add_url_rule('/', 'index', self.index)
self._app.add_url_rule('/ui', 'ui', self.ui)
self._app.add_url_rule('/shutdown', 'shutdown', self.shutdown, methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.restart, methods=['POST'])
self._app.add_url_rule('/', 'index', self.with_auth(self.index))
self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
# inbox
self._app.add_url_rule('/inbox', 'inbox', self.with_auth(self.inbox))
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.with_auth(self.inbox_profile))
self._app.add_url_rule('/inbox/peers', 'inbox_peers', self.with_auth(self.inbox_peers))
self._app.add_url_rule('/inbox/<id>', 'show_message', self.with_auth(self.show_message))
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.with_auth(self.mark_message))
self._app.add_url_rule('/inbox/new', 'new_message', self.with_auth(self.new_message))
self._app.add_url_rule('/inbox/send', 'send_message', self.with_auth(self.send_message), methods=['POST'])
# plugins
self._app.add_url_rule('/plugins', 'plugins', self.plugins, strict_slashes=False,
plugins_with_auth = self.with_auth(self.plugins)
self._app.add_url_rule('/plugins', 'plugins', plugins_with_auth, strict_slashes=False,
defaults={'name': None, 'subpath': None})
self._app.add_url_rule('/plugins/<name>', 'plugins', self.plugins, strict_slashes=False,
self._app.add_url_rule('/plugins/<name>', 'plugins', plugins_with_auth, strict_slashes=False,
methods=['GET', 'POST'], defaults={'subpath': None})
self._app.add_url_rule('/plugins/<name>/<path:subpath>', 'plugins', self.plugins, methods=['GET', 'POST'])
self._app.add_url_rule('/plugins/<name>/<path:subpath>', 'plugins', plugins_with_auth, methods=['GET', 'POST'])
def _check_creds(self, u, p):
# trying to be timing attack safe
return secrets.compare_digest(u, self._config['username']) and \
secrets.compare_digest(p, self._config['password'])
def with_auth(self, f):
@wraps(f)
def wrapper(*args, **kwargs):
auth = request.authorization
if not auth or not auth.username or not auth.password or not self._check_creds(auth.username,
auth.password):
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'})
return f(*args, **kwargs)
return wrapper
def index(self):
return render_template('index.html', title=pwnagotchi.name(),
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU')
return render_template('index.html',
title=pwnagotchi.name(),
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU',
fingerprint=self._agent.fingerprint())
def inbox(self):
page = request.args.get("p", default=1, type=int)
inbox = {
"pages": 1,
"records": 0,
"messages": []
}
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
inbox = grid.inbox(page, with_pager=True)
except Exception as e:
logging.exception('error while reading pwnmail inbox')
error = str(e)
return render_template('inbox.html',
name=pwnagotchi.name(),
page=page,
error=error,
inbox=inbox)
def inbox_profile(self):
data = {}
error = None
try:
data = grid.get_advertisement_data()
except Exception as e:
logging.exception('error while reading pwngrid data')
error = str(e)
return render_template('profile.html',
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=json.dumps(data, indent=2),
error=error)
def inbox_peers(self):
peers = {}
error = None
try:
peers = grid.memory()
except Exception as e:
logging.exception('error while reading pwngrid peers')
error = str(e)
return render_template('peers.html',
name=pwnagotchi.name(),
peers=peers,
error=error)
def show_message(self, id):
message = {}
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
message = grid.inbox_message(id)
if message['data']:
message['data'] = base64.b64decode(message['data']).decode("utf-8")
except Exception as e:
logging.exception('error while reading pwnmail message %d' % int(id))
error = str(e)
return render_template('message.html',
name=pwnagotchi.name(),
error=error,
message=message)
def new_message(self):
to = request.args.get("to", default="")
return render_template('new_message.html', to=to)
def send_message(self):
to = request.form["to"]
message = request.form["message"]
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
grid.send_message(to, message)
except Exception as e:
error = str(e)
return jsonify({"error": error})
def mark_message(self, id, mark):
if not grid.is_connected():
abort(200)
logging.info("marking message %d as %s" % (int(id), mark))
grid.mark_message(id, mark)
return redirect("/inbox")
def plugins(self, name, subpath):
if name is None:

@ -13,16 +13,16 @@ from flask_wtf.csrf import CSRFProtect
from pwnagotchi.ui.web.handler import Handler
class Server:
def __init__(self, agent, config):
self._enabled = config['video']['enabled']
self._port = config['video']['port']
self._address = config['video']['address']
self._config = config['web']
self._enabled = self._config['enabled']
self._port = self._config['port']
self._address = self._config['address']
self._origin = None
self._agent = agent
if 'origin' in config['video']:
self._origin = config['video']['origin']
if 'origin' in self._config:
self._origin = self._config['origin']
if self._enabled:
_thread.start_new_thread(self._http_serve, ())
@ -35,13 +35,14 @@ class Server:
static_url_path='',
static_folder=os.path.join(web_path, 'static'),
template_folder=os.path.join(web_path, 'templates'))
app.secret_key = secrets.token_urlsafe(256)
if self._origin:
CORS(app, resources={r"*": {"origins": self._origin}})
CSRFProtect(app)
Handler(self._agent, app)
Handler(self._config, self._agent, app)
logging.info("web ui available at http://%s:%d/" % (self._address, self._port))

@ -1,22 +1,7 @@
form {
margin-block-end: 0;
.ui-image {
width: 100%;
}
.content {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
display: flex;
flex-direction: column;
}
/**
* make sure image is displayed without any blur
*/
.pixelated {
image-rendering: optimizeSpeed; /* Legal fallback */
image-rendering: -moz-crisp-edges; /* Firefox */
@ -33,36 +18,17 @@ form {
position: relative;
}
.ui-image {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
max-width: 100vw;
height: 100%;
object-fit: contain;
object-position: 50% 0;
}
.buttons-wrapper {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
padding: 0 16px;
}
.button {
border: 1px solid black;
border-radius: 4px;
font-size: 2em;
background: #f8b506;
margin: 16px;
}
div.status {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
a.read {
color: #777 !important;
}
p.messagebody {
padding: 1em;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

(image error) Size: 6.1 KiB

Binary file not shown.

After

(image error) Size: 219 B

Binary file not shown.

After

(image error) Size: 227 B

Binary file not shown.

After

(image error) Size: 244 B

Binary file not shown.

After

(image error) Size: 243 B

Binary file not shown.

After

(image error) Size: 146 B

Binary file not shown.

After

(image error) Size: 167 B

Binary file not shown.

After

(image error) Size: 173 B

Binary file not shown.

After

(image error) Size: 159 B

Binary file not shown.

After

(image error) Size: 171 B

Binary file not shown.

After

(image error) Size: 149 B

Binary file not shown.

After

(image error) Size: 149 B

Binary file not shown.

After

(image error) Size: 156 B

Binary file not shown.

After

(image error) Size: 147 B

Binary file not shown.

After

(image error) Size: 152 B

Binary file not shown.

After

(image error) Size: 147 B

Binary file not shown.

After

(image error) Size: 163 B

Binary file not shown.

After

(image error) Size: 169 B

Binary file not shown.

After

(image error) Size: 163 B

Binary file not shown.

After

(image error) Size: 165 B

Binary file not shown.

After

(image error) Size: 151 B

Binary file not shown.

After

(image error) Size: 307 B

Binary file not shown.

After

(image error) Size: 314 B

Binary file not shown.

After

(image error) Size: 233 B

Binary file not shown.

After

(image error) Size: 240 B

Binary file not shown.

After

(image error) Size: 132 B

Binary file not shown.

After

(image error) Size: 135 B

Binary file not shown.

After

(image error) Size: 147 B

Binary file not shown.

After

(image error) Size: 152 B

Binary file not shown.

After

(image error) Size: 146 B

Binary file not shown.

After

(image error) Size: 143 B

Binary file not shown.

After

(image error) Size: 250 B

Binary file not shown.

After

(image error) Size: 251 B

Binary file not shown.

After

(image error) Size: 207 B

Binary file not shown.

After

(image error) Size: 213 B

Binary file not shown.

After

(image error) Size: 174 B

Binary file not shown.

After

(image error) Size: 177 B

Binary file not shown.

After

(image error) Size: 184 B

Binary file not shown.

After

(image error) Size: 194 B

Binary file not shown.

After

(image error) Size: 196 B

Binary file not shown.

After

(image error) Size: 204 B

Binary file not shown.

After

(image error) Size: 169 B

Binary file not shown.

After

(image error) Size: 172 B

Binary file not shown.

After

(image error) Size: 310 B

Binary file not shown.

After

(image error) Size: 316 B

Binary file not shown.

After

(image error) Size: 212 B

Binary file not shown.

After

(image error) Size: 210 B

Binary file not shown.

After

(image error) Size: 165 B

Binary file not shown.

After

(image error) Size: 160 B

Binary file not shown.

After

(image error) Size: 171 B

Binary file not shown.

After

(image error) Size: 185 B

Binary file not shown.

After

(image error) Size: 163 B

Binary file not shown.

After

(image error) Size: 170 B

Binary file not shown.

After

(image error) Size: 249 B

Binary file not shown.

After

(image error) Size: 253 B

Binary file not shown.

After

(image error) Size: 299 B

Binary file not shown.

After

(image error) Size: 308 B

Binary file not shown.

After

(image error) Size: 233 B

Binary file not shown.

After

(image error) Size: 243 B

Binary file not shown.

After

(image error) Size: 318 B

Binary file not shown.

After

(image error) Size: 302 B

Binary file not shown.

After

(image error) Size: 160 B

Binary file not shown.

After

(image error) Size: 167 B

Binary file not shown.

After

(image error) Size: 242 B

Binary file not shown.

After

(image error) Size: 246 B

Binary file not shown.

After

(image error) Size: 150 B

Binary file not shown.

After

(image error) Size: 154 B

Some files were not shown because too many files have changed in this diff Show More