Compare commits
252 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a7164ea742 | ||
|
cae2a18016 | ||
|
9d63eba232 | ||
|
f141e15ba3 | ||
|
e68165ce06 | ||
|
3758806919 | ||
|
1f91c6f09e | ||
|
3e8b6eafbd | ||
|
44138ba463 | ||
|
4b71fea404 | ||
|
6babad0d02 | ||
|
e8513240ea | ||
|
00101ccd07 | ||
|
e0a66f5c99 | ||
|
81061cea24 | ||
|
93bb633010 | ||
|
dbb64e0fab | ||
|
fa8751017d | ||
|
9d56c97aa5 | ||
|
1523dfc1ef | ||
|
a02960b56d | ||
|
b6f59f99d4 | ||
|
09a00adab9 | ||
|
7fa30c2868 | ||
|
774d9c693c | ||
|
87b6cf7d40 | ||
|
88928eec82 | ||
|
60f7849838 | ||
|
3cf041617c | ||
|
ee6c06f306 | ||
|
2e22a17610 | ||
|
1615fc8817 | ||
|
714cb00610 | ||
|
a0a790635a | ||
|
9d17be959d | ||
|
4b651cd17b | ||
|
2f70512076 | ||
|
b79c59c639 | ||
|
56f7b67699 | ||
|
1a8472268e | ||
|
4fb7205281 | ||
|
f8ffab426b | ||
|
b4daf19401 | ||
|
e04e053cee | ||
|
4f153e3899 | ||
|
04720ecc42 | ||
|
a12e2aafa5 | ||
|
1721f67ec3 | ||
|
7693e42aa1 | ||
|
2ae48a2ef2 | ||
|
663bca41cd | ||
|
ede01e50cd | ||
|
19973574e7 | ||
|
a019b4c778 | ||
|
526c5bed87 | ||
|
4d6136633a | ||
|
6d90b75d10 | ||
|
03695be807 | ||
|
6a97476732 | ||
|
b5e620684b | ||
|
95557ab37d | ||
|
988d093e36 | ||
|
f563d71477 | ||
|
7b219fd139 | ||
|
42ed698583 | ||
|
6df7bcd885 | ||
|
1c299832ae | ||
|
11476433ca | ||
|
6e57e131b3 | ||
|
548b42ef20 | ||
|
99614c8cd4 | ||
|
e19ea999e2 | ||
|
2207a1eacf | ||
|
9509dd0aa5 | ||
|
608904daf8 | ||
|
f973997cdb | ||
|
9b594f7fb2 | ||
|
b8eed4f52a | ||
|
ad510429fe | ||
|
f5a94fde96 | ||
|
855bda9104 | ||
|
e72fd08fb4 | ||
|
e44ebac43f | ||
|
b5ddb716e2 | ||
|
82e7e09fa1 | ||
|
8b40e94ca8 | ||
|
cc5c46906f | ||
|
7cb52ba33a | ||
|
07f8e7bd4a | ||
|
48dc751d13 | ||
|
3c154ffe0c | ||
|
167f559d73 | ||
|
19775b7d27 | ||
|
48e3a372cc | ||
|
d2c44797e5 | ||
|
a03443986b | ||
|
a7ea499fac | ||
|
722a91655a | ||
|
93e06d7f59 | ||
|
7de5121033 | ||
|
83f741bbb0 | ||
|
a779fb9b0b | ||
|
c4a007e72a | ||
|
1a71615fa8 | ||
|
7a9f84f495 | ||
|
6e3f5a1181 | ||
|
d045ed5afa | ||
|
0ee0aaff37 | ||
|
0fb81a11c4 | ||
|
cfc0ad1b48 | ||
|
3351c251ef | ||
|
d9d399429c | ||
|
b1ad247e11 | ||
|
b5a148f287 | ||
|
7138f6469b | ||
|
785d678e30 | ||
|
9c8784e533 | ||
|
fd288b4acd | ||
|
b704614254 | ||
|
2dc36651df | ||
|
d8d6d52eda | ||
|
43c5ab7ecf | ||
|
de62424dbc | ||
|
8c51936c13 | ||
|
87e46610f9 | ||
|
a96dead519 | ||
|
8ed2950eb5 | ||
|
6f8133b2b8 | ||
|
16afa87112 | ||
|
ed22343877 | ||
|
c70f2c30e9 | ||
|
5111490c70 | ||
|
59ae35372e | ||
|
5f593a4231 | ||
|
56cc872daa | ||
|
6e1490da78 | ||
|
69597103b5 | ||
|
96ca5dd8e3 | ||
|
3efa3a935a | ||
|
39ccd141eb | ||
|
1a30b52a90 | ||
|
8965ad9272 | ||
|
5dae0ce982 | ||
|
92266a783a | ||
|
9e656d4ea6 | ||
|
1d255b577d | ||
|
1ff14c05a9 | ||
|
ab63ecccd7 | ||
|
a7e37115d9 | ||
|
b1d8aa3ba1 | ||
|
6e26463278 | ||
|
7261073073 | ||
|
a02c1d6d92 | ||
|
40caf3f51a | ||
|
21f1273bd8 | ||
|
a8c07ba997 | ||
|
6bd09c7f43 | ||
|
1830a19b37 | ||
|
9dcc647656 | ||
|
8fcfd4cafd | ||
|
369d7a65a8 | ||
|
d7ad8ee0d7 | ||
|
a5f9b9b2ee | ||
|
b266671864 | ||
|
c47b8f2d11 | ||
|
8129fb7dd2 | ||
|
c16cabc852 | ||
|
9a7de86057 | ||
|
440f2a470a | ||
|
81a89d43e0 | ||
|
91b409053b | ||
|
df01a03a4b | ||
|
e2be21004d | ||
|
0f3d9db01d | ||
|
19abc17816 | ||
|
4b9ebc2512 | ||
|
41a3fad43e | ||
|
f4b886cf7b | ||
|
0eb8e1829e | ||
|
30b1874a0a | ||
|
b903f636d2 | ||
|
52b40f049e | ||
|
3df35ef03b | ||
|
e87bcc4744 | ||
|
dfd534ac41 | ||
|
0b2c156d29 | ||
|
14064c3b5b | ||
|
313fd66634 | ||
|
c939af4248 | ||
|
e934181606 | ||
|
2505cbf14c | ||
|
b2c6de72cd | ||
|
307b3890f1 | ||
|
b9a909de2b | ||
|
b180f16aa6 | ||
|
2d517e3de5 | ||
|
a1746da7f1 | ||
|
1a1a70d6e8 | ||
|
229e2671e8 | ||
|
92c1b6b005 | ||
|
a2ac679499 | ||
|
d7e1c59709 | ||
|
a5ed09cd08 | ||
|
8c83f8129c | ||
|
eddfdb3ebc | ||
|
7ca1168fed | ||
|
d41e5c1152 | ||
|
25eee18e7b | ||
|
38144a7abb | ||
|
1f17d3cbbe | ||
|
1da59b50b4 | ||
|
1130c72098 | ||
|
6b99deb7bd | ||
|
c3ed3509e9 | ||
|
e3a2e8c811 | ||
|
89046bf0c5 | ||
|
4cc61322de | ||
|
94521f2174 | ||
|
b50acd364c | ||
|
9bc7fcccb3 | ||
|
bd61196c3c | ||
|
186042aa20 | ||
|
78bf801273 | ||
|
89450ec1bd | ||
|
09f80cc842 | ||
|
fcb5c87ef0 | ||
|
bf0e480266 | ||
|
fce57ad8eb | ||
|
425fe7e55a | ||
|
5760864495 | ||
|
3d9c559cdb | ||
|
97a019fe25 | ||
|
8d5834232b | ||
|
ebc161e82f | ||
|
9485e53484 | ||
|
0d66f93ef3 | ||
|
ad80fab554 | ||
|
b7380018f1 | ||
|
2ea8e7fe6b | ||
|
e23e1affae | ||
|
8d8333b586 | ||
|
5712f5cd51 | ||
|
9cc15403c3 | ||
|
15fa7039e8 | ||
|
f83c820b38 | ||
|
323c9a74cc | ||
|
8a2d6eac9d | ||
|
61d8e28aad | ||
|
337ebd6f9f | ||
|
399dbf2b41 | ||
|
f952bcd298 | ||
|
9dc7c92c86 |
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
|
||||
|
192
bin/pwnagotchi
@@ -1,19 +1,93 @@
|
||||
#!/usr/bin/python3
|
||||
import logging
|
||||
import argparse
|
||||
import time
|
||||
import yaml
|
||||
import signal
|
||||
|
||||
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
|
||||
from pwnagotchi import restart
|
||||
|
||||
|
||||
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,15 +106,22 @@ 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()
|
||||
|
||||
if args.version:
|
||||
print(pwnagotchi.version)
|
||||
SystemExit(0)
|
||||
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 +129,20 @@ 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 = Agent(view=display, config=config, keypair=KeyPair(view=display))
|
||||
|
||||
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))
|
||||
def usr1_handler(*unused):
|
||||
logging.info('Received USR1 singal. Restart process ...')
|
||||
restart("MANU" if args.do_manual else "AUTO")
|
||||
|
||||
while True:
|
||||
display.on_manual_mode(agent.last_session)
|
||||
time.sleep(5)
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
signal.signal(signal.SIGUSR1, usr1_handler)
|
||||
|
||||
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)
|
||||
|
@@ -10,6 +10,8 @@ PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwnagotchi-launcher
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
TasksMax=infinity
|
||||
LimitNPROC=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -23,6 +23,7 @@
|
||||
- bettercap.service
|
||||
- pwngrid-peer.service
|
||||
- epd-fuse.service
|
||||
- fstrim.timer
|
||||
disable:
|
||||
- apt-daily.timer
|
||||
- apt-daily.service
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
|
||||
import pwnagotchi.ui.view as view
|
||||
import pwnagotchi
|
||||
|
||||
version = '1.2.1'
|
||||
version = '1.4.2'
|
||||
|
||||
_name = None
|
||||
|
||||
@@ -27,17 +27,17 @@ def set_name(new_name):
|
||||
if new_name != current:
|
||||
global _name
|
||||
|
||||
logging.info("setting unit hostname '%s' -> '%s'" % (current, new_name))
|
||||
logging.info("setting unit hostname '%s' -> '%s'", current, new_name)
|
||||
with open('/etc/hostname', 'wt') as fp:
|
||||
fp.write(new_name)
|
||||
|
||||
with open('/etc/hosts', 'rt') as fp:
|
||||
prev = fp.read()
|
||||
logging.debug("old hosts:\n%s\n" % prev)
|
||||
logging.debug("old hosts:\n%s\n", prev)
|
||||
|
||||
with open('/etc/hosts', 'wt') as fp:
|
||||
patched = prev.replace(current, new_name, -1)
|
||||
logging.debug("new hosts:\n%s\n" % patched)
|
||||
logging.debug("new hosts:\n%s\n", patched)
|
||||
fp.write(patched)
|
||||
|
||||
os.system("hostname '%s'" % new_name)
|
||||
@@ -109,7 +109,7 @@ def shutdown():
|
||||
|
||||
|
||||
def restart(mode):
|
||||
logging.warning("restarting in %s mode ..." % mode)
|
||||
logging.warning("restarting in %s mode ...", mode)
|
||||
|
||||
if mode == 'AUTO':
|
||||
os.system("touch /root/.pwnagotchi-auto")
|
||||
@@ -123,7 +123,7 @@ def restart(mode):
|
||||
def reboot(mode=None):
|
||||
if mode is not None:
|
||||
mode = mode.upper()
|
||||
logging.warning("rebooting in %s mode ..." % mode)
|
||||
logging.warning("rebooting in %s mode ...", mode)
|
||||
else:
|
||||
logging.warning("rebooting ...")
|
||||
|
||||
|
@@ -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
|
||||
|
||||
@@ -57,7 +63,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
return self._supported_channels
|
||||
|
||||
def setup_events(self):
|
||||
logging.info("connecting to %s ..." % self.url)
|
||||
logging.info("connecting to %s ...", self.url)
|
||||
|
||||
for tag in self._config['bettercap']['silence']:
|
||||
try:
|
||||
@@ -84,7 +90,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
s = self.session()
|
||||
for iface in s['interfaces']:
|
||||
if iface['name'] == mon_iface:
|
||||
logging.info("found monitor interface: %s" % iface['name'])
|
||||
logging.info("found monitor interface: %s", iface['name'])
|
||||
has_mon = True
|
||||
break
|
||||
|
||||
@@ -93,11 +99,11 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
logging.info("starting monitor interface ...")
|
||||
self.run('!%s' % mon_start_cmd)
|
||||
else:
|
||||
logging.info("waiting for monitor interface %s ..." % mon_iface)
|
||||
logging.info("waiting for monitor interface %s ...", mon_iface)
|
||||
time.sleep(1)
|
||||
|
||||
logging.info("supported channels: %s" % self._supported_channels)
|
||||
logging.info("handshakes will be collected inside %s" % self._config['bettercap']['handshakes'])
|
||||
logging.info("supported channels: %s", self._supported_channels)
|
||||
logging.info("handshakes will be collected inside %s", self._config['bettercap']['handshakes'])
|
||||
|
||||
self._reset_wifi_settings()
|
||||
|
||||
@@ -145,10 +151,10 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
|
||||
if not channels:
|
||||
self._current_channel = 0
|
||||
logging.debug("RECON %ds" % recon_time)
|
||||
logging.debug("RECON %ds", recon_time)
|
||||
self.run('wifi.recon.channel clear')
|
||||
else:
|
||||
logging.debug("RECON %ds ON CHANNELS %s" % (recon_time, ','.join(map(str, channels))))
|
||||
logging.debug("RECON %ds ON CHANNELS %s", recon_time, ','.join(map(str, channels)))
|
||||
try:
|
||||
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
|
||||
except Exception as e:
|
||||
@@ -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']
|
||||
@@ -195,7 +212,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
ch = ap['channel']
|
||||
# if we're sticking to a channel, skip anything
|
||||
# which is not on that channel
|
||||
if channels != [] and ch not in channels:
|
||||
if channels and ch not in channels:
|
||||
continue
|
||||
|
||||
if ch not in grouped:
|
||||
@@ -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):
|
||||
@@ -257,7 +274,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
pwnagotchi.reboot()
|
||||
|
||||
def _save_recovery_data(self):
|
||||
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
|
||||
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
|
||||
with open(RECOVERY_DATA_FILE, 'w') as fp:
|
||||
data = {
|
||||
'started_at': self._started_at,
|
||||
@@ -272,7 +289,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
try:
|
||||
with open(RECOVERY_DATA_FILE, 'rt') as fp:
|
||||
data = json.load(fp)
|
||||
logging.info("found recovery data: %s" % data)
|
||||
logging.info("found recovery data: %s", data)
|
||||
self._started_at = data['started_at']
|
||||
self._epoch.epoch = data['epoch']
|
||||
self._handshakes = data['handshakes']
|
||||
@@ -280,7 +297,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
self._last_pwnd = data['last_pwnd']
|
||||
|
||||
if delete:
|
||||
logging.info("deleting %s" % RECOVERY_DATA_FILE)
|
||||
logging.info("deleting %s", RECOVERY_DATA_FILE)
|
||||
os.unlink(RECOVERY_DATA_FILE)
|
||||
except:
|
||||
if not no_exceptions:
|
||||
@@ -317,21 +334,23 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
new_shakes += 1
|
||||
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
|
||||
if ap_and_station is None:
|
||||
logging.warning("!!! captured new handshake: %s !!!" % key)
|
||||
logging.warning("!!! captured new handshake: %s !!!", key)
|
||||
self._last_pwnd = ap_mac
|
||||
plugins.on('handshake', self, filename, ap_mac, sta_mac)
|
||||
else:
|
||||
(ap, sta) = ap_and_station
|
||||
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
||||
'hostname'] != '<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:
|
||||
logging.error("error: %s" % e)
|
||||
logging.error("error: %s", e)
|
||||
|
||||
finally:
|
||||
self._update_handshakes(new_shakes)
|
||||
@@ -373,15 +392,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
|
||||
def associate(self, ap, throttle=0):
|
||||
if self.is_stale():
|
||||
logging.debug("recon is stale, skipping assoc(%s)" % ap['mac'])
|
||||
logging.debug("recon is stale, skipping assoc(%s)", ap['mac'])
|
||||
return
|
||||
|
||||
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
|
||||
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:
|
||||
@@ -394,15 +413,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
|
||||
def deauth(self, ap, sta, throttle=0):
|
||||
if self.is_stale():
|
||||
logging.debug("recon is stale, skipping deauth(%s)" % sta['mac'])
|
||||
logging.debug("recon is stale, skipping deauth(%s)", sta['mac'])
|
||||
return
|
||||
|
||||
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
|
||||
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:
|
||||
@@ -415,7 +434,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
|
||||
def set_channel(self, channel, verbose=True):
|
||||
if self.is_stale():
|
||||
logging.debug("recon is stale, skipping set_channel(%d)" % channel)
|
||||
logging.debug("recon is stale, skipping set_channel(%d)", channel)
|
||||
return
|
||||
|
||||
# if in the previous loop no client stations has been deauthenticated
|
||||
@@ -431,12 +450,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
if channel != self._current_channel:
|
||||
if self._current_channel != 0 and wait > 0:
|
||||
if verbose:
|
||||
logging.info("waiting for %ds on channel %d ..." % (wait, self._current_channel))
|
||||
logging.info("waiting for %ds on channel %d ...", wait, self._current_channel)
|
||||
else:
|
||||
logging.debug("waiting for %ds on channel %d ..." % (wait, self._current_channel))
|
||||
logging.debug("waiting for %ds on channel %d ...", wait, self._current_channel)
|
||||
self.wait_for(wait)
|
||||
if verbose and self._epoch.any_activity:
|
||||
logging.info("CHANNEL %d" % channel)
|
||||
logging.info("CHANNEL %d", channel)
|
||||
try:
|
||||
self.run('wifi.recon.channel %d' % channel)
|
||||
self._current_channel = channel
|
||||
@@ -446,4 +465,4 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
plugins.on('channel_hop', self, channel)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("error: %s" % e)
|
||||
logging.error("error: %s", 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]))
|
||||
|
||||
|
@@ -12,19 +12,18 @@ class Automata(object):
|
||||
self._epoch = Epoch(config)
|
||||
|
||||
def _on_miss(self, who):
|
||||
logging.info("it looks like %s is not in range anymore :/" % who)
|
||||
logging.info("it looks like %s is not in range anymore :/", who)
|
||||
self._epoch.track(miss=True)
|
||||
self._view.on_miss(who)
|
||||
|
||||
def _on_error(self, who, e):
|
||||
error = "%s" % e
|
||||
# when we're trying to associate or deauth something that is not in range anymore
|
||||
# (if we are moving), we get the following error from bettercap:
|
||||
# error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list.
|
||||
if 'is an unknown BSSID' in error:
|
||||
if 'is an unknown BSSID' in str(e):
|
||||
self._on_miss(who)
|
||||
else:
|
||||
logging.error("%s" % e)
|
||||
logging.error(e)
|
||||
|
||||
def set_starting(self):
|
||||
self._view.on_starting()
|
||||
@@ -58,7 +57,7 @@ class Automata(object):
|
||||
def set_bored(self):
|
||||
factor = self._epoch.inactive_for / self._config['personality']['bored_num_epochs']
|
||||
if not self._has_support_network_for(factor):
|
||||
logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for)
|
||||
logging.warning("%d epochs with no activity -> bored", self._epoch.inactive_for)
|
||||
self._view.on_bored()
|
||||
plugins.on('bored', self)
|
||||
else:
|
||||
@@ -68,7 +67,7 @@ class Automata(object):
|
||||
def set_sad(self):
|
||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||
if not self._has_support_network_for(factor):
|
||||
logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for)
|
||||
logging.warning("%d epochs with no activity -> sad", self._epoch.inactive_for)
|
||||
self._view.on_sad()
|
||||
plugins.on('sad', self)
|
||||
else:
|
||||
@@ -77,7 +76,7 @@ class Automata(object):
|
||||
|
||||
def set_angry(self, factor):
|
||||
if not self._has_support_network_for(factor):
|
||||
logging.warning("%d epochs with no activity -> angry" % self._epoch.inactive_for)
|
||||
logging.warning("%d epochs with no activity -> angry", self._epoch.inactive_for)
|
||||
self._view.on_angry()
|
||||
plugins.on('angry', self)
|
||||
else:
|
||||
@@ -85,7 +84,7 @@ class Automata(object):
|
||||
self.set_grateful()
|
||||
|
||||
def set_excited(self):
|
||||
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
|
||||
logging.warning("%d epochs with activity -> excited", self._epoch.active_for)
|
||||
self._view.on_excited()
|
||||
plugins.on('excited', self)
|
||||
|
||||
@@ -118,7 +117,7 @@ class Automata(object):
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
logging.warning("agent missed %d interactions -> lonely" % did_miss)
|
||||
logging.warning("agent missed %d interactions -> lonely", did_miss)
|
||||
self.set_lonely()
|
||||
# after X times being bored, the status is set to sad or angry
|
||||
elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
|
||||
@@ -139,6 +138,6 @@ class Automata(object):
|
||||
plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
|
||||
|
||||
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
|
||||
logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for)
|
||||
logging.critical("%d epochs without visible access points -> rebooting ...", self._epoch.blind_for)
|
||||
self._reboot()
|
||||
self._epoch.blind_for = 0
|
||||
|
@@ -1,5 +1,4 @@
|
||||
# WARNING WARNING WARNING WARNING
|
||||
#
|
||||
#
|
||||
# This file is recreated with default settings on every pwnagotchi restart,
|
||||
# use /etc/pwnagotchi/config.yml to configure this unit.
|
||||
#
|
||||
@@ -19,26 +18,10 @@ main:
|
||||
report: false # don't report pwned networks by default!
|
||||
exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
|
||||
- YourHomeNetworkHere
|
||||
|
||||
auto-update:
|
||||
enabled: true
|
||||
install: true # if false, it will only warn that updates are available, if true it will install them
|
||||
interval: 1 # every 1 hour
|
||||
|
||||
auto-backup:
|
||||
enabled: false
|
||||
interval: 1 # every day
|
||||
max_tries: 0 # 0=infinity
|
||||
files:
|
||||
- /root/brain.nn
|
||||
- /root/brain.json
|
||||
- /root/.api-report.json
|
||||
- /root/handshakes/
|
||||
- /root/peers/
|
||||
- /etc/pwnagotchi/
|
||||
- /var/log/pwnagotchi.log
|
||||
commands:
|
||||
- 'tar czf /root/pwnagotchi-backup.tar.gz {files}'
|
||||
net-pos:
|
||||
enabled: false
|
||||
api_key: 'test'
|
||||
@@ -46,12 +29,8 @@ main:
|
||||
enabled: false
|
||||
speed: 19200
|
||||
device: /dev/ttyUSB0
|
||||
twitter:
|
||||
webgpsmap:
|
||||
enabled: false
|
||||
consumer_key: aaa
|
||||
consumer_secret: aaa
|
||||
access_token_key: aaa
|
||||
access_token_secret: aaa
|
||||
onlinehashcrack:
|
||||
enabled: false
|
||||
email: ~
|
||||
@@ -59,19 +38,12 @@ main:
|
||||
enabled: false
|
||||
api_key: ~
|
||||
api_url: "https://wpa-sec.stanev.org"
|
||||
download_results: false
|
||||
wigle:
|
||||
enabled: false
|
||||
api_key: ~
|
||||
screen_refresh:
|
||||
enabled: false
|
||||
refresh_interval: 50
|
||||
quickdic:
|
||||
enabled: false
|
||||
wordlist_folder: /opt/wordlists/
|
||||
AircrackOnly:
|
||||
enabled: false
|
||||
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
|
||||
@@ -97,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: ''
|
||||
@@ -106,24 +79,67 @@ 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: 'sudo touch /root/.pwnagotchi-auto && sudo 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
|
||||
led: 0
|
||||
# time in milliseconds for each element of the patterns
|
||||
delay: 200
|
||||
# o=on space=off, comment the ones you don't want
|
||||
patterns:
|
||||
loaded: 'oo oo oo oo oo oo oo'
|
||||
updating: 'oo oo oo oo oo oo oo'
|
||||
# internet_available: 'oo oo oo oo oo oo oo'
|
||||
unread_inbox: 'oo oo oo oo oo oo oo'
|
||||
ready: 'oo oo oo oo oo oo oo'
|
||||
ai_ready: 'oo oo oo oo oo oo oo'
|
||||
ai_training_start: 'oo oo oo oo oo oo oo'
|
||||
ai_best_reward: 'oo oo oo oo oo oo oo'
|
||||
ai_worst_reward: 'oo oo oo oo oo oo oo'
|
||||
bored: 'oo oo oo oo oo oo oo'
|
||||
sad: 'oo oo oo oo oo oo oo'
|
||||
excited: 'oo oo oo oo oo oo oo'
|
||||
lonely: 'oo oo oo oo oo oo oo'
|
||||
rebooting: 'oo oo oo oo oo oo oo'
|
||||
wait: 'oo oo oo oo oo oo oo'
|
||||
sleep: 'oo oo oo oo oo oo oo'
|
||||
wifi_update: 'oo oo oo oo oo oo oo'
|
||||
association: 'oo oo oo oo oo oo oo'
|
||||
deauthentication: 'oo oo oo oo oo oo oo'
|
||||
handshake: 'oo oo oo oo oo oo oo'
|
||||
epoch: 'oo oo oo oo oo oo oo'
|
||||
peer_detected: 'oo oo oo oo oo oo oo'
|
||||
peer_lost: 'oo oo oo oo oo oo oo'
|
||||
webcfg:
|
||||
enabled: false
|
||||
session-stats:
|
||||
enabled: true
|
||||
# monitor interface to use
|
||||
iface: mon0
|
||||
# command to run to bring the mon interface up in case it's not up already
|
||||
mon_start_cmd: /usr/bin/monstart
|
||||
mon_stop_cmd: /usr/bin/monstop
|
||||
mon_max_blind_epochs: 50
|
||||
# log file
|
||||
log: /var/log/pwnagotchi.log
|
||||
# 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
|
||||
log:
|
||||
# file to log to
|
||||
path: /var/log/pwnagotchi.log
|
||||
rotation:
|
||||
enabled: true
|
||||
# specify a maximum size to rotate ( format is 10/10B, 10K, 10M 10G )
|
||||
size: '10M'
|
||||
|
||||
ai:
|
||||
# if false, only the default 'personality' will be used
|
||||
@@ -227,25 +243,28 @@ 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
|
||||
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, lcdhat, waveshare154inch, waveshare27inch, dfrobot/df
|
||||
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, lcdhat, waveshare154inch, waveshare27inch, waveshare29inch, dfrobot/df, waveshare144lcd/ws144inch
|
||||
type: 'waveshare_2'
|
||||
# Possible options red/yellow/black (black used for monocromatic displays)
|
||||
# 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'))
|
||||
|
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"POT-Creation-Date: 2019-11-14 21:15+0100\n"
|
||||
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
|
||||
"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
|
||||
@@ -20,7 +20,7 @@ msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hi, ich bin ein Pwnagotchi! Starte ..."
|
||||
msgstr "Hi, ich bin ein Pwnagotchi! Starte..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
|
||||
@@ -35,23 +35,30 @@ msgid "The neural network is ready."
|
||||
msgstr "Das neurale Netz ist bereit."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Generiere Keys, nicht ausschalten ..."
|
||||
msgstr "Generiere Keys, nicht ausschalten..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, Channel {channel} ist frei! Dein AP wir des dir danken."
|
||||
msgstr "Hey, Channel {channel} ist frei! Dein AP wird es Dir danken."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Lese die Logs der letzten Session..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Bisher {lines_so_far} Zeilen im Log gelesen..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Mir ist langweilig..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Lass uns laufen gehen!"
|
||||
msgstr "Lass uns spazieren gehen!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Das ist der beste Tag meines Lebens."
|
||||
msgstr "Das ist der beste Tag meines Lebens!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Scheis Tag :/"
|
||||
msgstr "Scheißtag :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Mir ist sau langweilig..."
|
||||
@@ -62,6 +69,13 @@ msgstr "Ich bin sehr traurig..."
|
||||
msgid "I'm sad"
|
||||
msgstr "Ich bin traurig"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Lass mich in Ruhe..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Ich bin sauer auf Dich!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Ich lebe das Leben!"
|
||||
|
||||
@@ -75,27 +89,35 @@ msgid "I'm having so much fun!"
|
||||
msgstr "Ich habe sooo viel Spaß!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Mein Verbrechen ist das der Neugier ..."
|
||||
msgstr "Mein Verbrechen ist das der Neugier..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Hallo {name}, nett Dich kennenzulernen."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Jo {name}! Was geht!?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hey {name}, wie geht's?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Gerät {name} ist in der nähe!!"
|
||||
msgstr "Gerät {name} ist in der Nähe!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ...tschüß {name}"
|
||||
msgstr "Uhm... tschüß {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} ist weg ..."
|
||||
msgstr "{name} ist weg..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Whoops ...{name} ist weg."
|
||||
msgstr "Whoops... {name} ist weg."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
@@ -111,17 +133,17 @@ msgid "I love my friends!"
|
||||
msgstr "Ich liebe meine Freunde!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Niemand will mit mir spielen ..."
|
||||
msgstr "Niemand will mit mir spielen..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Ich fühl michso alleine ..."
|
||||
msgstr "Ich fühl' mich so allein..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Wo sind denn alle?"
|
||||
msgstr "Wo sind denn alle?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Schlafe für {secs}s"
|
||||
msgstr "Schlafe für {secs}s..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
@@ -138,7 +160,7 @@ msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Warte für {secs}s ..."
|
||||
msgstr "Warte für {secs}s..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
@@ -158,7 +180,7 @@ msgstr "Jo {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Ich denke, dass {mac} kein WiFi brauch!"
|
||||
msgstr "Ich denke, dass {mac} kein WiFi braucht!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
@@ -177,7 +199,7 @@ msgid "You have {count} new message{plural}!"
|
||||
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
|
||||
msgstr "Ops, da ist was schief gelaufen... Starte neu..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
|
@@ -3,12 +3,11 @@
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <7271496+quantumsheep@users.noreply.github.com>, 2019.
|
||||
#
|
||||
#,
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 18:37+0200\n"
|
||||
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
|
||||
"PO-Revision-Date: 2019-10-03 10:34+0200\n"
|
||||
"Last-Translator: quantumsheep <7271496+quantumsheep@users.noreply.github."
|
||||
"com>\n"
|
||||
@@ -43,6 +42,13 @@ msgstr "Génération des clés, ne pas éteindre..."
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, le canal {channel} est libre! Ton point d'accès va te remercier."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Lecture des logs de la dernière session ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Jusqu'ici, {lines_so_far} lignes lues dans le log ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Je m'ennuie..."
|
||||
|
||||
@@ -64,6 +70,13 @@ msgstr "Je suis très triste..."
|
||||
msgid "I'm sad"
|
||||
msgstr "Je suis triste"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Je me sens si seul..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Je t'en veux !"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Je vis la vie !"
|
||||
|
||||
@@ -83,6 +96,14 @@ msgstr "Mon crime, c'est la curiosité..."
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Bonjour {name} ! Ravi de te rencontrer."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Yo {name} ! Quoi de neuf ?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hey {name} comment vas-tu ?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "L'unité {name} est proche !"
|
||||
@@ -106,6 +127,12 @@ msgstr "{name} raté !"
|
||||
msgid "Missed!"
|
||||
msgstr "Raté !"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Les bons amis sont une bénédiction !"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "J'aime mes amis !"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Personne ne veut jouer avec moi..."
|
||||
|
||||
@@ -120,11 +147,11 @@ msgid "Napping for {secs}s ..."
|
||||
msgstr "Fais la sieste pendant {secs}s..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
msgstr "ZzzZzzz ({secs}s)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Bonne nuit."
|
||||
@@ -181,18 +208,18 @@ msgstr "{num} stations kick\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Fait {num} nouveaux amis\n"
|
||||
msgstr "A {num} nouveaux amis\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Récupéré {num} handshakes\n"
|
||||
msgstr "A {num} handshakes\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 peer rencontré"
|
||||
msgstr "1 pair rencontré"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} peers recontrés"
|
||||
msgstr "{num} pairs recontrés"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
|
BIN
pwnagotchi/locale/no/LC_MESSAGES/voice.mo
Normal file
248
pwnagotchi/locale/no/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,248 @@
|
||||
# pwnagotchi norwegian voice data
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR untech <edvbot@gmail.com>, 2019.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\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: Edvard Botten <edvbot@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: norwegian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hei, jeg er Pwnagotchi! Starter ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "En ny dag, ny jakt, og nye pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack planeten!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI klart."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Det nevrale nettverket er klart."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Generer nøkkler, ikke skru meg av ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hei, kanalen {channel} er åpen! AP-en din takker."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Leser forrige sesjonen's logs ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Har lest {lines_so_far} linjer hittil ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Kjeder meg ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "La oss stikke på tur!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Dette er den beste dagen i mitt liv!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Jævlig dag :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Kjeder livet av meg ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Jeg er veldig trist ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Jeg er trist ..."
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "La meg være alene ..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Jeg er sint på deg!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Lever livet, lett!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Jeg pwner derfor er jeg."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Så mange nettverk!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Jeg har det så gøy!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Nysgjerrighet er min eneste forbrytelse ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Hallo {name}! Hyggelig å treffe deg!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Yo {name}! Skjer'a?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Heisann {name} driver du med da?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "{name} er i nærheten!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ... Ha det {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} er borte ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Oi da ... {name} forsvant."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} bommet!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Bommet!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Gode venner er livet verdt!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Jeg digger vennene mine!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Ingen vil leke med meg ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Jeg er så ensom ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Hvor er alle sammen?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Sover i {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "God natt."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Venter i {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Ser meg rundt ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hei {what} la oss være venner!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Tilkobler til {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Bestemte meg att {mac} ikke lenger trenger WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Kobler av {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Kickbanner {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Fett, vi fikk {num} nye håndtrykk!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Du har {count} melding{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oi, noe gikk helt skakk ... Rebooter ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Kicket {num} stasjoner\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Møtte {num} nye venner\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Skaffet {num} håndtrykk\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Møtte 1 annen"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Møtte {num} andre"
|
||||
|
||||
#, 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 "Jeg har pwnet for {duration} og kicket {dauthed} klienter! Jeg har også "
|
||||
"møtt {associated} nye venner og spiste {handshakes} håndtrykk! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "timer"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minutter"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "sekunder"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "time"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minutt"
|
||||
|
||||
msgid "second"
|
||||
msgstr "sekund"
|
BIN
pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
Normal file
249
pwnagotchi/locale/ro/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,249 @@
|
||||
# Pwnagotchi translation.
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <radu.ungureanu@techie.com>, 2019.
|
||||
#
|
||||
#,
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
|
||||
"PO-Revision-Date: 2019-11-20 00:18+594\n"
|
||||
"Last-Translator: Ungureanu Radu-Andrei <radu.ungureanu@techie.com>\n"
|
||||
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github."
|
||||
"com>\n"
|
||||
"Language: ro\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Buna, sunt Pwnagotchi! Pornesc..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "O noua zi, o noua vanatoare, noi pwn-uri!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Pirateaza planeta!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI-ul e gata."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Rețeaua neuronală este gata."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Se generează chei, nu închide..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, canalul {channel} este liber! AP-ul tău îti va mulțumi."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Se citesc log-urile din sesiunea anterioara..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Am citit {lines_so_far} linii din log pana acum..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Sunt plictisit..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Hai să ne plimbăm!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Asta este cea mai buna zi din viața mea!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "O zi proasta :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Sunt extrem de plictisit..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Sunt foarte trist..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Sunt trist"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Lasă-mă in pace..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Sunt supărat pe tine!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Trăiesc viața!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Eu pwn-ez, deci aici sunt."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Atât de multe rețele!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Mă distrez așa de mult!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Crima mea este una de curiozitate..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Bună {name}! Mă bucur să te cunosc."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Yo {name}! Cmf?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hey {nume} ce mai faci?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Unitatea {name} este aproape!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm... Pa {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} a dispărut."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Oops... {name} a dispărut."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} ratat!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Ratat!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Prietenii buni sunt o binecuvântare!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Îmi iubesc prietenii!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Nimeni nu vrea sa se joace cu mine..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Mă simt așa de singuratic..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Unde-i toată lumea?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Dorm pentru {secs}s..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}s)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Noapte bună."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Aștept pentru {secs}s..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Mă uit împrejur ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what} hai să fim prieteni!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Mă asociez cu {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Yo {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Am decis că lui {mac} nu-i trebuie WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Îl deautentific pe {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Îi dau kickban lui {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Șmecher, avem {num} de handshake-uri noi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Ai {count} mesaj(e) nou/noi!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Am dat afară {num} de stații\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Am făcut {num} prieteni noi \n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Am primit {num} de handshake-uri\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Am întalnit un peer"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Am întalnit {num} de peer-uri"
|
||||
|
||||
#, 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 "Eu am făcut pwning pentru {duration} și am dat afara {deauthed} clienți! "
|
||||
"De asemenea, am întalnit {associated} prieteni noi și am mancat {handshakes} de "
|
||||
"handshake-uri! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "ore"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minute"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "secunde"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "oră"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minut"
|
||||
|
||||
msgid "second"
|
||||
msgstr "secundă"
|
@@ -1,45 +1,51 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Pwnagotchi Russian translation.
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
|
||||
#
|
||||
# Second author <https://github.com/mbgroot>, 2019
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-03 16:47+0200\n"
|
||||
"PO-Revision-Date: 2019-10-05 18:50+0300\n"
|
||||
"Language-Team: \n"
|
||||
"Project-Id-Version: 0.0.2"
|
||||
"Report-Msgid-Bugs-To: m-b-g@yandex.ru"
|
||||
"POT-Creation-Date: 2019-11-27 16:47+0200"
|
||||
"PO-Revision-Date: 2019-11-27 18:50+0300"
|
||||
"Last-Translator: Evgeny Zelenin <m-b-g@yandex.ru>"
|
||||
"Language-Team: ru"
|
||||
"Language: ru"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
"Last-Translator: Elliot Manson\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
|
||||
"Language: ru_RU\n"
|
||||
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
msgstr "Хрррр..."
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Привет, я Pwnagotchi! Поехали …"
|
||||
msgstr "Привет, я Pwnagotchi! Стартуем!"
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Новый день, новая охота, новые взломы!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Хак зе планет!"
|
||||
msgstr "Взломай эту Планету!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI готов."
|
||||
msgstr "A.I. готов."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Нейронная сеть готова."
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Генерация ключей, не выключайте..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Чтение логов последнего сеанса..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Чтение {lines_so_far} строк журнала..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Мне скучно …"
|
||||
@@ -61,9 +67,14 @@ msgstr "Мне очень грустно …"
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Мне грустно"
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Оставь меня в покое..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Я зол на тебя!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Угараю по полной!"
|
||||
msgstr "Живу полной жизнью!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Я взламываю, поэтому я существую."
|
||||
@@ -75,15 +86,22 @@ msgid "I'm having so much fun!"
|
||||
msgstr "Мне так весело!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Моe преступление - это любопытство …"
|
||||
msgstr "Моё преступление - это любопытство…"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Привет, {name}! Приятно познакомиться. {name}"
|
||||
msgstr "Привет, {name}! Рад встрече с тобой!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Цель {name} близко! {name}"
|
||||
msgstr "Цель {name} близко!"
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Хэй {nume}! Как дела?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Цель {name} рядом!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
@@ -91,11 +109,11 @@ msgstr "Хм … до свидания {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} исчезла …"
|
||||
msgstr "{name} ушла…"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Упс … {name} исчезла."
|
||||
msgstr "Упс… {name} исчезла."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
@@ -103,12 +121,17 @@ msgstr "{name} упустил!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Промахнулся!"
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Хорошие друзья - это благословение!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Я люблю своих друзей!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Никто не хочет со мной играть ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Мне так одиноко …"
|
||||
msgstr "Я так одинок…"
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Где все?!"
|
||||
@@ -118,11 +141,16 @@ msgid "Napping for {secs}s ..."
|
||||
msgstr "Дремлет {secs}с …"
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
msgstr "Хррр..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}c)"
|
||||
msgstr "Хррррр.. ({secs}c)"
|
||||
msgid "Good night."
|
||||
msgstr "Доброй ночи."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Хрррр"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
@@ -130,7 +158,7 @@ msgstr "Ждем {secs}c …"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Оглядываюсь вокруг ({secs}с)"
|
||||
msgstr "Осматриваюсь вокруг ({secs}с)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
@@ -173,7 +201,7 @@ msgstr "Заимел {num} новых друзей\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Получил {num} рукопожатие\n"
|
||||
msgstr "Получил {num} рукопожатий\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Встретился один знакомый"
|
||||
|
BIN
pwnagotchi/locale/sk/LC_MESSAGES/voice.mo
Normal file
227
pwnagotchi/locale/sk/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,227 @@
|
||||
# Slovak language
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# mil1200 <mil.kyselica@gmail.com>, 2019.
|
||||
#
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-8 17:55+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Milan Kyselica <mil.kyselica@gmail.com>\n"
|
||||
"Language-Team: SK\n"
|
||||
"Language: sk\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Ahoj, ja som Pwnagotchi! Začíname ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Nový deň, nový lov, nové pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hacknime Planétu!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI pripravené."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Neurónová sieť je pripravená."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Generujú sa kľúče, nevypínaj ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hej, kanál {channel} je voľný! Váš AP vám poďakuje."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Nudím sa ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Poďme na prechádzku!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Toto je najlepší deň môjho života!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Na hovno deň :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Veľmi sa nudím ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Som veľmi smutný ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Som smutný"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Žijem život!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "I pwn therefore I am."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Toľko sietí !!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Zabávam sa!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Môj zločin je zvedavosť ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Dobrý deň, {name}! Rád som ťa spoznal."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Jednotka {name} je blízko!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ... zbohom {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} je preč ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Hups ... {name} je preč."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} nechytené!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Vedľa!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Dobrí priatelia sú požehnaním!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Milujem svojich priateľov!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Nikto sa so mnou nechce hrať ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Cítim sa tak sám ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Kde sú všetci ?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Zdriemnem si na {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}s)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Dobrú noc."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Čaká sa {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Rozhliadam sa okolo ({secs} s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Ahoj {what} buďme priatelia!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Spájam sa s {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Yo {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Rozhodol som sa že {mac} nepotrebuje Wi-Fi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Deautentifikujem {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Kickujem {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Super, máme {num} nový handshake{plural}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Máte {count} novú správu{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, niečo sa pokazilo ... Reštartujem sa ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Kicknutá/ých {num} stanica/íc\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Získaní {num} noví kamaráti\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Získali sme {num} handshake/-y/ov rúk\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Sretli sme 1 rovesníka"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Stretli sme {num} rovesníkov"
|
||||
|
||||
#, 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 "Pwnoval som {duration} a kickol som {deauthed} klienta/ov! Tiež som"
|
||||
"stretol {associated} nového/ých kamaráta/ov a zjedol {handshakes} handshake/y!"
|
||||
" #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "hodiny"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minúty"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "sekundy"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "hodina"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minúta"
|
||||
|
||||
msgid "second"
|
||||
msgstr "sekunda"
|
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"
|
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
|
||||
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@@ -26,7 +26,7 @@ class LastSession(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.voice = Voice(lang=config['main']['lang'])
|
||||
self.path = config['main']['log']
|
||||
self.path = config['main']['log']['path']
|
||||
self.last_session = []
|
||||
self.last_session_id = ''
|
||||
self.last_saved_session_id = ''
|
||||
|
@@ -1,4 +1,5 @@
|
||||
NumChannels = 140
|
||||
NumChannelsExt = 165 # see https://github.com/evilsocket/pwnagotchi/issues/583
|
||||
|
||||
|
||||
def freq_to_channel(freq):
|
||||
|
@@ -1,37 +1,81 @@
|
||||
import os
|
||||
import glob
|
||||
import _thread
|
||||
import threading
|
||||
import importlib, importlib.util
|
||||
import logging
|
||||
from pwnagotchi.ui import view
|
||||
|
||||
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
|
||||
loaded = {}
|
||||
database = {}
|
||||
locks = {}
|
||||
|
||||
|
||||
class Plugin:
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
global loaded
|
||||
global loaded, locks
|
||||
|
||||
plugin_name = cls.__module__.split('.')[0]
|
||||
plugin_instance = cls()
|
||||
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
|
||||
loaded[plugin_name] = plugin_instance
|
||||
|
||||
for attr_name in plugin_instance.__dir__():
|
||||
if attr_name.startswith('on_'):
|
||||
cb = getattr(plugin_instance, attr_name, None)
|
||||
if cb is not None and callable(cb):
|
||||
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
|
||||
|
||||
|
||||
def toggle_plugin(name, enable=True):
|
||||
"""
|
||||
Load or unload a plugin
|
||||
|
||||
returns True if changed, otherwise False
|
||||
"""
|
||||
global loaded, database
|
||||
if not enable and name in loaded:
|
||||
if getattr(loaded[name], 'on_unload', None):
|
||||
loaded[name].on_unload(view.ROOT)
|
||||
del loaded[name]
|
||||
return True
|
||||
|
||||
if enable and name in database and name not in loaded:
|
||||
load_from_file(database[name])
|
||||
one(name, 'loaded')
|
||||
one(name, 'ui_setup', view.ROOT)
|
||||
one(name, 'ready', view.ROOT._agent)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def on(event_name, *args, **kwargs):
|
||||
for plugin_name, plugin in loaded.items():
|
||||
one(plugin_name, event_name, *args, **kwargs)
|
||||
|
||||
|
||||
def locked_cb(lock_name, cb, *args, **kwargs):
|
||||
global locks
|
||||
with locks[lock_name]:
|
||||
cb(*args, *kwargs)
|
||||
|
||||
|
||||
def one(plugin_name, event_name, *args, **kwargs):
|
||||
global loaded
|
||||
|
||||
if plugin_name in loaded:
|
||||
plugin = loaded[plugin_name]
|
||||
cb_name = 'on_%s' % event_name
|
||||
callback = getattr(plugin, cb_name, None)
|
||||
if callback is not None and callable(callback):
|
||||
try:
|
||||
callback(*args, **kwargs)
|
||||
lock_name = "%s::%s" % (plugin_name, cb_name)
|
||||
locked_cb_args = (lock_name, callback, *args, *kwargs)
|
||||
_thread.start_new_thread(locked_cb, locked_cb_args)
|
||||
except Exception as e:
|
||||
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
|
||||
logging.error(e, exc_info=True)
|
||||
@@ -47,10 +91,11 @@ def load_from_file(filename):
|
||||
|
||||
|
||||
def load_from_path(path, enabled=()):
|
||||
global loaded
|
||||
global loaded, database
|
||||
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
|
||||
for filename in glob.glob(os.path.join(path, "*.py")):
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
database[plugin_name] = filename
|
||||
if plugin_name in enabled:
|
||||
try:
|
||||
load_from_file(filename)
|
||||
|
@@ -1,57 +0,0 @@
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import string
|
||||
import os
|
||||
|
||||
'''
|
||||
Aircrack-ng needed, to install:
|
||||
> apt-get install aircrack-ng
|
||||
'''
|
||||
|
||||
|
||||
class AircrackOnly(plugins.Plugin):
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.1'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'confirm pcap contains handshake/PMKID or delete it'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self)
|
||||
self.text_to_set = ""
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("aircrackonly plugin loaded")
|
||||
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
display = agent._view
|
||||
todelete = 0
|
||||
handshakeFound = 0
|
||||
|
||||
result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "1 handshake" | awk \'{print $2}\''),
|
||||
shell=True, stdout=subprocess.PIPE)
|
||||
result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace})
|
||||
if result:
|
||||
handshakeFound = 1
|
||||
logging.info("[AircrackOnly] contains handshake")
|
||||
|
||||
if handshakeFound == 0:
|
||||
result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "PMKID" | awk \'{print $2}\''),
|
||||
shell=True, stdout=subprocess.PIPE)
|
||||
result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace})
|
||||
if result:
|
||||
logging.info("[AircrackOnly] contains PMKID")
|
||||
else:
|
||||
todelete = 1
|
||||
|
||||
if todelete == 1:
|
||||
os.remove(filename)
|
||||
self.text_to_set = "Removed an uncrackable pcap"
|
||||
display.update(force=True)
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if self.text_to_set:
|
||||
ui.set('face', "(>.<)")
|
||||
ui.set('status', self.text_to_set)
|
||||
self.text_to_set = ""
|
@@ -1,65 +0,0 @@
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
class AutoBackup(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin backups files when internet is available.'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.tries = 0
|
||||
self.status = StatusFile('/root/.auto-backup')
|
||||
|
||||
def on_loaded(self):
|
||||
for opt in ['files', 'interval', 'commands', 'max_tries']:
|
||||
if opt not in self.options or (opt in self.options and self.options[opt] is None):
|
||||
logging.error(f"AUTO-BACKUP: Option {opt} is not set.")
|
||||
return
|
||||
|
||||
self.ready = True
|
||||
logging.info("AUTO-BACKUP: Successfully loaded.")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
if self.options['max_tries'] and self.tries >= self.options['max_tries']:
|
||||
return
|
||||
|
||||
if self.status.newer_then_days(self.options['interval']):
|
||||
return
|
||||
|
||||
# Only backup existing files to prevent errors
|
||||
existing_files = list(filter(lambda f: os.path.exists(f), self.options['files']))
|
||||
files_to_backup = " ".join(existing_files)
|
||||
|
||||
try:
|
||||
display = agent.view()
|
||||
|
||||
logging.info("AUTO-BACKUP: Backing up ...")
|
||||
display.set('status', 'Backing up ...')
|
||||
display.update()
|
||||
|
||||
for cmd in self.options['commands']:
|
||||
logging.info(f"AUTO-BACKUP: Running {cmd.format(files=files_to_backup)}")
|
||||
process = subprocess.Popen(cmd.format(files=files_to_backup), shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
raise OSError(f"Command failed (rc: {process.returncode})")
|
||||
|
||||
logging.info("AUTO-BACKUP: backup done")
|
||||
display.set('status', 'Backup done!')
|
||||
display.update()
|
||||
self.status.update()
|
||||
except OSError as os_e:
|
||||
self.tries += 1
|
||||
logging.info(f"AUTO-BACKUP: Error: {os_e}")
|
||||
display.set('status', 'Backup failed!')
|
||||
display.update()
|
@@ -7,6 +7,7 @@ import platform
|
||||
import shutil
|
||||
import glob
|
||||
import pkg_resources
|
||||
from threading import Lock
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.plugins as plugins
|
||||
@@ -150,6 +151,7 @@ class AutoUpdate(plugins.Plugin):
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.status = StatusFile('/root/.auto-update')
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
if 'interval' not in self.options or ('interval' in self.options and self.options['interval'] is None):
|
||||
@@ -159,59 +161,61 @@ class AutoUpdate(plugins.Plugin):
|
||||
logging.info("[update] plugin loaded.")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||
with self.lock:
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||
|
||||
if not self.ready:
|
||||
return
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
if self.status.newer_then_hours(self.options['interval']):
|
||||
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
|
||||
return
|
||||
if self.status.newer_then_hours(self.options['interval']):
|
||||
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
|
||||
return
|
||||
|
||||
logging.info("[update] checking for updates ...")
|
||||
logging.info("[update] checking for updates ...")
|
||||
|
||||
display = agent.view()
|
||||
prev_status = display.get('status')
|
||||
display = agent.view()
|
||||
prev_status = display.get('status')
|
||||
|
||||
try:
|
||||
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
||||
try:
|
||||
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
||||
|
||||
to_install = []
|
||||
to_check = [
|
||||
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
||||
('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
|
||||
]
|
||||
to_install = []
|
||||
to_check = [
|
||||
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
||||
('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
|
||||
]
|
||||
|
||||
for repo, local_version, is_native, svc_name in to_check:
|
||||
info = check(local_version, repo, is_native)
|
||||
if info['url'] is not None:
|
||||
logging.warning(
|
||||
"update for %s available (local version is '%s'): %s" % (
|
||||
repo, info['current'], info['url']))
|
||||
info['service'] = svc_name
|
||||
to_install.append(info)
|
||||
for repo, local_version, is_native, svc_name in to_check:
|
||||
info = check(local_version, repo, is_native)
|
||||
if info['url'] is not None:
|
||||
logging.warning(
|
||||
"update for %s available (local version is '%s'): %s" % (
|
||||
repo, info['current'], info['url']))
|
||||
info['service'] = svc_name
|
||||
to_install.append(info)
|
||||
|
||||
num_updates = len(to_install)
|
||||
num_installed = 0
|
||||
num_updates = len(to_install)
|
||||
num_installed = 0
|
||||
|
||||
if num_updates > 0:
|
||||
if self.options['install']:
|
||||
for update in to_install:
|
||||
if install(display, update):
|
||||
num_installed += 1
|
||||
else:
|
||||
prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
|
||||
if num_updates > 0:
|
||||
if self.options['install']:
|
||||
for update in to_install:
|
||||
plugins.on('updating')
|
||||
if install(display, update):
|
||||
num_installed += 1
|
||||
else:
|
||||
prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
|
||||
|
||||
logging.info("[update] done")
|
||||
logging.info("[update] done")
|
||||
|
||||
self.status.update()
|
||||
self.status.update()
|
||||
|
||||
if num_installed > 0:
|
||||
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
||||
pwnagotchi.reboot()
|
||||
if num_installed > 0:
|
||||
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
||||
pwnagotchi.reboot()
|
||||
|
||||
except Exception as e:
|
||||
logging.error("[update] %s" % e)
|
||||
except Exception as e:
|
||||
logging.error("[update] %s" % e)
|
||||
|
||||
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
|
||||
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from threading import Lock
|
||||
|
||||
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 +384,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 +396,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
|
||||
@@ -424,6 +427,7 @@ class BTTether(plugins.Plugin):
|
||||
self.ready = False
|
||||
self.options = dict()
|
||||
self.devices = dict()
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
# new config
|
||||
@@ -461,110 +465,119 @@ 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_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element('bluetooth')
|
||||
|
||||
def on_ui_setup(self, ui):
|
||||
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
with ui._lock:
|
||||
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
devices_to_try = list()
|
||||
connected_priorities = list()
|
||||
any_device_connected = False # if this is true, last status on screen should be C
|
||||
with self.lock:
|
||||
devices_to_try = list()
|
||||
connected_priorities = list()
|
||||
any_device_connected = False # if this is true, last status on screen should be C
|
||||
|
||||
for _, device in self.devices.items():
|
||||
if device.connected():
|
||||
connected_priorities.append(device.priority)
|
||||
any_device_connected = True
|
||||
continue
|
||||
for _, device in self.devices.items():
|
||||
if device.connected():
|
||||
connected_priorities.append(device.priority)
|
||||
any_device_connected = True
|
||||
continue
|
||||
|
||||
if not device.max_tries or (device.max_tries > device.tries):
|
||||
if not device.status.newer_then_minutes(device.interval):
|
||||
devices_to_try.append(device)
|
||||
device.status.update()
|
||||
device.tries += 1
|
||||
if not device.max_tries or (device.max_tries > device.tries):
|
||||
if not device.status.newer_then_minutes(device.interval):
|
||||
devices_to_try.append(device)
|
||||
device.status.update()
|
||||
device.tries += 1
|
||||
|
||||
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
|
||||
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
|
||||
|
||||
for device in sorted_devices:
|
||||
bt = BTNap(device.mac)
|
||||
for device in sorted_devices:
|
||||
bt = BTNap(device.mac)
|
||||
|
||||
try:
|
||||
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
|
||||
dev_remote = bt.wait_for_device(timeout=device.scantime)
|
||||
if dev_remote is None:
|
||||
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
|
||||
try:
|
||||
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
|
||||
dev_remote = bt.wait_for_device(timeout=device.scantime)
|
||||
if dev_remote is None:
|
||||
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
|
||||
ui.set('bluetooth', 'NF')
|
||||
continue
|
||||
except Exception as bt_ex:
|
||||
logging.error(bt_ex)
|
||||
ui.set('bluetooth', 'NF')
|
||||
continue
|
||||
except Exception as bt_ex:
|
||||
logging.error(bt_ex)
|
||||
ui.set('bluetooth', 'NF')
|
||||
continue
|
||||
|
||||
paired = bt.is_paired()
|
||||
if not paired:
|
||||
if BTNap.pair(dev_remote):
|
||||
logging.debug('BT-TETHER: Paired with %s.', device.name)
|
||||
paired = bt.is_paired()
|
||||
if not paired:
|
||||
if BTNap.pair(dev_remote):
|
||||
logging.debug('BT-TETHER: Paired with %s.', device.name)
|
||||
else:
|
||||
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
|
||||
ui.set('bluetooth', 'PE')
|
||||
continue
|
||||
else:
|
||||
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
|
||||
ui.set('bluetooth', 'PE')
|
||||
continue
|
||||
else:
|
||||
logging.debug('BT-TETHER: Already paired.')
|
||||
logging.debug('BT-TETHER: Already paired.')
|
||||
|
||||
|
||||
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
|
||||
device.network, success = BTNap.nap(dev_remote)
|
||||
interface = None
|
||||
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
|
||||
device.network, success = BTNap.nap(dev_remote)
|
||||
interface = None
|
||||
|
||||
if success:
|
||||
try:
|
||||
interface = device.interface()
|
||||
except Exception:
|
||||
if success:
|
||||
try:
|
||||
interface = device.interface()
|
||||
except Exception:
|
||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||
continue
|
||||
|
||||
if interface is None:
|
||||
ui.set('bluetooth', 'BE')
|
||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||
continue
|
||||
|
||||
logging.debug('BT-TETHER: Created interface (%s)', interface)
|
||||
ui.set('bluetooth', 'C')
|
||||
any_device_connected = True
|
||||
device.tries = 0 # reset tries
|
||||
else:
|
||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||
ui.set('bluetooth', 'NF')
|
||||
continue
|
||||
|
||||
if interface is None:
|
||||
ui.set('bluetooth', 'BE')
|
||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||
addr = f"{device.ip}/{device.netmask}"
|
||||
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)
|
||||
if not wrapped_interface.set_addr(addr):
|
||||
ui.set('bluetooth', 'AE')
|
||||
logging.debug("BT-TETHER: Could not add ip to %s", interface)
|
||||
continue
|
||||
|
||||
logging.debug('BT-TETHER: Created interface (%s)', interface)
|
||||
if device.share_internet:
|
||||
if not connected_priorities or device.priority > max(connected_priorities):
|
||||
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
|
||||
IfaceWrapper.set_route(gateway, interface)
|
||||
connected_priorities.append(device.priority)
|
||||
|
||||
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
|
||||
with open('/etc/resolv.conf', 'r+') as resolv:
|
||||
nameserver = resolv.read()
|
||||
if 'nameserver 9.9.9.9' not in nameserver:
|
||||
logging.debug('BT-TETHER: Added nameserver')
|
||||
resolv.seek(0)
|
||||
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
||||
|
||||
if any_device_connected:
|
||||
ui.set('bluetooth', 'C')
|
||||
any_device_connected = True
|
||||
device.tries = 0 # reset tries
|
||||
else:
|
||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||
ui.set('bluetooth', 'NF')
|
||||
continue
|
||||
|
||||
addr = f"{device.ip}/{device.netmask}"
|
||||
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
|
||||
|
||||
wrapped_interface = IfaceWrapper(interface)
|
||||
logging.debug('BT-TETHER: Add ip to %s', interface)
|
||||
if not wrapped_interface.set_addr(addr):
|
||||
ui.set('bluetooth', 'AE')
|
||||
logging.debug("BT-TETHER: Could not add ip to %s", interface)
|
||||
continue
|
||||
|
||||
if device.share_internet:
|
||||
if not connected_priorities or device.priority > max(connected_priorities):
|
||||
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
|
||||
IfaceWrapper.set_route(gateway, interface)
|
||||
connected_priorities.append(device.priority)
|
||||
|
||||
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
|
||||
with open('/etc/resolv.conf', 'r+') as resolv:
|
||||
nameserver = resolv.read()
|
||||
if 'nameserver 9.9.9.9' not in nameserver:
|
||||
logging.debug('BT-TETHER: Added nameserver')
|
||||
resolv.seek(0)
|
||||
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
||||
|
||||
if any_device_connected:
|
||||
ui.set('bluetooth', 'C')
|
||||
|
@@ -16,27 +16,20 @@ class Example(plugins.Plugin):
|
||||
logging.debug("example plugin created")
|
||||
|
||||
# called when http://<host>:<port>/plugins/<plugin>/ is called
|
||||
# must return a response
|
||||
def on_webhook(self, path, args, req_method):
|
||||
# must return a html page
|
||||
# IMPORTANT: If you use "POST"s, add a csrf-token (via csrf_token() and render_template_string)
|
||||
def on_webhook(self, path, request):
|
||||
pass
|
||||
|
||||
# called when the plugin is loaded
|
||||
def on_loaded(self):
|
||||
logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
|
||||
|
||||
# called when <host>:<port>/plugins/<pluginname> is opened
|
||||
def on_webhook(self, response, path):
|
||||
res = "<html><body><a>Hook triggered</a></body></html>"
|
||||
response.send_response(200)
|
||||
response.send_header('Content-type', 'text/html')
|
||||
response.end_headers()
|
||||
# called before the plugin is unloaded
|
||||
def on_unload(self, ui):
|
||||
pass
|
||||
|
||||
try:
|
||||
response.wfile.write(bytes(res, "utf-8"))
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
|
||||
# called in manual mode when there's internet connectivity
|
||||
# called hen there's internet connectivity
|
||||
def on_internet_available(self, agent):
|
||||
pass
|
||||
|
||||
@@ -106,7 +99,7 @@ class Example(plugins.Plugin):
|
||||
pass
|
||||
|
||||
# called when the status is set to excited
|
||||
def on_excited(aself, gent):
|
||||
def on_excited(self, agent):
|
||||
pass
|
||||
|
||||
# called when the status is set to lonely
|
||||
@@ -129,6 +122,11 @@ class Example(plugins.Plugin):
|
||||
def on_wifi_update(self, agent, access_points):
|
||||
pass
|
||||
|
||||
# called when the agent refreshed an unfiltered access point list
|
||||
# this list contains all access points that were detected BEFORE filtering
|
||||
def on_unfiltered_ap_list(self, agent, access_points):
|
||||
pass
|
||||
|
||||
# called when the agent is sending an association frame
|
||||
def on_association(self, agent, access_point):
|
||||
pass
|
||||
|
@@ -31,10 +31,9 @@ class GPIOButtons(plugins.Plugin):
|
||||
# set gpio numbering
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
|
||||
for i in gpios:
|
||||
gpio = list(i)[0]
|
||||
command = i[gpio]
|
||||
for gpio, command in gpios.items():
|
||||
gpio = int(gpio)
|
||||
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,132 @@
|
||||
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_waveshare_v1():
|
||||
lat_pos = (130, 70)
|
||||
lon_pos = (125, 80)
|
||||
alt_pos = (130, 90)
|
||||
elif ui.is_inky():
|
||||
# guessed values, add tested ones if you can
|
||||
lat_pos = (112, 30)
|
||||
lon_pos = (112, 49)
|
||||
alt_pos = (87, 63)
|
||||
elif ui.is_waveshare144lcd():
|
||||
# guessed values, add tested ones if you can
|
||||
lat_pos = (67, 73)
|
||||
lon_pos = (62, 83)
|
||||
alt_pos = (67, 93)
|
||||
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_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element('latitude')
|
||||
ui.remove_element('longitude')
|
||||
ui.remove_element('altitude')
|
||||
|
||||
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 ")
|
||||
|
@@ -67,7 +67,8 @@ class Grid(plugins.Plugin):
|
||||
logging.info("grid plugin loaded.")
|
||||
|
||||
def set_reported(self, reported, net_id):
|
||||
reported.append(net_id)
|
||||
if net_id not in reported:
|
||||
reported.append(net_id)
|
||||
self.report.update(data={'reported': reported})
|
||||
|
||||
def check_inbox(self, agent):
|
||||
@@ -77,6 +78,7 @@ class Grid(plugins.Plugin):
|
||||
self.unread_messages = len([m for m in messages if m['seen_at'] is None])
|
||||
|
||||
if self.unread_messages:
|
||||
plugins.on('unread_inbox', self.unread_messages)
|
||||
logging.debug("[grid] unread:%d total:%d" % (self.unread_messages, self.total_messages))
|
||||
agent.view().on_unread_messages(self.unread_messages, self.total_messages)
|
||||
|
||||
|
159
pwnagotchi/plugins/default/led.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from threading import Event
|
||||
import _thread
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
class Led(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin blinks the PWR led with different patterns depending on the event.'
|
||||
|
||||
def __init__(self):
|
||||
self._is_busy = False
|
||||
self._event = Event()
|
||||
self._event_name = None
|
||||
self._led_file = "/sys/class/leds/led0/brightness"
|
||||
self._delay = 200
|
||||
|
||||
# called when the plugin is loaded
|
||||
def on_loaded(self):
|
||||
self._led_file = "/sys/class/leds/led%d/brightness" % self.options['led']
|
||||
self._delay = int(self.options['delay'])
|
||||
|
||||
logging.info("[led] plugin loaded for %s" % self._led_file)
|
||||
self._on_event('loaded')
|
||||
_thread.start_new_thread(self._worker, ())
|
||||
|
||||
def _on_event(self, event):
|
||||
if not self._is_busy:
|
||||
self._event_name = event
|
||||
self._event.set()
|
||||
logging.debug("[led] event '%s' set", event)
|
||||
else:
|
||||
logging.debug("[led] skipping event '%s' because the worker is busy", event)
|
||||
|
||||
def _led(self, on):
|
||||
with open(self._led_file, 'wt') as fp:
|
||||
fp.write(str(on))
|
||||
|
||||
def _blink(self, pattern):
|
||||
logging.debug("[led] using pattern '%s' ..." % pattern)
|
||||
for c in pattern:
|
||||
if c == ' ':
|
||||
self._led(1)
|
||||
else:
|
||||
self._led(0)
|
||||
time.sleep(self._delay / 1000.0)
|
||||
# reset
|
||||
self._led(0)
|
||||
|
||||
def _worker(self):
|
||||
while True:
|
||||
self._event.wait()
|
||||
self._event.clear()
|
||||
self._is_busy = True
|
||||
|
||||
try:
|
||||
if self._event_name in self.options['patterns']:
|
||||
pattern = self.options['patterns'][self._event_name]
|
||||
self._blink(pattern)
|
||||
else:
|
||||
logging.debug("[led] no pattern defined for %s" % self._event_name)
|
||||
except Exception as e:
|
||||
logging.exception("[led] error while blinking")
|
||||
|
||||
finally:
|
||||
self._is_busy = False
|
||||
|
||||
# called when the unit is updating its software
|
||||
def on_updating(self):
|
||||
self._on_event('updating')
|
||||
|
||||
# called when there's one or more unread pwnmail messages
|
||||
def on_unread_inbox(self, num_unread):
|
||||
self._on_event('unread_inbox')
|
||||
|
||||
# called when there's internet connectivity
|
||||
def on_internet_available(self, agent):
|
||||
self._on_event('internet_available')
|
||||
|
||||
# called when everything is ready and the main loop is about to start
|
||||
def on_ready(self, agent):
|
||||
self._on_event('ready')
|
||||
|
||||
# called when the AI finished loading
|
||||
def on_ai_ready(self, agent):
|
||||
self._on_event('ai_ready')
|
||||
|
||||
# called when the AI starts training for a given number of epochs
|
||||
def on_ai_training_start(self, agent, epochs):
|
||||
self._on_event('ai_training_start')
|
||||
|
||||
# called when the AI got the best reward so far
|
||||
def on_ai_best_reward(self, agent, reward):
|
||||
self._on_event('ai_best_reward')
|
||||
|
||||
# called when the AI got the worst reward so far
|
||||
def on_ai_worst_reward(self, agent, reward):
|
||||
self._on_event('ai_worst_reward')
|
||||
|
||||
# called when the status is set to bored
|
||||
def on_bored(self, agent):
|
||||
self._on_event('bored')
|
||||
|
||||
# called when the status is set to sad
|
||||
def on_sad(self, agent):
|
||||
self._on_event('sad')
|
||||
|
||||
# called when the status is set to excited
|
||||
def on_excited(self, agent):
|
||||
self._on_event('excited')
|
||||
|
||||
# called when the status is set to lonely
|
||||
def on_lonely(self, agent):
|
||||
self._on_event('lonely')
|
||||
|
||||
# called when the agent is rebooting the board
|
||||
def on_rebooting(self, agent):
|
||||
self._on_event('rebooting')
|
||||
|
||||
# called when the agent is waiting for t seconds
|
||||
def on_wait(self, agent, t):
|
||||
self._on_event('wait')
|
||||
|
||||
# called when the agent is sleeping for t seconds
|
||||
def on_sleep(self, agent, t):
|
||||
self._on_event('sleep')
|
||||
|
||||
# called when the agent refreshed its access points list
|
||||
def on_wifi_update(self, agent, access_points):
|
||||
self._on_event('wifi_update')
|
||||
|
||||
# called when the agent is sending an association frame
|
||||
def on_association(self, agent, access_point):
|
||||
self._on_event('association')
|
||||
|
||||
# called when the agent is deauthenticating a client station from an AP
|
||||
def on_deauthentication(self, agent, access_point, client_station):
|
||||
self._on_event('deauthentication')
|
||||
|
||||
# called when a new handshake is captured, access_point and client_station are json objects
|
||||
# if the agent could match the BSSIDs to the current list, otherwise they are just the strings of the BSSIDs
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
self._on_event('handshake')
|
||||
|
||||
# called when an epoch is over (where an epoch is a single loop of the main algorithm)
|
||||
def on_epoch(self, agent, epoch, epoch_data):
|
||||
self._on_event('epoch')
|
||||
|
||||
# called when a new peer is detected
|
||||
def on_peer_detected(self, agent, peer):
|
||||
self._on_event('peer_detected')
|
||||
|
||||
# called when a known peer is lost
|
||||
def on_peer_lost(self, agent, peer):
|
||||
self._on_event('peer_lost')
|
@@ -44,24 +44,52 @@ class MemTemp(plugins.Plugin):
|
||||
if ui.is_waveshare_v2():
|
||||
h_pos = (180, 80)
|
||||
v_pos = (180, 61)
|
||||
elif ui.is_waveshare_v1():
|
||||
h_pos = (170, 80)
|
||||
v_pos = (170, 61)
|
||||
elif ui.is_waveshare144lcd():
|
||||
h_pos = (53, 77)
|
||||
v_pos = (78, 67)
|
||||
elif ui.is_inky():
|
||||
h_pos = (140, 68)
|
||||
v_pos = (165, 54)
|
||||
elif ui.is_waveshare27inch():
|
||||
h_pos = (192, 138)
|
||||
v_pos = (216, 122)
|
||||
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_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element('memtemp')
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if self.options['orientation'] == "horizontal":
|
||||
ui.set('memtemp',
|
||||
" mem cpu temp\n %s%% %s%% %sc" % (self.mem_usage(), self.cpu_load(), pwnagotchi.temperature()))
|
||||
if self.options['scale'] == "fahrenheit":
|
||||
temp = (pwnagotchi.temperature() * 9 / 5) + 32
|
||||
symbol = "f"
|
||||
elif self.options['scale'] == "kelvin":
|
||||
temp = pwnagotchi.temperature() + 273.15
|
||||
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(), pwnagotchi.temperature()))
|
||||
" 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))
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import requests
|
||||
import time
|
||||
import pwnagotchi.plugins as plugins
|
||||
@@ -11,7 +12,7 @@ MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
||||
|
||||
class NetPos(plugins.Plugin):
|
||||
__author__ = 'zenzen san'
|
||||
__version__ = '2.0.1'
|
||||
__version__ = '2.0.2'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = """Saves a json file with the access points with more signal
|
||||
whenever a handshake is captured.
|
||||
@@ -22,6 +23,7 @@ class NetPos(plugins.Plugin):
|
||||
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
|
||||
self.skip = list()
|
||||
self.ready = False
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
||||
@@ -45,60 +47,64 @@ class NetPos(plugins.Plugin):
|
||||
saved_file.write(x + "\n")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
with self.lock:
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
|
||||
all_files = os.listdir(handshake_dir)
|
||||
all_np_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.net-pos.json')]
|
||||
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
|
||||
all_files = os.listdir(handshake_dir)
|
||||
all_np_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.net-pos.json')]
|
||||
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
|
||||
|
||||
if new_np_files:
|
||||
logging.info("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
|
||||
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
|
||||
display.update(force=True)
|
||||
for idx, np_file in enumerate(new_np_files):
|
||||
if new_np_files:
|
||||
logging.debug("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
|
||||
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
|
||||
display.update(force=True)
|
||||
for idx, np_file in enumerate(new_np_files):
|
||||
|
||||
geo_file = np_file.replace('.net-pos.json', '.geo.json')
|
||||
if os.path.exists(geo_file):
|
||||
# got already the position
|
||||
reported.append(np_file)
|
||||
self.report.update(data={'reported': reported})
|
||||
continue
|
||||
|
||||
try:
|
||||
geo_data = self._get_geo_data(np_file) # returns json obj
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
logging.error("NET-POS: %s - RequestException: %s", np_file, req_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
except json.JSONDecodeError as js_e:
|
||||
logging.error("NET-POS: %s - JSONDecodeError: %s, removing it...", np_file, js_e)
|
||||
os.remove(np_file)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
|
||||
with open(geo_file, 'w+t') as sf:
|
||||
json.dump(geo_data, sf)
|
||||
|
||||
geo_file = np_file.replace('.net-pos.json', '.geo.json')
|
||||
if os.path.exists(geo_file):
|
||||
# got already the position
|
||||
reported.append(np_file)
|
||||
self.report.update(data={'reported': reported})
|
||||
continue
|
||||
|
||||
try:
|
||||
geo_data = self._get_geo_data(np_file) # returns json obj
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
logging.error("NET-POS: %s", req_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
except json.JSONDecodeError as js_e:
|
||||
logging.error("NET-POS: %s", js_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
|
||||
with open(geo_file, 'w+t') as sf:
|
||||
json.dump(geo_data, sf)
|
||||
|
||||
reported.append(np_file)
|
||||
self.report.update(data={'reported': reported})
|
||||
|
||||
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
|
||||
display.update(force=True)
|
||||
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
|
||||
display.update(force=True)
|
||||
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
netpos = self._get_netpos(agent)
|
||||
if not netpos['wifiAccessPoints']:
|
||||
return
|
||||
|
||||
netpos["ts"] = int("%.0f" % time.time())
|
||||
netpos_filename = filename.replace('.pcap', '.net-pos.json')
|
||||
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
|
||||
logging.debug("NET-POS: Saving net-location to %s", netpos_filename)
|
||||
|
||||
try:
|
||||
with open(netpos_filename, 'w+t') as net_pos_file:
|
||||
@@ -106,6 +112,7 @@ class NetPos(plugins.Plugin):
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
|
||||
|
||||
def _get_netpos(self, agent):
|
||||
aps = agent.get_access_points()
|
||||
netpos = dict()
|
||||
|
@@ -2,20 +2,27 @@ import os
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import pwnagotchi.plugins as plugins
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
|
||||
class OnlineHashCrack(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.0'
|
||||
__version__ = '2.0.1'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
try:
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
except JSONDecodeError as json_err:
|
||||
os.remove('/root/.ohc_uploads')
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
@@ -68,38 +75,40 @@ class OnlineHashCrack(plugins.Plugin):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
if self.ready:
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
with self.lock:
|
||||
if self.ready:
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
|
||||
# pull out whitelisted APs
|
||||
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
|
||||
# pull out whitelisted APs
|
||||
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
|
||||
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status',
|
||||
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_ohc(handshake)
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info(f"OHC: Successfully uploaded {handshake}")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", os_e)
|
||||
continue
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status',
|
||||
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_ohc(handshake)
|
||||
if handshake not in reported:
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info(f"OHC: Successfully uploaded {handshake}")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", os_e)
|
||||
continue
|
||||
|
@@ -3,8 +3,8 @@ 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:
|
||||
https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
|
||||
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
|
||||
GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
|
||||
'''
|
||||
|
||||
|
||||
@@ -22,10 +22,12 @@ 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')
|
||||
gps_filename = filename.replace('.pcap', '.paw-gps.json')
|
||||
|
||||
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
|
||||
with open(gps_filename, 'w+t') as f:
|
||||
|
@@ -1,51 +0,0 @@
|
||||
import logging
|
||||
import subprocess
|
||||
import string
|
||||
import re
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
'''
|
||||
Aircrack-ng needed, to install:
|
||||
> apt-get install aircrack-ng
|
||||
Upload wordlist files in .txt format to folder in config file (Default: /opt/wordlists/)
|
||||
Cracked handshakes stored in handshake folder as [essid].pcap.cracked
|
||||
'''
|
||||
|
||||
|
||||
class QuickDic(plugins.Plugin):
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Run a quick dictionary scan against captured handshakes'
|
||||
|
||||
def __init__(self):
|
||||
self.text_to_set = ""
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("Quick dictionary check plugin loaded")
|
||||
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
display = agent.view()
|
||||
result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "1 handshake" | awk \'{print $2}\''),
|
||||
shell=True, stdout=subprocess.PIPE)
|
||||
result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace})
|
||||
if not result:
|
||||
logging.info("[quickdic] No handshake")
|
||||
else:
|
||||
logging.info("[quickdic] Handshake confirmed")
|
||||
result2 = subprocess.run(('aircrack-ng -w `echo ' + self.options[
|
||||
'wordlist_folder'] + '*.txt | sed \'s/\ /,/g\'` -l ' + filename + '.cracked -q -b ' + result + ' ' + filename + ' | grep KEY'),
|
||||
shell=True, stdout=subprocess.PIPE)
|
||||
result2 = result2.stdout.decode('utf-8').strip()
|
||||
logging.info("[quickdic] " + result2)
|
||||
if result2 != "KEY NOT FOUND":
|
||||
key = re.search('\[(.*)\]', result2)
|
||||
pwd = str(key.group(1))
|
||||
self.text_to_set = "Cracked password: " + pwd
|
||||
display.update(force=True)
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if self.text_to_set:
|
||||
ui.set('face', "(·ω·)")
|
||||
ui.set('status', self.text_to_set)
|
||||
self.text_to_set = ""
|
@@ -1,23 +0,0 @@
|
||||
import logging
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
class ScreenRefresh(plugins.Plugin):
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Refresh he e-ink display after X amount of updates'
|
||||
|
||||
def __init__(self):
|
||||
self.update_count = 0;
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("Screen refresh plugin loaded")
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
self.update_count += 1
|
||||
if self.update_count == self.options['refresh_interval']:
|
||||
ui.init_display()
|
||||
ui.set('status', "Screen cleaned")
|
||||
logging.info("Screen refreshing")
|
||||
self.update_count = 0
|
195
pwnagotchi/plugins/default/session-stats.py
Normal file
@@ -0,0 +1,195 @@
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from pwnagotchi import plugins
|
||||
from flask import render_template_string
|
||||
from flask import jsonify
|
||||
|
||||
TEMPLATE = """
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "plugins" %}
|
||||
{% block title %}
|
||||
Session stats
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/css/jquery.jqplot.min.css"/>
|
||||
<link rel="stylesheet" href="/css/jquery.jqplot.css"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="/js/jquery.jqplot.min.js"></script>
|
||||
<script type="text/javascript" src="/js/jquery.jqplot.js"></script>
|
||||
<script type="text/javascript" src="/js/plugins/jqplot.mobile.js"></script>
|
||||
<script type="text/javascript" src="/js/plugins/jqplot.json2.js"></script>
|
||||
<script type="text/javascript" src="/js/plugins/jqplot.dateAxisRenderer.js"></script>
|
||||
<script type="text/javascript" src="/js/plugins/jqplot.highlighter.js"></script>
|
||||
<script type="text/javascript" src="/js/plugins/jqplot.cursor.js"></script>
|
||||
<script type="text/javascript" src="/js/plugins/jqplot.enhancedLegendRenderer.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
$(document).ready(function(){
|
||||
var ajaxDataRenderer = function(url, plot, options) {
|
||||
var ret = null;
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: url,
|
||||
dataType:"json",
|
||||
success: function(data) {
|
||||
ret = data;
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
function loadData(url, elm, title) {
|
||||
var data = ajaxDataRenderer(url);
|
||||
var plot_os = $.jqplot(elm, data.values,{
|
||||
title: title,
|
||||
stackSeries: true,
|
||||
seriesDefaults: {
|
||||
showMarker: false,
|
||||
fill: true,
|
||||
fillAndStroke: true
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
renderer: $.jqplot.EnhancedLegendRenderer,
|
||||
placement: 'outsideGrid',
|
||||
labels: data.labels,
|
||||
location: 's',
|
||||
rendererOptions: {
|
||||
numberRows: '2',
|
||||
},
|
||||
rowSpacing: '0px'
|
||||
},
|
||||
axes:{
|
||||
xaxis:{
|
||||
renderer:$.jqplot.DateAxisRenderer,
|
||||
tickOptions:{formatString:'%H:%M:%S'}
|
||||
},
|
||||
yaxis:{
|
||||
min: 0,
|
||||
tickOptions:{formatString:'%.2f'}
|
||||
}
|
||||
},
|
||||
highlighter: {
|
||||
show: true,
|
||||
sizeAdjust: 7.5
|
||||
},
|
||||
cursor:{
|
||||
show: true,
|
||||
tooltipLocation:'sw'
|
||||
}
|
||||
}).replot({
|
||||
axes:{
|
||||
xaxis:{
|
||||
renderer:$.jqplot.DateAxisRenderer,
|
||||
tickOptions:{formatString:'%H:%M:%S'}
|
||||
},
|
||||
yaxis:{
|
||||
min: 0,
|
||||
tickOptions:{formatString:'%.2f'}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
loadData('/plugins/session-stats/os', 'chart_os', 'OS')
|
||||
loadData('/plugins/session-stats/temp', 'chart_temp', 'Temp')
|
||||
loadData('/plugins/session-stats/nums', 'chart_nums', 'Wifi')
|
||||
loadData('/plugins/session-stats/duration', 'chart_duration', 'Sleeping')
|
||||
loadData('/plugins/session-stats/epoch', 'chart_epoch', 'Epochs')
|
||||
}
|
||||
|
||||
loadAll();
|
||||
setInterval(loadAll, 60000);
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="chart_os" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_temp" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_nums" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_duration" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_epoch" style="height:400px;width:100%; "></div>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
class SessionStats(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '0.1.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin displays stats of the current session.'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.lock = threading.Lock()
|
||||
self.options = dict()
|
||||
self.stats = dict()
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
logging.info("Session-stats plugin loaded.")
|
||||
self.ready = True
|
||||
|
||||
def on_unloaded(self, ui):
|
||||
pass
|
||||
|
||||
def on_epoch(self, agent, epoch, epoch_data):
|
||||
"""
|
||||
Save the epoch_data to self.stats
|
||||
"""
|
||||
with self.lock:
|
||||
self.stats[datetime.now().strftime("%H:%M:%S")] = epoch_data
|
||||
|
||||
@staticmethod
|
||||
def extract_key_values(data, subkeys):
|
||||
result = dict()
|
||||
result['values'] = list()
|
||||
result['labels'] = subkeys
|
||||
for plot_key in subkeys:
|
||||
v = [ [ts,d[plot_key]] for ts, d in data.items()]
|
||||
result['values'].append(v)
|
||||
return result
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
if not path or path == "/":
|
||||
return render_template_string(TEMPLATE)
|
||||
|
||||
if path == "os":
|
||||
extract_keys = ['cpu_load','mem_usage',]
|
||||
elif path == "temp":
|
||||
extract_keys = ['temperature']
|
||||
elif path == "nums":
|
||||
extract_keys = [
|
||||
'missed_interactions',
|
||||
'num_hops',
|
||||
'num_peers',
|
||||
'tot_bond',
|
||||
'avg_bond',
|
||||
'num_deauths',
|
||||
'num_associations',
|
||||
'num_handshakes',
|
||||
]
|
||||
elif path == "duration":
|
||||
extract_keys = [
|
||||
'duration_secs',
|
||||
'slept_for_secs',
|
||||
]
|
||||
elif path == "epoch":
|
||||
extract_keys = [
|
||||
'blind_for_epochs',
|
||||
'inactive_for_epochs',
|
||||
'active_for_epochs',
|
||||
]
|
||||
|
||||
|
||||
|
||||
with self.lock:
|
||||
return jsonify(SessionStats.extract_key_values(self.stats, extract_keys))
|
@@ -1,49 +0,0 @@
|
||||
import logging
|
||||
from pwnagotchi.voice import Voice
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
class Twitter(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin creates tweets about the recent activity of pwnagotchi'
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("twitter plugin loaded.")
|
||||
|
||||
# called in manual mode when there's internet connectivity
|
||||
def on_internet_available(self, agent):
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
last_session = agent.last_session
|
||||
|
||||
if last_session.is_new() and last_session.handshakes > 0:
|
||||
try:
|
||||
import tweepy
|
||||
except ImportError:
|
||||
logging.error("Couldn't import tweepy")
|
||||
return
|
||||
|
||||
logging.info("detected a new session and internet connectivity!")
|
||||
|
||||
picture = '/root/pwnagotchi.png'
|
||||
|
||||
display.on_manual_mode(last_session)
|
||||
display.update(force=True)
|
||||
display.image().save(picture, 'png')
|
||||
display.set('status', 'Tweeting...')
|
||||
display.update(force=True)
|
||||
|
||||
try:
|
||||
auth = tweepy.OAuthHandler(self.options['consumer_key'], self.options['consumer_secret'])
|
||||
auth.set_access_token(self.options['access_token_key'], self.options['access_token_secret'])
|
||||
api = tweepy.API(auth)
|
||||
|
||||
tweet = Voice(lang=config['main']['lang']).on_last_session_tweet(last_session)
|
||||
api.update_with_media(filename=picture, status=tweet)
|
||||
last_session.save_session_id()
|
||||
|
||||
logging.info("tweeted: %s" % tweet)
|
||||
except Exception as e:
|
||||
logging.exception("error while tweeting")
|
@@ -55,8 +55,12 @@ 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_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element('ups')
|
||||
|
||||
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())
|
||||
|
512
pwnagotchi/plugins/default/webcfg.py
Normal file
@@ -0,0 +1,512 @@
|
||||
import logging
|
||||
import json
|
||||
import yaml
|
||||
import _thread
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi import restart
|
||||
from flask import abort
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
INDEX = """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=0" />
|
||||
<title>
|
||||
webcfg
|
||||
</title>
|
||||
<style>
|
||||
#divTop {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#searchText {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.remove {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: 2px solid #f44336;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
margin: 4px 2px;
|
||||
-webkit-transition-duration: 0.4s; /* Safari */
|
||||
transition-duration: 0.4s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#btnSave {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
background-color: #0061b0;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#divTop {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
#divTop > * {
|
||||
display: table-cell;
|
||||
}
|
||||
#divTop > span {
|
||||
width: 1%;
|
||||
}
|
||||
#divTop > input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width:700px) {
|
||||
table, tr, td {
|
||||
padding:0;
|
||||
border:1px solid black;
|
||||
}
|
||||
|
||||
table {
|
||||
border:none;
|
||||
}
|
||||
|
||||
tr:first-child, thead, th {
|
||||
display:none;
|
||||
border:none;
|
||||
}
|
||||
|
||||
tr {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
td {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding:1em;
|
||||
}
|
||||
|
||||
td::before {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
background: #eee;
|
||||
border-right:2px solid black;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
|
||||
.del_btn_wrapper {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
background: #eee;
|
||||
border-right:2px solid black;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="divTop">
|
||||
<input type="text" id="searchText" onkeyup="filterTable()" placeholder="Search for options ..." title="Type an option name">
|
||||
<span><select id="selAddType"><option value="text">Text</option><option value="number">Number</option></select></span>
|
||||
<span><button id="btnAdd" type="button" onclick="addOption()">+</button></span>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
<button id="btnSave" type="button" onclick="saveConfig()">Save</button>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
function addOption() {
|
||||
var input, table, tr, td, divDelBtn, btnDel, selType, selTypeVal;
|
||||
input = document.getElementById("searchText");
|
||||
inputVal = input.value;
|
||||
selType = document.getElementById("selAddType");
|
||||
selTypeVal = selType.options[selType.selectedIndex].value;
|
||||
table = document.getElementById("tableOptions");
|
||||
if (table) {
|
||||
tr = table.insertRow();
|
||||
// del button
|
||||
divDelBtn = document.createElement("div");
|
||||
divDelBtn.className = "del_btn_wrapper";
|
||||
td = document.createElement("td");
|
||||
td.setAttribute("data-label", "");
|
||||
btnDel = document.createElement("Button");
|
||||
btnDel.innerHTML = "X";
|
||||
btnDel.onclick = function(){ delRow(this);};
|
||||
btnDel.className = "remove";
|
||||
divDelBtn.appendChild(btnDel);
|
||||
td.appendChild(divDelBtn);
|
||||
tr.appendChild(td);
|
||||
// option
|
||||
td = document.createElement("td");
|
||||
td.setAttribute("data-label", "Option");
|
||||
td.innerHTML = inputVal;
|
||||
tr.appendChild(td);
|
||||
// value
|
||||
td = document.createElement("td");
|
||||
td.setAttribute("data-label", "Value");
|
||||
input = document.createElement("input");
|
||||
input.type = selTypeVal;
|
||||
input.value = "";
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(){
|
||||
// get table
|
||||
var table = document.getElementById("tableOptions");
|
||||
if (table) {
|
||||
var json = tableToJson(table);
|
||||
sendJSON("webcfg/save-config", json, function(response) {
|
||||
if (response) {
|
||||
if (response.status == "200") {
|
||||
alert("Config got updated");
|
||||
} else {
|
||||
alert("Error while updating the config (err-code: " + response.status + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function filterTable(){
|
||||
var input, filter, table, tr, td, i, txtValue;
|
||||
input = document.getElementById("searchText");
|
||||
filter = input.value.toUpperCase();
|
||||
table = document.getElementById("tableOptions");
|
||||
if (table) {
|
||||
tr = table.getElementsByTagName("tr");
|
||||
|
||||
for (i = 0; i < tr.length; i++) {
|
||||
td = tr[i].getElementsByTagName("td")[1];
|
||||
if (td) {
|
||||
txtValue = td.textContent || td.innerText;
|
||||
if (txtValue.toUpperCase().indexOf(filter) > -1) {
|
||||
tr[i].style.display = "";
|
||||
}else{
|
||||
tr[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendJSON(url, data, callback) {
|
||||
var xobj = new XMLHttpRequest();
|
||||
var csrf = "{{ csrf_token() }}";
|
||||
xobj.open('POST', url);
|
||||
xobj.setRequestHeader("Content-Type", "application/json");
|
||||
xobj.setRequestHeader('x-csrf-token', csrf);
|
||||
xobj.onreadystatechange = function () {
|
||||
if (xobj.readyState == 4) {
|
||||
callback(xobj);
|
||||
}
|
||||
};
|
||||
xobj.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
function loadJSON(url, callback) {
|
||||
var xobj = new XMLHttpRequest();
|
||||
xobj.overrideMimeType("application/json");
|
||||
xobj.open('GET', url, true);
|
||||
xobj.onreadystatechange = function () {
|
||||
if (xobj.readyState == 4 && xobj.status == "200") {
|
||||
callback(JSON.parse(xobj.responseText));
|
||||
}
|
||||
};
|
||||
xobj.send(null);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects
|
||||
function unFlattenJson(data) {
|
||||
"use strict";
|
||||
if (Object(data) !== data || Array.isArray(data))
|
||||
return data;
|
||||
var result = {}, cur, prop, idx, last, temp, inarray;
|
||||
for(var p in data) {
|
||||
cur = result, prop = "", last = 0, inarray = false;
|
||||
do {
|
||||
idx = p.indexOf(".", last);
|
||||
temp = p.substring(last, idx !== -1 ? idx : undefined);
|
||||
inarray = temp.startsWith('#') && !isNaN(parseInt(temp.substring(1)))
|
||||
cur = cur[prop] || (cur[prop] = (inarray ? [] : {}));
|
||||
if (inarray){
|
||||
prop = temp.substring(1);
|
||||
}else{
|
||||
prop = temp;
|
||||
}
|
||||
last = idx + 1;
|
||||
} while(idx >= 0);
|
||||
cur[prop] = data[p];
|
||||
}
|
||||
return result[""];
|
||||
}
|
||||
|
||||
function flattenJson(data) {
|
||||
var result = {};
|
||||
function recurse (cur, prop) {
|
||||
if (Object(cur) !== cur) {
|
||||
result[prop] = cur;
|
||||
} else if (Array.isArray(cur)) {
|
||||
for(var i=0, l=cur.length; i<l; i++)
|
||||
recurse(cur[i], prop ? prop+".#"+i : ""+i);
|
||||
if (l == 0)
|
||||
result[prop] = [];
|
||||
} else {
|
||||
var isEmpty = true;
|
||||
for (var p in cur) {
|
||||
isEmpty = false;
|
||||
recurse(cur[p], prop ? prop+"."+p : p);
|
||||
}
|
||||
if (isEmpty)
|
||||
result[prop] = {};
|
||||
}
|
||||
}
|
||||
recurse(data, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
function delRow(btn) {
|
||||
var tr = btn.parentNode.parentNode.parentNode;
|
||||
tr.parentNode.removeChild(tr);
|
||||
}
|
||||
|
||||
function jsonToTable(json) {
|
||||
var table = document.createElement("table");
|
||||
table.id = "tableOptions";
|
||||
|
||||
// create header
|
||||
var tr = table.insertRow();
|
||||
var thDel = document.createElement("th");
|
||||
thDel.innerHTML = "";
|
||||
var thOpt = document.createElement("th");
|
||||
thOpt.innerHTML = "Option";
|
||||
var thVal = document.createElement("th");
|
||||
thVal.innerHTML = "Value";
|
||||
tr.appendChild(thDel);
|
||||
tr.appendChild(thOpt);
|
||||
tr.appendChild(thVal);
|
||||
|
||||
var td, divDelBtn, btnDel;
|
||||
// iterate over keys
|
||||
Object.keys(json).forEach(function(key) {
|
||||
tr = table.insertRow();
|
||||
// del button
|
||||
divDelBtn = document.createElement("div");
|
||||
divDelBtn.className = "del_btn_wrapper";
|
||||
td = document.createElement("td");
|
||||
td.setAttribute("data-label", "");
|
||||
btnDel = document.createElement("Button");
|
||||
btnDel.innerHTML = "X";
|
||||
btnDel.onclick = function(){ delRow(this);};
|
||||
btnDel.className = "remove";
|
||||
divDelBtn.appendChild(btnDel);
|
||||
td.appendChild(divDelBtn);
|
||||
tr.appendChild(td);
|
||||
// option
|
||||
td = document.createElement("td");
|
||||
td.setAttribute("data-label", "Option");
|
||||
td.innerHTML = key;
|
||||
tr.appendChild(td);
|
||||
// value
|
||||
td = document.createElement("td");
|
||||
td.setAttribute("data-label", "Value");
|
||||
if(typeof(json[key])==='boolean'){
|
||||
input = document.createElement("select");
|
||||
input.setAttribute("id", "boolSelect");
|
||||
tvalue = document.createElement("option");
|
||||
tvalue.setAttribute("value", "true");
|
||||
ttext = document.createTextNode("True")
|
||||
tvalue.appendChild(ttext);
|
||||
fvalue = document.createElement("option");
|
||||
fvalue.setAttribute("value", "false");
|
||||
ftext = document.createTextNode("False");
|
||||
fvalue.appendChild(ftext);
|
||||
input.appendChild(tvalue);
|
||||
input.appendChild(fvalue);
|
||||
input.value = json[key];
|
||||
document.body.appendChild(input);
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
} else {
|
||||
input = document.createElement("input");
|
||||
if(Array.isArray(json[key])) {
|
||||
input.type = 'text';
|
||||
input.value = '[]';
|
||||
}else{
|
||||
input.type = typeof(json[key]);
|
||||
input.value = json[key];
|
||||
}
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
}
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
function tableToJson(table) {
|
||||
var rows = table.getElementsByTagName("tr");
|
||||
var i, td, key, value;
|
||||
var json = {};
|
||||
|
||||
for (i = 0; i < rows.length; i++) {
|
||||
td = rows[i].getElementsByTagName("td");
|
||||
if (td.length == 3) {
|
||||
// td[0] = del button
|
||||
key = td[1].textContent || td[1].innerText;
|
||||
var input = td[2].getElementsByTagName("input");
|
||||
var select = td[2].getElementsByTagName("select");
|
||||
if (input && input != undefined && input.length > 0 ) {
|
||||
if (input[0].type == "text") {
|
||||
if (input[0].value.startsWith("[") && input[0].value.endsWith("]")) {
|
||||
json[key] = JSON.parse(input[0].value);
|
||||
}else{
|
||||
json[key] = input[0].value;
|
||||
}
|
||||
}else if (input[0].type == "number") {
|
||||
json[key] = Number(input[0].value);
|
||||
}
|
||||
} else if(select && select != undefined && select.length > 0) {
|
||||
var myValue = select[0].options[select[0].selectedIndex].value;
|
||||
json[key] = myValue === 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
return unFlattenJson(json);
|
||||
}
|
||||
|
||||
loadJSON("webcfg/get-config", function(response) {
|
||||
var flat_json = flattenJson(response);
|
||||
var table = jsonToTable(flat_json);
|
||||
var divContent = document.getElementById("content");
|
||||
divContent.innerHTML = "";
|
||||
divContent.appendChild(table);
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def serializer(obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
raise TypeError
|
||||
|
||||
class WebConfig(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin allows the user to make runtime changes.'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
|
||||
def on_ready(self, agent):
|
||||
self.config = agent.config()
|
||||
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
|
||||
self.ready = True
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
self.config = agent.config()
|
||||
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
|
||||
self.ready = True
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
logging.info("webcfg: Plugin loaded.")
|
||||
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
"""
|
||||
Serves the current configuration
|
||||
"""
|
||||
if not self.ready:
|
||||
return "Plugin not ready"
|
||||
|
||||
if request.method == "GET":
|
||||
if path == "/" or not path:
|
||||
return render_template_string(INDEX)
|
||||
elif path == "get-config":
|
||||
# send configuration
|
||||
return json.dumps(self.config, default=serializer)
|
||||
else:
|
||||
abort(404)
|
||||
elif request.method == "POST":
|
||||
if path == "save-config":
|
||||
try:
|
||||
parsed_yaml = yaml.safe_load(str(request.get_json()))
|
||||
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
|
||||
yaml.safe_dump(parsed_yaml, config_file, encoding='utf-8',
|
||||
allow_unicode=True, default_flow_style=False)
|
||||
|
||||
_thread.start_new_thread(restart, (self.mode,))
|
||||
return "success"
|
||||
except yaml.YAMLError as yaml_ex:
|
||||
return "config error"
|
||||
abort(404)
|
274
pwnagotchi/plugins/default/webgpsmap.html
Normal file
@@ -0,0 +1,274 @@
|
||||
<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 { 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: 2vw;color:#a0a0a0;}
|
||||
#filterbox { position: fixed;top:0px;left:0px;z-index:999999;margin-left:55px;width:100%;height:20px;border-bottom:2px solid #303030;display: grid;grid-template-columns: 1fr 0.1fr;grid-template-rows: 1fr;grid-template-areas: ". .";}
|
||||
#search { grid-area: 1 / 1 / 2 / 2;height:30px;padding:3px;background-color:#000;color:#e0e0e0;border:none;}
|
||||
#matchcount { grid-area: 1 / 2 / 2 / 3;height:30px;margin-right:55px;padding-right:5px;background-color:#000;color:#a0a0a0;font-weight:bold;}
|
||||
#mapdiv { width:100%; height: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mapdiv"></div>
|
||||
<div id="filterbox">
|
||||
<input type="text" id="search" placeholder="filter: #cracked #notcracked AA:BB:CC aabbcc AndroidAP ..."/>
|
||||
<div id="matchcount">0 APs</div>
|
||||
</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');
|
||||
|
||||
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 positionsLoaded = false;
|
||||
var positions = [];
|
||||
var accuracys = [];
|
||||
var markers = [];
|
||||
var marker_pos = [];
|
||||
var markerClusters = L.markerClusterGroup();
|
||||
|
||||
function drawPositions() {
|
||||
count = 0;
|
||||
//mymap.removeLayer(markerClusters);
|
||||
mymap.eachLayer(function (layer) {
|
||||
mymap.removeLayer(layer);
|
||||
});
|
||||
Esri_WorldImagery.addTo(mymap);
|
||||
CartoDB_DarkMatter.addTo(mymap);
|
||||
markerClusters = L.markerClusterGroup();
|
||||
accuracys = [];
|
||||
markers = [];
|
||||
marker_pos = [];
|
||||
filterText = document.getElementById("search").value;
|
||||
//console.log(filterText);
|
||||
Object.keys(positions).forEach(function(key) {
|
||||
if(positions[key].lng){
|
||||
filterPattern =
|
||||
positions[key].ssid + ' ' +
|
||||
formatMacAddress(positions[key].mac) + ' ' +
|
||||
positions[key].mac
|
||||
;
|
||||
if (positions[key].pass) {
|
||||
filterPattern += positions[key].pass + ' #cracked';
|
||||
} else {
|
||||
filterPattern += ' #notcracked';
|
||||
}
|
||||
filterPattern = filterPattern.toLowerCase();
|
||||
//console.log(filterPattern);
|
||||
var matched = true;
|
||||
if (filterText) {
|
||||
filterText.split(" ").forEach(function (item) {
|
||||
if (!filterPattern.includes(item.toLowerCase())) {
|
||||
matched = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (matched) {
|
||||
count++;
|
||||
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)
|
||||
);
|
||||
}
|
||||
passInfo = '';
|
||||
if (positions[key].pass) {
|
||||
passInfo = '<br/><b>Pass:</b> '+escapeHtml(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);
|
||||
}
|
||||
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 );
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById("matchcount").innerHTML = count + " APs";
|
||||
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 :(";
|
||||
}
|
||||
}
|
||||
|
||||
// draw map on Enter in FilterInputField
|
||||
const node = document.getElementById("search").addEventListener("keyup", function(event) {
|
||||
if (event.key === "Enter") {
|
||||
if (positionsLoaded) {
|
||||
drawPositions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// load positions
|
||||
loadJSON("/plugins/webgpsmap/all", function(response) {
|
||||
positions = JSON.parse(response);
|
||||
positionsLoaded = true;
|
||||
drawPositions();
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
408
pwnagotchi/plugins/default/webgpsmap.py
Normal file
@@ -0,0 +1,408 @@
|
||||
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+test 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.3.0'
|
||||
__name__ = 'webgpsmap'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
|
||||
|
||||
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 error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
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 error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
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 error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
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 for bad boys</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 error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
return
|
||||
|
||||
# cache 2048 items
|
||||
@lru_cache(maxsize=2048, 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(f"[webgpsmap] scanning {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(f"[webgpsmap] found: {filename_base}")
|
||||
filename_position = None
|
||||
|
||||
logging.debug("[webgpsmap] search for .gps.json")
|
||||
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))
|
||||
|
||||
logging.debug("[webgpsmap] search for .geo.json")
|
||||
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))
|
||||
|
||||
logging.debug("[webgpsmap] search for .paw-gps.json")
|
||||
check_for = os.path.basename(filename_base) + ".paw-gps.json"
|
||||
if check_for in all_files:
|
||||
filename_position = str(os.path.join(handshake_dir, check_for))
|
||||
|
||||
logging.debug(f"[webgpsmap] end search for position data files and use {filename_position}")
|
||||
|
||||
if filename_position is not None:
|
||||
all_geo_or_gps_files.append(filename_position)
|
||||
|
||||
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skipped networks? No!
|
||||
|
||||
if newest_only:
|
||||
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
|
||||
|
||||
logging.info(f"[webgpsmap] Found {len(all_geo_or_gps_files)} position-data files from {len(all_pcap_files)} handshakes. Fetching positions ...")
|
||||
|
||||
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 and not pos.type() == PositionFile.PAWGPS:
|
||||
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")
|
||||
pos_type = 'unknown'
|
||||
if pos.type() == PositionFile.GPS:
|
||||
pos_type = 'gps'
|
||||
elif pos.type() == PositionFile.GEO:
|
||||
pos_type = 'geo'
|
||||
elif pos.type() == PositionFile.PAWGPS:
|
||||
pos_type = 'paw'
|
||||
gps_data[ssid+"_"+mac] = {
|
||||
'ssid': ssid,
|
||||
'mac': mac,
|
||||
'type': pos_type,
|
||||
'lng': pos.lng(),
|
||||
'lat': pos.lat(),
|
||||
'acc': pos.accuracy(),
|
||||
'ts_first': pos.timestamp_first(),
|
||||
'ts_last': pos.timestamp_last(),
|
||||
}
|
||||
|
||||
# get ap password if exist
|
||||
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 error:
|
||||
self.SKIP += pos_file
|
||||
logging.error(f"[webgpsmap] JSONDecodeError in: {error}")
|
||||
continue
|
||||
except ValueError as error:
|
||||
self.SKIP += pos_file
|
||||
logging.error(f"[webgpsmap] ValueError: {error}")
|
||||
continue
|
||||
except OSError as error:
|
||||
self.SKIP += pos_file
|
||||
logging.error(f"[webgpsmap] OSError: {error}")
|
||||
continue
|
||||
logging.info(f"[webgpsmap] loaded {len(gps_data)} positions")
|
||||
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 error:
|
||||
logging.error(f"[webgpsmap] error loading template file {template_file} - error: {error}")
|
||||
return html_data
|
||||
|
||||
|
||||
class PositionFile:
|
||||
"""
|
||||
Wraps gps / net-pos files
|
||||
"""
|
||||
GPS = 1
|
||||
GEO = 2
|
||||
PAWGPS = 3
|
||||
|
||||
def __init__(self, path):
|
||||
self._file = path
|
||||
self._filename = os.path.basename(path)
|
||||
try:
|
||||
logging.debug(f"[webgpsmap] loading {path}")
|
||||
with open(path, 'r') as json_file:
|
||||
self._json = json.load(json_file)
|
||||
logging.debug(f"[webgpsmap] loaded {path}")
|
||||
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|paw-gps)\.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|paw-gps)\.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']
|
||||
#fix timezone "Z": "2019-11-28T04:44:46.79231Z" >> "2019-11-28T04:44:46.79231+00:00"
|
||||
if date_iso_formated.endswith("Z"):
|
||||
date_iso_formated = date_iso_formated[:-1] + "+00:00"
|
||||
# bad microseconds fix: fill/cut microseconds to 6 numbers
|
||||
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
|
||||
part2 = part2.ljust(6, '0')[:6]
|
||||
# timezone fix: 0200 >>> 02:00
|
||||
if len(part3) == 4:
|
||||
part3 = part3[1:2].rjust(2, '0') + ':' + part3[3:4].rjust(2, '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 json file
|
||||
return_ts = int("%.0f" % os.path.getmtime(self._file))
|
||||
return return_ts
|
||||
|
||||
def password(self):
|
||||
"""
|
||||
returns the password from file.pcap.cracked or None
|
||||
"""
|
||||
return_pass = None
|
||||
# 2do: make better filename split/remove extension because this one has problems with "." in path
|
||||
base_filename, ext1, ext2 = re.split('\.', self._file)
|
||||
password_file_path = base_filename + ".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 error:
|
||||
logging.error(f"[webgpsmap] OS error: {format(error)}")
|
||||
except:
|
||||
logging.error(f"[webgpsmap] 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
|
||||
if self._file.endswith('.paw-gps.json'):
|
||||
return PositionFile.PAWGPS
|
||||
return None
|
||||
|
||||
def lat(self):
|
||||
try:
|
||||
lat = None
|
||||
# try to get value from known formats
|
||||
if 'Latitude' in self._json:
|
||||
lat = self._json['Latitude']
|
||||
if 'lat' in self._json:
|
||||
lat = self._json['lat'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
|
||||
if 'location' in self._json:
|
||||
if 'lat' in self._json['location']:
|
||||
lat = self._json['location']['lat']
|
||||
# check value
|
||||
if lat is None:
|
||||
raise ValueError(f"Lat is None in {self._filename}")
|
||||
if lat == 0:
|
||||
raise ValueError(f"Lat is 0 in {self._filename}")
|
||||
return lat
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def lng(self):
|
||||
try:
|
||||
lng = None
|
||||
# try to get value from known formats
|
||||
if 'Longitude' in self._json:
|
||||
lng = self._json['Longitude']
|
||||
if 'long' in self._json:
|
||||
lng = self._json['long'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
|
||||
if 'location' in self._json:
|
||||
if 'lng' in self._json['location']:
|
||||
lng = self._json['location']['lng']
|
||||
# check value
|
||||
if lng is None:
|
||||
raise ValueError(f"Lng is None in {self._filename}")
|
||||
if lng == 0:
|
||||
raise ValueError(f"Lng is 0 in {self._filename}")
|
||||
return lng
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def accuracy(self):
|
||||
if self.type() == PositionFile.GPS:
|
||||
return 50.0 # a default
|
||||
if self.type() == PositionFile.PAWGPS:
|
||||
return 50.0 # a default
|
||||
if self.type() == PositionFile.GEO:
|
||||
try:
|
||||
return self._json['accuracy']
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
@@ -1,19 +1,27 @@
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi import plugins
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
|
||||
class WpaSec(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.1'
|
||||
__version__ = '2.1.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
self.lock = Lock()
|
||||
try:
|
||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
except JSONDecodeError as json_err:
|
||||
os.remove("/root/.wpa_sec_uploads")
|
||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
self.options = dict()
|
||||
self.skip = list()
|
||||
|
||||
@@ -35,6 +43,29 @@ class WpaSec(plugins.Plugin):
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
|
||||
|
||||
def _download_from_wpasec(self, output, timeout=30):
|
||||
"""
|
||||
Downloads the results from wpasec and safes them to output
|
||||
|
||||
Output-Format: bssid, station_mac, ssid, password
|
||||
"""
|
||||
api_url = self.options['api_url']
|
||||
if not api_url.endswith('/'):
|
||||
api_url = f"{api_url}/"
|
||||
api_url = f"{api_url}?api&dl=1"
|
||||
|
||||
cookie = {'key': self.options['api_key']}
|
||||
try:
|
||||
result = requests.get(api_url, cookies=cookie, timeout=timeout)
|
||||
with open(output, 'wb') as output_file:
|
||||
output_file.write(result.content)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
except OSError as os_e:
|
||||
raise os_e
|
||||
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
@@ -53,32 +84,48 @@ class WpaSec(plugins.Plugin):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
with self.lock:
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
|
||||
if handshake_new:
|
||||
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_wpasec(handshake)
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("WPA_SEC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("WPA_SEC: %s", os_e)
|
||||
continue
|
||||
|
||||
if 'download_results' in self.options and self.options['download_results']:
|
||||
cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
|
||||
if os.path.exists(cracked_file):
|
||||
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
|
||||
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
|
||||
return
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_wpasec(handshake)
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
|
||||
self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile'))
|
||||
logging.info("WPA_SEC: Downloaded cracked passwords.")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("WPA_SEC: %s", req_e)
|
||||
continue
|
||||
logging.debug("WPA_SEC: %s", req_e)
|
||||
except OSError as os_e:
|
||||
logging.error("WPA_SEC: %s", os_e)
|
||||
continue
|
||||
logging.debug("WPA_SEC: %s", os_e)
|
||||
|
@@ -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'
|
||||
|
||||
@@ -55,12 +52,18 @@ class Display(View):
|
||||
def is_dfrobot(self):
|
||||
return self._implementation.name == 'dfrobot'
|
||||
|
||||
def is_waveshare144lcd(self):
|
||||
return self._implementation.name == 'waveshare144lcd'
|
||||
|
||||
def is_waveshare154inch(self):
|
||||
return self._implementation.name == 'waveshare154inch'
|
||||
|
||||
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 +94,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)
|
||||
|
||||
|
@@ -7,9 +7,10 @@ from pwnagotchi.ui.hw.waveshare1 import WaveshareV1
|
||||
from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
|
||||
from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
|
||||
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
|
||||
from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd
|
||||
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
|
||||
@@ -40,8 +41,14 @@ def display_for(config):
|
||||
elif config['ui']['display']['type'] == 'waveshare29inch':
|
||||
return Waveshare29inch(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare144lcd':
|
||||
return Waveshare144lcd(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare154inch':
|
||||
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))
|
||||
|
319
pwnagotchi/ui/hw/libs/waveshare/lcdhat144/LCD_1in44.py
Normal file
@@ -0,0 +1,319 @@
|
||||
# -*- coding:UTF-8 -*-
|
||||
##
|
||||
# | file : LCD_1IN44.py
|
||||
# | version : V2.0
|
||||
# | date : 2018-07-16
|
||||
# | function : On the ST7735S chip driver and clear screen, drawing lines, drawing, writing
|
||||
# and other functions to achieve
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documnetation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import numpy as np
|
||||
from . import config
|
||||
|
||||
LCD_1IN44 = 1
|
||||
LCD_1IN8 = 0
|
||||
if LCD_1IN44 == 1:
|
||||
LCD_WIDTH = 128 #LCD width
|
||||
LCD_HEIGHT = 128 #LCD height
|
||||
LCD_X = 2
|
||||
LCD_Y = 1
|
||||
if LCD_1IN8 == 1:
|
||||
LCD_WIDTH = 160
|
||||
LCD_HEIGHT = 128
|
||||
LCD_X = 1
|
||||
LCD_Y = 2
|
||||
|
||||
LCD_X_MAXPIXEL = 132 #LCD width maximum memory
|
||||
LCD_Y_MAXPIXEL = 162 #LCD height maximum memory
|
||||
|
||||
#scanning method
|
||||
L2R_U2D = 1
|
||||
L2R_D2U = 2
|
||||
R2L_U2D = 3
|
||||
R2L_D2U = 4
|
||||
U2D_L2R = 5
|
||||
U2D_R2L = 6
|
||||
D2U_L2R = 7
|
||||
D2U_R2L = 8
|
||||
SCAN_DIR_DFT = U2D_R2L
|
||||
|
||||
|
||||
class LCD:
|
||||
def __init__(self):
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
self.width = LCD_WIDTH
|
||||
self.height = LCD_HEIGHT
|
||||
self.LCD_Scan_Dir = SCAN_DIR_DFT
|
||||
self.LCD_X_Adjust = LCD_X
|
||||
self.LCD_Y_Adjust = LCD_Y
|
||||
|
||||
""" Hardware reset """
|
||||
def LCD_Reset(self):
|
||||
GPIO.output(config.LCD_RST_PIN, GPIO.HIGH)
|
||||
config.Driver_Delay_ms(100)
|
||||
GPIO.output(config.LCD_RST_PIN, GPIO.LOW)
|
||||
config.Driver_Delay_ms(100)
|
||||
GPIO.output(config.LCD_RST_PIN, GPIO.HIGH)
|
||||
config.Driver_Delay_ms(100)
|
||||
|
||||
""" Write register address and data """
|
||||
def LCD_WriteReg(self, Reg):
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(config.LCD_DC_PIN, GPIO.OUT)
|
||||
GPIO.output(config.LCD_DC_PIN, GPIO.LOW)
|
||||
config.SPI_Write_Byte([Reg])
|
||||
|
||||
def LCD_WriteData_8bit(self, Data):
|
||||
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
|
||||
config.SPI_Write_Byte([Data])
|
||||
|
||||
def LCD_WriteData_NLen16Bit(self, Data, DataLen):
|
||||
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
|
||||
for i in range(0, DataLen):
|
||||
config.SPI_Write_Byte([Data >> 8])
|
||||
config.SPI_Write_Byte([Data & 0xff])
|
||||
|
||||
""" Common register initialization """
|
||||
def LCD_InitReg(self):
|
||||
#ST7735R Frame Rate
|
||||
self.LCD_WriteReg(0xB1)
|
||||
self.LCD_WriteData_8bit(0x01)
|
||||
self.LCD_WriteData_8bit(0x2C)
|
||||
self.LCD_WriteData_8bit(0x2D)
|
||||
|
||||
self.LCD_WriteReg(0xB2)
|
||||
self.LCD_WriteData_8bit(0x01)
|
||||
self.LCD_WriteData_8bit(0x2C)
|
||||
self.LCD_WriteData_8bit(0x2D)
|
||||
|
||||
self.LCD_WriteReg(0xB3)
|
||||
self.LCD_WriteData_8bit(0x01)
|
||||
self.LCD_WriteData_8bit(0x2C)
|
||||
self.LCD_WriteData_8bit(0x2D)
|
||||
self.LCD_WriteData_8bit(0x01)
|
||||
self.LCD_WriteData_8bit(0x2C)
|
||||
self.LCD_WriteData_8bit(0x2D)
|
||||
|
||||
#Column inversion
|
||||
self.LCD_WriteReg(0xB4)
|
||||
self.LCD_WriteData_8bit(0x07)
|
||||
|
||||
#ST7735R Power Sequence
|
||||
self.LCD_WriteReg(0xC0)
|
||||
self.LCD_WriteData_8bit(0xA2)
|
||||
self.LCD_WriteData_8bit(0x02)
|
||||
self.LCD_WriteData_8bit(0x84)
|
||||
self.LCD_WriteReg(0xC1)
|
||||
self.LCD_WriteData_8bit(0xC5)
|
||||
|
||||
self.LCD_WriteReg(0xC2)
|
||||
self.LCD_WriteData_8bit(0x0A)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
|
||||
self.LCD_WriteReg(0xC3)
|
||||
self.LCD_WriteData_8bit(0x8A)
|
||||
self.LCD_WriteData_8bit(0x2A)
|
||||
self.LCD_WriteReg(0xC4)
|
||||
self.LCD_WriteData_8bit(0x8A)
|
||||
self.LCD_WriteData_8bit(0xEE)
|
||||
|
||||
self.LCD_WriteReg(0xC5)#VCOM
|
||||
self.LCD_WriteData_8bit(0x0E)
|
||||
|
||||
#ST7735R Gamma Sequence
|
||||
self.LCD_WriteReg(0xe0)
|
||||
self.LCD_WriteData_8bit(0x0f)
|
||||
self.LCD_WriteData_8bit(0x1a)
|
||||
self.LCD_WriteData_8bit(0x0f)
|
||||
self.LCD_WriteData_8bit(0x18)
|
||||
self.LCD_WriteData_8bit(0x2f)
|
||||
self.LCD_WriteData_8bit(0x28)
|
||||
self.LCD_WriteData_8bit(0x20)
|
||||
self.LCD_WriteData_8bit(0x22)
|
||||
self.LCD_WriteData_8bit(0x1f)
|
||||
self.LCD_WriteData_8bit(0x1b)
|
||||
self.LCD_WriteData_8bit(0x23)
|
||||
self.LCD_WriteData_8bit(0x37)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
self.LCD_WriteData_8bit(0x07)
|
||||
self.LCD_WriteData_8bit(0x02)
|
||||
self.LCD_WriteData_8bit(0x10)
|
||||
|
||||
self.LCD_WriteReg(0xe1)
|
||||
self.LCD_WriteData_8bit(0x0f)
|
||||
self.LCD_WriteData_8bit(0x1b)
|
||||
self.LCD_WriteData_8bit(0x0f)
|
||||
self.LCD_WriteData_8bit(0x17)
|
||||
self.LCD_WriteData_8bit(0x33)
|
||||
self.LCD_WriteData_8bit(0x2c)
|
||||
self.LCD_WriteData_8bit(0x29)
|
||||
self.LCD_WriteData_8bit(0x2e)
|
||||
self.LCD_WriteData_8bit(0x30)
|
||||
self.LCD_WriteData_8bit(0x30)
|
||||
self.LCD_WriteData_8bit(0x39)
|
||||
self.LCD_WriteData_8bit(0x3f)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
self.LCD_WriteData_8bit(0x07)
|
||||
self.LCD_WriteData_8bit(0x03)
|
||||
self.LCD_WriteData_8bit(0x10)
|
||||
|
||||
#Enable test command
|
||||
self.LCD_WriteReg(0xF0)
|
||||
self.LCD_WriteData_8bit(0x01)
|
||||
|
||||
#Disable ram power save mode
|
||||
self.LCD_WriteReg(0xF6)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
|
||||
#65k mode
|
||||
self.LCD_WriteReg(0x3A)
|
||||
self.LCD_WriteData_8bit(0x05)
|
||||
|
||||
#********************************************************************************
|
||||
#function: Set the display scan and color transfer modes
|
||||
#parameter:
|
||||
# Scan_dir : Scan direction
|
||||
# Colorchose : RGB or GBR color format
|
||||
#********************************************************************************
|
||||
def LCD_SetGramScanWay(self, Scan_dir):
|
||||
#Get the screen scan direction
|
||||
self.LCD_Scan_Dir = Scan_dir
|
||||
|
||||
#Get GRAM and LCD width and height
|
||||
if (Scan_dir == L2R_U2D) or (Scan_dir == L2R_D2U) or (Scan_dir == R2L_U2D) or (Scan_dir == R2L_D2U) :
|
||||
self.width = LCD_HEIGHT
|
||||
self.height = LCD_WIDTH
|
||||
if Scan_dir == L2R_U2D:
|
||||
MemoryAccessReg_Data = 0X00 | 0x00
|
||||
elif Scan_dir == L2R_D2U:
|
||||
MemoryAccessReg_Data = 0X00 | 0x80
|
||||
elif Scan_dir == R2L_U2D:
|
||||
MemoryAccessReg_Data = 0x40 | 0x00
|
||||
else: #R2L_D2U:
|
||||
MemoryAccessReg_Data = 0x40 | 0x80
|
||||
else:
|
||||
self.width = LCD_WIDTH
|
||||
self.height = LCD_HEIGHT
|
||||
if Scan_dir == U2D_L2R:
|
||||
MemoryAccessReg_Data = 0X00 | 0x00 | 0x20
|
||||
elif Scan_dir == U2D_R2L:
|
||||
MemoryAccessReg_Data = 0X00 | 0x40 | 0x20
|
||||
elif Scan_dir == D2U_L2R:
|
||||
MemoryAccessReg_Data = 0x80 | 0x00 | 0x20
|
||||
else: #R2L_D2U
|
||||
MemoryAccessReg_Data = 0x40 | 0x80 | 0x20
|
||||
|
||||
#please set (MemoryAccessReg_Data & 0x10) != 1
|
||||
if (MemoryAccessReg_Data & 0x10) != 1:
|
||||
self.LCD_X_Adjust = LCD_Y
|
||||
self.LCD_Y_Adjust = LCD_X
|
||||
else:
|
||||
self.LCD_X_Adjust = LCD_X
|
||||
self.LCD_Y_Adjust = LCD_Y
|
||||
|
||||
# Set the read / write scan direction of the frame memory
|
||||
self.LCD_WriteReg(0x36) #MX, MY, RGB mode
|
||||
if LCD_1IN44 == 1:
|
||||
self.LCD_WriteData_8bit( MemoryAccessReg_Data | 0x08) #0x08 set RGB
|
||||
else:
|
||||
self.LCD_WriteData_8bit( MemoryAccessReg_Data & 0xf7) #RGB color filter panel
|
||||
|
||||
#/********************************************************************************
|
||||
#function:
|
||||
# initialization
|
||||
#********************************************************************************/
|
||||
def LCD_Init(self, Lcd_ScanDir):
|
||||
if (config.GPIO_Init() != 0):
|
||||
return -1
|
||||
|
||||
#Turn on the backlight
|
||||
#GPIO.setup(config.LCD_BL_PIN, GPIO.OUT)
|
||||
GPIO.output(config.LCD_BL_PIN,GPIO.HIGH)
|
||||
|
||||
#Hardware reset
|
||||
self.LCD_Reset()
|
||||
|
||||
#Set the initialization register
|
||||
self.LCD_InitReg()
|
||||
|
||||
#Set the display scan and color transfer modes
|
||||
self.LCD_SetGramScanWay(Lcd_ScanDir)
|
||||
config.Driver_Delay_ms(200)
|
||||
|
||||
#sleep out
|
||||
self.LCD_WriteReg(0x11)
|
||||
config.Driver_Delay_ms(120)
|
||||
|
||||
#Turn on the LCD display
|
||||
self.LCD_WriteReg(0x29)
|
||||
|
||||
#/********************************************************************************
|
||||
#function: Sets the start position and size of the display area
|
||||
#parameter:
|
||||
# Xstart : X direction Start coordinates
|
||||
# Ystart : Y direction Start coordinates
|
||||
# Xend : X direction end coordinates
|
||||
# Yend : Y direction end coordinates
|
||||
#********************************************************************************/
|
||||
def LCD_SetWindows(self, Xstart, Ystart, Xend, Yend):
|
||||
#set the X coordinates
|
||||
self.LCD_WriteReg(0x2A)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
self.LCD_WriteData_8bit((Xstart & 0xff) + self.LCD_X_Adjust)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
self.LCD_WriteData_8bit(((Xend - 1) & 0xff) + self.LCD_X_Adjust)
|
||||
|
||||
#set the Y coordinates
|
||||
self.LCD_WriteReg (0x2B)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
self.LCD_WriteData_8bit((Ystart & 0xff) + self.LCD_Y_Adjust)
|
||||
self.LCD_WriteData_8bit(0x00)
|
||||
self.LCD_WriteData_8bit(((Yend - 1) & 0xff )+ self.LCD_Y_Adjust)
|
||||
|
||||
self.LCD_WriteReg(0x2C)
|
||||
|
||||
def LCD_Clear(self):
|
||||
#hello
|
||||
_buffer = [0xff]*(self.width * self.height * 2)
|
||||
self.LCD_SetWindows(0, 0, self.width, self.height)
|
||||
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
|
||||
for i in range(0,len(_buffer),4096):
|
||||
config.SPI_Write_Byte(_buffer[i:i+4096])
|
||||
|
||||
def LCD_ShowImage(self,Image,Xstart,Ystart):
|
||||
if (Image == None):
|
||||
return
|
||||
imwidth, imheight = Image.size
|
||||
if imwidth != self.width or imheight != self.height:
|
||||
raise ValueError('Image must be same dimensions as display \
|
||||
({0}x{1}).' .format(self.width, self.height))
|
||||
img = np.asarray(Image)
|
||||
pix = np.zeros((self.width,self.height,2), dtype = np.uint8)
|
||||
pix[...,[0]] = np.add(np.bitwise_and(img[...,[0]],0xF8),np.right_shift(img[...,[1]],5))
|
||||
pix[...,[1]] = np.add(np.bitwise_and(np.left_shift(img[...,[1]],3),0xE0),np.right_shift(img[...,[2]],3))
|
||||
pix = pix.flatten().tolist()
|
||||
self.LCD_SetWindows(0, 0, self.width , self.height)
|
||||
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
|
||||
for i in range(0,len(pix),4096):
|
||||
config.SPI_Write_Byte(pix[i:i+4096])
|
73
pwnagotchi/ui/hw/libs/waveshare/lcdhat144/config.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# /*****************************************************************************
|
||||
# * | File : config.py
|
||||
# * | Author :
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V1.0
|
||||
# * | Date : 2019-12-06
|
||||
# * | Info :
|
||||
# ******************************************************************************/
|
||||
Device_SPI = 1
|
||||
Device_I2C = 0
|
||||
Device = Device_SPI
|
||||
#spi = spidev.SpiDev(0, 0)
|
||||
|
||||
##
|
||||
# @filename : DEV_Config.py
|
||||
# @brief : LCD hardware interface implements (GPIO, SPI)
|
||||
# @author : Yehui from Waveshare
|
||||
#
|
||||
# Copyright (C) Waveshare July 10 2017
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documnetation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
# Pin definition
|
||||
LCD_RST_PIN = 27
|
||||
LCD_DC_PIN = 25
|
||||
LCD_CS_PIN = 8
|
||||
LCD_BL_PIN = 24
|
||||
|
||||
# SPI device, bus = 0, device = 0
|
||||
SPI = spidev.SpiDev(0, 0)
|
||||
|
||||
def epd_digital_write(pin, value):
|
||||
GPIO.output(pin, value)
|
||||
|
||||
def Driver_Delay_ms(xms):
|
||||
time.sleep(xms / 1000.0)
|
||||
|
||||
def SPI_Write_Byte(data):
|
||||
SPI.writebytes(data)
|
||||
|
||||
def GPIO_Init():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(LCD_RST_PIN, GPIO.OUT)
|
||||
GPIO.setup(LCD_DC_PIN, GPIO.OUT)
|
||||
GPIO.setup(LCD_CS_PIN, GPIO.OUT)
|
||||
GPIO.setup(LCD_BL_PIN, GPIO.OUT)
|
||||
SPI.max_speed_hz = 9000000
|
||||
SPI.mode = 0b00
|
||||
return 0;
|
||||
### END OF FILE ###
|
32
pwnagotchi/ui/hw/libs/waveshare/lcdhat144/epd.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Waveshare 1.44inch LCD HAT
|
||||
# https://www.waveshare.com/1.44inch-lcd-hat.htm
|
||||
# https://www.waveshare.com/wiki/1.44inch_LCD_HAT
|
||||
# Driver: ST7735S
|
||||
# Interface: SPI
|
||||
# Display color: RGB, 65K color
|
||||
# Resolution: 128x128
|
||||
# Backlight: LED
|
||||
# Operating voltage: 3.3V
|
||||
|
||||
from . import config
|
||||
from . import LCD_1in44
|
||||
from PIL import ImageOps
|
||||
|
||||
class EPD(object):
|
||||
def __init__(self):
|
||||
self.width = 128
|
||||
self.height = 128
|
||||
self.LCD = LCD_1in44.LCD()
|
||||
self.LCD.Lcd_ScanDir = LCD_1in44.SCAN_DIR_DFT #SCAN_DIR_DFT = D2U_L2R
|
||||
self.LCD.LCD_Init(self.LCD.Lcd_ScanDir)
|
||||
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
#self.LCD.LCD_Clear()
|
||||
pass
|
||||
|
||||
def display(self, image):
|
||||
rgb_im = ImageOps.colorize(image.convert("L"), black ="green", white ="black")
|
||||
self.LCD.LCD_ShowImage(rgb_im, 0, 0)
|
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()
|
46
pwnagotchi/ui/hw/waveshare144lcd.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class Waveshare144lcd(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(Waveshare144lcd, self).__init__(config, 'waveshare144lcd')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 8, 10, 18)
|
||||
self._layout['width'] = 128
|
||||
self._layout['height'] = 128
|
||||
self._layout['face'] = (0, 43)
|
||||
self._layout['name'] = (0, 14)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (0, 71)
|
||||
self._layout['uptime'] = (0, 25)
|
||||
self._layout['line1'] = [0, 12, 127, 12]
|
||||
self._layout['line2'] = [0, 116, 127, 116]
|
||||
self._layout['friend_face'] = (12, 88)
|
||||
self._layout['friend_name'] = (1, 103)
|
||||
self._layout['shakes'] = (26, 117)
|
||||
self._layout['mode'] = (0, 117)
|
||||
self._layout['status'] = {
|
||||
'pos': (65, 26),
|
||||
'font': fonts.Small,
|
||||
'max': 12
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
logging.info("initializing waveshare 1.44 inch lcd display")
|
||||
from pwnagotchi.ui.hw.libs.waveshare.lcdhat144.epd import EPD
|
||||
self._display = EPD()
|
||||
self._display.init()
|
||||
self._display.clear()
|
||||
|
||||
def render(self, canvas):
|
||||
self._display.display(canvas)
|
||||
|
||||
def clear(self):
|
||||
pass
|
||||
#self._display.clear()
|
@@ -6,7 +6,7 @@ from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
class Waveshare27inch(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(Waveshare27inch, self).__init__(config, 'waveshare_2_7inch')
|
||||
super(Waveshare27inch, self).__init__(config, 'waveshare27inch')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
|
@@ -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,56 +1,198 @@
|
||||
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('/reboot', 'reboot', self.with_auth(self.reboot), methods=['POST'])
|
||||
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
|
||||
|
||||
# inbox
|
||||
self._app.add_url_rule('/inbox', 'inbox', self.with_auth(self.inbox))
|
||||
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.with_auth(self.inbox_profile))
|
||||
self._app.add_url_rule('/inbox/peers', 'inbox_peers', self.with_auth(self.inbox_peers))
|
||||
self._app.add_url_rule('/inbox/<id>', 'show_message', self.with_auth(self.show_message))
|
||||
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.with_auth(self.mark_message))
|
||||
self._app.add_url_rule('/inbox/new', 'new_message', self.with_auth(self.new_message))
|
||||
self._app.add_url_rule('/inbox/send', 'send_message', self.with_auth(self.send_message), methods=['POST'])
|
||||
|
||||
# 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:
|
||||
# show plugins overview
|
||||
abort(404)
|
||||
return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database)
|
||||
|
||||
if name == 'toggle' and request.method == 'POST':
|
||||
checked = True if 'enabled' in request.form else False
|
||||
return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed'
|
||||
|
||||
if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'):
|
||||
try:
|
||||
return plugins.loaded[name].on_webhook(subpath, request)
|
||||
except Exception:
|
||||
abort(500)
|
||||
else:
|
||||
|
||||
# call plugin on_webhook
|
||||
arguments = request.args
|
||||
req_method = request.method
|
||||
|
||||
# need to return something here
|
||||
if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'):
|
||||
return render_template_string(
|
||||
plugins.loaded[name].on_webhook(subpath, args=arguments, req_method=req_method))
|
||||
|
||||
abort(500)
|
||||
abort(404)
|
||||
|
||||
# serve a message and shuts down the unit
|
||||
def shutdown(self):
|
||||
@@ -60,6 +202,14 @@ class Handler:
|
||||
finally:
|
||||
_thread.start_new_thread(pwnagotchi.shutdown, ())
|
||||
|
||||
# serve a message and reboot the unit
|
||||
def reboot(self):
|
||||
try:
|
||||
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
|
||||
message='Rebooting ...')
|
||||
finally:
|
||||
_thread.start_new_thread(pwnagotchi.reboot, ())
|
||||
|
||||
# serve a message and restart the unit in the other mode
|
||||
def restart(self):
|
||||
mode = request.form['mode']
|
||||
|
@@ -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))
|
||||
|
||||
|
259
pwnagotchi/ui/web/static/css/jquery.jqplot.css
Normal file
@@ -0,0 +1,259 @@
|
||||
/*rules for the plot target div. These will be cascaded down to all plot elements according to css rules*/
|
||||
.jqplot-target {
|
||||
position: relative;
|
||||
color: #666666;
|
||||
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
|
||||
font-size: 1em;
|
||||
/* height: 300px;
|
||||
width: 400px;*/
|
||||
}
|
||||
|
||||
/*rules applied to all axes*/
|
||||
.jqplot-axis {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.jqplot-xaxis {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.jqplot-x2axis {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.jqplot-yaxis {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.jqplot-y2axis, .jqplot-y3axis, .jqplot-y4axis, .jqplot-y5axis, .jqplot-y6axis, .jqplot-y7axis, .jqplot-y8axis, .jqplot-y9axis, .jqplot-yMidAxis {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/*rules applied to all axis tick divs*/
|
||||
.jqplot-axis-tick, .jqplot-xaxis-tick, .jqplot-yaxis-tick, .jqplot-x2axis-tick, .jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick, .jqplot-yMidAxis-tick {
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
||||
.jqplot-xaxis-tick {
|
||||
top: 0px;
|
||||
/* initial position untill tick is drawn in proper place */
|
||||
left: 15px;
|
||||
/* padding-top: 10px;*/
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.jqplot-x2axis-tick {
|
||||
bottom: 0px;
|
||||
/* initial position untill tick is drawn in proper place */
|
||||
left: 15px;
|
||||
/* padding-bottom: 10px;*/
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.jqplot-yaxis-tick {
|
||||
right: 0px;
|
||||
/* initial position untill tick is drawn in proper place */
|
||||
top: 15px;
|
||||
/* padding-right: 10px;*/
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.jqplot-yaxis-tick.jqplot-breakTick {
|
||||
right: -20px;
|
||||
margin-right: 0px;
|
||||
padding:1px 5px 1px 5px;
|
||||
/*background-color: white;*/
|
||||
z-index: 2;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick {
|
||||
left: 0px;
|
||||
/* initial position untill tick is drawn in proper place */
|
||||
top: 15px;
|
||||
/* padding-left: 10px;*/
|
||||
/* padding-right: 15px;*/
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.jqplot-yMidAxis-tick {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jqplot-xaxis-label {
|
||||
margin-top: 10px;
|
||||
font-size: 11pt;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.jqplot-x2axis-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 11pt;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.jqplot-yaxis-label {
|
||||
margin-right: 10px;
|
||||
/* text-align: center;*/
|
||||
font-size: 11pt;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.jqplot-yMidAxis-label {
|
||||
font-size: 11pt;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.jqplot-y2axis-label, .jqplot-y3axis-label, .jqplot-y4axis-label, .jqplot-y5axis-label, .jqplot-y6axis-label, .jqplot-y7axis-label, .jqplot-y8axis-label, .jqplot-y9axis-label {
|
||||
/* text-align: center;*/
|
||||
font-size: 11pt;
|
||||
margin-left: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.jqplot-meterGauge-tick {
|
||||
font-size: 0.75em;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.jqplot-meterGauge-label {
|
||||
font-size: 1em;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
table.jqplot-table-legend {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
table.jqplot-table-legend, table.jqplot-cursor-legend {
|
||||
background-color: rgba(255,255,255,0.6);
|
||||
border: 1px solid #cccccc;
|
||||
position: absolute;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
td.jqplot-table-legend {
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
/*
|
||||
These rules could be used instead of assigning
|
||||
element styles and relying on js object properties.
|
||||
*/
|
||||
|
||||
/*
|
||||
td.jqplot-table-legend-swatch {
|
||||
padding-top: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tr.jqplot-table-legend:first td.jqplot-table-legend-swatch {
|
||||
padding-top: 0px;
|
||||
}
|
||||
*/
|
||||
|
||||
td.jqplot-seriesToggle:hover, td.jqplot-seriesToggle:active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jqplot-table-legend .jqplot-series-hidden {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
div.jqplot-table-legend-swatch-outline {
|
||||
border: 1px solid #cccccc;
|
||||
padding:1px;
|
||||
}
|
||||
|
||||
div.jqplot-table-legend-swatch {
|
||||
width:0px;
|
||||
height:0px;
|
||||
border-top-width: 5px;
|
||||
border-bottom-width: 5px;
|
||||
border-left-width: 6px;
|
||||
border-right-width: 6px;
|
||||
border-top-style: solid;
|
||||
border-bottom-style: solid;
|
||||
border-left-style: solid;
|
||||
border-right-style: solid;
|
||||
}
|
||||
|
||||
.jqplot-title {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
padding-bottom: 0.5em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
table.jqplot-cursor-tooltip {
|
||||
border: 1px solid #cccccc;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
|
||||
.jqplot-cursor-tooltip {
|
||||
border: 1px solid #cccccc;
|
||||
font-size: 0.75em;
|
||||
white-space: nowrap;
|
||||
background: rgba(208,208,208,0.5);
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.jqplot-highlighter-tooltip, .jqplot-canvasOverlay-tooltip {
|
||||
border: 1px solid #cccccc;
|
||||
font-size: 0.75em;
|
||||
white-space: nowrap;
|
||||
background: rgba(208,208,208,0.5);
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.jqplot-point-label {
|
||||
font-size: 0.75em;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
td.jqplot-cursor-legend-swatch {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.jqplot-cursor-legend-swatch {
|
||||
width: 1.2em;
|
||||
height: 0.7em;
|
||||
}
|
||||
|
||||
.jqplot-error {
|
||||
/* Styles added to the plot target container when there is an error go here.*/
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jqplot-error-message {
|
||||
/* Styling of the custom error message div goes here.*/
|
||||
position: relative;
|
||||
top: 46%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.jqplot-bubble-label {
|
||||
font-size: 0.8em;
|
||||
/* background: rgba(90%, 90%, 90%, 0.15);*/
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
color: rgb(20%, 20%, 20%);
|
||||
}
|
||||
|
||||
div.jqplot-bubble-label.jqplot-bubble-label-highlight {
|
||||
background: rgba(90%, 90%, 90%, 0.7);
|
||||
}
|
||||
|
||||
div.jqplot-noData-container {
|
||||
text-align: center;
|
||||
background-color: rgba(96%, 96%, 96%, 0.3);
|
||||
}
|
1
pwnagotchi/ui/web/static/css/jquery.jqplot.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.jqplot-xaxis,.jqplot-xaxis-label{margin-top:10px}.jqplot-x2axis,.jqplot-x2axis-label{margin-bottom:10px}.jqplot-target{position:relative;color:#666;font-family:"Trebuchet MS",Arial,Helvetica,sans-serif;font-size:1em}.jqplot-axis{font-size:.75em}.jqplot-yaxis{margin-right:10px}.jqplot-y2axis,.jqplot-y3axis,.jqplot-y4axis,.jqplot-y5axis,.jqplot-y6axis,.jqplot-y7axis,.jqplot-y8axis,.jqplot-y9axis,.jqplot-yMidAxis{margin-left:10px;margin-right:10px}.jqplot-axis-tick,.jqplot-x2axis-tick,.jqplot-xaxis-tick,.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick,.jqplot-yMidAxis-tick,.jqplot-yaxis-tick{position:absolute;white-space:pre}.jqplot-xaxis-tick{top:0;left:15px;vertical-align:top}.jqplot-x2axis-tick{bottom:0;left:15px;vertical-align:bottom}.jqplot-yaxis-tick{right:0;top:15px;text-align:right}.jqplot-yaxis-tick.jqplot-breakTick{right:-20px;margin-right:0;padding:1px 5px;z-index:2;font-size:1.5em}.jqplot-x2axis-label,.jqplot-xaxis-label,.jqplot-yMidAxis-label,.jqplot-yaxis-label{font-size:11pt;position:absolute}.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{left:0;top:15px;text-align:left}.jqplot-yMidAxis-tick{text-align:center;white-space:nowrap}.jqplot-yaxis-label{margin-right:10px}.jqplot-y2axis-label,.jqplot-y3axis-label,.jqplot-y4axis-label,.jqplot-y5axis-label,.jqplot-y6axis-label,.jqplot-y7axis-label,.jqplot-y8axis-label,.jqplot-y9axis-label{font-size:11pt;margin-left:10px;position:absolute}.jqplot-meterGauge-tick{font-size:.75em;color:#999}.jqplot-meterGauge-label{font-size:1em;color:#999}table.jqplot-table-legend{margin:12px}table.jqplot-cursor-legend,table.jqplot-table-legend{background-color:rgba(255,255,255,.6);border:1px solid #ccc;position:absolute;font-size:.75em}td.jqplot-table-legend{vertical-align:middle}td.jqplot-seriesToggle:active,td.jqplot-seriesToggle:hover{cursor:pointer}.jqplot-table-legend .jqplot-series-hidden{text-decoration:line-through}div.jqplot-table-legend-swatch-outline{border:1px solid #ccc;padding:1px}div.jqplot-table-legend-swatch{width:0;height:0;border-width:5px 6px;border-style:solid}.jqplot-title{top:0;left:0;padding-bottom:.5em;font-size:1.2em}table.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em}.jqplot-canvasOverlay-tooltip,.jqplot-cursor-tooltip,.jqplot-highlighter-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,.5);padding:1px}.jqplot-point-label{font-size:.75em;z-index:2}td.jqplot-cursor-legend-swatch{vertical-align:middle;text-align:center}div.jqplot-cursor-legend-swatch{width:1.2em;height:.7em}.jqplot-error{text-align:center}.jqplot-error-message{position:relative;top:46%;display:inline-block}div.jqplot-bubble-label{font-size:.8em;padding-left:2px;padding-right:2px;color:rgb(20%,20%,20%)}div.jqplot-bubble-label.jqplot-bubble-label-highlight{background:rgba(90%,90%,90%,.7)}div.jqplot-noData-container{text-align:center;background-color:rgba(96%,96%,96%,.3)}
|
@@ -1,42 +1,82 @@
|
||||
.block {
|
||||
-webkit-appearance: button;
|
||||
-moz-appearance: button;
|
||||
appearance: button;
|
||||
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img#ui {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.full {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
.ui-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pixelated {
|
||||
image-rendering:optimizeSpeed; /* Legal fallback */
|
||||
image-rendering:-moz-crisp-edges; /* Firefox */
|
||||
image-rendering:-o-crisp-edges; /* Opera */
|
||||
image-rendering:-webkit-optimize-contrast; /* Safari */
|
||||
image-rendering:optimize-contrast; /* CSS3 Proposed */
|
||||
image-rendering:crisp-edges; /* CSS4 Proposed */
|
||||
image-rendering:pixelated; /* CSS4 Proposed */
|
||||
-ms-interpolation-mode:nearest-neighbor; /* IE8+ */
|
||||
image-rendering: optimizeSpeed; /* Legal fallback */
|
||||
image-rendering: -moz-crisp-edges; /* Firefox */
|
||||
image-rendering: -o-crisp-edges; /* Opera */
|
||||
image-rendering: -webkit-optimize-contrast; /* Safari */
|
||||
image-rendering: optimize-contrast; /* CSS3 Proposed */
|
||||
image-rendering: crisp-edges; /* CSS4 Proposed */
|
||||
image-rendering: pixelated; /* CSS4 Proposed */
|
||||
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */
|
||||
}
|
||||
|
||||
form.action {
|
||||
display:inline;
|
||||
.image-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.status {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
}
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.read {
|
||||
color: #777 !important;
|
||||
}
|
||||
|
||||
p.messagebody {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
li.navitem {
|
||||
width: 16.66% !important;
|
||||
clear: none !important;
|
||||
}
|
||||
|
||||
/* Custom indentations are needed because the length of custom labels differs from
|
||||
the length of the standard labels */
|
||||
.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on {
|
||||
text-indent: -5.9em;
|
||||
}
|
||||
|
||||
.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off {
|
||||
text-indent: 0.5em;
|
||||
}
|
||||
|
||||
/* Custom widths are needed because the length of custom labels differs from
|
||||
the length of the standard labels */
|
||||
.custom-size-flipswitch.ui-flipswitch {
|
||||
width: 8.875em;
|
||||
}
|
||||
|
||||
.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active {
|
||||
padding-left: 7em;
|
||||
width: 1.875em;
|
||||
}
|
||||
|
||||
@media (min-width: 28em) {
|
||||
/*Repeated from rule .ui-flipswitch above*/
|
||||
.ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch {
|
||||
width: 1.875em;
|
||||
}
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.plugins-box {
|
||||
margin: 0.5rem;
|
||||
padding: 0.2rem;
|
||||
border-style: groove;
|
||||
border-radius: 0.5rem;
|
||||
background-color: lightgrey;
|
||||
text-align: center;
|
||||
}
|
||||
|
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
11477
pwnagotchi/ui/web/static/js/jquery.jqplot.js
Normal file
8
pwnagotchi/ui/web/static/js/jquery.jqplot.min.js
vendored
Normal file
BIN
pwnagotchi/ui/web/static/js/jquery.mobile/images/ajax-loader.gif
Normal file
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 219 B |
After Width: | Height: | Size: 227 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 243 B |
After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 171 B |
After Width: | Height: | Size: 149 B |
After Width: | Height: | Size: 149 B |
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 147 B |
After Width: | Height: | Size: 152 B |
After Width: | Height: | Size: 147 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 169 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 165 B |
After Width: | Height: | Size: 151 B |
After Width: | Height: | Size: 307 B |
After Width: | Height: | Size: 314 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 240 B |
After Width: | Height: | Size: 132 B |
After Width: | Height: | Size: 135 B |
After Width: | Height: | Size: 147 B |