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
|
||||
|
186
bin/pwnagotchi
@ -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'))
|
||||
|
248
pwnagotchi/locale/spa/LC_MESSAGES/voice.po
Normal file
@ -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())
|
||||
|
218
pwnagotchi/plugins/default/webgpsmap.html
Normal file
@ -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>(⌐■ <span id="loading_ap_img"></span> ■)</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
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 © Esri — 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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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>
|
369
pwnagotchi/plugins/default/webgpsmap.py
Normal file
@ -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
pwnagotchi/ui/hw/libs/fb/__init__.py
Normal file
144
pwnagotchi/ui/hw/libs/fb/fb.py
Normal file
@ -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))
|
||||
|
52
pwnagotchi/ui/hw/spotpear24inch.py
Normal file
@ -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;
|
||||
}
|
5
pwnagotchi/ui/web/static/js/jquery-1.12.4.min.js
vendored
Normal file
2
pwnagotchi/ui/web/static/js/jquery-qrcode-0.17.0.min.js
vendored
Normal file
BIN
pwnagotchi/ui/web/static/js/jquery.mobile/images/ajax-loader.gif
Normal file
After ![]() (image error) Size: 6.1 KiB |
After ![]() (image error) Size: 219 B |
After ![]() (image error) Size: 227 B |
After ![]() (image error) Size: 244 B |
After ![]() (image error) Size: 243 B |
After ![]() (image error) Size: 146 B |
After ![]() (image error) Size: 167 B |
After ![]() (image error) Size: 173 B |
After ![]() (image error) Size: 159 B |
After ![]() (image error) Size: 171 B |
After ![]() (image error) Size: 149 B |
After ![]() (image error) Size: 149 B |
After ![]() (image error) Size: 156 B |
After ![]() (image error) Size: 147 B |
After ![]() (image error) Size: 152 B |
After ![]() (image error) Size: 147 B |
After ![]() (image error) Size: 163 B |
After ![]() (image error) Size: 169 B |
After ![]() (image error) Size: 163 B |
After ![]() (image error) Size: 165 B |
After ![]() (image error) Size: 151 B |
After ![]() (image error) Size: 307 B |
After ![]() (image error) Size: 314 B |
After ![]() (image error) Size: 233 B |
After ![]() (image error) Size: 240 B |
After ![]() (image error) Size: 132 B |
After ![]() (image error) Size: 135 B |
After ![]() (image error) Size: 147 B |
After ![]() (image error) Size: 152 B |
After ![]() (image error) Size: 146 B |
After ![]() (image error) Size: 143 B |
After ![]() (image error) Size: 250 B |
After ![]() (image error) Size: 251 B |
After ![]() (image error) Size: 207 B |
After ![]() (image error) Size: 213 B |
After ![]() (image error) Size: 174 B |
After ![]() (image error) Size: 177 B |
After ![]() (image error) Size: 184 B |
After ![]() (image error) Size: 194 B |
After ![]() (image error) Size: 196 B |
After ![]() (image error) Size: 204 B |
After ![]() (image error) Size: 169 B |
After ![]() (image error) Size: 172 B |
After ![]() (image error) Size: 310 B |
After ![]() (image error) Size: 316 B |
After ![]() (image error) Size: 212 B |
After ![]() (image error) Size: 210 B |
After ![]() (image error) Size: 165 B |
After ![]() (image error) Size: 160 B |
After ![]() (image error) Size: 171 B |
After ![]() (image error) Size: 185 B |
After ![]() (image error) Size: 163 B |
After ![]() (image error) Size: 170 B |
After ![]() (image error) Size: 249 B |
After ![]() (image error) Size: 253 B |
After ![]() (image error) Size: 299 B |
After ![]() (image error) Size: 308 B |
After ![]() (image error) Size: 233 B |
After ![]() (image error) Size: 243 B |
After ![]() (image error) Size: 318 B |
After ![]() (image error) Size: 302 B |
After ![]() (image error) Size: 160 B |
After ![]() (image error) Size: 167 B |
After ![]() (image error) Size: 242 B |
After ![]() (image error) Size: 246 B |
After ![]() (image error) Size: 150 B |
After ![]() (image error) Size: 154 B |