Compare commits
259 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cedcc17391 | ||
|
6478b3827d | ||
|
463f4b50e4 | ||
|
793cde7147 | ||
|
518b9e665d | ||
|
91ea7bdb9b | ||
|
3ce88f1d80 | ||
|
6d45d01baf | ||
|
1f2dd73976 | ||
|
5d8d86204a | ||
|
7017e39c6d | ||
|
1360c734ff | ||
|
7c90050b17 | ||
|
9ca8aacdf6 | ||
|
d39c849daf | ||
|
58bbae89c2 | ||
|
0dedd0974f | ||
|
5bac678771 | ||
|
3b9aacdd16 | ||
|
305f837486 | ||
|
54ffbbcb0b | ||
|
60167fb8fe | ||
|
9a1565813c | ||
|
03c014f414 | ||
|
d10bf6bf1d | ||
|
9a22321799 | ||
|
76b71f5c3f | ||
|
4aa05bb834 | ||
|
71c4458858 | ||
|
8260b41bab | ||
|
86530d4b97 | ||
|
c7d9a757f6 | ||
|
b6a0ae9d3a | ||
|
34f52b0d3a | ||
|
c68cefe80d | ||
|
052c99b858 | ||
|
ccea7cbeef | ||
|
a16a5f7bcb | ||
|
a5df77d737 | ||
|
489bce0c01 | ||
|
da4319f81b | ||
|
0e1a1f4c79 | ||
|
b3bdb34e3f | ||
|
c791c86bee | ||
|
61e5872229 | ||
|
23616095ba | ||
|
3c8e7fbea4 | ||
|
4a5d2d36cc | ||
|
52d432e5b6 | ||
|
93bdf2e3a1 | ||
|
fe97315b0f | ||
|
7e80a7b9ca | ||
|
64385f43ed | ||
|
6a4d7a895e | ||
|
5606ad7281 | ||
|
23f09bc4b6 | ||
|
238b90d988 | ||
|
138316d55a | ||
|
67bbcfef9b | ||
|
1a0e0c46d2 | ||
|
4cd0f46ad7 | ||
|
155ea54d08 | ||
|
461e53ed79 | ||
|
6d40388002 | ||
|
37b25a142f | ||
|
665ad938b4 | ||
|
301a3d99cf | ||
|
c4e0acad17 | ||
|
9339ecb2fd | ||
|
a28c9a1176 | ||
|
c5d6f6d362 | ||
|
ff843f0367 | ||
|
717cb02743 | ||
|
8be643b2e0 | ||
|
814392daa5 | ||
|
4cc1c2ac1f | ||
|
fae6a0942b | ||
|
53ab63cf8a | ||
|
0764304be9 | ||
|
cdc0e0fa3e | ||
|
5ccd65e46e | ||
|
afc3636939 | ||
|
f154b97ab9 | ||
|
66acecb387 | ||
|
a31d0a5e19 | ||
|
026b9fc513 | ||
|
1cdc1641fc | ||
|
71de5925ee | ||
|
4733e90e77 | ||
|
7cf0a2ef4b | ||
|
97e03843bd | ||
|
8b078383c2 | ||
|
3e5bece3cf | ||
|
7645be6f3e | ||
|
779da95f78 | ||
|
51e13aa1ad | ||
|
e489678cf5 | ||
|
52015014b4 | ||
|
f691f737ab | ||
|
2617a6edea | ||
|
6001b7630b | ||
|
78fba1f74b | ||
|
6075296884 | ||
|
9457622713 | ||
|
7ef1c1f2f0 | ||
|
8e0488e16f | ||
|
5934ac4a55 | ||
|
15bae093fb | ||
|
0c76cd7ea7 | ||
|
5834b27ed8 | ||
|
a8ed9bcc1c | ||
|
0e88c3aa6a | ||
|
b1d61d95e6 | ||
|
215af0fc88 | ||
|
c09b72ff7e | ||
|
2f1b35b3fa | ||
|
d435ef2ba9 | ||
|
4164e7c067 | ||
|
bb7737762c | ||
|
c300e73726 | ||
|
0587c4b09a | ||
|
63dc672b11 | ||
|
0dac137df0 | ||
|
3db9ccb47e | ||
|
f375e4905f | ||
|
8d17cf0bd2 | ||
|
d981b26842 | ||
|
a7164ea742 | ||
|
cae2a18016 | ||
|
9d63eba232 | ||
|
f141e15ba3 | ||
|
e68165ce06 | ||
|
3758806919 | ||
|
1f91c6f09e | ||
|
3e8b6eafbd | ||
|
44138ba463 | ||
|
4b71fea404 | ||
|
6babad0d02 | ||
|
e8513240ea | ||
|
00101ccd07 | ||
|
a0bc911c0e | ||
|
6d71bcd965 | ||
|
91447a2a31 | ||
|
e06480e474 | ||
|
819146f83a | ||
|
cdd4c13336 | ||
|
704d7ceaa1 | ||
|
a4daf4af61 | ||
|
eddcf32b62 | ||
|
6117235c52 | ||
|
e0a66f5c99 | ||
|
81061cea24 | ||
|
93bb633010 | ||
|
dbb64e0fab | ||
|
fa8751017d | ||
|
9d56c97aa5 | ||
|
10f7161240 | ||
|
9b02548176 | ||
|
f5f47c4f88 | ||
|
1523dfc1ef | ||
|
a02960b56d | ||
|
b6f59f99d4 | ||
|
09a00adab9 | ||
|
7fa30c2868 | ||
|
774d9c693c | ||
|
87b6cf7d40 | ||
|
88928eec82 | ||
|
60f7849838 | ||
|
3cf041617c | ||
|
ee6c06f306 | ||
|
2e22a17610 | ||
|
1615fc8817 | ||
|
714cb00610 | ||
|
a0a790635a | ||
|
9d17be959d | ||
|
4b651cd17b | ||
|
2f70512076 | ||
|
b79c59c639 | ||
|
56f7b67699 | ||
|
1a8472268e | ||
|
4fb7205281 | ||
|
f8ffab426b | ||
|
b4daf19401 | ||
|
e04e053cee | ||
|
4f153e3899 | ||
|
04720ecc42 | ||
|
a12e2aafa5 | ||
|
1721f67ec3 | ||
|
7693e42aa1 | ||
|
2ae48a2ef2 | ||
|
663bca41cd | ||
|
ede01e50cd | ||
|
19973574e7 | ||
|
a019b4c778 | ||
|
526c5bed87 | ||
|
4d6136633a | ||
|
6d90b75d10 | ||
|
03695be807 | ||
|
6a97476732 | ||
|
b5e620684b | ||
|
95557ab37d | ||
|
988d093e36 | ||
|
f563d71477 | ||
|
7b219fd139 | ||
|
42ed698583 | ||
|
6df7bcd885 | ||
|
1c299832ae | ||
|
11476433ca | ||
|
6e57e131b3 | ||
|
548b42ef20 | ||
|
99614c8cd4 | ||
|
e19ea999e2 | ||
|
2207a1eacf | ||
|
9509dd0aa5 | ||
|
608904daf8 | ||
|
f973997cdb | ||
|
9b594f7fb2 | ||
|
b8eed4f52a | ||
|
ad510429fe | ||
|
f5a94fde96 | ||
|
855bda9104 | ||
|
e72fd08fb4 | ||
|
e44ebac43f | ||
|
b5ddb716e2 | ||
|
82e7e09fa1 | ||
|
8b40e94ca8 | ||
|
cc5c46906f | ||
|
7cb52ba33a | ||
|
07f8e7bd4a | ||
|
48dc751d13 | ||
|
3c154ffe0c | ||
|
167f559d73 | ||
|
19775b7d27 | ||
|
48e3a372cc | ||
|
d2c44797e5 | ||
|
a03443986b | ||
|
a7ea499fac | ||
|
722a91655a | ||
|
93e06d7f59 | ||
|
7de5121033 | ||
|
83f741bbb0 | ||
|
a779fb9b0b | ||
|
c4a007e72a | ||
|
1a71615fa8 | ||
|
7a9f84f495 | ||
|
6e3f5a1181 | ||
|
d045ed5afa | ||
|
0ee0aaff37 | ||
|
0fb81a11c4 | ||
|
cfc0ad1b48 | ||
|
3351c251ef | ||
|
d9d399429c | ||
|
b1ad247e11 | ||
|
b5a148f287 | ||
|
7138f6469b | ||
|
30b1874a0a | ||
|
b903f636d2 | ||
|
92c1b6b005 | ||
|
eddfdb3ebc |
@@ -1,7 +1,31 @@
|
|||||||
# top-most EditorConfig file
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
# Matches the exact files either package.json or .travis.yml
|
[*]
|
||||||
[{*.yml,*.yaml,config.yml,defaults.yml}]
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
insert_final_newline = ignore
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
indent_style = ignore
|
||||||
|
insert_final_newline = ignore
|
||||||
|
|
||||||
|
[*.{md,txt}]
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ dist
|
|||||||
pwnagotchi.egg-info
|
pwnagotchi.egg-info
|
||||||
*backup*.tgz
|
*backup*.tgz
|
||||||
*backup*.gz
|
*backup*.gz
|
||||||
|
.vscode
|
||||||
|
3
Makefile
3
Makefile
@@ -1,10 +1,11 @@
|
|||||||
|
PACKER_VERSION=1.5.2
|
||||||
PWN_HOSTNAME=pwnagotchi
|
PWN_HOSTNAME=pwnagotchi
|
||||||
PWN_VERSION=master
|
PWN_VERSION=master
|
||||||
|
|
||||||
all: clean install image
|
all: clean install image
|
||||||
|
|
||||||
install:
|
install:
|
||||||
curl https://releases.hashicorp.com/packer/1.3.5/packer_1.3.5_linux_amd64.zip -o /tmp/packer.zip
|
curl https://releases.hashicorp.com/packer/$(PACKER_VERSION)/packer_$(PACKER_VERSION)_linux_amd64.zip -o /tmp/packer.zip
|
||||||
unzip /tmp/packer.zip -d /tmp
|
unzip /tmp/packer.zip -d /tmp
|
||||||
sudo mv /tmp/packer /usr/bin/packer
|
sudo mv /tmp/packer /usr/bin/packer
|
||||||
git clone https://github.com/solo-io/packer-builder-arm-image /tmp/packer-builder-arm-image
|
git clone https://github.com/solo-io/packer-builder-arm-image /tmp/packer-builder-arm-image
|
||||||
|
@@ -2,22 +2,23 @@
|
|||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
import time
|
import time
|
||||||
import yaml
|
import signal
|
||||||
|
import sys
|
||||||
|
import toml
|
||||||
|
|
||||||
import pwnagotchi
|
import pwnagotchi
|
||||||
import pwnagotchi.grid as grid
|
from pwnagotchi import utils
|
||||||
import pwnagotchi.utils as utils
|
from pwnagotchi.plugins import cmd as plugins_cmd
|
||||||
import pwnagotchi.plugins as plugins
|
from pwnagotchi import log
|
||||||
|
from pwnagotchi import restart
|
||||||
from pwnagotchi.identity import KeyPair
|
from pwnagotchi import fs
|
||||||
from pwnagotchi.agent import Agent
|
from pwnagotchi.utils import DottedTomlEncoder
|
||||||
from pwnagotchi.ui.display import Display
|
|
||||||
|
|
||||||
|
|
||||||
def do_clear(display):
|
def do_clear(display):
|
||||||
logging.info("clearing the display ...")
|
logging.info("clearing the display ...")
|
||||||
display.clear()
|
display.clear()
|
||||||
exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def do_manual_mode(agent):
|
def do_manual_mode(agent):
|
||||||
@@ -82,15 +83,16 @@ def do_auto_mode(agent):
|
|||||||
plugins.on('internet_available', agent)
|
plugins.on('internet_available', agent)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("main loop exception")
|
logging.exception("main loop exception (%s)", e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
parser = plugins_cmd.add_parsers(parser)
|
||||||
|
|
||||||
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.yml',
|
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.toml',
|
||||||
help='Main configuration file.')
|
help='Main configuration file.')
|
||||||
parser.add_argument('-U', '--user-config', action='store', dest='user_config', default='/etc/pwnagotchi/config.yml',
|
parser.add_argument('-U', '--user-config', action='store', dest='user_config', default='/etc/pwnagotchi/config.toml',
|
||||||
help='If this file exists, configuration will be merged and this will override default values.')
|
help='If this file exists, configuration will be merged and this will override default values.')
|
||||||
|
|
||||||
parser.add_argument('--manual', dest="do_manual", action="store_true", default=False, help="Manual mode.")
|
parser.add_argument('--manual', dest="do_manual", action="store_true", default=False, help="Manual mode.")
|
||||||
@@ -111,16 +113,34 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if plugins_cmd.used_plugin_cmd(args):
|
||||||
|
config = utils.load_config(args)
|
||||||
|
log.setup_logging(args, config)
|
||||||
|
rc = plugins_cmd.handle_cmd(args, config)
|
||||||
|
sys.exit(rc)
|
||||||
|
|
||||||
if args.version:
|
if args.version:
|
||||||
print(pwnagotchi.version)
|
print(pwnagotchi.__version__)
|
||||||
exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
config = utils.load_config(args)
|
config = utils.load_config(args)
|
||||||
if args.print_config:
|
|
||||||
print(yaml.dump(config, default_flow_style=False))
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
utils.setup_logging(args, config)
|
if args.print_config:
|
||||||
|
print(toml.dumps(config, encoder=DottedTomlEncoder()))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
from pwnagotchi.identity import KeyPair
|
||||||
|
from pwnagotchi.agent import Agent
|
||||||
|
from pwnagotchi.ui import fonts
|
||||||
|
from pwnagotchi.ui.display import Display
|
||||||
|
from pwnagotchi import grid
|
||||||
|
from pwnagotchi import plugins
|
||||||
|
|
||||||
|
pwnagotchi.config = config
|
||||||
|
fs.setup_mounts(config)
|
||||||
|
log.setup_logging(args, config)
|
||||||
|
fonts.init(config)
|
||||||
|
|
||||||
pwnagotchi.set_name(config['main']['name'])
|
pwnagotchi.set_name(config['main']['name'])
|
||||||
|
|
||||||
@@ -130,10 +150,16 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
if args.do_clear:
|
if args.do_clear:
|
||||||
do_clear(display)
|
do_clear(display)
|
||||||
exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
|
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
|
||||||
|
|
||||||
|
def usr1_handler(*unused):
|
||||||
|
logging.info('Received USR1 singal. Restart process ...')
|
||||||
|
restart("MANU" if args.do_manual else "AUTO")
|
||||||
|
|
||||||
|
signal.signal(signal.SIGUSR1, usr1_handler)
|
||||||
|
|
||||||
if args.do_manual:
|
if args.do_manual:
|
||||||
do_manual_mode(agent)
|
do_manual_mode(agent)
|
||||||
else:
|
else:
|
||||||
|
@@ -6,10 +6,15 @@ After=pwngrid-peer.service
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
WorkingDirectory=/tmp
|
||||||
PermissionsStartOnly=true
|
PermissionsStartOnly=true
|
||||||
ExecStart=/usr/bin/pwnagotchi-launcher
|
ExecStart=/usr/bin/pwnagotchi-launcher
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
|
TasksMax=infinity
|
||||||
|
LimitNPROC=infinity
|
||||||
|
StandardOutput=null
|
||||||
|
StandardError=null
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
source /usr/bin/pwnlib
|
source /usr/bin/pwnlib
|
||||||
|
|
||||||
|
# we need to decrypt something
|
||||||
|
if is_crypted_mode; then
|
||||||
|
while ! is_decrypted; do
|
||||||
|
echo "Waiting for decryption..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# start mon0
|
# start mon0
|
||||||
start_monitor_interface
|
start_monitor_interface
|
||||||
|
|
||||||
|
71
builder/data/usr/bin/decryption-webserver
Executable file
71
builder/data/usr/bin/decryption-webserver
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
|
||||||
|
HTML_FORM = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Decryption</title>
|
||||||
|
<style>
|
||||||
|
body { text-align: center; padding: 150px; }
|
||||||
|
h1 { font-size: 50px; }
|
||||||
|
body { font: 20px Helvetica, sans-serif; color: #333; }
|
||||||
|
article { display: block; text-align: center; width: 650px; margin: 0 auto;}
|
||||||
|
input {
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 8px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
input[type=password] {
|
||||||
|
width: 75%;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
input[type=submit] {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
input[type=submit]:hover {
|
||||||
|
background-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article>
|
||||||
|
<h1>Decryption</h1>
|
||||||
|
<p>Some of your files are encrypted.</p>
|
||||||
|
<p>Please provide the decryption password.</p>
|
||||||
|
<div>
|
||||||
|
<form action="/set-password" method="POST">
|
||||||
|
<input type="password" id="password" name="password" value=""><br>
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(HTML_FORM.encode())
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
password = body.decode('UTF-8').split('=')[1]
|
||||||
|
|
||||||
|
with open('/tmp/.pwnagotchi-secret', 'wt') as pwfile:
|
||||||
|
pwfile.write(password)
|
||||||
|
|
||||||
|
|
||||||
|
httpd = HTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
|
||||||
|
httpd.serve_forever()
|
@@ -1,6 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
source /usr/bin/pwnlib
|
source /usr/bin/pwnlib
|
||||||
|
|
||||||
|
# we need to decrypt something
|
||||||
|
if is_crypted_mode; then
|
||||||
|
while ! is_decrypted; do
|
||||||
|
echo "Waiting for decryption..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# blink 10 times to signal ready state
|
# blink 10 times to signal ready state
|
||||||
blink_led 10 &
|
blink_led 10 &
|
||||||
|
|
||||||
@@ -8,4 +16,4 @@ if is_auto_mode; then
|
|||||||
/usr/local/bin/pwnagotchi
|
/usr/local/bin/pwnagotchi
|
||||||
else
|
else
|
||||||
/usr/local/bin/pwnagotchi --manual
|
/usr/local/bin/pwnagotchi --manual
|
||||||
fi
|
fi
|
||||||
|
@@ -84,4 +84,80 @@ is_auto_mode_no_delete() {
|
|||||||
|
|
||||||
# no override, but none of the interfaces is up -> AUTO
|
# no override, but none of the interfaces is up -> AUTO
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# check if we need to decrypt something
|
||||||
|
is_crypted_mode() {
|
||||||
|
if [ -f /root/.pwnagotchi-crypted ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# decryption loop
|
||||||
|
is_decrypted() {
|
||||||
|
while read -r mapping container mount; do
|
||||||
|
# mapping = name the device or file will be mapped to
|
||||||
|
# container = the luks encrypted device or file
|
||||||
|
# mount = the mountpoint
|
||||||
|
|
||||||
|
# fail if not mounted
|
||||||
|
if ! mountpoint -q "$mount" >/dev/null 2>&1; then
|
||||||
|
if [ -f /tmp/.pwnagotchi-secret ]; then
|
||||||
|
</tmp/.pwnagotchi-secret read -r SECRET
|
||||||
|
if ! test -b /dev/disk/by-id/dm-uuid-*"$(cryptsetup luksUUID "$container" | tr -d -)"*; then
|
||||||
|
if echo -n "$SECRET" | cryptsetup luksOpen -d- "$container" "$mapping" >/dev/null 2>&1; then
|
||||||
|
echo "Container decrypted!"
|
||||||
|
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mount /dev/mapper/"$mapping" "$mount" >/dev/null 2>&1; then
|
||||||
|
echo "Mounted /dev/mapper/$mapping to $mount"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ip -4 addr show wlan0 | grep inet >/dev/null 2>&1; then
|
||||||
|
>/dev/null 2>&1 ip addr add 192.168.0.10/24 dev wlan0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! pgrep -f decryption-webserver >/dev/null 2>&1; then
|
||||||
|
>/dev/null 2>&1 decryption-webserver &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! pgrep wpa_supplicant >/dev/null 2>&1; then
|
||||||
|
>/tmp/wpa_supplicant.conf cat <<EOF
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
ap_scan=2
|
||||||
|
|
||||||
|
network={
|
||||||
|
ssid="DECRYPT-ME"
|
||||||
|
mode=2
|
||||||
|
key_mgmt=WPA-PSK
|
||||||
|
psk="pwnagotchi"
|
||||||
|
frequency=2437
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
>/dev/null 2>&1 wpa_supplicant -D nl80211 -i wlan0 -c /tmp/wpa_supplicant.conf &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! pgrep dnsmasq >/dev/null 2>&1; then
|
||||||
|
>/dev/null 2>&1 dnsmasq -k -p 53 -h -O "6,192.168.0.10" -A "/#/192.168.0.10" -i wlan0 -K -F 192.168.0.50,192.168.0.60,255.255.255.0,24h &
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done </root/.pwnagotchi-crypted
|
||||||
|
|
||||||
|
# overwrite password
|
||||||
|
>/tmp/.pwnagotchi-secret python3 -c 'print("A"*4096)'
|
||||||
|
sync # flush
|
||||||
|
|
||||||
|
pkill wpa_supplicant
|
||||||
|
pkill dnsmasq
|
||||||
|
kill "$(pgrep -f "decryption-webserver")"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
@@ -98,6 +98,7 @@
|
|||||||
{
|
{
|
||||||
"type": "ansible-local",
|
"type": "ansible-local",
|
||||||
"playbook_file": "pwnagotchi.yml",
|
"playbook_file": "pwnagotchi.yml",
|
||||||
|
"extra_arguments": [ "--extra-vars \"ansible_python_interpreter=/usr/bin/python3\"" ],
|
||||||
"command": "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 PWN_VERSION={{user `pwn_version`}} PWN_HOSTNAME={{user `pwn_hostname`}} ansible-playbook"
|
"command": "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 PWN_VERSION={{user `pwn_version`}} PWN_HOSTNAME={{user `pwn_hostname`}} ansible-playbook"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
- ifup@wlan0.service
|
- ifup@wlan0.service
|
||||||
packages:
|
packages:
|
||||||
bettercap:
|
bettercap:
|
||||||
url: "https://github.com/bettercap/bettercap/releases/download/v2.26.1/bettercap_linux_armhf_v2.26.1.zip"
|
url: "https://github.com/bettercap/bettercap/releases/download/v2.27.1/bettercap_linux_armhf_v2.27.1.zip"
|
||||||
ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip"
|
ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip"
|
||||||
pwngrid:
|
pwngrid:
|
||||||
url: "https://github.com/evilsocket/pwngrid/releases/download/v1.10.1/pwngrid_linux_armhf_v1.10.1.zip"
|
url: "https://github.com/evilsocket/pwngrid/releases/download/v1.10.1/pwngrid_linux_armhf_v1.10.1.zip"
|
||||||
@@ -47,12 +47,13 @@
|
|||||||
- firmware-misc-nonfree
|
- firmware-misc-nonfree
|
||||||
- firmware-realtek
|
- firmware-realtek
|
||||||
remove:
|
remove:
|
||||||
- rasberrypi-net-mods
|
- raspberrypi-net-mods
|
||||||
- dhcpcd5
|
- dhcpcd5
|
||||||
- triggerhappy
|
- triggerhappy
|
||||||
- wpa_supplicant
|
- wpa_supplicant
|
||||||
- nfs-common
|
- nfs-common
|
||||||
install:
|
install:
|
||||||
|
- rsync
|
||||||
- vim
|
- vim
|
||||||
- screen
|
- screen
|
||||||
- golang
|
- golang
|
||||||
@@ -100,9 +101,9 @@
|
|||||||
- bc
|
- bc
|
||||||
- fonts-freefont-ttf
|
- fonts-freefont-ttf
|
||||||
- fbi
|
- fbi
|
||||||
- python3-flask
|
- fonts-ipaexfont-gothic
|
||||||
- python3-flask-cors
|
- cryptsetup
|
||||||
- python3-flaskext.wtf
|
- dnsmasq
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: change hostname
|
- name: change hostname
|
||||||
@@ -216,9 +217,19 @@
|
|||||||
dest: /usr/local/src/pwnagotchi
|
dest: /usr/local/src/pwnagotchi
|
||||||
register: pwnagotchigit
|
register: pwnagotchigit
|
||||||
|
|
||||||
|
- name: create /usr/local/share/pwnagotchi/ folder
|
||||||
|
file:
|
||||||
|
path: /usr/local/share/pwnagotchi/
|
||||||
|
state: directory
|
||||||
|
|
||||||
|
- name: clone pwnagotchi plugins repository
|
||||||
|
git:
|
||||||
|
repo: https://github.com/evilsocket/pwnagotchi-plugins-contrib.git
|
||||||
|
dest: /usr/local/share/pwnagotchi/availaible-plugins
|
||||||
|
|
||||||
- name: fetch pwnagotchi version
|
- name: fetch pwnagotchi version
|
||||||
set_fact:
|
set_fact:
|
||||||
pwnagotchi_version: "{{ lookup('file', '/usr/local/src/pwnagotchi/pwnagotchi/__init__.py') | replace('\n', ' ') | regex_replace('.*version.*=.*''([0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*)''.*', '\\1') }}"
|
pwnagotchi_version: "{{ lookup('file', '/usr/local/src/pwnagotchi/pwnagotchi/_version.py') | regex_replace('.*__version__.*=.*''([0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*)''.*', '\\1') }}"
|
||||||
|
|
||||||
- name: pwnagotchi version found
|
- name: pwnagotchi version found
|
||||||
debug:
|
debug:
|
||||||
@@ -228,7 +239,7 @@
|
|||||||
command: "python3 setup.py sdist bdist_wheel"
|
command: "python3 setup.py sdist bdist_wheel"
|
||||||
args:
|
args:
|
||||||
chdir: /usr/local/src/pwnagotchi
|
chdir: /usr/local/src/pwnagotchi
|
||||||
when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi.version)
|
when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi_version)
|
||||||
|
|
||||||
- name: install opencv-python
|
- name: install opencv-python
|
||||||
pip:
|
pip:
|
||||||
@@ -246,7 +257,7 @@
|
|||||||
pip:
|
pip:
|
||||||
name: "{{ lookup('fileglob', '/usr/local/src/pwnagotchi/dist/pwnagotchi*.whl') }}"
|
name: "{{ lookup('fileglob', '/usr/local/src/pwnagotchi/dist/pwnagotchi*.whl') }}"
|
||||||
extra_args: "--no-cache-dir"
|
extra_args: "--no-cache-dir"
|
||||||
when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi.version)
|
when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi_version)
|
||||||
|
|
||||||
- name: download and install pwngrid
|
- name: download and install pwngrid
|
||||||
unarchive:
|
unarchive:
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
import subprocess
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import pwnagotchi.ui.view as view
|
|
||||||
import pwnagotchi
|
|
||||||
|
|
||||||
version = '1.3.0'
|
|
||||||
|
|
||||||
|
from pwnagotchi._version import __version__
|
||||||
|
|
||||||
_name = None
|
_name = None
|
||||||
|
config = None
|
||||||
|
|
||||||
|
|
||||||
def set_name(new_name):
|
def set_name(new_name):
|
||||||
@@ -27,17 +27,17 @@ def set_name(new_name):
|
|||||||
if new_name != current:
|
if new_name != current:
|
||||||
global _name
|
global _name
|
||||||
|
|
||||||
logging.info("setting unit hostname '%s' -> '%s'" % (current, new_name))
|
logging.info("setting unit hostname '%s' -> '%s'", current, new_name)
|
||||||
with open('/etc/hostname', 'wt') as fp:
|
with open('/etc/hostname', 'wt') as fp:
|
||||||
fp.write(new_name)
|
fp.write(new_name)
|
||||||
|
|
||||||
with open('/etc/hosts', 'rt') as fp:
|
with open('/etc/hosts', 'rt') as fp:
|
||||||
prev = fp.read()
|
prev = fp.read()
|
||||||
logging.debug("old hosts:\n%s\n" % prev)
|
logging.debug("old hosts:\n%s\n", prev)
|
||||||
|
|
||||||
with open('/etc/hosts', 'wt') as fp:
|
with open('/etc/hosts', 'wt') as fp:
|
||||||
patched = prev.replace(current, new_name, -1)
|
patched = prev.replace(current, new_name, -1)
|
||||||
logging.debug("new hosts:\n%s\n" % patched)
|
logging.debug("new hosts:\n%s\n", patched)
|
||||||
fp.write(patched)
|
fp.write(patched)
|
||||||
|
|
||||||
os.system("hostname '%s'" % new_name)
|
os.system("hostname '%s'" % new_name)
|
||||||
@@ -65,8 +65,6 @@ def mem_usage():
|
|||||||
kb_mem_total = int(line.split()[1])
|
kb_mem_total = int(line.split()[1])
|
||||||
if line.startswith("MemFree:"):
|
if line.startswith("MemFree:"):
|
||||||
kb_mem_free = int(line.split()[1])
|
kb_mem_free = int(line.split()[1])
|
||||||
if line.startswith("MemAvailable:"):
|
|
||||||
kb_mem_available = int(line.split()[1])
|
|
||||||
if line.startswith("Buffers:"):
|
if line.startswith("Buffers:"):
|
||||||
kb_main_buffers = int(line.split()[1])
|
kb_main_buffers = int(line.split()[1])
|
||||||
if line.startswith("Cached:"):
|
if line.startswith("Cached:"):
|
||||||
@@ -77,18 +75,27 @@ def mem_usage():
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cpu_load():
|
def _cpu_stat():
|
||||||
|
"""
|
||||||
|
Returns the splitted first line of the /proc/stat file
|
||||||
|
"""
|
||||||
with open('/proc/stat', 'rt') as fp:
|
with open('/proc/stat', 'rt') as fp:
|
||||||
for line in fp:
|
return list(map(int,fp.readline().split()[1:]))
|
||||||
line = line.strip()
|
|
||||||
if line.startswith('cpu '):
|
|
||||||
parts = list(map(int, line.split()[1:]))
|
def cpu_load():
|
||||||
user_n = parts[0]
|
"""
|
||||||
sys_n = parts[2]
|
Returns the current cpuload
|
||||||
idle_n = parts[3]
|
"""
|
||||||
tot = user_n + sys_n + idle_n
|
parts0 = _cpu_stat()
|
||||||
return (user_n + sys_n) / tot
|
time.sleep(0.1)
|
||||||
return 0
|
parts1 = _cpu_stat()
|
||||||
|
parts_diff = [p1 - p0 for (p0, p1) in zip(parts0, parts1)]
|
||||||
|
user, nice, sys, idle, iowait, irq, softirq, steal, _guest, _guest_nice = parts_diff
|
||||||
|
idle_sum = idle + iowait
|
||||||
|
non_idle_sum = user + nice + sys + irq + softirq + steal
|
||||||
|
total = idle_sum + non_idle_sum
|
||||||
|
return non_idle_sum / total
|
||||||
|
|
||||||
|
|
||||||
def temperature(celsius=True):
|
def temperature(celsius=True):
|
||||||
@@ -99,7 +106,15 @@ def temperature(celsius=True):
|
|||||||
|
|
||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
|
logging.warning("syncing...")
|
||||||
|
|
||||||
|
from pwnagotchi import fs
|
||||||
|
for m in fs.mounts:
|
||||||
|
m.sync()
|
||||||
|
|
||||||
logging.warning("shutting down ...")
|
logging.warning("shutting down ...")
|
||||||
|
|
||||||
|
from pwnagotchi.ui import view
|
||||||
if view.ROOT:
|
if view.ROOT:
|
||||||
view.ROOT.on_shutdown()
|
view.ROOT.on_shutdown()
|
||||||
# give it some time to refresh the ui
|
# give it some time to refresh the ui
|
||||||
@@ -109,7 +124,7 @@ def shutdown():
|
|||||||
|
|
||||||
|
|
||||||
def restart(mode):
|
def restart(mode):
|
||||||
logging.warning("restarting in %s mode ..." % mode)
|
logging.warning("restarting in %s mode ...", mode)
|
||||||
|
|
||||||
if mode == 'AUTO':
|
if mode == 'AUTO':
|
||||||
os.system("touch /root/.pwnagotchi-auto")
|
os.system("touch /root/.pwnagotchi-auto")
|
||||||
@@ -123,10 +138,11 @@ def restart(mode):
|
|||||||
def reboot(mode=None):
|
def reboot(mode=None):
|
||||||
if mode is not None:
|
if mode is not None:
|
||||||
mode = mode.upper()
|
mode = mode.upper()
|
||||||
logging.warning("rebooting in %s mode ..." % mode)
|
logging.warning("rebooting in %s mode ...", mode)
|
||||||
else:
|
else:
|
||||||
logging.warning("rebooting ...")
|
logging.warning("rebooting ...")
|
||||||
|
|
||||||
|
from pwnagotchi.ui import view
|
||||||
if view.ROOT:
|
if view.ROOT:
|
||||||
view.ROOT.on_rebooting()
|
view.ROOT.on_rebooting()
|
||||||
# give it some time to refresh the ui
|
# give it some time to refresh the ui
|
||||||
|
1
pwnagotchi/_version.py
Normal file
1
pwnagotchi/_version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = '1.5.1'
|
@@ -3,6 +3,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
import _thread
|
import _thread
|
||||||
|
|
||||||
import pwnagotchi
|
import pwnagotchi
|
||||||
@@ -30,7 +31,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
AsyncTrainer.__init__(self, config)
|
AsyncTrainer.__init__(self, config)
|
||||||
|
|
||||||
self._started_at = time.time()
|
self._started_at = time.time()
|
||||||
self._filter = None if config['main']['filter'] is None else re.compile(config['main']['filter'])
|
self._filter = None if not config['main']['filter'] else re.compile(config['main']['filter'])
|
||||||
self._current_channel = 0
|
self._current_channel = 0
|
||||||
self._tot_aps = 0
|
self._tot_aps = 0
|
||||||
self._aps_on_channel = 0
|
self._aps_on_channel = 0
|
||||||
@@ -49,9 +50,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
if not os.path.exists(config['bettercap']['handshakes']):
|
if not os.path.exists(config['bettercap']['handshakes']):
|
||||||
os.makedirs(config['bettercap']['handshakes'])
|
os.makedirs(config['bettercap']['handshakes'])
|
||||||
|
|
||||||
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), self.fingerprint(), pwnagotchi.version))
|
logging.info("%s@%s (v%s)", pwnagotchi.name(), self.fingerprint(), pwnagotchi.__version__)
|
||||||
for _, plugin in plugins.loaded.items():
|
for _, plugin in plugins.loaded.items():
|
||||||
logging.debug("plugin '%s' v%s" % (plugin.__class__.__name__, plugin.__version__))
|
logging.debug("plugin '%s' v%s", plugin.__class__.__name__, plugin.__version__)
|
||||||
|
|
||||||
def config(self):
|
def config(self):
|
||||||
return self._config
|
return self._config
|
||||||
@@ -63,12 +64,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
return self._supported_channels
|
return self._supported_channels
|
||||||
|
|
||||||
def setup_events(self):
|
def setup_events(self):
|
||||||
logging.info("connecting to %s ..." % self.url)
|
logging.info("connecting to %s ...", self.url)
|
||||||
|
|
||||||
for tag in self._config['bettercap']['silence']:
|
for tag in self._config['bettercap']['silence']:
|
||||||
try:
|
try:
|
||||||
self.run('events.ignore %s' % tag, verbose_errors=False)
|
self.run('events.ignore %s' % tag, verbose_errors=False)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _reset_wifi_settings(self):
|
def _reset_wifi_settings(self):
|
||||||
@@ -90,7 +91,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
s = self.session()
|
s = self.session()
|
||||||
for iface in s['interfaces']:
|
for iface in s['interfaces']:
|
||||||
if iface['name'] == mon_iface:
|
if iface['name'] == mon_iface:
|
||||||
logging.info("found monitor interface: %s" % iface['name'])
|
logging.info("found monitor interface: %s", iface['name'])
|
||||||
has_mon = True
|
has_mon = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -99,11 +100,11 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
logging.info("starting monitor interface ...")
|
logging.info("starting monitor interface ...")
|
||||||
self.run('!%s' % mon_start_cmd)
|
self.run('!%s' % mon_start_cmd)
|
||||||
else:
|
else:
|
||||||
logging.info("waiting for monitor interface %s ..." % mon_iface)
|
logging.info("waiting for monitor interface %s ...", mon_iface)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
logging.info("supported channels: %s" % self._supported_channels)
|
logging.info("supported channels: %s", self._supported_channels)
|
||||||
logging.info("handshakes will be collected inside %s" % self._config['bettercap']['handshakes'])
|
logging.info("handshakes will be collected inside %s", self._config['bettercap']['handshakes'])
|
||||||
|
|
||||||
self._reset_wifi_settings()
|
self._reset_wifi_settings()
|
||||||
|
|
||||||
@@ -121,9 +122,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
def _wait_bettercap(self):
|
def _wait_bettercap(self):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
s = self.session()
|
_s = self.session()
|
||||||
return
|
return
|
||||||
except:
|
except Exception:
|
||||||
logging.info("waiting for bettercap API to be available ...")
|
logging.info("waiting for bettercap API to be available ...")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
@@ -134,6 +135,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
self.set_starting()
|
self.set_starting()
|
||||||
self.start_monitor_mode()
|
self.start_monitor_mode()
|
||||||
self.start_event_polling()
|
self.start_event_polling()
|
||||||
|
self.start_session_fetcher()
|
||||||
# print initial stats
|
# print initial stats
|
||||||
self.next_epoch()
|
self.next_epoch()
|
||||||
self.set_ready()
|
self.set_ready()
|
||||||
@@ -151,14 +153,14 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
|
|
||||||
if not channels:
|
if not channels:
|
||||||
self._current_channel = 0
|
self._current_channel = 0
|
||||||
logging.debug("RECON %ds" % recon_time)
|
logging.debug("RECON %ds", recon_time)
|
||||||
self.run('wifi.recon.channel clear')
|
self.run('wifi.recon.channel clear')
|
||||||
else:
|
else:
|
||||||
logging.debug("RECON %ds ON CHANNELS %s" % (recon_time, ','.join(map(str, channels))))
|
logging.debug("RECON %ds ON CHANNELS %s", recon_time, ','.join(map(str, channels)))
|
||||||
try:
|
try:
|
||||||
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
|
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("error")
|
logging.exception("Error while setting wifi.recon.channels (%s)", e)
|
||||||
|
|
||||||
self.wait_for(recon_time, sleeping=False)
|
self.wait_for(recon_time, sleeping=False)
|
||||||
|
|
||||||
@@ -188,7 +190,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
if self._filter_included(ap):
|
if self._filter_included(ap):
|
||||||
aps.append(ap)
|
aps.append(ap)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("error")
|
logging.exception("Error while getting acces points (%s)", e)
|
||||||
|
|
||||||
aps.sort(key=lambda ap: ap['channel'])
|
aps.sort(key=lambda ap: ap['channel'])
|
||||||
return self.set_access_points(aps)
|
return self.set_access_points(aps)
|
||||||
@@ -212,7 +214,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
ch = ap['channel']
|
ch = ap['channel']
|
||||||
# if we're sticking to a channel, skip anything
|
# if we're sticking to a channel, skip anything
|
||||||
# which is not on that channel
|
# which is not on that channel
|
||||||
if channels != [] and ch not in channels:
|
if channels and ch not in channels:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ch not in grouped:
|
if ch not in grouped:
|
||||||
@@ -274,7 +276,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
pwnagotchi.reboot()
|
pwnagotchi.reboot()
|
||||||
|
|
||||||
def _save_recovery_data(self):
|
def _save_recovery_data(self):
|
||||||
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
|
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
|
||||||
with open(RECOVERY_DATA_FILE, 'w') as fp:
|
with open(RECOVERY_DATA_FILE, 'w') as fp:
|
||||||
data = {
|
data = {
|
||||||
'started_at': self._started_at,
|
'started_at': self._started_at,
|
||||||
@@ -289,7 +291,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
try:
|
try:
|
||||||
with open(RECOVERY_DATA_FILE, 'rt') as fp:
|
with open(RECOVERY_DATA_FILE, 'rt') as fp:
|
||||||
data = json.load(fp)
|
data = json.load(fp)
|
||||||
logging.info("found recovery data: %s" % data)
|
logging.info("found recovery data: %s", data)
|
||||||
self._started_at = data['started_at']
|
self._started_at = data['started_at']
|
||||||
self._epoch.epoch = data['epoch']
|
self._epoch.epoch = data['epoch']
|
||||||
self._handshakes = data['handshakes']
|
self._handshakes = data['handshakes']
|
||||||
@@ -297,66 +299,73 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
self._last_pwnd = data['last_pwnd']
|
self._last_pwnd = data['last_pwnd']
|
||||||
|
|
||||||
if delete:
|
if delete:
|
||||||
logging.info("deleting %s" % RECOVERY_DATA_FILE)
|
logging.info("deleting %s", RECOVERY_DATA_FILE)
|
||||||
os.unlink(RECOVERY_DATA_FILE)
|
os.unlink(RECOVERY_DATA_FILE)
|
||||||
except:
|
except:
|
||||||
if not no_exceptions:
|
if not no_exceptions:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _event_poller(self):
|
|
||||||
self._load_recovery_data()
|
|
||||||
|
|
||||||
|
def start_session_fetcher(self):
|
||||||
|
_thread.start_new_thread(self._fetch_stats, ())
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_stats(self):
|
||||||
|
while True:
|
||||||
|
s = self.session()
|
||||||
|
self._update_uptime(s)
|
||||||
|
self._update_advertisement(s)
|
||||||
|
self._update_peers()
|
||||||
|
self._update_counters()
|
||||||
|
self._update_handshakes(0)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
async def _on_event(self, msg):
|
||||||
|
found_handshake = False
|
||||||
|
jmsg = json.loads(msg)
|
||||||
|
|
||||||
|
if jmsg['tag'] == 'wifi.client.handshake':
|
||||||
|
filename = jmsg['data']['file']
|
||||||
|
sta_mac = jmsg['data']['station']
|
||||||
|
ap_mac = jmsg['data']['ap']
|
||||||
|
key = "%s -> %s" % (sta_mac, ap_mac)
|
||||||
|
if key not in self._handshakes:
|
||||||
|
self._handshakes[key] = jmsg
|
||||||
|
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
|
||||||
|
if ap_and_station is None:
|
||||||
|
logging.warning("!!! captured new handshake: %s !!!", key)
|
||||||
|
self._last_pwnd = ap_mac
|
||||||
|
plugins.on('handshake', self, filename, ap_mac, sta_mac)
|
||||||
|
else:
|
||||||
|
(ap, sta) = ap_and_station
|
||||||
|
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
||||||
|
'hostname'] != '<hidden>' else ap_mac
|
||||||
|
logging.warning(
|
||||||
|
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
|
||||||
|
ap['channel'],
|
||||||
|
ap['rssi'],
|
||||||
|
sta['mac'], sta['vendor'],
|
||||||
|
ap['hostname'], ap['mac'], ap['vendor'])
|
||||||
|
plugins.on('handshake', self, filename, ap, sta)
|
||||||
|
found_handshake = True
|
||||||
|
self._update_handshakes(1 if found_handshake else 0)
|
||||||
|
|
||||||
|
def _event_poller(self, loop):
|
||||||
|
self._load_recovery_data()
|
||||||
self.run('events.clear')
|
self.run('events.clear')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
new_shakes = 0
|
|
||||||
|
|
||||||
logging.debug("polling events ...")
|
logging.debug("polling events ...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
s = self.session()
|
loop.create_task(self.start_websocket(self._on_event))
|
||||||
self._update_uptime(s)
|
loop.run_forever()
|
||||||
|
except Exception as ex:
|
||||||
self._update_advertisement(s)
|
logging.debug("Error while polling via websocket (%s)", ex)
|
||||||
self._update_peers()
|
|
||||||
self._update_counters()
|
|
||||||
|
|
||||||
for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
|
|
||||||
filename = h['data']['file']
|
|
||||||
sta_mac = h['data']['station']
|
|
||||||
ap_mac = h['data']['ap']
|
|
||||||
key = "%s -> %s" % (sta_mac, ap_mac)
|
|
||||||
|
|
||||||
if key not in self._handshakes:
|
|
||||||
self._handshakes[key] = h
|
|
||||||
new_shakes += 1
|
|
||||||
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
|
|
||||||
if ap_and_station is None:
|
|
||||||
logging.warning("!!! captured new handshake: %s !!!" % key)
|
|
||||||
self._last_pwnd = ap_mac
|
|
||||||
plugins.on('handshake', self, filename, ap_mac, sta_mac)
|
|
||||||
else:
|
|
||||||
(ap, sta) = ap_and_station
|
|
||||||
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
|
||||||
'hostname'] != '<hidden>' else ap_mac
|
|
||||||
logging.warning(
|
|
||||||
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!" % (
|
|
||||||
ap['channel'],
|
|
||||||
ap['rssi'],
|
|
||||||
sta['mac'], sta['vendor'],
|
|
||||||
ap['hostname'], ap['mac'], ap['vendor']))
|
|
||||||
plugins.on('handshake', self, filename, ap, sta)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error("error: %s" % e)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self._update_handshakes(new_shakes)
|
|
||||||
|
|
||||||
def start_event_polling(self):
|
def start_event_polling(self):
|
||||||
_thread.start_new_thread(self._event_poller, ())
|
# start a thread and pass in the mainloop
|
||||||
|
_thread.start_new_thread(self._event_poller, (asyncio.new_event_loop(),))
|
||||||
|
|
||||||
|
|
||||||
def is_module_running(self, module):
|
def is_module_running(self, module):
|
||||||
s = self.session()
|
s = self.session()
|
||||||
@@ -392,15 +401,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
|
|
||||||
def associate(self, ap, throttle=0):
|
def associate(self, ap, throttle=0):
|
||||||
if self.is_stale():
|
if self.is_stale():
|
||||||
logging.debug("recon is stale, skipping assoc(%s)" % ap['mac'])
|
logging.debug("recon is stale, skipping assoc(%s)", ap['mac'])
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
|
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
|
||||||
self._view.on_assoc(ap)
|
self._view.on_assoc(ap)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm..." % ( \
|
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']))
|
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi'])
|
||||||
self.run('wifi.assoc %s' % ap['mac'])
|
self.run('wifi.assoc %s' % ap['mac'])
|
||||||
self._epoch.track(assoc=True)
|
self._epoch.track(assoc=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -413,15 +422,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
|
|
||||||
def deauth(self, ap, sta, throttle=0):
|
def deauth(self, ap, sta, throttle=0):
|
||||||
if self.is_stale():
|
if self.is_stale():
|
||||||
logging.debug("recon is stale, skipping deauth(%s)" % sta['mac'])
|
logging.debug("recon is stale, skipping deauth(%s)", sta['mac'])
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
|
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
|
||||||
self._view.on_deauth(sta)
|
self._view.on_deauth(sta)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ..." % (
|
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']))
|
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi'])
|
||||||
self.run('wifi.deauth %s' % sta['mac'])
|
self.run('wifi.deauth %s' % sta['mac'])
|
||||||
self._epoch.track(deauth=True)
|
self._epoch.track(deauth=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -434,7 +443,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
|
|
||||||
def set_channel(self, channel, verbose=True):
|
def set_channel(self, channel, verbose=True):
|
||||||
if self.is_stale():
|
if self.is_stale():
|
||||||
logging.debug("recon is stale, skipping set_channel(%d)" % channel)
|
logging.debug("recon is stale, skipping set_channel(%d)", channel)
|
||||||
return
|
return
|
||||||
|
|
||||||
# if in the previous loop no client stations has been deauthenticated
|
# if in the previous loop no client stations has been deauthenticated
|
||||||
@@ -450,12 +459,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
if channel != self._current_channel:
|
if channel != self._current_channel:
|
||||||
if self._current_channel != 0 and wait > 0:
|
if self._current_channel != 0 and wait > 0:
|
||||||
if verbose:
|
if verbose:
|
||||||
logging.info("waiting for %ds on channel %d ..." % (wait, self._current_channel))
|
logging.info("waiting for %ds on channel %d ...", wait, self._current_channel)
|
||||||
else:
|
else:
|
||||||
logging.debug("waiting for %ds on channel %d ..." % (wait, self._current_channel))
|
logging.debug("waiting for %ds on channel %d ...", wait, self._current_channel)
|
||||||
self.wait_for(wait)
|
self.wait_for(wait)
|
||||||
if verbose and self._epoch.any_activity:
|
if verbose and self._epoch.any_activity:
|
||||||
logging.info("CHANNEL %d" % channel)
|
logging.info("CHANNEL %d", channel)
|
||||||
try:
|
try:
|
||||||
self.run('wifi.recon.channel %d' % channel)
|
self.run('wifi.recon.channel %d' % channel)
|
||||||
self._current_channel = channel
|
self._current_channel = channel
|
||||||
@@ -465,4 +474,4 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
|||||||
plugins.on('channel_hop', self, channel)
|
plugins.on('channel_hop', self, channel)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("error: %s" % e)
|
logging.error("Error while setting channel (%s)", e)
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import warnings
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/40426502/is-there-a-way-to-suppress-the-messages-tensorflow-prints/40426709
|
# https://stackoverflow.com/questions/40426502/is-there-a-way-to-suppress-the-messages-tensorflow-prints/40426709
|
||||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # or any {'0', '1', '2'}
|
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # or any {'0', '1', '2'}
|
||||||
# https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
|
|
||||||
warnings.simplefilter(action='ignore', category=FutureWarning)
|
|
||||||
|
|
||||||
|
|
||||||
def load(config, agent, epoch, from_disk=True):
|
def load(config, agent, epoch, from_disk=True):
|
||||||
@@ -59,7 +56,7 @@ def load(config, agent, epoch, from_disk=True):
|
|||||||
|
|
||||||
return a2c
|
return a2c
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("error while starting AI")
|
logging.exception("error while starting AI (%s)", e)
|
||||||
|
|
||||||
logging.warning("[ai] AI not loaded!")
|
logging.warning("[ai] AI not loaded!")
|
||||||
return False
|
return False
|
||||||
|
@@ -19,6 +19,10 @@ class Epoch(object):
|
|||||||
self.active_for = 0
|
self.active_for = 0
|
||||||
# number of epochs with no visible access points
|
# number of epochs with no visible access points
|
||||||
self.blind_for = 0
|
self.blind_for = 0
|
||||||
|
# number of epochs in sad state
|
||||||
|
self.sad_for = 0
|
||||||
|
# number of epochs in bored state
|
||||||
|
self.bored_for = 0
|
||||||
# did deauth in this epoch in the current channel?
|
# did deauth in this epoch in the current channel?
|
||||||
self.did_deauth = False
|
self.did_deauth = False
|
||||||
# number of deauths in this epoch
|
# number of deauths in this epoch
|
||||||
@@ -99,13 +103,13 @@ class Epoch(object):
|
|||||||
try:
|
try:
|
||||||
aps_per_chan[ch_idx] += 1.0
|
aps_per_chan[ch_idx] += 1.0
|
||||||
sta_per_chan[ch_idx] += len(ap['clients'])
|
sta_per_chan[ch_idx] += len(ap['clients'])
|
||||||
except IndexError as e:
|
except IndexError:
|
||||||
logging.error("got data on channel %d, we can store %d channels" % (ap['channel'], wifi.NumChannels))
|
logging.error("got data on channel %d, we can store %d channels" % (ap['channel'], wifi.NumChannels))
|
||||||
|
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
try:
|
try:
|
||||||
peers_per_chan[peer.last_channel - 1] += 1.0
|
peers_per_chan[peer.last_channel - 1] += 1.0
|
||||||
except IndexError as e:
|
except IndexError:
|
||||||
logging.error(
|
logging.error(
|
||||||
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
|
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
|
||||||
|
|
||||||
@@ -157,6 +161,20 @@ class Epoch(object):
|
|||||||
else:
|
else:
|
||||||
self.active_for += 1
|
self.active_for += 1
|
||||||
self.inactive_for = 0
|
self.inactive_for = 0
|
||||||
|
self.sad_for = 0
|
||||||
|
self.bored_for = 0
|
||||||
|
|
||||||
|
if self.inactive_for >= self.config['personality']['sad_num_epochs']:
|
||||||
|
# sad > bored; cant be sad and bored
|
||||||
|
self.bored_for = 0
|
||||||
|
self.sad_for += 1
|
||||||
|
elif self.inactive_for >= self.config['personality']['bored_num_epochs']:
|
||||||
|
# sad_treshhold > inactive > bored_treshhold; cant be sad and bored
|
||||||
|
self.sad_for = 0
|
||||||
|
self.bored_for += 1
|
||||||
|
else:
|
||||||
|
self.sad_for = 0
|
||||||
|
self.bored_for = 0
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
cpu = pwnagotchi.cpu_load()
|
cpu = pwnagotchi.cpu_load()
|
||||||
@@ -172,6 +190,8 @@ class Epoch(object):
|
|||||||
'blind_for_epochs': self.blind_for,
|
'blind_for_epochs': self.blind_for,
|
||||||
'inactive_for_epochs': self.inactive_for,
|
'inactive_for_epochs': self.inactive_for,
|
||||||
'active_for_epochs': self.active_for,
|
'active_for_epochs': self.active_for,
|
||||||
|
'sad_for_epochs': self.sad_for,
|
||||||
|
'bored_for_epochs': self.bored_for,
|
||||||
'missed_interactions': self.num_missed,
|
'missed_interactions': self.num_missed,
|
||||||
'num_hops': self.num_hops,
|
'num_hops': self.num_hops,
|
||||||
'num_peers': self.num_peers,
|
'num_peers': self.num_peers,
|
||||||
@@ -188,13 +208,15 @@ class Epoch(object):
|
|||||||
self._epoch_data['reward'] = self._reward(self.epoch + 1, self._epoch_data)
|
self._epoch_data['reward'] = self._reward(self.epoch + 1, self._epoch_data)
|
||||||
self._epoch_data_ready.set()
|
self._epoch_data_ready.set()
|
||||||
|
|
||||||
logging.info("[epoch %d] duration=%s slept_for=%s blind=%d inactive=%d active=%d peers=%d tot_bond=%.2f "
|
logging.info("[epoch %d] duration=%s slept_for=%s blind=%d sad=%d bored=%d inactive=%d active=%d peers=%d tot_bond=%.2f "
|
||||||
"avg_bond=%.2f hops=%d missed=%d deauths=%d assocs=%d handshakes=%d cpu=%d%% mem=%d%% "
|
"avg_bond=%.2f hops=%d missed=%d deauths=%d assocs=%d handshakes=%d cpu=%d%% mem=%d%% "
|
||||||
"temperature=%dC reward=%s" % (
|
"temperature=%dC reward=%s" % (
|
||||||
self.epoch,
|
self.epoch,
|
||||||
utils.secs_to_hhmmss(self.epoch_duration),
|
utils.secs_to_hhmmss(self.epoch_duration),
|
||||||
utils.secs_to_hhmmss(self.num_slept),
|
utils.secs_to_hhmmss(self.num_slept),
|
||||||
self.blind_for,
|
self.blind_for,
|
||||||
|
self.sad_for,
|
||||||
|
self.bored_for,
|
||||||
self.inactive_for,
|
self.inactive_for,
|
||||||
self.active_for,
|
self.active_for,
|
||||||
self.num_peers,
|
self.num_peers,
|
||||||
|
@@ -18,4 +18,10 @@ class RewardFunction(object):
|
|||||||
m = -.3 * (state['missed_interactions'] / tot_interactions)
|
m = -.3 * (state['missed_interactions'] / tot_interactions)
|
||||||
i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
|
i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
|
||||||
|
|
||||||
return h + a + c + b + i + m
|
# include emotions if state >= 5 epochs
|
||||||
|
_sad = state['sad_for_epochs'] if state['sad_for_epochs'] >= 5 else 0
|
||||||
|
_bored = state['bored_for_epochs'] if state['bored_for_epochs'] >= 5 else 0
|
||||||
|
s = -.2 * (_sad / tot_epochs)
|
||||||
|
l = -.1 * (_bored / tot_epochs)
|
||||||
|
|
||||||
|
return h + a + c + b + i + m + s + l
|
||||||
|
@@ -176,7 +176,7 @@ class AsyncTrainer(object):
|
|||||||
self.set_training(True, epochs_per_episode)
|
self.set_training(True, epochs_per_episode)
|
||||||
self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
|
self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("[ai] error while training")
|
logging.exception("[ai] error while training (%s)", e)
|
||||||
finally:
|
finally:
|
||||||
self.set_training(False)
|
self.set_training(False)
|
||||||
obs = self._model.env.reset()
|
obs = self._model.env.reset()
|
||||||
|
@@ -12,19 +12,18 @@ class Automata(object):
|
|||||||
self._epoch = Epoch(config)
|
self._epoch = Epoch(config)
|
||||||
|
|
||||||
def _on_miss(self, who):
|
def _on_miss(self, who):
|
||||||
logging.info("it looks like %s is not in range anymore :/" % who)
|
logging.info("it looks like %s is not in range anymore :/", who)
|
||||||
self._epoch.track(miss=True)
|
self._epoch.track(miss=True)
|
||||||
self._view.on_miss(who)
|
self._view.on_miss(who)
|
||||||
|
|
||||||
def _on_error(self, who, e):
|
def _on_error(self, who, e):
|
||||||
error = "%s" % e
|
|
||||||
# when we're trying to associate or deauth something that is not in range anymore
|
# when we're trying to associate or deauth something that is not in range anymore
|
||||||
# (if we are moving), we get the following error from bettercap:
|
# (if we are moving), we get the following error from bettercap:
|
||||||
# error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list.
|
# error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list.
|
||||||
if 'is an unknown BSSID' in error:
|
if 'is an unknown BSSID' in str(e):
|
||||||
self._on_miss(who)
|
self._on_miss(who)
|
||||||
else:
|
else:
|
||||||
logging.error("%s" % e)
|
logging.error(e)
|
||||||
|
|
||||||
def set_starting(self):
|
def set_starting(self):
|
||||||
self._view.on_starting()
|
self._view.on_starting()
|
||||||
@@ -58,7 +57,7 @@ class Automata(object):
|
|||||||
def set_bored(self):
|
def set_bored(self):
|
||||||
factor = self._epoch.inactive_for / self._config['personality']['bored_num_epochs']
|
factor = self._epoch.inactive_for / self._config['personality']['bored_num_epochs']
|
||||||
if not self._has_support_network_for(factor):
|
if not self._has_support_network_for(factor):
|
||||||
logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for)
|
logging.warning("%d epochs with no activity -> bored", self._epoch.inactive_for)
|
||||||
self._view.on_bored()
|
self._view.on_bored()
|
||||||
plugins.on('bored', self)
|
plugins.on('bored', self)
|
||||||
else:
|
else:
|
||||||
@@ -68,7 +67,7 @@ class Automata(object):
|
|||||||
def set_sad(self):
|
def set_sad(self):
|
||||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||||
if not self._has_support_network_for(factor):
|
if not self._has_support_network_for(factor):
|
||||||
logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for)
|
logging.warning("%d epochs with no activity -> sad", self._epoch.inactive_for)
|
||||||
self._view.on_sad()
|
self._view.on_sad()
|
||||||
plugins.on('sad', self)
|
plugins.on('sad', self)
|
||||||
else:
|
else:
|
||||||
@@ -77,7 +76,7 @@ class Automata(object):
|
|||||||
|
|
||||||
def set_angry(self, factor):
|
def set_angry(self, factor):
|
||||||
if not self._has_support_network_for(factor):
|
if not self._has_support_network_for(factor):
|
||||||
logging.warning("%d epochs with no activity -> angry" % self._epoch.inactive_for)
|
logging.warning("%d epochs with no activity -> angry", self._epoch.inactive_for)
|
||||||
self._view.on_angry()
|
self._view.on_angry()
|
||||||
plugins.on('angry', self)
|
plugins.on('angry', self)
|
||||||
else:
|
else:
|
||||||
@@ -85,7 +84,7 @@ class Automata(object):
|
|||||||
self.set_grateful()
|
self.set_grateful()
|
||||||
|
|
||||||
def set_excited(self):
|
def set_excited(self):
|
||||||
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
|
logging.warning("%d epochs with activity -> excited", self._epoch.active_for)
|
||||||
self._view.on_excited()
|
self._view.on_excited()
|
||||||
plugins.on('excited', self)
|
plugins.on('excited', self)
|
||||||
|
|
||||||
@@ -118,17 +117,17 @@ class Automata(object):
|
|||||||
if factor >= 2.0:
|
if factor >= 2.0:
|
||||||
self.set_angry(factor)
|
self.set_angry(factor)
|
||||||
else:
|
else:
|
||||||
logging.warning("agent missed %d interactions -> lonely" % did_miss)
|
logging.warning("agent missed %d interactions -> lonely", did_miss)
|
||||||
self.set_lonely()
|
self.set_lonely()
|
||||||
# after X times being bored, the status is set to sad or angry
|
# after X times being bored, the status is set to sad or angry
|
||||||
elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
|
elif self._epoch.sad_for:
|
||||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||||
if factor >= 2.0:
|
if factor >= 2.0:
|
||||||
self.set_angry(factor)
|
self.set_angry(factor)
|
||||||
else:
|
else:
|
||||||
self.set_sad()
|
self.set_sad()
|
||||||
# after X times being inactive, the status is set to bored
|
# after X times being inactive, the status is set to bored
|
||||||
elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']:
|
elif self._epoch.bored_for:
|
||||||
self.set_bored()
|
self.set_bored()
|
||||||
# after X times being active, the status is set to happy / excited
|
# after X times being active, the status is set to happy / excited
|
||||||
elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']:
|
elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']:
|
||||||
@@ -139,6 +138,6 @@ class Automata(object):
|
|||||||
plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
|
plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
|
||||||
|
|
||||||
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
|
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
|
||||||
logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for)
|
logging.critical("%d epochs without visible access points -> rebooting ...", self._epoch.blind_for)
|
||||||
self._reboot()
|
self._reboot()
|
||||||
self._epoch.blind_for = 0
|
self._epoch.blind_for = 0
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
import websockets
|
||||||
|
|
||||||
from requests.auth import HTTPBasicAuth
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
|
|
||||||
@@ -25,15 +28,25 @@ class Client(object):
|
|||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.url = "%s://%s:%d/api" % (scheme, hostname, port)
|
self.url = "%s://%s:%d/api" % (scheme, hostname, port)
|
||||||
|
self.websocket = "ws://%s:%s@%s:%d/api" % (username, password, hostname, port)
|
||||||
self.auth = HTTPBasicAuth(username, password)
|
self.auth = HTTPBasicAuth(username, password)
|
||||||
|
|
||||||
def session(self):
|
def session(self):
|
||||||
r = requests.get("%s/session" % self.url, auth=self.auth)
|
r = requests.get("%s/session" % self.url, auth=self.auth)
|
||||||
return decode(r)
|
return decode(r)
|
||||||
|
|
||||||
def events(self):
|
async def start_websocket(self, consumer):
|
||||||
r = requests.get("%s/events" % self.url, auth=self.auth)
|
s = "%s/events" % self.websocket
|
||||||
return decode(r)
|
while True:
|
||||||
|
try:
|
||||||
|
async with websockets.connect(s, ping_interval=60, ping_timeout=90) as ws:
|
||||||
|
async for msg in ws:
|
||||||
|
try:
|
||||||
|
await consumer(msg)
|
||||||
|
except Exception as ex:
|
||||||
|
logging.debug("Error while parsing event (%s)", ex)
|
||||||
|
except websockets.exceptions.ConnectionClosedError:
|
||||||
|
logging.debug("Lost websocket connection. Reconnecting...")
|
||||||
|
|
||||||
def run(self, command, verbose_errors=True):
|
def run(self, command, verbose_errors=True):
|
||||||
r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command})
|
r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command})
|
||||||
|
230
pwnagotchi/defaults.toml
Normal file
230
pwnagotchi/defaults.toml
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
main.name = ""
|
||||||
|
main.lang = "en"
|
||||||
|
main.confd = "/etc/pwnagotchi/conf.d/"
|
||||||
|
main.custom_plugins = ""
|
||||||
|
main.iface = "mon0"
|
||||||
|
main.mon_start_cmd = "/usr/bin/monstart"
|
||||||
|
main.mon_stop_cmd = "/usr/bin/monstop"
|
||||||
|
main.mon_max_blind_epochs = 50
|
||||||
|
main.no_restart = false
|
||||||
|
main.whitelist = [
|
||||||
|
"EXAMPLE_NETWORK",
|
||||||
|
"ANOTHER_EXAMPLE_NETWORK",
|
||||||
|
"fo:od:ba:be:fo:od",
|
||||||
|
"fo:od:ba"
|
||||||
|
]
|
||||||
|
main.filter = ""
|
||||||
|
|
||||||
|
main.plugins.grid.enabled = true
|
||||||
|
main.plugins.grid.report = false
|
||||||
|
main.plugins.grid.exclude = [
|
||||||
|
"YourHomeNetworkHere"
|
||||||
|
]
|
||||||
|
|
||||||
|
main.plugins.auto-update.enabled = true
|
||||||
|
main.plugins.auto-update.install = true
|
||||||
|
main.plugins.auto-update.interval = 1
|
||||||
|
|
||||||
|
main.plugins.net-pos.enabled = false
|
||||||
|
main.plugins.net-pos.api_key = "test"
|
||||||
|
|
||||||
|
main.plugins.gps.enabled = false
|
||||||
|
main.plugins.gps.speed = 19200
|
||||||
|
main.plugins.gps.device = "/dev/ttyUSB0"
|
||||||
|
|
||||||
|
main.plugins.webgpsmap.enabled = false
|
||||||
|
|
||||||
|
main.plugins.onlinehashcrack.enabled = false
|
||||||
|
main.plugins.onlinehashcrack.email = ""
|
||||||
|
main.plugins.onlinehashcrack.dashboard = ""
|
||||||
|
main.plugins.onlinehashcrack.single_files = false
|
||||||
|
main.plugins.onlinehashcrack.whitelist = []
|
||||||
|
|
||||||
|
main.plugins.wpa-sec.enabled = false
|
||||||
|
main.plugins.wpa-sec.api_key = ""
|
||||||
|
main.plugins.wpa-sec.api_url = "https://wpa-sec.stanev.org"
|
||||||
|
main.plugins.wpa-sec.download_results = false
|
||||||
|
main.plugins.wpa-sec.whitelist = []
|
||||||
|
|
||||||
|
main.plugins.wigle.enabled = false
|
||||||
|
main.plugins.wigle.api_key = ""
|
||||||
|
main.plugins.wigle.whitelist = []
|
||||||
|
|
||||||
|
main.plugins.bt-tether.enabled = false
|
||||||
|
|
||||||
|
main.plugins.bt-tether.devices.android-phone.enabled = false
|
||||||
|
main.plugins.bt-tether.devices.android-phone.search_order = 1
|
||||||
|
main.plugins.bt-tether.devices.android-phone.mac = ""
|
||||||
|
main.plugins.bt-tether.devices.android-phone.ip = "192.168.44.44"
|
||||||
|
main.plugins.bt-tether.devices.android-phone.netmask = 24
|
||||||
|
main.plugins.bt-tether.devices.android-phone.interval = 1
|
||||||
|
main.plugins.bt-tether.devices.android-phone.scantime = 10
|
||||||
|
main.plugins.bt-tether.devices.android-phone.max_tries = 10
|
||||||
|
main.plugins.bt-tether.devices.android-phone.share_internet = false
|
||||||
|
main.plugins.bt-tether.devices.android-phone.priority = 1
|
||||||
|
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.enabled = false
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.search_order = 2
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.mac = ""
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.ip = "172.20.10.6"
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.netmask = 24
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.interval = 5
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.scantime = 20
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.max_tries = 0
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.share_internet = false
|
||||||
|
main.plugins.bt-tether.devices.ios-phone.priority = 999
|
||||||
|
|
||||||
|
main.plugins.memtemp.enabled = false
|
||||||
|
main.plugins.memtemp.scale = "celsius"
|
||||||
|
main.plugins.memtemp.orientation = "horizontal"
|
||||||
|
|
||||||
|
main.plugins.paw-gps.enabled = false
|
||||||
|
main.plugins.paw-gps.ip = ""
|
||||||
|
|
||||||
|
main.plugins.gpio_buttons.enabled = false
|
||||||
|
|
||||||
|
main.plugins.led.enabled = true
|
||||||
|
main.plugins.led.led = 0
|
||||||
|
main.plugins.led.delay = 200
|
||||||
|
main.plugins.led.patterns.loaded = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.updating = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.unread_inbox = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.ready = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.ai_ready = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.ai_training_start = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.ai_best_reward = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.ai_worst_reward = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.bored = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.sad = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.excited = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.lonely = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.rebooting = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.wait = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.sleep = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.wifi_update = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.association = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.deauthentication = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.handshake = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.epoch = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.peer_detected = "oo oo oo oo oo oo oo"
|
||||||
|
main.plugins.led.patterns.peer_lost = "oo oo oo oo oo oo oo"
|
||||||
|
|
||||||
|
main.plugins.logtail.enabled = false
|
||||||
|
|
||||||
|
main.plugins.session-stats.enabled = true
|
||||||
|
main.plugins.session-stats.save_directory = "/var/tmp/pwnagotchi/sessions/"
|
||||||
|
|
||||||
|
main.log.path = "/var/log/pwnagotchi.log"
|
||||||
|
main.log.rotation.enabled = true
|
||||||
|
main.log.rotation.size = "10M"
|
||||||
|
|
||||||
|
ai.enabled = true
|
||||||
|
ai.path = "/root/brain.nn"
|
||||||
|
ai.laziness = 0.1
|
||||||
|
ai.epochs_per_episode = 50
|
||||||
|
|
||||||
|
ai.params.gamma = 0.99
|
||||||
|
ai.params.n_steps = 1
|
||||||
|
ai.params.vf_coef = 0.25
|
||||||
|
ai.params.ent_coef = 0.01
|
||||||
|
ai.params.max_grad_norm = 0.5
|
||||||
|
ai.params.learning_rate = 0.001
|
||||||
|
ai.params.alpha = 0.99
|
||||||
|
ai.params.epsilon = 0.00001
|
||||||
|
ai.params.verbose = 1
|
||||||
|
ai.params.lr_schedule = "constant"
|
||||||
|
|
||||||
|
personality.advertise = true
|
||||||
|
personality.deauth = true
|
||||||
|
personality.associate = true
|
||||||
|
personality.channels = []
|
||||||
|
personality.min_rssi = -200
|
||||||
|
personality.ap_ttl = 120
|
||||||
|
personality.sta_ttl = 300
|
||||||
|
personality.recon_time = 30
|
||||||
|
personality.max_inactive_scale = 2
|
||||||
|
personality.recon_inactive_multiplier = 2
|
||||||
|
personality.hop_recon_time = 10
|
||||||
|
personality.min_recon_time = 5
|
||||||
|
personality.max_interactions = 3
|
||||||
|
personality.max_misses_for_recon = 5
|
||||||
|
personality.excited_num_epochs = 10
|
||||||
|
personality.bored_num_epochs = 15
|
||||||
|
personality.sad_num_epochs = 25
|
||||||
|
personality.bond_encounters_factor = 20000
|
||||||
|
|
||||||
|
ui.fps = 0.0
|
||||||
|
ui.font.name = "DejaVuSansMono" # for japanese: fonts-japanese-gothic
|
||||||
|
ui.font.size_offset = 0 # will be added to the font size
|
||||||
|
|
||||||
|
ui.faces.look_r = "( ⚆_⚆)"
|
||||||
|
ui.faces.look_l = "(☉_☉ )"
|
||||||
|
ui.faces.look_r_happy = "( ◕‿◕)"
|
||||||
|
ui.faces.look_l_happy = "(◕‿◕ )"
|
||||||
|
ui.faces.sleep = "(⇀‿‿↼)"
|
||||||
|
ui.faces.sleep2 = "(≖‿‿≖)"
|
||||||
|
ui.faces.awake = "(◕‿‿◕)"
|
||||||
|
ui.faces.bored = "(-__-)"
|
||||||
|
ui.faces.intense = "(°▃▃°)"
|
||||||
|
ui.faces.cool = "(⌐■_■)"
|
||||||
|
ui.faces.happy = "(•‿‿•)"
|
||||||
|
ui.faces.excited = "(ᵔ◡◡ᵔ)"
|
||||||
|
ui.faces.grateful = "(^‿‿^)"
|
||||||
|
ui.faces.motivated = "(☼‿‿☼)"
|
||||||
|
ui.faces.demotivated = "(≖__≖)"
|
||||||
|
ui.faces.smart = "(✜‿‿✜)"
|
||||||
|
ui.faces.lonely = "(ب__ب)"
|
||||||
|
ui.faces.sad = "(╥☁╥ )"
|
||||||
|
ui.faces.angry = "(-_-')"
|
||||||
|
ui.faces.friend = "(♥‿‿♥)"
|
||||||
|
ui.faces.broken = "(☓‿‿☓)"
|
||||||
|
ui.faces.debug = "(#__#)"
|
||||||
|
|
||||||
|
ui.web.enabled = true
|
||||||
|
ui.web.address = "0.0.0.0"
|
||||||
|
ui.web.username = "changeme"
|
||||||
|
ui.web.password = "changeme"
|
||||||
|
ui.web.origin = ""
|
||||||
|
ui.web.port = 8080
|
||||||
|
ui.web.on_frame = ""
|
||||||
|
|
||||||
|
ui.display.enabled = true
|
||||||
|
ui.display.rotation = 180
|
||||||
|
ui.display.type = "waveshare_2"
|
||||||
|
ui.display.color = "black"
|
||||||
|
|
||||||
|
bettercap.scheme = "http"
|
||||||
|
bettercap.hostname = "localhost"
|
||||||
|
bettercap.port = 8081
|
||||||
|
bettercap.username = "pwnagotchi"
|
||||||
|
bettercap.password = "pwnagotchi"
|
||||||
|
bettercap.handshakes = "/root/handshakes"
|
||||||
|
bettercap.silence = [
|
||||||
|
"ble.device.new",
|
||||||
|
"ble.device.lost",
|
||||||
|
"ble.device.disconnected",
|
||||||
|
"ble.device.connected",
|
||||||
|
"ble.device.service.discovered",
|
||||||
|
"ble.device.characteristic.discovered",
|
||||||
|
"wifi.client.new",
|
||||||
|
"wifi.client.lost",
|
||||||
|
"wifi.client.probe",
|
||||||
|
"wifi.ap.new",
|
||||||
|
"wifi.ap.lost",
|
||||||
|
"mod.started"
|
||||||
|
]
|
||||||
|
|
||||||
|
fs.memory.enabled = false
|
||||||
|
fs.memory.mounts.log.enabled = false
|
||||||
|
fs.memory.mounts.log.mount = "/var/log"
|
||||||
|
fs.memory.mounts.log.size = "50M"
|
||||||
|
fs.memory.mounts.log.sync = 60
|
||||||
|
fs.memory.mounts.log.zram = true
|
||||||
|
fs.memory.mounts.log.rsync = true
|
||||||
|
|
||||||
|
fs.memory.mounts.data.enabled = false
|
||||||
|
fs.memory.mounts.data.mount = "/var/tmp/pwnagotchi"
|
||||||
|
fs.memory.mounts.data.size = "10M"
|
||||||
|
fs.memory.mounts.data.sync = 3600
|
||||||
|
fs.memory.mounts.data.zram = false
|
||||||
|
fs.memory.mounts.data.rsync = true
|
@@ -1,293 +0,0 @@
|
|||||||
# WARNING WARNING WARNING WARNING
|
|
||||||
#
|
|
||||||
# This file is recreated with default settings on every pwnagotchi restart,
|
|
||||||
# use /etc/pwnagotchi/config.yml to configure this unit.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# main algorithm configuration
|
|
||||||
main:
|
|
||||||
# if set this will set the hostname of the unit. min length is 2, max length 25, only a-zA-Z0-9- allowed
|
|
||||||
name: ''
|
|
||||||
# currently implemented: en (default), de, el, fr, it, mk, nl, ru, se, pt-BR, es, pt
|
|
||||||
lang: en
|
|
||||||
# custom plugins path, if null only default plugins with be loaded
|
|
||||||
custom_plugins:
|
|
||||||
# which plugins to load and enable
|
|
||||||
plugins:
|
|
||||||
grid:
|
|
||||||
enabled: true
|
|
||||||
report: false # don't report pwned networks by default!
|
|
||||||
exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
|
|
||||||
- YourHomeNetworkHere
|
|
||||||
auto-update:
|
|
||||||
enabled: true
|
|
||||||
install: true # if false, it will only warn that updates are available, if true it will install them
|
|
||||||
interval: 1 # every 1 hour
|
|
||||||
net-pos:
|
|
||||||
enabled: false
|
|
||||||
api_key: 'test'
|
|
||||||
gps:
|
|
||||||
enabled: false
|
|
||||||
speed: 19200
|
|
||||||
device: /dev/ttyUSB0
|
|
||||||
webgpsmap:
|
|
||||||
enabled: false
|
|
||||||
onlinehashcrack:
|
|
||||||
enabled: false
|
|
||||||
email: ~
|
|
||||||
wpa-sec:
|
|
||||||
enabled: false
|
|
||||||
api_key: ~
|
|
||||||
api_url: "https://wpa-sec.stanev.org"
|
|
||||||
wigle:
|
|
||||||
enabled: false
|
|
||||||
api_key: ~
|
|
||||||
bt-tether:
|
|
||||||
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
|
|
||||||
paw-gps:
|
|
||||||
enabled: false
|
|
||||||
#The IP Address of your phone with Paw Server running, default (option is empty) is 192.168.44.1
|
|
||||||
ip: ''
|
|
||||||
gpio_buttons:
|
|
||||||
enabled: false
|
|
||||||
#The following is a list of the GPIO number for your button, and the command you want to run when it is pressed
|
|
||||||
gpios:
|
|
||||||
#20: 'touch /root/.pwnagotchi-auto && systemctl restart pwnagotchi'
|
|
||||||
#21: 'shutdown -h now'
|
|
||||||
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
|
|
||||||
# if true, will not restart the wifi module
|
|
||||||
no_restart: false
|
|
||||||
# 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
|
|
||||||
enabled: true
|
|
||||||
path: /root/brain.nn
|
|
||||||
# 1.0 - laziness = probability of start training
|
|
||||||
laziness: 0.1
|
|
||||||
# how many epochs to train on
|
|
||||||
epochs_per_episode: 50
|
|
||||||
params:
|
|
||||||
# discount factor
|
|
||||||
gamma: 0.99
|
|
||||||
# the number of steps to run for each environment per update
|
|
||||||
n_steps: 1
|
|
||||||
# value function coefficient for the loss calculation
|
|
||||||
vf_coef: 0.25
|
|
||||||
# entropy coefficient for the loss calculation
|
|
||||||
ent_coef: 0.01
|
|
||||||
# maximum value for the gradient clipping
|
|
||||||
max_grad_norm: 0.5
|
|
||||||
# the learning rate
|
|
||||||
learning_rate: 0.0010
|
|
||||||
# rmsprop decay parameter
|
|
||||||
alpha: 0.99
|
|
||||||
# rmsprop epsilon
|
|
||||||
epsilon: 0.00001
|
|
||||||
# the verbosity level: 0 none, 1 training information, 2 tensorflow debug
|
|
||||||
verbose: 1
|
|
||||||
# type of scheduler for the learning rate update ('linear', 'constant', 'double_linear_con', 'middle_drop' or 'double_middle_drop')
|
|
||||||
lr_schedule: 'constant'
|
|
||||||
# the log location for tensorboard (if None, no logging)
|
|
||||||
tensorboard_log: null
|
|
||||||
|
|
||||||
personality:
|
|
||||||
# advertise our presence
|
|
||||||
advertise: true
|
|
||||||
# perform a deauthentication attack to client stations in order to get full or half handshakes
|
|
||||||
deauth: true
|
|
||||||
# send association frames to APs in order to get the PMKID
|
|
||||||
associate: true
|
|
||||||
# list of channels to recon on, or empty for all channels
|
|
||||||
channels: []
|
|
||||||
# minimum WiFi signal strength in dBm
|
|
||||||
min_rssi: -200
|
|
||||||
# number of seconds for wifi.ap.ttl
|
|
||||||
ap_ttl: 120
|
|
||||||
# number of seconds for wifi.sta.ttl
|
|
||||||
sta_ttl: 300
|
|
||||||
# time in seconds to wait during channel recon
|
|
||||||
recon_time: 30
|
|
||||||
# number of inactive epochs after which recon_time gets multiplied by recon_inactive_multiplier
|
|
||||||
max_inactive_scale: 2
|
|
||||||
# if more than max_inactive_scale epochs are inactive, recon_time *= recon_inactive_multiplier
|
|
||||||
recon_inactive_multiplier: 2
|
|
||||||
# time in seconds to wait during channel hopping if activity has been performed
|
|
||||||
hop_recon_time: 10
|
|
||||||
# time in seconds to wait during channel hopping if no activity has been performed
|
|
||||||
min_recon_time: 5
|
|
||||||
# maximum amount of deauths/associations per BSSID per session
|
|
||||||
max_interactions: 3
|
|
||||||
# maximum amount of misses before considering the data stale and triggering a new recon
|
|
||||||
max_misses_for_recon: 5
|
|
||||||
# number of active epochs that triggers the excited state
|
|
||||||
excited_num_epochs: 10
|
|
||||||
# number of inactive epochs that triggers the bored state
|
|
||||||
bored_num_epochs: 15
|
|
||||||
# number of inactive epochs that triggers the sad state
|
|
||||||
sad_num_epochs: 25
|
|
||||||
# number of encounters (times met on a channel) with another unit before considering it a friend and bond
|
|
||||||
# also used for cumulative bonding score of nearby units
|
|
||||||
bond_encounters_factor: 20000
|
|
||||||
|
|
||||||
# ui configuration
|
|
||||||
ui:
|
|
||||||
# here you can customize the faces
|
|
||||||
faces:
|
|
||||||
look_r: '( ⚆_⚆)'
|
|
||||||
look_l: '(☉_☉ )'
|
|
||||||
look_r_happy: '( ◕‿◕)'
|
|
||||||
look_l_happy: '(◕‿◕ )'
|
|
||||||
sleep: '(⇀‿‿↼)'
|
|
||||||
sleep2: '(≖‿‿≖)'
|
|
||||||
awake: '(◕‿‿◕)'
|
|
||||||
bored: '(-__-)'
|
|
||||||
intense: '(°▃▃°)'
|
|
||||||
cool: '(⌐■_■)'
|
|
||||||
happy: '(•‿‿•)'
|
|
||||||
excited: '(ᵔ◡◡ᵔ)'
|
|
||||||
grateful: '(^‿‿^)'
|
|
||||||
motivated: '(☼‿‿☼)'
|
|
||||||
demotivated: '(≖__≖)'
|
|
||||||
smart: '(✜‿‿✜)'
|
|
||||||
lonely: '(ب__ب)'
|
|
||||||
sad: '(╥☁╥ )'
|
|
||||||
angry: "(-_-')"
|
|
||||||
friend: '(♥‿‿♥)'
|
|
||||||
broken: '(☓‿‿☓)'
|
|
||||||
debug: '(#__#)'
|
|
||||||
# ePaper display can update every 3 secs anyway, set to 0 to only refresh for major data changes
|
|
||||||
# IMPORTANT: The lifespan of an eINK display depends on the cumulative amount of refreshes. If you want to
|
|
||||||
# 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, 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'
|
|
||||||
|
|
||||||
# bettercap rest api configuration
|
|
||||||
bettercap:
|
|
||||||
# api scheme://hostname:port username and password
|
|
||||||
scheme: http
|
|
||||||
hostname: localhost
|
|
||||||
port: 8081
|
|
||||||
username: pwnagotchi
|
|
||||||
password: pwnagotchi
|
|
||||||
# folder where bettercap stores the WPA handshakes, given that
|
|
||||||
# wifi.handshakes.aggregate will be set to false and individual
|
|
||||||
# pcap files will be created in order to minimize the chances
|
|
||||||
# of a single pcap file to get corrupted
|
|
||||||
handshakes: /root/handshakes
|
|
||||||
# events to mute in bettercap's events stream
|
|
||||||
silence:
|
|
||||||
- ble.device.new
|
|
||||||
- ble.device.lost
|
|
||||||
- ble.device.disconnected
|
|
||||||
- ble.device.connected
|
|
||||||
- ble.device.service.discovered
|
|
||||||
- ble.device.characteristic.discovered
|
|
||||||
- wifi.client.new
|
|
||||||
- wifi.client.lost
|
|
||||||
- wifi.client.probe
|
|
||||||
- wifi.ap.new
|
|
||||||
- wifi.ap.lost
|
|
||||||
- mod.started
|
|
190
pwnagotchi/fs/__init__.py
Normal file
190
pwnagotchi/fs/__init__.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import contextlib
|
||||||
|
import shutil
|
||||||
|
import _thread
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
from distutils.dir_util import copy_tree
|
||||||
|
|
||||||
|
mounts = list()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def ensure_write(filename, mode='w'):
|
||||||
|
path = os.path.dirname(filename)
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=path)
|
||||||
|
|
||||||
|
with os.fdopen(fd, mode) as f:
|
||||||
|
yield f
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
|
os.replace(tmp, filename)
|
||||||
|
|
||||||
|
|
||||||
|
def size_of(path):
|
||||||
|
"""
|
||||||
|
Calculate the sum of all the files in path
|
||||||
|
"""
|
||||||
|
total = 0
|
||||||
|
for root, _, files in os.walk(path):
|
||||||
|
for f in files:
|
||||||
|
total += os.path.getsize(os.path.join(root, f))
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def is_mountpoint(path):
|
||||||
|
"""
|
||||||
|
Checks if path is mountpoint
|
||||||
|
"""
|
||||||
|
return os.system(f"mountpoint -q {path}") == 0
|
||||||
|
|
||||||
|
|
||||||
|
def setup_mounts(config):
|
||||||
|
"""
|
||||||
|
Sets up all the configured mountpoints
|
||||||
|
"""
|
||||||
|
global mounts
|
||||||
|
fs_cfg = config['fs']['memory']
|
||||||
|
if not fs_cfg['enabled']:
|
||||||
|
return
|
||||||
|
|
||||||
|
for name, options in fs_cfg['mounts'].items():
|
||||||
|
if not options['enabled']:
|
||||||
|
continue
|
||||||
|
logging.debug("[FS] Trying to setup mount %s (%s)", name, options['mount'])
|
||||||
|
size,unit = re.match(r"(\d+)([a-zA-Z]+)", options['size']).groups()
|
||||||
|
target = os.path.join('/run/pwnagotchi/disk/', os.path.basename(options['mount']))
|
||||||
|
|
||||||
|
is_mounted = is_mountpoint(target)
|
||||||
|
logging.debug("[FS] %s is %s mounted", options['mount'],
|
||||||
|
"already" if is_mounted else "not yet")
|
||||||
|
|
||||||
|
m = MemoryFS(
|
||||||
|
options['mount'],
|
||||||
|
target,
|
||||||
|
size=options['size'],
|
||||||
|
zram=options['zram'],
|
||||||
|
zram_disk_size=f"{int(size)*2}{unit}",
|
||||||
|
rsync=options['rsync'])
|
||||||
|
|
||||||
|
if not is_mounted:
|
||||||
|
if not m.mount():
|
||||||
|
logging.debug(f"Error while mounting {m.mountpoint}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not m.sync(to_ram=True):
|
||||||
|
logging.debug(f"Error while syncing to {m.mountpoint}")
|
||||||
|
m.umount()
|
||||||
|
continue
|
||||||
|
|
||||||
|
interval = int(options['sync'])
|
||||||
|
if interval:
|
||||||
|
logging.debug("[FS] Starting thread to sync %s (interval: %d)",
|
||||||
|
options['mount'], interval)
|
||||||
|
_thread.start_new_thread(m.daemonize, (interval,))
|
||||||
|
else:
|
||||||
|
logging.debug("[FS] Not syncing %s, because interval is 0",
|
||||||
|
options['mount'])
|
||||||
|
|
||||||
|
mounts.append(m)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryFS:
|
||||||
|
@staticmethod
|
||||||
|
def zram_install():
|
||||||
|
if not os.path.exists("/sys/class/zram-control"):
|
||||||
|
logging.debug("[FS] Installing zram")
|
||||||
|
return os.system("modprobe zram") == 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def zram_dev():
|
||||||
|
logging.debug("[FS] Adding zram device")
|
||||||
|
return open("/sys/class/zram-control/hot_add", "rt").read().strip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, mount, disk, size="40M",
|
||||||
|
zram=True, zram_alg="lz4", zram_disk_size="100M",
|
||||||
|
zram_fs_type="ext4", rsync=True):
|
||||||
|
self.mountpoint = mount
|
||||||
|
self.disk = disk
|
||||||
|
self.size = size
|
||||||
|
self.zram = zram
|
||||||
|
self.zram_alg = zram_alg
|
||||||
|
self.zram_disk_size = zram_disk_size
|
||||||
|
self.zram_fs_type = zram_fs_type
|
||||||
|
self.zdev = None
|
||||||
|
self.rsync = True
|
||||||
|
self._setup()
|
||||||
|
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
if self.zram and MemoryFS.zram_install():
|
||||||
|
# setup zram
|
||||||
|
self.zdev = MemoryFS.zram_dev()
|
||||||
|
open(f"/sys/block/zram{self.zdev}/comp_algorithm", "wt").write(self.zram_alg)
|
||||||
|
open(f"/sys/block/zram{self.zdev}/disksize", "wt").write(self.zram_disk_size)
|
||||||
|
open(f"/sys/block/zram{self.zdev}/mem_limit", "wt").write(self.size)
|
||||||
|
logging.debug("[FS] Creating fs (type: %s)", self.zram_fs_type)
|
||||||
|
os.system(f"mke2fs -t {self.zram_fs_type} /dev/zram{self.zdev} >/dev/null 2>&1")
|
||||||
|
|
||||||
|
# ensure mountpoints exist
|
||||||
|
if not os.path.exists(self.disk):
|
||||||
|
logging.debug("[FS] Creating %s", self.disk)
|
||||||
|
os.makedirs(self.disk)
|
||||||
|
|
||||||
|
if not os.path.exists(self.mountpoint):
|
||||||
|
logging.debug("[FS] Creating %s", self.mountpoint)
|
||||||
|
os.makedirs(self.mountpoint)
|
||||||
|
|
||||||
|
|
||||||
|
def daemonize(self, interval=60):
|
||||||
|
logging.debug("[FS] Daemonized...")
|
||||||
|
while True:
|
||||||
|
self.sync()
|
||||||
|
sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(self, to_ram=False):
|
||||||
|
source, dest = (self.disk, self.mountpoint) if to_ram else (self.mountpoint, self.disk)
|
||||||
|
needed, actually_free = size_of(source), shutil.disk_usage(dest)[2]
|
||||||
|
if actually_free >= needed:
|
||||||
|
logging.debug("[FS] Syning %s -> %s", source,dest)
|
||||||
|
if self.rsync:
|
||||||
|
os.system(f"rsync -aXv --inplace --no-whole-file --delete-after {source}/ {dest}/ >/dev/null 2>&1")
|
||||||
|
else:
|
||||||
|
copy_tree(source, dest, preserve_symlinks=True)
|
||||||
|
os.system("sync")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def mount(self):
|
||||||
|
if os.system(f"mount --bind {self.mountpoint} {self.disk}"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if os.system(f"mount --make-private {self.disk}"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.zram and self.zdev is not None:
|
||||||
|
if os.system(f"mount -t {self.zram_fs_type} -o nosuid,noexec,nodev,user=pwnagotchi /dev/zram{self.zdev} {self.mountpoint}/"):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if os.system(f"mount -t tmpfs -o nosuid,noexec,nodev,mode=0755,size={self.size} pwnagotchi {self.mountpoint}/"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def umount(self):
|
||||||
|
if os.system(f"umount -l {self.mountpoint}"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if os.system(f"umount -l {self.disk}"):
|
||||||
|
return False
|
||||||
|
return True
|
@@ -85,7 +85,7 @@ def update_data(last_session):
|
|||||||
},
|
},
|
||||||
'uname': subprocess.getoutput("uname -a"),
|
'uname': subprocess.getoutput("uname -a"),
|
||||||
'brain': brain,
|
'brain': brain,
|
||||||
'version': pwnagotchi.version
|
'version': pwnagotchi.__version__
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.debug("updating grid data: %s" % data)
|
logging.debug("updating grid data: %s" % data)
|
||||||
|
Binary file not shown.
@@ -177,7 +177,7 @@ msgstr "Супер, имаме {num} нови handshake{plural}!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Имате {count} нови съобщения!"
|
msgstr "Имате {count} нови съобщения!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Упс, нещо се обърка ... Рестартиране ..."
|
msgstr "Упс, нещо се обърка ... Рестартиране ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -177,7 +177,7 @@ msgstr "太酷了, 我们抓到了{num}新的猎物{plural}!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "主人,有{count}新消息{plural}!"
|
msgstr "主人,有{count}新消息{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "行动,额等等有点小问题... 重启ing ..."
|
msgstr "行动,额等等有点小问题... 重启ing ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: 0.0.1\n"
|
"Project-Id-Version: 0.0.1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
"POT-Creation-Date: 2019-11-14 21:15+0100\n"
|
||||||
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
|
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
|
||||||
"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
|
"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
|
||||||
"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
|
"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
|
||||||
@@ -20,13 +20,13 @@ msgid "ZzzzZZzzzzZzzz"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||||
msgstr "Hi, ich bin ein Pwnagotchi! Starte ..."
|
msgstr "Hi, ich bin ein Pwnagotchi! Starte..."
|
||||||
|
|
||||||
msgid "New day, new hunt, new pwns!"
|
msgid "New day, new hunt, new pwns!"
|
||||||
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
|
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
|
||||||
|
|
||||||
msgid "Hack the Planet!"
|
msgid "Hack the Planet!"
|
||||||
msgstr "Hack den Planet!"
|
msgstr "Hack den Planeten!"
|
||||||
|
|
||||||
msgid "AI ready."
|
msgid "AI ready."
|
||||||
msgstr "KI bereit."
|
msgstr "KI bereit."
|
||||||
@@ -35,23 +35,30 @@ msgid "The neural network is ready."
|
|||||||
msgstr "Das neurale Netz ist bereit."
|
msgstr "Das neurale Netz ist bereit."
|
||||||
|
|
||||||
msgid "Generating keys, do not turn off ..."
|
msgid "Generating keys, do not turn off ..."
|
||||||
msgstr "Generiere Keys, nicht ausschalten ..."
|
msgstr "Generiere Schlüssel, nicht ausschalten..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
msgstr "Hey, Channel {channel} ist frei! Dein AP wir des dir danken."
|
msgstr "Hey, Channel {channel} ist frei! Dein AP wird es Dir danken."
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr "Lese die Logs der letzten Session..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "Bisher {lines_so_far} Zeilen im Log gelesen..."
|
||||||
|
|
||||||
msgid "I'm bored ..."
|
msgid "I'm bored ..."
|
||||||
msgstr "Mir ist langweilig..."
|
msgstr "Mir ist langweilig..."
|
||||||
|
|
||||||
msgid "Let's go for a walk!"
|
msgid "Let's go for a walk!"
|
||||||
msgstr "Lass uns laufen gehen!"
|
msgstr "Lass uns spazieren gehen!"
|
||||||
|
|
||||||
msgid "This is the best day of my life!"
|
msgid "This is the best day of my life!"
|
||||||
msgstr "Das ist der beste Tag meines Lebens."
|
msgstr "Das ist der beste Tag meines Lebens!"
|
||||||
|
|
||||||
msgid "Shitty day :/"
|
msgid "Shitty day :/"
|
||||||
msgstr "Scheis Tag :/"
|
msgstr "Scheißtag :/"
|
||||||
|
|
||||||
msgid "I'm extremely bored ..."
|
msgid "I'm extremely bored ..."
|
||||||
msgstr "Mir ist sau langweilig..."
|
msgstr "Mir ist sau langweilig..."
|
||||||
@@ -62,6 +69,13 @@ msgstr "Ich bin sehr traurig..."
|
|||||||
msgid "I'm sad"
|
msgid "I'm sad"
|
||||||
msgstr "Ich bin traurig"
|
msgstr "Ich bin traurig"
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "Lass mich in Ruhe..."
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "Ich bin sauer auf Dich!"
|
||||||
|
|
||||||
msgid "I'm living the life!"
|
msgid "I'm living the life!"
|
||||||
msgstr "Ich lebe das Leben!"
|
msgstr "Ich lebe das Leben!"
|
||||||
|
|
||||||
@@ -69,33 +83,41 @@ msgid "I pwn therefore I am."
|
|||||||
msgstr "Ich pwne, also bin ich."
|
msgstr "Ich pwne, also bin ich."
|
||||||
|
|
||||||
msgid "So many networks!!!"
|
msgid "So many networks!!!"
|
||||||
msgstr "So viele Netwerke!!!"
|
msgstr "So viele Netzwerke!!!"
|
||||||
|
|
||||||
msgid "I'm having so much fun!"
|
msgid "I'm having so much fun!"
|
||||||
msgstr "Ich habe sooo viel Spaß!"
|
msgstr "Ich habe sooo viel Spaß!"
|
||||||
|
|
||||||
msgid "My crime is that of curiosity ..."
|
msgid "My crime is that of curiosity ..."
|
||||||
msgstr "Mein Verbrechen ist das der Neugier ..."
|
msgstr "Mein Verbrechen ist das der Neugier..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hello {name}! Nice to meet you."
|
msgid "Hello {name}! Nice to meet you."
|
||||||
msgstr "Hallo {name}, nett Dich kennenzulernen."
|
msgstr "Hallo {name}, schön Dich kennenzulernen."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {name}! Sup?"
|
||||||
|
msgstr "Jo {name}! Was geht!?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "Hey {name}, wie geht's?"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unit {name} is nearby!"
|
msgid "Unit {name} is nearby!"
|
||||||
msgstr "Gerät {name} ist in der nähe!!"
|
msgstr "Gerät {name} ist in der Nähe!"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Uhm ... goodbye {name}"
|
msgid "Uhm ... goodbye {name}"
|
||||||
msgstr "Uhm ...tschüß {name}"
|
msgstr "Uhm... tschüß {name}"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is gone ..."
|
msgid "{name} is gone ..."
|
||||||
msgstr "{name} ist weg ..."
|
msgstr "{name} ist weg..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Whoops ... {name} is gone."
|
msgid "Whoops ... {name} is gone."
|
||||||
msgstr "Whoops ...{name} ist weg."
|
msgstr "Whoops... {name} ist weg."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} missed!"
|
msgid "{name} missed!"
|
||||||
@@ -111,17 +133,17 @@ msgid "I love my friends!"
|
|||||||
msgstr "Ich liebe meine Freunde!"
|
msgstr "Ich liebe meine Freunde!"
|
||||||
|
|
||||||
msgid "Nobody wants to play with me ..."
|
msgid "Nobody wants to play with me ..."
|
||||||
msgstr "Niemand will mit mir spielen ..."
|
msgstr "Niemand will mit mir spielen..."
|
||||||
|
|
||||||
msgid "I feel so alone ..."
|
msgid "I feel so alone ..."
|
||||||
msgstr "Ich fühl michso alleine ..."
|
msgstr "Ich fühl' mich so allein..."
|
||||||
|
|
||||||
msgid "Where's everybody?!"
|
msgid "Where's everybody?!"
|
||||||
msgstr "Wo sind denn alle?"
|
msgstr "Wo sind denn alle?!"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Napping for {secs}s ..."
|
msgid "Napping for {secs}s ..."
|
||||||
msgstr "Schlafe für {secs}s"
|
msgstr "Schlafe für {secs}s..."
|
||||||
|
|
||||||
msgid "Zzzzz"
|
msgid "Zzzzz"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -138,7 +160,7 @@ msgstr ""
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Waiting for {secs}s ..."
|
msgid "Waiting for {secs}s ..."
|
||||||
msgstr "Warte für {secs}s ..."
|
msgstr "Warte für {secs}s..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Looking around ({secs}s)"
|
msgid "Looking around ({secs}s)"
|
||||||
@@ -158,7 +180,7 @@ msgstr "Jo {what}!"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Just decided that {mac} needs no WiFi!"
|
msgid "Just decided that {mac} needs no WiFi!"
|
||||||
msgstr "Ich denke, dass {mac} kein WiFi brauch!"
|
msgstr "Ich denke, dass {mac} kein WiFi braucht!"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Deauthenticating {mac}"
|
msgid "Deauthenticating {mac}"
|
||||||
@@ -176,16 +198,16 @@ msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
|
msgstr "Ops, da ist was schief gelaufen... Starte neu..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Kicked {num} stations\n"
|
msgid "Kicked {num} stations\n"
|
||||||
msgstr "{num} Stationen gekicked\n"
|
msgstr "{num} Stationen gekickt\n"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Made {num} new friends\n"
|
msgid "Made {num} new friends\n"
|
||||||
msgstr "{num} Freunde gefunden\n"
|
msgstr "{num} neue Freunde gefunden\n"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Got {num} handshakes\n"
|
msgid "Got {num} handshakes\n"
|
||||||
@@ -225,3 +247,4 @@ msgstr "Minute"
|
|||||||
|
|
||||||
msgid "second"
|
msgid "second"
|
||||||
msgstr "Sekunde"
|
msgstr "Sekunde"
|
||||||
|
|
||||||
|
BIN
pwnagotchi/locale/dk/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/dk/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
248
pwnagotchi/locale/dk/LC_MESSAGES/voice.po
Normal file
248
pwnagotchi/locale/dk/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# pwnagotchi danish voice data
|
||||||
|
# Copyright (C) 2020
|
||||||
|
# This file is distributed under the same license as the pwnagotchi package.
|
||||||
|
# Dennis Kjær Jensen <signout@signout.dk>, 2020
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 0.0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
|
||||||
|
"PO-Revision-Date: 2020-01-18 21:56+ZONE\n"
|
||||||
|
"Last-Translator: Dennis Kjær Jensen <signout@signout.dk>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: Danish\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 "Hej. Jeg er Pwnagotchi. Starter ..."
|
||||||
|
|
||||||
|
msgid "New day, new hunt, new pwns!"
|
||||||
|
msgstr "Ny dag, ny jagt, nye pwns!"
|
||||||
|
|
||||||
|
msgid "Hack the Planet!"
|
||||||
|
msgstr "Hack planeten!"
|
||||||
|
|
||||||
|
msgid "AI ready."
|
||||||
|
msgstr "AI klar."
|
||||||
|
|
||||||
|
msgid "The neural network is ready."
|
||||||
|
msgstr "Det neurale netværk er klart."
|
||||||
|
|
||||||
|
msgid "Generating keys, do not turn off ..."
|
||||||
|
msgstr "Genererer nøgler, sluk ikke ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
|
msgstr "Hey, kanal {channel} er ubrugt! Dit AP vil takke dig."
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr "Læser seneste session logs ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "Har læst {lines_so_far} linjer indtil nu ..."
|
||||||
|
|
||||||
|
msgid "I'm bored ..."
|
||||||
|
msgstr "Jeg keder mig ..."
|
||||||
|
|
||||||
|
msgid "Let's go for a walk!"
|
||||||
|
msgstr "Lad os gå en tur!"
|
||||||
|
|
||||||
|
msgid "This is the best day of my life!"
|
||||||
|
msgstr "Det er den bedste dag i mit liv!"
|
||||||
|
|
||||||
|
msgid "Shitty day :/"
|
||||||
|
msgstr "Elendig dag :/"
|
||||||
|
|
||||||
|
msgid "I'm extremely bored ..."
|
||||||
|
msgstr "Jeg keder mig ekstremt meget ..."
|
||||||
|
|
||||||
|
msgid "I'm very sad ..."
|
||||||
|
msgstr "Jeg er meget trist ..."
|
||||||
|
|
||||||
|
msgid "I'm sad"
|
||||||
|
msgstr "Jeg er trist"
|
||||||
|
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "Lad mig være i fred"
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "Jeg er sur på dig!"
|
||||||
|
|
||||||
|
msgid "I'm living the life!"
|
||||||
|
msgstr "Jeg lever livet!"
|
||||||
|
|
||||||
|
msgid "I pwn therefore I am."
|
||||||
|
msgstr "Jeg pwner, derfor er jeg."
|
||||||
|
|
||||||
|
msgid "So many networks!!!"
|
||||||
|
msgstr "Så mange netværk!"
|
||||||
|
|
||||||
|
msgid "I'm having so much fun!"
|
||||||
|
msgstr "Jeg har det vildt sjovt!"
|
||||||
|
|
||||||
|
msgid "My crime is that of curiosity ..."
|
||||||
|
msgstr "Min forbrydelse er at være nysgerrig ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hello {name}! Nice to meet you."
|
||||||
|
msgstr "Hej {name}! Rart at møde dig."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {name}! Sup?"
|
||||||
|
msgstr "Hey {name}! Hvasså?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "Hej {name} hvordan har du det?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Unit {name} is nearby!"
|
||||||
|
msgstr "Enheden {name} er lige i nærheden!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Uhm ... goodbye {name}"
|
||||||
|
msgstr "Uhm ... farvel {name}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} is gone ..."
|
||||||
|
msgstr "{name} er væk ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Whoops ... {name} is gone."
|
||||||
|
msgstr "Hovsa ... {name} er væk."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} missed!"
|
||||||
|
msgstr "{name} glippede!"
|
||||||
|
|
||||||
|
msgid "Missed!"
|
||||||
|
msgstr "Fordømt!"
|
||||||
|
|
||||||
|
msgid "Good friends are a blessing!"
|
||||||
|
msgstr "Gode venner en velsignelse!"
|
||||||
|
|
||||||
|
msgid "I love my friends!"
|
||||||
|
msgstr "Jeg elsker mine venner!"
|
||||||
|
|
||||||
|
msgid "Nobody wants to play with me ..."
|
||||||
|
msgstr "Der er ingen der vil lege med mig ..."
|
||||||
|
|
||||||
|
msgid "I feel so alone ..."
|
||||||
|
msgstr "Jeg føler mig så alene ..."
|
||||||
|
|
||||||
|
msgid "Where's everybody?!"
|
||||||
|
msgstr "Hvor er alle henne?!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Napping for {secs}s ..."
|
||||||
|
msgstr "Sover i {secs} sekunder"
|
||||||
|
|
||||||
|
msgid "Zzzzz"
|
||||||
|
msgstr "Zzzzz"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "ZzzZzzz ({secs}s)"
|
||||||
|
msgstr "ZzzZzzz {secs} sekunder"
|
||||||
|
|
||||||
|
msgid "Good night."
|
||||||
|
msgstr "Godnat."
|
||||||
|
|
||||||
|
msgid "Zzz"
|
||||||
|
msgstr "Zzz"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Waiting for {secs}s ..."
|
||||||
|
msgstr "Venter i {secs} sekunder"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Looking around ({secs}s)"
|
||||||
|
msgstr "Kigger mig omkring i {secs} sekunder"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {what} let's be friends!"
|
||||||
|
msgstr "Hej {what} lad os være venner!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Associating to {what}"
|
||||||
|
msgstr "Associerer til {what}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {what}!"
|
||||||
|
msgstr "Hey {what}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Just decided that {mac} needs no WiFi!"
|
||||||
|
msgstr "Besluttede at {mac} ikke har brug for WiFi!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Deauthenticating {mac}"
|
||||||
|
msgstr "Afmelder {mac}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Kickbanning {mac}!"
|
||||||
|
msgstr "Kickbanner {mac}!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
|
msgstr "Fedt, vi har fået {num} nye handshake{plural}!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "You have {count} new message{plural}!"
|
||||||
|
msgstr "Du har {count} nye beskeder"
|
||||||
|
|
||||||
|
msgid "Ops, something went wrong ... Rebooting ..."
|
||||||
|
msgstr "Ups, noget gik galt ... Genstarter."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Kicked {num} stations\n"
|
||||||
|
msgstr "Sparkede {num} af\n"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Made {num} new friends\n"
|
||||||
|
msgstr "Har fået {num} nye venner\n"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Got {num} handshakes\n"
|
||||||
|
msgstr "Har fået {num} nyehandshakes\n"
|
||||||
|
|
||||||
|
msgid "Met 1 peer"
|
||||||
|
msgstr "Har mødt 1 peer"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Met {num} peers"
|
||||||
|
msgstr "Har mødt {num} peers"
|
||||||
|
|
||||||
|
#, 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 i {duration} og kicket {dauthed} klienter! Jeg har også "
|
||||||
|
"mødt {associated} nye venner og spist {handshakes} håndtryk! #pwnagotchi "
|
||||||
|
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||||
|
|
||||||
|
msgid "hours"
|
||||||
|
msgstr "timer"
|
||||||
|
|
||||||
|
msgid "minutes"
|
||||||
|
msgstr "minutter"
|
||||||
|
|
||||||
|
msgid "seconds"
|
||||||
|
msgstr "sekunder"
|
||||||
|
|
||||||
|
msgid "hour"
|
||||||
|
msgstr "time"
|
||||||
|
|
||||||
|
msgid "minute"
|
||||||
|
msgstr "minut"
|
||||||
|
|
||||||
|
msgid "second"
|
||||||
|
msgstr "sekund"
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Μπανάρω την {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Τέλεια δικέ μου, πήραμε {num} νέες χειραψίες!"
|
msgstr "Τέλεια δικέ μου, πήραμε {num} νέες χειραψίες!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ουπς, κάτιπήγε λάθος ... Επανεκκινούμαι ..."
|
msgstr "Ουπς, κάτιπήγε λάθος ... Επανεκκινούμαι ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -163,7 +163,7 @@ msgstr "Expulsando y banneando a {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Genial, obtuvimos {num} nuevo{plural} handshake{plural}!"
|
msgstr "Genial, obtuvimos {num} nuevo{plural} handshake{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Oops, algo salió mal ... Reiniciándo ..."
|
msgstr "Oops, algo salió mal ... Reiniciándo ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -3,12 +3,11 @@
|
|||||||
# This file is distributed under the same license as the pwnagotchi package.
|
# This file is distributed under the same license as the pwnagotchi package.
|
||||||
# FIRST AUTHOR <7271496+quantumsheep@users.noreply.github.com>, 2019.
|
# FIRST AUTHOR <7271496+quantumsheep@users.noreply.github.com>, 2019.
|
||||||
#
|
#
|
||||||
#,
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: 0.0.1\n"
|
"Project-Id-Version: 0.0.1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2019-10-23 18:37+0200\n"
|
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
|
||||||
"PO-Revision-Date: 2019-10-03 10:34+0200\n"
|
"PO-Revision-Date: 2019-10-03 10:34+0200\n"
|
||||||
"Last-Translator: quantumsheep <7271496+quantumsheep@users.noreply.github."
|
"Last-Translator: quantumsheep <7271496+quantumsheep@users.noreply.github."
|
||||||
"com>\n"
|
"com>\n"
|
||||||
@@ -43,6 +42,13 @@ msgstr "Génération des clés, ne pas éteindre..."
|
|||||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
msgstr "Hey, le canal {channel} est libre! Ton point d'accès va te remercier."
|
msgstr "Hey, le canal {channel} est libre! Ton point d'accès va te remercier."
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr "Lecture des logs de la dernière session ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "Jusqu'ici, {lines_so_far} lignes lues dans le log ..."
|
||||||
|
|
||||||
msgid "I'm bored ..."
|
msgid "I'm bored ..."
|
||||||
msgstr "Je m'ennuie..."
|
msgstr "Je m'ennuie..."
|
||||||
|
|
||||||
@@ -64,8 +70,15 @@ msgstr "Je suis très triste..."
|
|||||||
msgid "I'm sad"
|
msgid "I'm sad"
|
||||||
msgstr "Je suis triste"
|
msgstr "Je suis triste"
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "Lache moi..."
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "Je t'en veux !"
|
||||||
|
|
||||||
msgid "I'm living the life!"
|
msgid "I'm living the life!"
|
||||||
msgstr "Je vis la vie !"
|
msgstr "Je vis la belle vie !"
|
||||||
|
|
||||||
msgid "I pwn therefore I am."
|
msgid "I pwn therefore I am."
|
||||||
msgstr "Je pwn donc je suis."
|
msgstr "Je pwn donc je suis."
|
||||||
@@ -83,6 +96,14 @@ msgstr "Mon crime, c'est la curiosité..."
|
|||||||
msgid "Hello {name}! Nice to meet you."
|
msgid "Hello {name}! Nice to meet you."
|
||||||
msgstr "Bonjour {name} ! Ravi de te rencontrer."
|
msgstr "Bonjour {name} ! Ravi de te rencontrer."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {name}! Sup?"
|
||||||
|
msgstr "Yo {name} ! Quoi de neuf ?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "Hey {name} comment vas-tu ?"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unit {name} is nearby!"
|
msgid "Unit {name} is nearby!"
|
||||||
msgstr "L'unité {name} est proche !"
|
msgstr "L'unité {name} est proche !"
|
||||||
@@ -93,7 +114,7 @@ msgstr "Hum... au revoir {name}"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is gone ..."
|
msgid "{name} is gone ..."
|
||||||
msgstr "{name} est part ..."
|
msgstr "{name} est parti ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Whoops ... {name} is gone."
|
msgid "Whoops ... {name} is gone."
|
||||||
@@ -106,6 +127,12 @@ msgstr "{name} raté !"
|
|||||||
msgid "Missed!"
|
msgid "Missed!"
|
||||||
msgstr "Raté !"
|
msgstr "Raté !"
|
||||||
|
|
||||||
|
msgid "Good friends are a blessing!"
|
||||||
|
msgstr "Les bons amis sont une bénédiction !"
|
||||||
|
|
||||||
|
msgid "I love my friends!"
|
||||||
|
msgstr "J'aime mes amis !"
|
||||||
|
|
||||||
msgid "Nobody wants to play with me ..."
|
msgid "Nobody wants to play with me ..."
|
||||||
msgstr "Personne ne veut jouer avec moi..."
|
msgstr "Personne ne veut jouer avec moi..."
|
||||||
|
|
||||||
@@ -117,14 +144,14 @@ msgstr "Où est tout le monde ?!"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Napping for {secs}s ..."
|
msgid "Napping for {secs}s ..."
|
||||||
msgstr "Fais la sieste pendant {secs}s..."
|
msgstr "Je fais la sieste pendant {secs}s..."
|
||||||
|
|
||||||
msgid "Zzzzz"
|
msgid "Zzzzz"
|
||||||
msgstr ""
|
msgstr "Zzzzz"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "ZzzZzzz ({secs}s)"
|
msgid "ZzzZzzz ({secs}s)"
|
||||||
msgstr ""
|
msgstr "ZzzZzzz ({secs}s)"
|
||||||
|
|
||||||
msgid "Good night."
|
msgid "Good night."
|
||||||
msgstr "Bonne nuit."
|
msgstr "Bonne nuit."
|
||||||
@@ -134,11 +161,11 @@ msgstr "Zzz"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Waiting for {secs}s ..."
|
msgid "Waiting for {secs}s ..."
|
||||||
msgstr "Attends pendant {secs}s..."
|
msgstr "J'attends pendant {secs}s..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Looking around ({secs}s)"
|
msgid "Looking around ({secs}s)"
|
||||||
msgstr "Regarde autour ({secs}s)"
|
msgstr "J'observe ({secs}s)"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hey {what} let's be friends!"
|
msgid "Hey {what} let's be friends!"
|
||||||
@@ -166,13 +193,13 @@ msgstr "Je kick et je bannis {mac} !"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Cool, on a {num} nouveaux handshake{plural} !"
|
msgstr "Cool, on a {num} nouve(l/aux) handshake{plural} !"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Tu as {num} nouveaux message{plural} !"
|
msgstr "Tu as {num} nouveau(x) message{plural} !"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Oups, quelque chose s'est mal passé... Redémarrage..."
|
msgstr "Oups, quelque chose s'est mal passé... Redémarrage..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@@ -181,18 +208,18 @@ msgstr "{num} stations kick\n"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Made {num} new friends\n"
|
msgid "Made {num} new friends\n"
|
||||||
msgstr "Fait {num} nouveaux amis\n"
|
msgstr "A fait {num} nouve(l/aux) ami(s)\n"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Got {num} handshakes\n"
|
msgid "Got {num} handshakes\n"
|
||||||
msgstr "Récupéré {num} handshakes\n"
|
msgstr "A {num} handshakes\n"
|
||||||
|
|
||||||
msgid "Met 1 peer"
|
msgid "Met 1 peer"
|
||||||
msgstr "1 peer rencontré"
|
msgstr "1 camarade rencontré"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Met {num} peers"
|
msgid "Met {num} peers"
|
||||||
msgstr "{num} peers recontrés"
|
msgstr "{num} camarades recontrés"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
|
Binary file not shown.
@@ -164,7 +164,7 @@ msgstr "Chiceáil mé agus cosc mé ar {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Go hiontach, fuaireamar {num} handshake{plural}!"
|
msgstr "Go hiontach, fuaireamar {num} handshake{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Hoips...Tháinig ainghléas éigin..."
|
msgstr "Hoips...Tháinig ainghléas éigin..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
BIN
pwnagotchi/locale/hu/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/hu/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
249
pwnagotchi/locale/hu/LC_MESSAGES/voice.po
Normal file
249
pwnagotchi/locale/hu/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Hungarian translation.
|
||||||
|
# Copyright (C) 2020
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# Skeleton022 <skeleton022.pwnagotchi@gmail.com>, 2020.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 1.4.3\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2020-01-07 20:00+0100\n"
|
||||||
|
"PO-Revision-Date: 2020-03-23 0:10+0100\n"
|
||||||
|
"Last-Translator: Skeleton022\n"
|
||||||
|
"Language-Team: Skeleton022\n"
|
||||||
|
"Language: hungarian\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 "Hali, Pwnagotchi vagyok! Indítás ..."
|
||||||
|
|
||||||
|
msgid "New day, new hunt, new pwns!"
|
||||||
|
msgstr "Új nap, új vadászat, új hálózatok!"
|
||||||
|
|
||||||
|
msgid "Hack the Planet!"
|
||||||
|
msgstr "Törd meg a bolygót!"
|
||||||
|
|
||||||
|
msgid "AI ready."
|
||||||
|
msgstr "MI kész."
|
||||||
|
|
||||||
|
msgid "The neural network is ready."
|
||||||
|
msgstr "A neurális hálózat készen áll."
|
||||||
|
|
||||||
|
msgid "Generating keys, do not turn off ..."
|
||||||
|
msgstr "Kulcspár generálása, ne kapcsold ki az eszközt ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
|
msgstr "A {channel}. számú csatorna üres! Az AP-d meg fogja köszönni."
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "Az utolsó munkamenet logjainak olvasása ..."
|
||||||
|
|
||||||
|
msgid "I'm bored ..."
|
||||||
|
msgstr "Unatkozom ..."
|
||||||
|
|
||||||
|
msgid "Let's go for a walk!"
|
||||||
|
msgstr "Menjünk sétálni!"
|
||||||
|
|
||||||
|
msgid "This is the best day of my life!"
|
||||||
|
msgstr "Ez a legjobb nap az életemben!"
|
||||||
|
|
||||||
|
msgid "Shitty day :/"
|
||||||
|
msgstr "Szar egy nap :/"
|
||||||
|
|
||||||
|
msgid "I'm extremely bored ..."
|
||||||
|
msgstr "Nagyon unatkozom ..."
|
||||||
|
|
||||||
|
msgid "I'm very sad ..."
|
||||||
|
msgstr "Nagyon szomorú vagyok ..."
|
||||||
|
|
||||||
|
msgid "I'm sad"
|
||||||
|
msgstr "Szomorú vagyok"
|
||||||
|
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "Hagyj békén ..."
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "Mérges vagyok rád!"
|
||||||
|
|
||||||
|
msgid "I'm living the life!"
|
||||||
|
msgstr "Élvezem az életet!"
|
||||||
|
|
||||||
|
msgid "I pwn therefore I am."
|
||||||
|
msgstr "Hackelek, tehát vagyok."
|
||||||
|
|
||||||
|
msgid "So many networks!!!"
|
||||||
|
msgstr "Rengeteg hálózat!!!"
|
||||||
|
|
||||||
|
msgid "I'm having so much fun!"
|
||||||
|
msgstr "Nagyon jól érzem magam!"
|
||||||
|
|
||||||
|
msgid "My crime is that of curiosity ..."
|
||||||
|
msgstr "Kíváncsiság a bűnöm ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hello {name}! Nice to meet you."
|
||||||
|
msgstr "Hali {name}! Örülök, hogy találkoztunk."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {name}! Sup?"
|
||||||
|
msgstr "Hé {name}! Mizu?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "Hé {name} hogy vagy?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Unit {name} is nearby!"
|
||||||
|
msgstr "A {name} nevű egység a közelben van!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Uhm ... goodbye {name}"
|
||||||
|
msgstr "Ömm ... ég veled {name}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} is gone ..."
|
||||||
|
msgstr "{name} eltűnt ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Whoops ... {name} is gone."
|
||||||
|
msgstr "Whoops ... {name} eltűnt."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} missed!"
|
||||||
|
msgstr "{name} elhibázva!"
|
||||||
|
|
||||||
|
msgid "Missed!"
|
||||||
|
msgstr "Elvesztettem!"
|
||||||
|
|
||||||
|
msgid "Good friends are a blessing!"
|
||||||
|
msgstr "A jó barátok áldás az életben!"
|
||||||
|
|
||||||
|
msgid "I love my friends!"
|
||||||
|
msgstr "Szeretem a barátaimat!"
|
||||||
|
|
||||||
|
msgid "Nobody wants to play with me ..."
|
||||||
|
msgstr "Senki sem akar játszani velem ..."
|
||||||
|
|
||||||
|
msgid "I feel so alone ..."
|
||||||
|
msgstr "Egyedül vagyok ..."
|
||||||
|
|
||||||
|
msgid "Where's everybody?!"
|
||||||
|
msgstr "Hol vagytok?!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Napping for {secs}s ..."
|
||||||
|
msgstr "{secs} másodpercig szundikálok ..."
|
||||||
|
|
||||||
|
msgid "Zzzzz"
|
||||||
|
msgstr "Zzzzz"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "ZzzZzzz ({secs}s)"
|
||||||
|
msgstr "ZzzZzzz ({secs}msp)"
|
||||||
|
|
||||||
|
msgid "Good night."
|
||||||
|
msgstr "Jó éjszakát."
|
||||||
|
|
||||||
|
msgid "Zzz"
|
||||||
|
msgstr "Zzz"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Waiting for {secs}s ..."
|
||||||
|
msgstr "Várok {secs} másodpercig ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Looking around ({secs}s)"
|
||||||
|
msgstr "Körbenézek {secs} másodpercig"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {what} let's be friends!"
|
||||||
|
msgstr "Hey {what} legyünk barátok!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Associating to {what}"
|
||||||
|
msgstr "Társítás {what} -hoz/-hez"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {what}!"
|
||||||
|
msgstr "Hé {what}!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Just decided that {mac} needs no WiFi!"
|
||||||
|
msgstr "Úgydöntöttem, hogy {mac}-nek nem kell WiFi!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Deauthenticating {mac}"
|
||||||
|
msgstr "Kirúgom {mac}-et"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Kickbanning {mac}!"
|
||||||
|
msgstr "{mac} kirúgva és kitiltva!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
|
msgstr "Király, kaptunk {num} új üzenetet!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "You have {count} new message{plural}!"
|
||||||
|
msgstr "{count} új üzeneted van!"
|
||||||
|
|
||||||
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
|
msgstr "Ops, valami rosszul sikerült ... Újraindítás ..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Kicked {num} stations\n"
|
||||||
|
msgstr "Kirúgva {num} állomás\n"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Made {num} new friends\n"
|
||||||
|
msgstr "{num} új barátot\ntaláltam\n"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Got {num} handshakes\n"
|
||||||
|
msgstr "{num} kézfogást szereztem\n"
|
||||||
|
|
||||||
|
msgid "Met 1 peer"
|
||||||
|
msgstr "1 Társsal találkoztam"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Met {num} peers"
|
||||||
|
msgstr "Találkoztam {num} társsal"
|
||||||
|
|
||||||
|
#, 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 ""
|
||||||
|
"Már {duration} ideje dolgozom, kirúgtam {deauthed} klienst! Találkoztam még"
|
||||||
|
"{associated} új baráttal és elfogtam {handshakes} kézfogást! #pwnagotchi "
|
||||||
|
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||||
|
|
||||||
|
msgid "hours"
|
||||||
|
msgstr "óra"
|
||||||
|
|
||||||
|
msgid "minutes"
|
||||||
|
msgstr "perc"
|
||||||
|
|
||||||
|
msgid "seconds"
|
||||||
|
msgstr "másodperc"
|
||||||
|
|
||||||
|
msgid "hour"
|
||||||
|
msgstr "óra"
|
||||||
|
|
||||||
|
msgid "minute"
|
||||||
|
msgstr "perc"
|
||||||
|
|
||||||
|
msgid "second"
|
||||||
|
msgstr "másodperc"
|
||||||
|
|
Binary file not shown.
@@ -156,7 +156,7 @@ msgstr "Sto prendendo a calci {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Bene, abbiamo {num} handshake{plural} in più!"
|
msgstr "Bene, abbiamo {num} handshake{plural} in più!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ops, qualcosa è andato storto ... Riavvio ..."
|
msgstr "Ops, qualcosa è andato storto ... Riavvio ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -3,12 +3,11 @@
|
|||||||
# This file is distributed under the same license as the PACKAGE package.
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
# FIRST AUTHOR 24534649+wytshadow@users.noreply.github.com, 2019.
|
# FIRST AUTHOR 24534649+wytshadow@users.noreply.github.com, 2019.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: 0.0.1\n"
|
"Project-Id-Version: 0.0.1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2019-10-16 15:05+0200\n"
|
"POT-Creation-Date: 2020-01-25 21:57+0900\n"
|
||||||
"PO-Revision-Date: 2019-10-16 15:05+0200\n"
|
"PO-Revision-Date: 2019-10-16 15:05+0200\n"
|
||||||
"Last-Translator: wytshadow <24534649+wytshadow@users.noreply.github.com>\n"
|
"Last-Translator: wytshadow <24534649+wytshadow@users.noreply.github.com>\n"
|
||||||
"Language-Team: pwnagotchi <24534649+wytshadow@users.noreply.github.com>\n"
|
"Language-Team: pwnagotchi <24534649+wytshadow@users.noreply.github.com>\n"
|
||||||
@@ -21,170 +20,207 @@ msgid "ZzzzZZzzzzZzzz"
|
|||||||
msgstr "すやすや〜"
|
msgstr "すやすや〜"
|
||||||
|
|
||||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||||
msgstr "こんにちは、ポウナゴッチです!始めている。。。"
|
msgstr "僕、 ポーナゴッチです!"
|
||||||
|
|
||||||
msgid "New day, new hunt, new pwns!"
|
msgid "New day, new hunt, new pwns!"
|
||||||
msgstr ""
|
msgstr "ポーンしようよ。"
|
||||||
|
|
||||||
msgid "Hack the Planet!"
|
msgid "Hack the Planet!"
|
||||||
msgstr "ハックザプラネット!"
|
msgstr "ハックザプラネット!"
|
||||||
|
|
||||||
msgid "AI ready."
|
msgid "AI ready."
|
||||||
msgstr "人工知能の準備ができました。"
|
msgstr "AIの準備ができました。"
|
||||||
|
|
||||||
msgid "The neural network is ready."
|
msgid "The neural network is ready."
|
||||||
msgstr "ニューラルネットワークの準備ができました。"
|
msgstr "ニューラルネットワークの\n準備ができました。"
|
||||||
|
|
||||||
|
msgid "Generating keys, do not turn off ..."
|
||||||
|
msgstr "鍵生成をしてます。\n電源を落とさないでね。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
msgstr "ねえ、チャンネル{channel}は無料です! キミのAPは感謝を言います。"
|
msgstr "チャンネル\n {channel} \nはfreeだよ。ありがとうね。"
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr "session log を読んでます。"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "{lines_so_far} 行目長いよぉ。"
|
||||||
|
|
||||||
msgid "I'm bored ..."
|
msgid "I'm bored ..."
|
||||||
msgstr "退屈です。。。"
|
msgstr "退屈だぁ。。。"
|
||||||
|
|
||||||
msgid "Let's go for a walk!"
|
msgid "Let's go for a walk!"
|
||||||
msgstr "散歩に行きましょう!"
|
msgstr "散歩に行こうよ!"
|
||||||
|
|
||||||
msgid "This is the best day of my life!"
|
msgid "This is the best day of my life!"
|
||||||
msgstr "今日は私の人生で最高の日です!"
|
msgstr "人生最高の日だよ!"
|
||||||
|
|
||||||
msgid "Shitty day :/"
|
msgid "Shitty day :/"
|
||||||
msgstr ""
|
msgstr "がっかりな日だよ。orz"
|
||||||
|
|
||||||
msgid "I'm extremely bored ..."
|
msgid "I'm extremely bored ..."
|
||||||
msgstr "とても退屈です。"
|
msgstr "退屈だね。"
|
||||||
|
|
||||||
msgid "I'm very sad ..."
|
msgid "I'm very sad ..."
|
||||||
msgstr "とても悲しいです。。。"
|
msgstr "あ~悲しいよぉ。"
|
||||||
|
|
||||||
msgid "I'm sad"
|
msgid "I'm sad"
|
||||||
msgstr "悲しいです。"
|
msgstr "悲しいね。"
|
||||||
|
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "ひとりぼっちだよ。"
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "怒っちゃうよ。"
|
||||||
|
|
||||||
msgid "I'm living the life!"
|
msgid "I'm living the life!"
|
||||||
msgstr "人生を生きている!"
|
msgstr "わくわくするね。"
|
||||||
|
|
||||||
msgid "I pwn therefore I am."
|
msgid "I pwn therefore I am."
|
||||||
msgstr ""
|
msgstr "ポーンしてこそのオレ。"
|
||||||
|
|
||||||
msgid "So many networks!!!"
|
msgid "So many networks!!!"
|
||||||
msgstr "たくさんネットワークがある!!!"
|
msgstr "たくさん\nWiFiが飛んでるよ!"
|
||||||
|
|
||||||
msgid "I'm having so much fun!"
|
msgid "I'm having so much fun!"
|
||||||
msgstr "とても楽しんでいます!"
|
msgstr "楽しいよぉ!"
|
||||||
|
|
||||||
msgid "My crime is that of curiosity ..."
|
msgid "My crime is that of curiosity ..."
|
||||||
msgstr ""
|
msgstr "APに興味津々..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hello {name}! Nice to meet you. {name}"
|
msgid "Hello {name}! Nice to meet you."
|
||||||
msgstr "こんにちは{name}!初めまして。{name}"
|
msgstr "こんにちは{name}!\n初めまして。{name}"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unit {name} is nearby! {name}"
|
msgid "Yo {name}! Sup?"
|
||||||
msgstr ""
|
msgstr "ねぇねぇ、\n{name} どうしたの?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "{name} こんにちは"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Unit {name} is nearby!"
|
||||||
|
msgstr "{name} が近くにいるよ。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Uhm ... goodbye {name}"
|
msgid "Uhm ... goodbye {name}"
|
||||||
msgstr "ええと。。。さようなら{name}"
|
msgstr "じゃあね、さようなら {name}"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is gone ..."
|
msgid "{name} is gone ..."
|
||||||
msgstr "{name}がなくなった。。。"
|
msgstr "{name}\nがいなくなったよ。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Whoops ... {name} is gone."
|
msgid "Whoops ... {name} is gone."
|
||||||
msgstr "おっと。。。{name}がなくなった。"
|
msgstr "あらら、\n{name}\nがいなくなったね。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} missed!"
|
msgid "{name} missed!"
|
||||||
msgstr "{name}逃した!"
|
msgstr "{name} が逃げた!"
|
||||||
|
|
||||||
msgid "Missed!"
|
msgid "Missed!"
|
||||||
msgstr "逃した!"
|
msgstr "残念、逃した!"
|
||||||
|
|
||||||
|
msgid "Good friends are a blessing!"
|
||||||
|
msgstr "良い仲間にめぐりあえたよ。"
|
||||||
|
|
||||||
|
msgid "I love my friends!"
|
||||||
|
msgstr "友達は大好きだよ。"
|
||||||
|
|
||||||
msgid "Nobody wants to play with me ..."
|
msgid "Nobody wants to play with me ..."
|
||||||
msgstr "誰も僕と一緒にプレーしたくない。。。"
|
msgstr "誰も僕と一緒に\nあそんでくれない。"
|
||||||
|
|
||||||
msgid "I feel so alone ..."
|
msgid "I feel so alone ..."
|
||||||
msgstr "僕は孤独を感じる。。。"
|
msgstr "ひとりぼっちだよ。"
|
||||||
|
|
||||||
msgid "Where's everybody?!"
|
msgid "Where's everybody?!"
|
||||||
msgstr "みんなどこ?!"
|
msgstr "みんなどこにいるの?!"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Napping for {secs}s ..."
|
msgid "Napping for {secs}s ..."
|
||||||
msgstr "{secs}寝ている。"
|
msgstr "{secs}秒 寝ます。"
|
||||||
|
|
||||||
msgid "Zzzzz"
|
msgid "Zzzzz"
|
||||||
msgstr "すや〜"
|
msgstr "ぐぅ〜"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "ZzzZzzz ({secs}s)"
|
msgid "ZzzZzzz ({secs}s)"
|
||||||
msgstr "すやすや〜 ({secs})"
|
msgstr "すやすや〜 ({secs}秒)"
|
||||||
|
|
||||||
msgid "Good night."
|
msgid "Good night."
|
||||||
msgstr "お休みなさい。"
|
msgstr "おやすみなさい。"
|
||||||
|
|
||||||
msgid "Zzz"
|
msgid "Zzz"
|
||||||
msgstr "す〜"
|
msgstr "ぐぅ~"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Waiting for {secs}s ..."
|
msgid "Waiting for {secs}s ..."
|
||||||
msgstr "{secs}を待っている。。。"
|
msgstr "{secs}秒 待ちです。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Looking around ({secs}s)"
|
msgid "Looking around ({secs}s)"
|
||||||
msgstr "{secs}を探している。"
|
msgstr "{secs}秒 探してます。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hey {what} let's be friends!"
|
msgid "Hey {what} let's be friends!"
|
||||||
msgstr "ちょっと{what}友だちになりましょう!"
|
msgstr "ねぇねぇ\n{what} \n友だちになろうよ。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Associating to {what}"
|
msgid "Associating to {what}"
|
||||||
msgstr ""
|
msgstr "{what} \nとつながるかな?"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Yo {what}!"
|
msgid "Yo {what}!"
|
||||||
msgstr "よー{what}!"
|
msgstr "ねぇねぇ\n{what}"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Just decided that {mac} needs no WiFi!"
|
msgid "Just decided that {mac} needs no WiFi!"
|
||||||
msgstr ""
|
msgstr "{mac}\nはWiFiじゃないのね。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Deauthenticating {mac}"
|
msgid "Deauthenticating {mac}"
|
||||||
msgstr ""
|
msgstr "{mac}\nの認証取得中..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Kickbanning {mac}!"
|
msgid "Kickbanning {mac}!"
|
||||||
msgstr ""
|
msgstr "{mac}\nに拒否られた。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "よし、{num}新しいハンドシェイクがある!"
|
msgstr "おぉ、\n{num}回\nハンドシェイクがあったよ!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "You have {count} new message{plural}!"
|
||||||
|
msgstr "おぉ、\n{count}個メッセージがあるよ!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Ops, something went wrong ... Rebooting ..."
|
||||||
msgstr "おっと!何かが間違っていた。。。リブートしている。。。"
|
msgstr "何か間違った。\nリブートしている。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Kicked {num} stations\n"
|
msgid "Kicked {num} stations\n"
|
||||||
msgstr ""
|
msgstr "{num}回拒否された。\n"
|
||||||
|
|
||||||
|
msgid "Made >999 new friends\n"
|
||||||
|
msgstr "1000人以上友達ができた。\n"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Made {num} new friends\n"
|
msgid "Made {num} new friends\n"
|
||||||
msgstr "{num}人の新しい友達を作りました\n"
|
msgstr "{num}人友達ができた。\n"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Got {num} handshakes\n"
|
msgid "Got {num} handshakes\n"
|
||||||
msgstr "{num}ハンドシェイクがある。\n"
|
msgstr "{num}回ハンドシェイクした。\n"
|
||||||
|
|
||||||
msgid "Met 1 peer"
|
msgid "Met 1 peer"
|
||||||
msgstr "1人の仲間を会いました。"
|
msgstr "1人 仲間に会いました。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Met {num} peers"
|
msgid "Met {num} peers"
|
||||||
msgstr "{num}人の仲間を会いました。"
|
msgstr "{num}人 仲間に会いました。"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -192,6 +228,9 @@ msgid ""
|
|||||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"{duration}中{deauthed}のAPに拒否されたけど、{associated}回チャンスがあって"
|
||||||
|
"{handshakes}回ハンドシェイクがあったよ。。 #pwnagotchi #pwnlog #pwnlife "
|
||||||
|
"#hacktheplanet #skynet"
|
||||||
|
|
||||||
msgid "hours"
|
msgid "hours"
|
||||||
msgstr "時間"
|
msgstr "時間"
|
||||||
@@ -203,7 +242,7 @@ msgid "seconds"
|
|||||||
msgstr "秒"
|
msgstr "秒"
|
||||||
|
|
||||||
msgid "hour"
|
msgid "hour"
|
||||||
msgstr "時間"
|
msgstr "時"
|
||||||
|
|
||||||
msgid "minute"
|
msgid "minute"
|
||||||
msgstr "分"
|
msgstr "分"
|
||||||
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Кикбан {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Кул, фативме {num} нови ракувања!"
|
msgstr "Кул, фативме {num} нови ракувања!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Упс, нешто не еко што треба ... Рестартирам ..."
|
msgstr "Упс, нешто не еко што треба ... Рестартирам ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -157,7 +157,7 @@ msgstr "Ik ga {mac} even kicken!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Gaaf, we hebben {num} nieuwe handshake{plural}!"
|
msgstr "Gaaf, we hebben {num} nieuwe handshake{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Oops, iets ging fout ...Rebooting ..."
|
msgstr "Oops, iets ging fout ...Rebooting ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -198,7 +198,7 @@ msgstr "Fett, vi fikk {num} nye håndtrykk!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Du har {count} melding{plural}!"
|
msgstr "Du har {count} melding{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Oi, noe gikk helt skakk ... Rebooter ..."
|
msgstr "Oi, noe gikk helt skakk ... Rebooter ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -197,7 +197,7 @@ msgstr "Super, zdobyliśmy {num} nowych handshake'ów!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Masz {count} nowych wiadomości!"
|
msgstr "Masz {count} nowych wiadomości!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ups, coś poszło nie tak ... Restaruję ..."
|
msgstr "Ups, coś poszło nie tak ... Restaruję ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Kickbanning {mac}"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Legal, nos capturamos {num} handshake{plural} novo{plural}!"
|
msgstr "Legal, nos capturamos {num} handshake{plural} novo{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ops, algo falhou ... Reiniciando ..."
|
msgstr "Ops, algo falhou ... Reiniciando ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -164,7 +164,7 @@ msgstr "A chutar {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Porreiro, temos {num} novo handshake{plural}!"
|
msgstr "Porreiro, temos {num} novo handshake{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ups, algo correu mal ... A reiniciar ..."
|
msgstr "Ups, algo correu mal ... A reiniciar ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
BIN
pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
249
pwnagotchi/locale/ro/LC_MESSAGES/voice.po
Normal file
249
pwnagotchi/locale/ro/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Pwnagotchi translation.
|
||||||
|
# Copyright (C) 2019
|
||||||
|
# This file is distributed under the same license as the pwnagotchi package.
|
||||||
|
# FIRST AUTHOR <radu.ungureanu@techie.com>, 2019.
|
||||||
|
#
|
||||||
|
#,
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 0.0.1\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
|
||||||
|
"PO-Revision-Date: 2019-11-20 00:18+594\n"
|
||||||
|
"Last-Translator: Ungureanu Radu-Andrei <radu.ungureanu@techie.com>\n"
|
||||||
|
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github."
|
||||||
|
"com>\n"
|
||||||
|
"Language: ro\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
msgid "ZzzzZZzzzzZzzz"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||||
|
msgstr "Buna, sunt Pwnagotchi! Pornesc..."
|
||||||
|
|
||||||
|
msgid "New day, new hunt, new pwns!"
|
||||||
|
msgstr "O noua zi, o noua vanatoare, noi pwn-uri!"
|
||||||
|
|
||||||
|
msgid "Hack the Planet!"
|
||||||
|
msgstr "Pirateaza planeta!"
|
||||||
|
|
||||||
|
msgid "AI ready."
|
||||||
|
msgstr "AI-ul e gata."
|
||||||
|
|
||||||
|
msgid "The neural network is ready."
|
||||||
|
msgstr "Rețeaua neuronală este gata."
|
||||||
|
|
||||||
|
msgid "Generating keys, do not turn off ..."
|
||||||
|
msgstr "Se generează chei, nu închide..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
|
msgstr "Hey, canalul {channel} este liber! AP-ul tău îti va mulțumi."
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr "Se citesc log-urile din sesiunea anterioara..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "Am citit {lines_so_far} linii din log pana acum..."
|
||||||
|
|
||||||
|
msgid "I'm bored ..."
|
||||||
|
msgstr "Sunt plictisit..."
|
||||||
|
|
||||||
|
msgid "Let's go for a walk!"
|
||||||
|
msgstr "Hai să ne plimbăm!"
|
||||||
|
|
||||||
|
msgid "This is the best day of my life!"
|
||||||
|
msgstr "Asta este cea mai buna zi din viața mea!"
|
||||||
|
|
||||||
|
msgid "Shitty day :/"
|
||||||
|
msgstr "O zi proasta :/"
|
||||||
|
|
||||||
|
msgid "I'm extremely bored ..."
|
||||||
|
msgstr "Sunt extrem de plictisit..."
|
||||||
|
|
||||||
|
msgid "I'm very sad ..."
|
||||||
|
msgstr "Sunt foarte trist..."
|
||||||
|
|
||||||
|
msgid "I'm sad"
|
||||||
|
msgstr "Sunt trist"
|
||||||
|
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "Lasă-mă in pace..."
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "Sunt supărat pe tine!"
|
||||||
|
|
||||||
|
msgid "I'm living the life!"
|
||||||
|
msgstr "Trăiesc viața!"
|
||||||
|
|
||||||
|
msgid "I pwn therefore I am."
|
||||||
|
msgstr "Eu pwn-ez, deci aici sunt."
|
||||||
|
|
||||||
|
msgid "So many networks!!!"
|
||||||
|
msgstr "Atât de multe rețele!"
|
||||||
|
|
||||||
|
msgid "I'm having so much fun!"
|
||||||
|
msgstr "Mă distrez așa de mult!"
|
||||||
|
|
||||||
|
msgid "My crime is that of curiosity ..."
|
||||||
|
msgstr "Crima mea este una de curiozitate..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hello {name}! Nice to meet you."
|
||||||
|
msgstr "Bună {name}! Mă bucur să te cunosc."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {name}! Sup?"
|
||||||
|
msgstr "Yo {name}! Cmf?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "Hey {nume} ce mai faci?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Unit {name} is nearby!"
|
||||||
|
msgstr "Unitatea {name} este aproape!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Uhm ... goodbye {name}"
|
||||||
|
msgstr "Uhm... Pa {name}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} is gone ..."
|
||||||
|
msgstr "{name} a dispărut."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Whoops ... {name} is gone."
|
||||||
|
msgstr "Oops... {name} a dispărut."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} missed!"
|
||||||
|
msgstr "{name} ratat!"
|
||||||
|
|
||||||
|
msgid "Missed!"
|
||||||
|
msgstr "Ratat!"
|
||||||
|
|
||||||
|
msgid "Good friends are a blessing!"
|
||||||
|
msgstr "Prietenii buni sunt o binecuvântare!"
|
||||||
|
|
||||||
|
msgid "I love my friends!"
|
||||||
|
msgstr "Îmi iubesc prietenii!"
|
||||||
|
|
||||||
|
msgid "Nobody wants to play with me ..."
|
||||||
|
msgstr "Nimeni nu vrea sa se joace cu mine..."
|
||||||
|
|
||||||
|
msgid "I feel so alone ..."
|
||||||
|
msgstr "Mă simt așa de singuratic..."
|
||||||
|
|
||||||
|
msgid "Where's everybody?!"
|
||||||
|
msgstr "Unde-i toată lumea?!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Napping for {secs}s ..."
|
||||||
|
msgstr "Dorm pentru {secs}s..."
|
||||||
|
|
||||||
|
msgid "Zzzzz"
|
||||||
|
msgstr "Zzzzz"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "ZzzZzzz ({secs}s)"
|
||||||
|
msgstr "ZzzZzzz ({secs}s)"
|
||||||
|
|
||||||
|
msgid "Good night."
|
||||||
|
msgstr "Noapte bună."
|
||||||
|
|
||||||
|
msgid "Zzz"
|
||||||
|
msgstr "Zzz"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Waiting for {secs}s ..."
|
||||||
|
msgstr "Aștept pentru {secs}s..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Looking around ({secs}s)"
|
||||||
|
msgstr "Mă uit împrejur ({secs}s)"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {what} let's be friends!"
|
||||||
|
msgstr "Hey {what} hai să fim prieteni!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Associating to {what}"
|
||||||
|
msgstr "Mă asociez cu {what}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Yo {what}!"
|
||||||
|
msgstr "Yo {what}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Just decided that {mac} needs no WiFi!"
|
||||||
|
msgstr "Am decis că lui {mac} nu-i trebuie WiFi!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Deauthenticating {mac}"
|
||||||
|
msgstr "Îl deautentific pe {mac}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Kickbanning {mac}!"
|
||||||
|
msgstr "Îi dau kickban lui {mac}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
|
msgstr "Șmecher, avem {num} de handshake-uri noi!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "You have {count} new message{plural}!"
|
||||||
|
msgstr "Ai {count} mesaj(e) nou/noi!"
|
||||||
|
|
||||||
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
|
msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Kicked {num} stations\n"
|
||||||
|
msgstr "Am dat afară {num} de stații\n"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Made {num} new friends\n"
|
||||||
|
msgstr "Am făcut {num} prieteni noi \n"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Got {num} handshakes\n"
|
||||||
|
msgstr "Am primit {num} de handshake-uri\n"
|
||||||
|
|
||||||
|
msgid "Met 1 peer"
|
||||||
|
msgstr "Am întalnit un peer"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Met {num} peers"
|
||||||
|
msgstr "Am întalnit {num} de peer-uri"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid ""
|
||||||
|
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||||
|
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||||
|
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||||
|
msgstr "Eu am făcut pwning pentru {duration} și am dat afara {deauthed} clienți! "
|
||||||
|
"De asemenea, am întalnit {associated} prieteni noi și am mancat {handshakes} de "
|
||||||
|
"handshake-uri! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||||
|
|
||||||
|
msgid "hours"
|
||||||
|
msgstr "ore"
|
||||||
|
|
||||||
|
msgid "minutes"
|
||||||
|
msgstr "minute"
|
||||||
|
|
||||||
|
msgid "seconds"
|
||||||
|
msgstr "secunde"
|
||||||
|
|
||||||
|
msgid "hour"
|
||||||
|
msgstr "oră"
|
||||||
|
|
||||||
|
msgid "minute"
|
||||||
|
msgstr "minut"
|
||||||
|
|
||||||
|
msgid "second"
|
||||||
|
msgstr "secundă"
|
Binary file not shown.
@@ -1,46 +1,58 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
# Pwnagotchi Russian translation.
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
# Copyright (C) 2019
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
# This file is distributed under the same license as the pwnagotchi package.
|
||||||
# FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
|
# FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
|
||||||
#
|
# Second author <https://github.com/mbgroot>, 2019
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: Pwnagotchi Russian translation v 0.0.2\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2019-10-03 16:47+0200\n"
|
|
||||||
"PO-Revision-Date: 2019-10-05 18:50+0300\n"
|
|
||||||
"Language-Team: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: Poedit 2.2.4\n"
|
|
||||||
"Last-Translator: Elliot Manson\n"
|
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||||
"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
|
"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
|
||||||
"Language: ru_RU\n"
|
"POT-Creation-Date: \n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"X-Generator: Poedit 2.2.4\n"
|
||||||
|
"X-Poedit-SourceCharset: UTF-8\n"
|
||||||
|
"X-Poedit-Basepath: .\n"
|
||||||
|
"X-Poedit-SearchPath-0: voice.po\n"
|
||||||
|
|
||||||
msgid "ZzzzZZzzzzZzzz"
|
msgid "ZzzzZZzzzzZzzz"
|
||||||
msgstr "ZzzzZZzzzzZzzz"
|
msgstr "Хрррр..."
|
||||||
|
|
||||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||||
msgstr "Привет, я Pwnagotchi! Поехали …"
|
msgstr "Привет, я Pwnagotchi! Стартуем!"
|
||||||
|
|
||||||
msgid "New day, new hunt, new pwns!"
|
msgid "New day, new hunt, new pwns!"
|
||||||
msgstr "Новый день, новая охота, новые взломы!"
|
msgstr "Новый день, новая охота, новые взломы!"
|
||||||
|
|
||||||
msgid "Hack the Planet!"
|
msgid "Hack the Planet!"
|
||||||
msgstr "Хак зе планет!"
|
msgstr "Взломай эту Планету!"
|
||||||
|
|
||||||
msgid "AI ready."
|
msgid "AI ready."
|
||||||
msgstr "AI готов."
|
msgstr "A.I. готов."
|
||||||
|
|
||||||
msgid "The neural network is ready."
|
msgid "The neural network is ready."
|
||||||
msgstr "Нейронная сеть готова."
|
msgstr "Нейронная сеть готова."
|
||||||
|
|
||||||
|
msgid "Generating keys, do not turn off ..."
|
||||||
|
msgstr "Генерация ключей, не выключайте..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||||
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
|
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
|
||||||
|
|
||||||
|
msgid "Reading last session logs ..."
|
||||||
|
msgstr "Чтение логов последнего сеанса..."
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Read {lines_so_far} log lines so far ..."
|
||||||
|
msgstr "Чтение {lines_so_far} строк журнала..."
|
||||||
|
|
||||||
msgid "I'm bored ..."
|
msgid "I'm bored ..."
|
||||||
msgstr "Мне скучно …"
|
msgstr "Мне скучно …"
|
||||||
|
|
||||||
@@ -62,8 +74,14 @@ msgstr "Мне очень грустно …"
|
|||||||
msgid "I'm sad"
|
msgid "I'm sad"
|
||||||
msgstr "Мне грустно"
|
msgstr "Мне грустно"
|
||||||
|
|
||||||
|
msgid "Leave me alone ..."
|
||||||
|
msgstr "Оставь меня в покое..."
|
||||||
|
|
||||||
|
msgid "I'm mad at you!"
|
||||||
|
msgstr "Я зол на тебя!"
|
||||||
|
|
||||||
msgid "I'm living the life!"
|
msgid "I'm living the life!"
|
||||||
msgstr "Угараю по полной!"
|
msgstr "Живу полной жизнью!"
|
||||||
|
|
||||||
msgid "I pwn therefore I am."
|
msgid "I pwn therefore I am."
|
||||||
msgstr "Я взламываю, поэтому я существую."
|
msgstr "Я взламываю, поэтому я существую."
|
||||||
@@ -75,15 +93,23 @@ msgid "I'm having so much fun!"
|
|||||||
msgstr "Мне так весело!"
|
msgstr "Мне так весело!"
|
||||||
|
|
||||||
msgid "My crime is that of curiosity ..."
|
msgid "My crime is that of curiosity ..."
|
||||||
msgstr "Моe преступление - это любопытство …"
|
msgstr "Моё преступление - это любопытство…"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hello {name}! Nice to meet you. {name}"
|
msgid "Hello {name}! Nice to meet you. {name}"
|
||||||
msgstr "Привет, {name}! Приятно познакомиться. {name}"
|
msgstr "Привет, {name}! Рад встрече с тобой!"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unit {name} is nearby! {name}"
|
msgid "Unit {name} is nearby! {name}"
|
||||||
msgstr "Цель {name} близко! {name}"
|
msgstr "Цель {name} близко!"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Hey {name} how are you doing?"
|
||||||
|
msgstr "Хэй {name}! Как дела?"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Unit {name} is nearby!"
|
||||||
|
msgstr "Цель {name} рядом!"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Uhm ... goodbye {name}"
|
msgid "Uhm ... goodbye {name}"
|
||||||
@@ -91,11 +117,11 @@ msgstr "Хм … до свидания {name}"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is gone ..."
|
msgid "{name} is gone ..."
|
||||||
msgstr "{name} исчезла …"
|
msgstr "{name} ушла…"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Whoops ... {name} is gone."
|
msgid "Whoops ... {name} is gone."
|
||||||
msgstr "Упс … {name} исчезла."
|
msgstr "Упс… {name} исчезла."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} missed!"
|
msgid "{name} missed!"
|
||||||
@@ -104,11 +130,17 @@ msgstr "{name} упустил!"
|
|||||||
msgid "Missed!"
|
msgid "Missed!"
|
||||||
msgstr "Промахнулся!"
|
msgstr "Промахнулся!"
|
||||||
|
|
||||||
|
msgid "Good friends are a blessing!"
|
||||||
|
msgstr "Хорошие друзья - это благословение!"
|
||||||
|
|
||||||
|
msgid "I love my friends!"
|
||||||
|
msgstr "Я люблю своих друзей!"
|
||||||
|
|
||||||
msgid "Nobody wants to play with me ..."
|
msgid "Nobody wants to play with me ..."
|
||||||
msgstr "Никто не хочет со мной играть ..."
|
msgstr "Никто не хочет со мной играть ..."
|
||||||
|
|
||||||
msgid "I feel so alone ..."
|
msgid "I feel so alone ..."
|
||||||
msgstr "Мне так одиноко …"
|
msgstr "Я так одинок…"
|
||||||
|
|
||||||
msgid "Where's everybody?!"
|
msgid "Where's everybody?!"
|
||||||
msgstr "Где все?!"
|
msgstr "Где все?!"
|
||||||
@@ -118,11 +150,17 @@ msgid "Napping for {secs}s ..."
|
|||||||
msgstr "Дремлет {secs}с …"
|
msgstr "Дремлет {secs}с …"
|
||||||
|
|
||||||
msgid "Zzzzz"
|
msgid "Zzzzz"
|
||||||
msgstr "Zzzzz"
|
msgstr "Хррр..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "ZzzZzzz ({secs}s)"
|
msgid "ZzzZzzz ({secs}s)"
|
||||||
msgstr "ZzzZzzz ({secs}c)"
|
msgstr "Хррррр.. ({secs}c)"
|
||||||
|
|
||||||
|
msgid "Good night."
|
||||||
|
msgstr "Доброй ночи."
|
||||||
|
|
||||||
|
msgid "Zzz"
|
||||||
|
msgstr "Хрррр"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Waiting for {secs}s ..."
|
msgid "Waiting for {secs}s ..."
|
||||||
@@ -130,7 +168,7 @@ msgstr "Ждем {secs}c …"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Looking around ({secs}s)"
|
msgid "Looking around ({secs}s)"
|
||||||
msgstr "Оглядываюсь вокруг ({secs}с)"
|
msgstr "Осматриваюсь вокруг ({secs}с)"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Hey {what} let's be friends!"
|
msgid "Hey {what} let's be friends!"
|
||||||
@@ -160,7 +198,7 @@ msgstr "Кикаю {mac}!"
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Круто, мы получили {num} новое рукопожатие!"
|
msgstr "Круто, мы получили {num} новое рукопожатие!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ой, что-то пошло не так … Перезагружаюсь …"
|
msgstr "Ой, что-то пошло не так … Перезагружаюсь …"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@@ -173,7 +211,7 @@ msgstr "Заимел {num} новых друзей\n"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Got {num} handshakes\n"
|
msgid "Got {num} handshakes\n"
|
||||||
msgstr "Получил {num} рукопожатие\n"
|
msgstr "Получил {num} рукопожатий\n"
|
||||||
|
|
||||||
msgid "Met 1 peer"
|
msgid "Met 1 peer"
|
||||||
msgstr "Встретился один знакомый"
|
msgstr "Встретился один знакомый"
|
||||||
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr ""
|
|||||||
msgid "Cool, we got {num} new handshake{plural}!"
|
msgid "Cool, we got {num} new handshake{plural}!"
|
||||||
msgstr "Lysande, vi har {num} ny handskakningar{plural}!"
|
msgstr "Lysande, vi har {num} ny handskakningar{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Hoppsan, någpt gick fel ... Startar om ..."
|
msgstr "Hoppsan, någpt gick fel ... Startar om ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -177,7 +177,7 @@ msgstr "Super, máme {num} nový handshake{plural}!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Máte {count} novú správu{plural}!"
|
msgstr "Máte {count} novú správu{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ops, niečo sa pokazilo ... Reštartujem sa ..."
|
msgstr "Ops, niečo sa pokazilo ... Reštartujem sa ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
BIN
pwnagotchi/locale/spa/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/spa/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
@@ -198,7 +198,7 @@ msgstr "Bien, obtuvimos {num} nuevos handshake{plural}!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Tienes {count} nuevos mensajes{plural}!"
|
msgstr "Tienes {count} nuevos mensajes{plural}!"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Oops, algo salio mal ... Reiniciando ..."
|
msgstr "Oops, algo salio mal ... Reiniciando ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
Binary file not shown.
@@ -177,7 +177,7 @@ msgstr "Отакої, у нас є {num} нових рукостискань!"
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr "Нових повідомлень: {count}"
|
msgstr "Нових повідомлень: {count}"
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr "Ой, щось пішло не так ... Перезавантажуюсь ..."
|
msgstr "Ой, щось пішло не так ... Перезавантажуюсь ..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
|
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -198,7 +198,7 @@ msgstr ""
|
|||||||
msgid "You have {count} new message{plural}!"
|
msgid "You have {count} new message{plural}!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Ops, something went wrong ... Rebooting ..."
|
msgid "Oops, something went wrong ... Rebooting ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@@ -3,6 +3,9 @@ import time
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import shutil
|
||||||
|
import gzip
|
||||||
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pwnagotchi.voice import Voice
|
from pwnagotchi.voice import Voice
|
||||||
@@ -209,3 +212,98 @@ class LastSession(object):
|
|||||||
|
|
||||||
def is_new(self):
|
def is_new(self):
|
||||||
return self.last_session_id != self.last_saved_session_id
|
return self.last_session_id != self.last_saved_session_id
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(args, config):
|
||||||
|
cfg = config['main']['log']
|
||||||
|
filename = cfg['path']
|
||||||
|
|
||||||
|
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
|
||||||
|
root = logging.getLogger()
|
||||||
|
|
||||||
|
root.setLevel(logging.DEBUG if args.debug else logging.INFO)
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
# since python default log rotation might break session data in different files,
|
||||||
|
# we need to do log rotation ourselves
|
||||||
|
log_rotation(filename, cfg)
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler(filename)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
root.addHandler(file_handler)
|
||||||
|
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
root.addHandler(console_handler)
|
||||||
|
|
||||||
|
if not args.debug:
|
||||||
|
# disable scapy and tensorflow logging
|
||||||
|
logging.getLogger("scapy").disabled = True
|
||||||
|
logging.getLogger('tensorflow').disabled = True
|
||||||
|
# https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
|
||||||
|
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||||||
|
warnings.simplefilter(action='ignore', category=DeprecationWarning)
|
||||||
|
# https://stackoverflow.com/questions/24344045/how-can-i-completely-remove-any-logging-from-requests-module-in-python?noredirect=1&lq=1
|
||||||
|
logging.getLogger("urllib3").propagate = False
|
||||||
|
requests_log = logging.getLogger("requests")
|
||||||
|
requests_log.addHandler(logging.NullHandler())
|
||||||
|
requests_log.prpagate = False
|
||||||
|
|
||||||
|
|
||||||
|
def log_rotation(filename, cfg):
|
||||||
|
rotation = cfg['rotation']
|
||||||
|
if not rotation['enabled']:
|
||||||
|
return
|
||||||
|
elif not os.path.isfile(filename):
|
||||||
|
return
|
||||||
|
|
||||||
|
stats = os.stat(filename)
|
||||||
|
# specify a maximum size to rotate ( format is 10/10B, 10K, 10M 10G )
|
||||||
|
if rotation['size']:
|
||||||
|
max_size = parse_max_size(rotation['size'])
|
||||||
|
if stats.st_size >= max_size:
|
||||||
|
do_rotate(filename, stats, cfg)
|
||||||
|
else:
|
||||||
|
raise Exception("log rotation is enabled but log.rotation.size was not specified")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_max_size(s):
|
||||||
|
parts = re.findall(r'(^\d+)([bBkKmMgG]?)', s)
|
||||||
|
if len(parts) != 1 or len(parts[0]) != 2:
|
||||||
|
raise Exception("can't parse %s as a max size" % s)
|
||||||
|
|
||||||
|
num, unit = parts[0]
|
||||||
|
num = int(num)
|
||||||
|
unit = unit.lower()
|
||||||
|
|
||||||
|
if unit == 'k':
|
||||||
|
return num * 1024
|
||||||
|
elif unit == 'm':
|
||||||
|
return num * 1024 * 1024
|
||||||
|
elif unit == 'g':
|
||||||
|
return num * 1024 * 1024 * 1024
|
||||||
|
else:
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def do_rotate(filename, stats, cfg):
|
||||||
|
base_path = os.path.dirname(filename)
|
||||||
|
name = os.path.splitext(os.path.basename(filename))[0]
|
||||||
|
archive_filename = os.path.join(base_path, "%s.gz" % name)
|
||||||
|
counter = 2
|
||||||
|
|
||||||
|
while os.path.exists(archive_filename):
|
||||||
|
archive_filename = os.path.join(base_path, "%s-%d.gz" % (name, counter))
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
log_filename = archive_filename.replace('gz', 'log')
|
||||||
|
|
||||||
|
print("%s is %d bytes big, rotating to %s ..." % (filename, stats.st_size, log_filename))
|
||||||
|
|
||||||
|
shutil.move(filename, log_filename)
|
||||||
|
|
||||||
|
print("compressing to %s ..." % archive_filename)
|
||||||
|
|
||||||
|
with open(log_filename, 'rb') as src:
|
||||||
|
with gzip.open(archive_filename, 'wb') as dst:
|
||||||
|
dst.writelines(src)
|
||||||
|
@@ -17,7 +17,7 @@ class AsyncAdvertiser(object):
|
|||||||
self._keypair = keypair
|
self._keypair = keypair
|
||||||
self._advertisement = {
|
self._advertisement = {
|
||||||
'name': pwnagotchi.name(),
|
'name': pwnagotchi.name(),
|
||||||
'version': pwnagotchi.version,
|
'version': pwnagotchi.__version__,
|
||||||
'identity': self._keypair.fingerprint,
|
'identity': self._keypair.fingerprint,
|
||||||
'face': faces.FRIEND,
|
'face': faces.FRIEND,
|
||||||
'pwnd_run': 0,
|
'pwnd_run': 0,
|
||||||
|
@@ -1,38 +1,98 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
import _thread
|
import _thread
|
||||||
|
import threading
|
||||||
import importlib, importlib.util
|
import importlib, importlib.util
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
|
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
|
||||||
loaded = {}
|
loaded = {}
|
||||||
|
database = {}
|
||||||
|
locks = {}
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
global loaded
|
global loaded, locks
|
||||||
|
|
||||||
plugin_name = cls.__module__.split('.')[0]
|
plugin_name = cls.__module__.split('.')[0]
|
||||||
plugin_instance = cls()
|
plugin_instance = cls()
|
||||||
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
|
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
|
||||||
loaded[plugin_name] = plugin_instance
|
loaded[plugin_name] = plugin_instance
|
||||||
|
|
||||||
|
for attr_name in plugin_instance.__dir__():
|
||||||
|
if attr_name.startswith('on_'):
|
||||||
|
cb = getattr(plugin_instance, attr_name, None)
|
||||||
|
if cb is not None and callable(cb):
|
||||||
|
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_plugin(name, enable=True):
|
||||||
|
"""
|
||||||
|
Load or unload a plugin
|
||||||
|
|
||||||
|
returns True if changed, otherwise False
|
||||||
|
"""
|
||||||
|
import pwnagotchi
|
||||||
|
from pwnagotchi.ui import view
|
||||||
|
from pwnagotchi.utils import save_config
|
||||||
|
|
||||||
|
global loaded, database
|
||||||
|
|
||||||
|
if pwnagotchi.config:
|
||||||
|
pwnagotchi.config['main']['plugins'][name]['enabled'] = enable
|
||||||
|
save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml')
|
||||||
|
|
||||||
|
if not enable and name in loaded:
|
||||||
|
if getattr(loaded[name], 'on_unload', None):
|
||||||
|
loaded[name].on_unload(view.ROOT)
|
||||||
|
del loaded[name]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
if enable and name in database and name not in loaded:
|
||||||
|
load_from_file(database[name])
|
||||||
|
one(name, 'loaded')
|
||||||
|
if pwnagotchi.config:
|
||||||
|
one(name, 'config_changed', pwnagotchi.config)
|
||||||
|
one(name, 'ui_setup', view.ROOT)
|
||||||
|
one(name, 'ready', view.ROOT._agent)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def on(event_name, *args, **kwargs):
|
def on(event_name, *args, **kwargs):
|
||||||
for plugin_name, plugin in loaded.items():
|
for plugin_name in loaded.keys():
|
||||||
one(plugin_name, event_name, *args, **kwargs)
|
one(plugin_name, event_name, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def locked_cb(lock_name, cb, *args, **kwargs):
|
||||||
|
global locks
|
||||||
|
|
||||||
|
if lock_name not in locks:
|
||||||
|
locks[lock_name] = threading.Lock()
|
||||||
|
|
||||||
|
with locks[lock_name]:
|
||||||
|
cb(*args, *kwargs)
|
||||||
|
|
||||||
|
|
||||||
def one(plugin_name, event_name, *args, **kwargs):
|
def one(plugin_name, event_name, *args, **kwargs):
|
||||||
global loaded
|
global loaded
|
||||||
|
|
||||||
if plugin_name in loaded:
|
if plugin_name in loaded:
|
||||||
plugin = loaded[plugin_name]
|
plugin = loaded[plugin_name]
|
||||||
cb_name = 'on_%s' % event_name
|
cb_name = 'on_%s' % event_name
|
||||||
callback = getattr(plugin, cb_name, None)
|
callback = getattr(plugin, cb_name, None)
|
||||||
if callback is not None and callable(callback):
|
if callback is not None and callable(callback):
|
||||||
try:
|
try:
|
||||||
_thread.start_new_thread(callback, (*args, *kwargs))
|
lock_name = "%s::%s" % (plugin_name, cb_name)
|
||||||
|
locked_cb_args = (lock_name, callback, *args, *kwargs)
|
||||||
|
_thread.start_new_thread(locked_cb, locked_cb_args)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
|
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
|
||||||
logging.error(e, exc_info=True)
|
logging.error(e, exc_info=True)
|
||||||
@@ -48,10 +108,11 @@ def load_from_file(filename):
|
|||||||
|
|
||||||
|
|
||||||
def load_from_path(path, enabled=()):
|
def load_from_path(path, enabled=()):
|
||||||
global loaded
|
global loaded, database
|
||||||
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
|
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
|
||||||
for filename in glob.glob(os.path.join(path, "*.py")):
|
for filename in glob.glob(os.path.join(path, "*.py")):
|
||||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||||
|
database[plugin_name] = filename
|
||||||
if plugin_name in enabled:
|
if plugin_name in enabled:
|
||||||
try:
|
try:
|
||||||
load_from_file(filename)
|
load_from_file(filename)
|
||||||
@@ -79,3 +140,4 @@ def load(config):
|
|||||||
plugin.options = config['main']['plugins'][name]
|
plugin.options = config['main']['plugins'][name]
|
||||||
|
|
||||||
on('loaded')
|
on('loaded')
|
||||||
|
on('config_changed', config)
|
||||||
|
389
pwnagotchi/plugins/cmd.py
Normal file
389
pwnagotchi/plugins/cmd.py
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# Handles the commandline stuff
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import glob
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
from pwnagotchi.utils import download_file, unzip, save_config, parse_version, md5
|
||||||
|
from pwnagotchi.plugins import default_path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_URL = 'https://github.com/evilsocket/pwnagotchi-plugins-contrib/archive/master.zip'
|
||||||
|
SAVE_DIR = '/usr/local/share/pwnagotchi/availaible-plugins/'
|
||||||
|
DEFAULT_INSTALL_PATH = '/usr/local/share/pwnagotchi/installed-plugins/'
|
||||||
|
|
||||||
|
|
||||||
|
def add_parsers(parser):
|
||||||
|
"""
|
||||||
|
Adds the plugins subcommand to a given argparse.ArgumentParser
|
||||||
|
"""
|
||||||
|
subparsers = parser.add_subparsers()
|
||||||
|
## pwnagotchi plugins
|
||||||
|
parser_plugins = subparsers.add_parser('plugins')
|
||||||
|
plugin_subparsers = parser_plugins.add_subparsers(dest='plugincmd')
|
||||||
|
|
||||||
|
## pwnagotchi plugins search
|
||||||
|
parser_plugins_search = plugin_subparsers.add_parser('search', help='Search for pwnagotchi plugins')
|
||||||
|
parser_plugins_search.add_argument('pattern', type=str, help="Search expression (wildcards allowed)")
|
||||||
|
|
||||||
|
## pwnagotchi plugins list
|
||||||
|
parser_plugins_list = plugin_subparsers.add_parser('list', help='List available pwnagotchi plugins')
|
||||||
|
parser_plugins_list.add_argument('-i', '--installed', action='store_true', required=False, help='List also installed plugins')
|
||||||
|
|
||||||
|
## pwnagotchi plugins update
|
||||||
|
parser_plugins_update = plugin_subparsers.add_parser('update', help='Updates the database')
|
||||||
|
|
||||||
|
## pwnagotchi plugins upgrade
|
||||||
|
parser_plugins_upgrade = plugin_subparsers.add_parser('upgrade', help='Upgrades plugins')
|
||||||
|
parser_plugins_upgrade.add_argument('pattern', type=str, nargs='?', default='*', help="Filter expression (wildcards allowed)")
|
||||||
|
|
||||||
|
## pwnagotchi plugins enable
|
||||||
|
parser_plugins_enable = plugin_subparsers.add_parser('enable', help='Enables a plugin')
|
||||||
|
parser_plugins_enable.add_argument('name', type=str, help='Name of the plugin')
|
||||||
|
|
||||||
|
## pwnagotchi plugins disable
|
||||||
|
parser_plugins_disable = plugin_subparsers.add_parser('disable', help='Disables a plugin')
|
||||||
|
parser_plugins_disable.add_argument('name', type=str, help='Name of the plugin')
|
||||||
|
|
||||||
|
## pwnagotchi plugins install
|
||||||
|
parser_plugins_install = plugin_subparsers.add_parser('install', help='Installs a plugin')
|
||||||
|
parser_plugins_install.add_argument('name', type=str, help='Name of the plugin')
|
||||||
|
|
||||||
|
## pwnagotchi plugins uninstall
|
||||||
|
parser_plugins_uninstall = plugin_subparsers.add_parser('uninstall', help='Uninstalls a plugin')
|
||||||
|
parser_plugins_uninstall.add_argument('name', type=str, help='Name of the plugin')
|
||||||
|
|
||||||
|
## pwnagotchi plugins edit
|
||||||
|
parser_plugins_edit = plugin_subparsers.add_parser('edit', help='Edit the options')
|
||||||
|
parser_plugins_edit.add_argument('name', type=str, help='Name of the plugin')
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def used_plugin_cmd(args):
|
||||||
|
"""
|
||||||
|
Checks if the plugins subcommand was used
|
||||||
|
"""
|
||||||
|
return hasattr(args, 'plugincmd')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_cmd(args, config):
|
||||||
|
"""
|
||||||
|
Parses the arguments and does the thing the user wants
|
||||||
|
"""
|
||||||
|
if args.plugincmd == 'update':
|
||||||
|
return update()
|
||||||
|
elif args.plugincmd == 'search':
|
||||||
|
args.installed = True # also search in installed plugins
|
||||||
|
return list_plugins(args, config, args.pattern)
|
||||||
|
elif args.plugincmd == 'install':
|
||||||
|
return install(args, config)
|
||||||
|
elif args.plugincmd == 'uninstall':
|
||||||
|
return uninstall(args, config)
|
||||||
|
elif args.plugincmd == 'list':
|
||||||
|
return list_plugins(args, config)
|
||||||
|
elif args.plugincmd == 'enable':
|
||||||
|
return enable(args, config)
|
||||||
|
elif args.plugincmd == 'disable':
|
||||||
|
return disable(args, config)
|
||||||
|
elif args.plugincmd == 'upgrade':
|
||||||
|
return upgrade(args, config, args.pattern)
|
||||||
|
elif args.plugincmd == 'edit':
|
||||||
|
return edit(args, config)
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def edit(args, config):
|
||||||
|
"""
|
||||||
|
Edit the config of the plugin
|
||||||
|
"""
|
||||||
|
plugin = args.name
|
||||||
|
editor = os.environ.get('EDITOR', 'vim') # because vim is the best
|
||||||
|
|
||||||
|
if plugin not in config['main']['plugins']:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
plugin_config = {'main': {'plugins': {plugin: config['main']['plugins'][plugin]}}}
|
||||||
|
|
||||||
|
import toml
|
||||||
|
from subprocess import call
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from pwnagotchi.utils import DottedTomlEncoder
|
||||||
|
|
||||||
|
new_plugin_config = None
|
||||||
|
with NamedTemporaryFile(suffix=".tmp", mode='r+t') as tmp:
|
||||||
|
tmp.write(toml.dumps(plugin_config, encoder=DottedTomlEncoder()))
|
||||||
|
tmp.flush()
|
||||||
|
rc = call([editor, tmp.name])
|
||||||
|
if rc != 0:
|
||||||
|
return rc
|
||||||
|
tmp.seek(0)
|
||||||
|
new_plugin_config = toml.load(tmp)
|
||||||
|
|
||||||
|
config['main']['plugins'][plugin] = new_plugin_config['main']['plugins'][plugin]
|
||||||
|
save_config(config, args.user_config)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def enable(args, config):
|
||||||
|
"""
|
||||||
|
Enables the given plugin and saves the config to disk
|
||||||
|
"""
|
||||||
|
if args.name not in config['main']['plugins']:
|
||||||
|
config['main']['plugins'][args.name] = dict()
|
||||||
|
config['main']['plugins'][args.name]['enabled'] = True
|
||||||
|
save_config(config, args.user_config)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def disable(args, config):
|
||||||
|
"""
|
||||||
|
Disables the given plugin and saves the config to disk
|
||||||
|
"""
|
||||||
|
if args.name not in config['main']['plugins']:
|
||||||
|
config['main']['plugins'][args.name] = dict()
|
||||||
|
config['main']['plugins'][args.name]['enabled'] = False
|
||||||
|
save_config(config, args.user_config)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(args, config, pattern='*'):
|
||||||
|
"""
|
||||||
|
Upgrades the given plugin
|
||||||
|
"""
|
||||||
|
available = _get_available()
|
||||||
|
installed = _get_installed(config)
|
||||||
|
|
||||||
|
for plugin, filename in installed.items():
|
||||||
|
if not fnmatch(plugin, pattern) or plugin not in available:
|
||||||
|
continue
|
||||||
|
|
||||||
|
available_version = _extract_version(available[plugin])
|
||||||
|
installed_version = _extract_version(filename)
|
||||||
|
|
||||||
|
if installed_version and available_version:
|
||||||
|
if available_version <= installed_version:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.info('Upgrade %s from %s to %s', plugin, '.'.join(installed_version), '.'.join(available_version))
|
||||||
|
shutil.copyfile(available[plugin], installed[plugin])
|
||||||
|
|
||||||
|
# maybe has config
|
||||||
|
for conf in glob.glob(available[plugin].replace('.py', '.y?ml')):
|
||||||
|
dst = os.path.join(os.path.dirname(installed[plugin]), os.path.basename(conf))
|
||||||
|
if os.path.exists(dst) and md5(dst) != md5(conf):
|
||||||
|
# backup
|
||||||
|
logging.info('Backing up config: %s', os.path.basename(conf))
|
||||||
|
shutil.move(dst, dst + '.bak')
|
||||||
|
shutil.copyfile(conf, dst)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def list_plugins(args, config, pattern='*'):
|
||||||
|
"""
|
||||||
|
Lists the available and installed plugins
|
||||||
|
"""
|
||||||
|
found = False
|
||||||
|
|
||||||
|
line = "|{name:^{width}}|{version:^9}|{enabled:^10}|{status:^15}|"
|
||||||
|
|
||||||
|
available = _get_available()
|
||||||
|
installed = _get_installed(config)
|
||||||
|
|
||||||
|
available_and_installed = set(list(available.keys()) + list(installed.keys()))
|
||||||
|
available_not_installed = set(available.keys()) - set(installed.keys())
|
||||||
|
|
||||||
|
max_len_list = available_and_installed if args.installed else available_not_installed
|
||||||
|
max_len = max(map(len, max_len_list))
|
||||||
|
header = line.format(name='Plugin', width=max_len, version='Version', enabled='Active', status='Status')
|
||||||
|
line_length = max(max_len, len('Plugin')) + len(header) - len('Plugin') - 12 # lol
|
||||||
|
|
||||||
|
print('-' * line_length)
|
||||||
|
print(header)
|
||||||
|
print('-' * line_length)
|
||||||
|
|
||||||
|
if args.installed:
|
||||||
|
# only installed (maybe update available?)
|
||||||
|
for plugin, filename in sorted(installed.items()):
|
||||||
|
if not fnmatch(plugin, pattern):
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
installed_version = _extract_version(filename)
|
||||||
|
available_version = None
|
||||||
|
if plugin in available:
|
||||||
|
available_version = _extract_version(available[plugin])
|
||||||
|
|
||||||
|
status = "installed"
|
||||||
|
if installed_version and available_version:
|
||||||
|
if available_version > installed_version:
|
||||||
|
status = "installed (^)"
|
||||||
|
|
||||||
|
enabled = 'enabled' if plugin in config['main']['plugins'] and \
|
||||||
|
'enabled' in config['main']['plugins'][plugin] and \
|
||||||
|
config['main']['plugins'][plugin]['enabled'] \
|
||||||
|
else 'disabled'
|
||||||
|
|
||||||
|
print(line.format(name=plugin, width=max_len, version='.'.join(installed_version), enabled=enabled, status=status))
|
||||||
|
|
||||||
|
|
||||||
|
for plugin in sorted(available_not_installed):
|
||||||
|
if not fnmatch(plugin, pattern):
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
available_version = _extract_version(available[plugin])
|
||||||
|
print(line.format(name=plugin, width=max_len, version='.'.join(available_version), enabled='-', status='available'))
|
||||||
|
|
||||||
|
print('-' * line_length)
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
logging.info('Maybe try: pwnagotchi plugins update')
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_version(filename):
|
||||||
|
"""
|
||||||
|
Extracts the version from a python file
|
||||||
|
"""
|
||||||
|
plugin_content = open(filename, 'rt').read()
|
||||||
|
m = re.search(r'__version__[\t ]*=[\t ]*[\'\"]([^\"\']+)', plugin_content)
|
||||||
|
if m:
|
||||||
|
return parse_version(m.groups()[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_available():
|
||||||
|
"""
|
||||||
|
Get all availaible plugins
|
||||||
|
"""
|
||||||
|
available = dict()
|
||||||
|
for filename in glob.glob(os.path.join(SAVE_DIR, "*.py")):
|
||||||
|
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||||
|
available[plugin_name] = filename
|
||||||
|
return available
|
||||||
|
|
||||||
|
|
||||||
|
def _get_installed(config):
|
||||||
|
"""
|
||||||
|
Get all installed plugins
|
||||||
|
"""
|
||||||
|
installed = dict()
|
||||||
|
search_dirs = [ default_path, config['main']['custom_plugins'] ]
|
||||||
|
for search_dir in search_dirs:
|
||||||
|
if search_dir:
|
||||||
|
for filename in glob.glob(os.path.join(search_dir, "*.py")):
|
||||||
|
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||||
|
installed[plugin_name] = filename
|
||||||
|
return installed
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall(args, config):
|
||||||
|
"""
|
||||||
|
Uninstalls a plugin
|
||||||
|
"""
|
||||||
|
plugin_name = args.name
|
||||||
|
installed = _get_installed(config)
|
||||||
|
if plugin_name not in installed:
|
||||||
|
logging.error('Plugin %s is not installed.', plugin_name)
|
||||||
|
return 1
|
||||||
|
os.remove(installed[plugin_name])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def install(args, config):
|
||||||
|
"""
|
||||||
|
Installs the given plugin
|
||||||
|
"""
|
||||||
|
global DEFAULT_INSTALL_PATH
|
||||||
|
plugin_name = args.name
|
||||||
|
available = _get_available()
|
||||||
|
installed = _get_installed(config)
|
||||||
|
|
||||||
|
if plugin_name not in available:
|
||||||
|
logging.error('%s not found.', plugin_name)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if plugin_name in installed:
|
||||||
|
logging.error('%s already installed.', plugin_name)
|
||||||
|
|
||||||
|
# install into custom_plugins path
|
||||||
|
install_path = config['main']['custom_plugins']
|
||||||
|
if not install_path:
|
||||||
|
install_path = DEFAULT_INSTALL_PATH
|
||||||
|
config['main']['custom_plugins'] = install_path
|
||||||
|
save_config(config, args.user_config)
|
||||||
|
|
||||||
|
os.makedirs(install_path, exist_ok=True)
|
||||||
|
|
||||||
|
shutil.copyfile(available[plugin_name], os.path.join(install_path, os.path.basename(available[plugin_name])))
|
||||||
|
|
||||||
|
# maybe has config
|
||||||
|
for conf in glob.glob(available[plugin_name].replace('.py', '.y?ml')):
|
||||||
|
dst = os.path.join(install_path, os.path.basename(conf))
|
||||||
|
if os.path.exists(dst) and md5(dst) != md5(conf):
|
||||||
|
# backup
|
||||||
|
logging.info('Backing up config: %s', os.path.basename(conf))
|
||||||
|
shutil.move(dst, dst + '.bak')
|
||||||
|
shutil.copyfile(conf, dst)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _analyse_dir(path):
|
||||||
|
results = dict()
|
||||||
|
path += '*' if path.endswith('/') else '/*'
|
||||||
|
for filename in glob.glob(path, recursive=True):
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
results[filename] = md5(filename)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def update():
|
||||||
|
"""
|
||||||
|
Updates the database
|
||||||
|
"""
|
||||||
|
global REPO_URL, SAVE_DIR
|
||||||
|
|
||||||
|
DEST = os.path.join(SAVE_DIR, 'plugins.zip')
|
||||||
|
logging.info('Downloading plugins to %s', DEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(SAVE_DIR, exist_ok=True)
|
||||||
|
before_update = _analyse_dir(SAVE_DIR)
|
||||||
|
|
||||||
|
download_file(REPO_URL, os.path.join(SAVE_DIR, DEST))
|
||||||
|
|
||||||
|
logging.info('Unzipping...')
|
||||||
|
unzip(DEST, SAVE_DIR, strip_dirs=1)
|
||||||
|
|
||||||
|
after_update = _analyse_dir(SAVE_DIR)
|
||||||
|
|
||||||
|
b_len = len(before_update)
|
||||||
|
a_len = len(after_update)
|
||||||
|
|
||||||
|
if a_len > b_len:
|
||||||
|
logging.info('Found %d new file(s).', a_len - b_len)
|
||||||
|
|
||||||
|
changed = 0
|
||||||
|
for filename, filehash in after_update.items():
|
||||||
|
if filename in before_update and filehash != before_update[filename]:
|
||||||
|
changed += 1
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
logging.info('%d file(s) were changed.', changed)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
except Exception as ex:
|
||||||
|
logging.error('Error while updating plugins %s', ex)
|
||||||
|
return 1
|
@@ -6,11 +6,11 @@ import requests
|
|||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import glob
|
import glob
|
||||||
import pkg_resources
|
from threading import Lock
|
||||||
|
|
||||||
import pwnagotchi
|
import pwnagotchi
|
||||||
import pwnagotchi.plugins as plugins
|
import pwnagotchi.plugins as plugins
|
||||||
from pwnagotchi.utils import StatusFile
|
from pwnagotchi.utils import StatusFile, parse_version as version_to_tuple
|
||||||
|
|
||||||
|
|
||||||
def check(version, repo, native=True):
|
def check(version, repo, native=True):
|
||||||
@@ -29,8 +29,8 @@ def check(version, repo, native=True):
|
|||||||
info['available'] = latest_ver = latest['tag_name'].replace('v', '')
|
info['available'] = latest_ver = latest['tag_name'].replace('v', '')
|
||||||
is_arm = info['arch'].startswith('arm')
|
is_arm = info['arch'].startswith('arm')
|
||||||
|
|
||||||
local = pkg_resources.parse_version(info['current'])
|
local = version_to_tuple(info['current'])
|
||||||
remote = pkg_resources.parse_version(latest_ver)
|
remote = version_to_tuple(latest_ver)
|
||||||
if remote > local:
|
if remote > local:
|
||||||
if not native:
|
if not native:
|
||||||
info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name'])
|
info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name'])
|
||||||
@@ -150,69 +150,74 @@ class AutoUpdate(plugins.Plugin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ready = False
|
self.ready = False
|
||||||
self.status = StatusFile('/root/.auto-update')
|
self.status = StatusFile('/root/.auto-update')
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
if 'interval' not in self.options or ('interval' in self.options and self.options['interval'] is None):
|
if 'interval' not in self.options or ('interval' in self.options and not self.options['interval']):
|
||||||
logging.error("[update] main.plugins.auto-update.interval is not set")
|
logging.error("[update] main.plugins.auto-update.interval is not set")
|
||||||
return
|
return
|
||||||
self.ready = True
|
self.ready = True
|
||||||
logging.info("[update] plugin loaded.")
|
logging.info("[update] plugin loaded.")
|
||||||
|
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
if self.lock.locked():
|
||||||
|
|
||||||
if not self.ready:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.status.newer_then_hours(self.options['interval']):
|
with self.lock:
|
||||||
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
|
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||||
return
|
|
||||||
|
|
||||||
logging.info("[update] checking for updates ...")
|
if not self.ready:
|
||||||
|
return
|
||||||
|
|
||||||
display = agent.view()
|
if self.status.newer_then_hours(self.options['interval']):
|
||||||
prev_status = display.get('status')
|
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
logging.info("[update] checking for updates ...")
|
||||||
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
|
||||||
|
|
||||||
to_install = []
|
display = agent.view()
|
||||||
to_check = [
|
prev_status = display.get('status')
|
||||||
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
|
||||||
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
|
||||||
('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
|
|
||||||
]
|
|
||||||
|
|
||||||
for repo, local_version, is_native, svc_name in to_check:
|
try:
|
||||||
info = check(local_version, repo, is_native)
|
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
||||||
if info['url'] is not None:
|
|
||||||
logging.warning(
|
|
||||||
"update for %s available (local version is '%s'): %s" % (
|
|
||||||
repo, info['current'], info['url']))
|
|
||||||
info['service'] = svc_name
|
|
||||||
to_install.append(info)
|
|
||||||
|
|
||||||
num_updates = len(to_install)
|
to_install = []
|
||||||
num_installed = 0
|
to_check = [
|
||||||
|
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||||
|
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
||||||
|
('evilsocket/pwnagotchi', pwnagotchi.__version__, False, 'pwnagotchi')
|
||||||
|
]
|
||||||
|
|
||||||
if num_updates > 0:
|
for repo, local_version, is_native, svc_name in to_check:
|
||||||
if self.options['install']:
|
info = check(local_version, repo, is_native)
|
||||||
for update in to_install:
|
if info['url'] is not None:
|
||||||
plugins.on('updating')
|
logging.warning(
|
||||||
if install(display, update):
|
"update for %s available (local version is '%s'): %s" % (
|
||||||
num_installed += 1
|
repo, info['current'], info['url']))
|
||||||
else:
|
info['service'] = svc_name
|
||||||
prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
|
to_install.append(info)
|
||||||
|
|
||||||
logging.info("[update] done")
|
num_updates = len(to_install)
|
||||||
|
num_installed = 0
|
||||||
|
|
||||||
self.status.update()
|
if num_updates > 0:
|
||||||
|
if self.options['install']:
|
||||||
|
for update in to_install:
|
||||||
|
plugins.on('updating')
|
||||||
|
if install(display, update):
|
||||||
|
num_installed += 1
|
||||||
|
else:
|
||||||
|
prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
|
||||||
|
|
||||||
if num_installed > 0:
|
logging.info("[update] done")
|
||||||
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
|
||||||
pwnagotchi.reboot()
|
|
||||||
|
|
||||||
except Exception as e:
|
self.status.update()
|
||||||
logging.error("[update] %s" % e)
|
|
||||||
|
|
||||||
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
|
if num_installed > 0:
|
||||||
|
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
||||||
|
pwnagotchi.reboot()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("[update] %s" % e)
|
||||||
|
|
||||||
|
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
|
||||||
|
@@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
import dbus
|
import dbus
|
||||||
|
|
||||||
@@ -426,6 +427,7 @@ class BTTether(plugins.Plugin):
|
|||||||
self.ready = False
|
self.ready = False
|
||||||
self.options = dict()
|
self.options = dict()
|
||||||
self.devices = dict()
|
self.devices = dict()
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
# new config
|
# new config
|
||||||
@@ -435,7 +437,7 @@ class BTTether(plugins.Plugin):
|
|||||||
for device_opt in ['enabled', 'priority', 'scantime', 'search_order',
|
for device_opt in ['enabled', 'priority', 'scantime', 'search_order',
|
||||||
'max_tries', 'share_internet', 'mac', 'ip',
|
'max_tries', 'share_internet', 'mac', 'ip',
|
||||||
'netmask', 'interval']:
|
'netmask', 'interval']:
|
||||||
if device_opt not in options or (device_opt in options and options[device_opt] is None):
|
if device_opt not in options or options[device_opt] is None:
|
||||||
logging.error("BT-TETHER: Please specify the %s for device %s.",
|
logging.error("BT-TETHER: Please specify the %s for device %s.",
|
||||||
device_opt, device)
|
device_opt, device)
|
||||||
break
|
break
|
||||||
@@ -446,7 +448,7 @@ class BTTether(plugins.Plugin):
|
|||||||
# legacy
|
# legacy
|
||||||
if 'mac' in self.options:
|
if 'mac' in self.options:
|
||||||
for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
|
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):
|
if opt not in self.options or self.options[opt] is None:
|
||||||
logging.error("BT-TETHER: Please specify the %s in your config.yml.", opt)
|
logging.error("BT-TETHER: Please specify the %s in your config.yml.", opt)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -466,110 +468,116 @@ class BTTether(plugins.Plugin):
|
|||||||
logging.info("BT-TETHER: Successfully loaded ...")
|
logging.info("BT-TETHER: Successfully loaded ...")
|
||||||
self.ready = True
|
self.ready = True
|
||||||
|
|
||||||
|
def on_unload(self, ui):
|
||||||
|
with ui._lock:
|
||||||
|
ui.remove_element('bluetooth')
|
||||||
|
|
||||||
def on_ui_setup(self, ui):
|
def on_ui_setup(self, ui):
|
||||||
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
|
with ui._lock:
|
||||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
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):
|
def on_ui_update(self, ui):
|
||||||
if not self.ready:
|
if not self.ready:
|
||||||
return
|
return
|
||||||
|
|
||||||
devices_to_try = list()
|
with self.lock:
|
||||||
connected_priorities = list()
|
devices_to_try = list()
|
||||||
any_device_connected = False # if this is true, last status on screen should be C
|
connected_priorities = list()
|
||||||
|
any_device_connected = False # if this is true, last status on screen should be C
|
||||||
|
|
||||||
for _, device in self.devices.items():
|
for _, device in self.devices.items():
|
||||||
if device.connected():
|
if device.connected():
|
||||||
connected_priorities.append(device.priority)
|
connected_priorities.append(device.priority)
|
||||||
any_device_connected = True
|
any_device_connected = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not device.max_tries or (device.max_tries > device.tries):
|
if not device.max_tries or (device.max_tries > device.tries):
|
||||||
if not device.status.newer_then_minutes(device.interval):
|
if not device.status.newer_then_minutes(device.interval):
|
||||||
devices_to_try.append(device)
|
devices_to_try.append(device)
|
||||||
device.status.update()
|
device.status.update()
|
||||||
device.tries += 1
|
device.tries += 1
|
||||||
|
|
||||||
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
|
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
|
||||||
|
|
||||||
for device in sorted_devices:
|
for device in sorted_devices:
|
||||||
bt = BTNap(device.mac)
|
bt = BTNap(device.mac)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
|
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
|
||||||
dev_remote = bt.wait_for_device(timeout=device.scantime)
|
dev_remote = bt.wait_for_device(timeout=device.scantime)
|
||||||
if dev_remote is None:
|
if dev_remote is None:
|
||||||
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
|
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')
|
ui.set('bluetooth', 'NF')
|
||||||
continue
|
continue
|
||||||
except Exception as bt_ex:
|
|
||||||
logging.error(bt_ex)
|
|
||||||
ui.set('bluetooth', 'NF')
|
|
||||||
continue
|
|
||||||
|
|
||||||
paired = bt.is_paired()
|
paired = bt.is_paired()
|
||||||
if not paired:
|
if not paired:
|
||||||
if BTNap.pair(dev_remote):
|
if BTNap.pair(dev_remote):
|
||||||
logging.debug('BT-TETHER: Paired with %s.', device.name)
|
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:
|
else:
|
||||||
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
|
logging.debug('BT-TETHER: Already paired.')
|
||||||
ui.set('bluetooth', 'PE')
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logging.debug('BT-TETHER: Already paired.')
|
|
||||||
|
|
||||||
|
|
||||||
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
|
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
|
||||||
device.network, success = BTNap.nap(dev_remote)
|
device.network, success = BTNap.nap(dev_remote)
|
||||||
interface = None
|
interface = None
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
try:
|
try:
|
||||||
interface = device.interface()
|
interface = device.interface()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if interface is None:
|
||||||
|
ui.set('bluetooth', 'BE')
|
||||||
|
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.debug('BT-TETHER: Created interface (%s)', interface)
|
||||||
|
ui.set('bluetooth', 'C')
|
||||||
|
any_device_connected = True
|
||||||
|
device.tries = 0 # reset tries
|
||||||
|
else:
|
||||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
||||||
|
ui.set('bluetooth', 'NF')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if interface is None:
|
addr = f"{device.ip}/{device.netmask}"
|
||||||
ui.set('bluetooth', 'BE')
|
if device.gateway:
|
||||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
gateway = device.gateway
|
||||||
|
else:
|
||||||
|
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
|
||||||
|
|
||||||
|
wrapped_interface = IfaceWrapper(interface)
|
||||||
|
logging.debug('BT-TETHER: Add ip to %s', interface)
|
||||||
|
if not wrapped_interface.set_addr(addr):
|
||||||
|
ui.set('bluetooth', 'AE')
|
||||||
|
logging.debug("BT-TETHER: Could not add ip to %s", interface)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.debug('BT-TETHER: Created interface (%s)', interface)
|
if device.share_internet:
|
||||||
|
if not connected_priorities or device.priority > max(connected_priorities):
|
||||||
|
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
|
||||||
|
IfaceWrapper.set_route(gateway, interface)
|
||||||
|
connected_priorities.append(device.priority)
|
||||||
|
|
||||||
|
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
|
||||||
|
with open('/etc/resolv.conf', 'r+') as resolv:
|
||||||
|
nameserver = resolv.read()
|
||||||
|
if 'nameserver 9.9.9.9' not in nameserver:
|
||||||
|
logging.debug('BT-TETHER: Added nameserver')
|
||||||
|
resolv.seek(0)
|
||||||
|
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
||||||
|
|
||||||
|
if any_device_connected:
|
||||||
ui.set('bluetooth', 'C')
|
ui.set('bluetooth', 'C')
|
||||||
any_device_connected = True
|
|
||||||
device.tries = 0 # reset tries
|
|
||||||
else:
|
|
||||||
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
|
|
||||||
ui.set('bluetooth', 'NF')
|
|
||||||
continue
|
|
||||||
|
|
||||||
addr = f"{device.ip}/{device.netmask}"
|
|
||||||
if device.gateway:
|
|
||||||
gateway = device.gateway
|
|
||||||
else:
|
|
||||||
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
|
|
||||||
|
|
||||||
wrapped_interface = IfaceWrapper(interface)
|
|
||||||
logging.debug('BT-TETHER: Add ip to %s', interface)
|
|
||||||
if not wrapped_interface.set_addr(addr):
|
|
||||||
ui.set('bluetooth', 'AE')
|
|
||||||
logging.debug("BT-TETHER: Could not add ip to %s", interface)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if device.share_internet:
|
|
||||||
if not connected_priorities or device.priority > max(connected_priorities):
|
|
||||||
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
|
|
||||||
IfaceWrapper.set_route(gateway, interface)
|
|
||||||
connected_priorities.append(device.priority)
|
|
||||||
|
|
||||||
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
|
|
||||||
with open('/etc/resolv.conf', 'r+') as resolv:
|
|
||||||
nameserver = resolv.read()
|
|
||||||
if 'nameserver 9.9.9.9' not in nameserver:
|
|
||||||
logging.debug('BT-TETHER: Added nameserver')
|
|
||||||
resolv.seek(0)
|
|
||||||
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
|
||||||
|
|
||||||
if any_device_connected:
|
|
||||||
ui.set('bluetooth', 'C')
|
|
||||||
|
@@ -25,6 +25,10 @@ class Example(plugins.Plugin):
|
|||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
|
logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
|
||||||
|
|
||||||
|
# called before the plugin is unloaded
|
||||||
|
def on_unload(self, ui):
|
||||||
|
pass
|
||||||
|
|
||||||
# called hen there's internet connectivity
|
# called hen there's internet connectivity
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
pass
|
pass
|
||||||
@@ -118,6 +122,11 @@ class Example(plugins.Plugin):
|
|||||||
def on_wifi_update(self, agent, access_points):
|
def on_wifi_update(self, agent, access_points):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# called when the agent refreshed an unfiltered access point list
|
||||||
|
# this list contains all access points that were detected BEFORE filtering
|
||||||
|
def on_unfiltered_ap_list(self, agent, access_points):
|
||||||
|
pass
|
||||||
|
|
||||||
# called when the agent is sending an association frame
|
# called when the agent is sending an association frame
|
||||||
def on_association(self, agent, access_point):
|
def on_association(self, agent, access_point):
|
||||||
pass
|
pass
|
||||||
|
@@ -32,6 +32,7 @@ class GPIOButtons(plugins.Plugin):
|
|||||||
GPIO.setmode(GPIO.BCM)
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
|
||||||
for gpio, command in gpios.items():
|
for gpio, command in gpios.items():
|
||||||
|
gpio = int(gpio)
|
||||||
self.ports[gpio] = command
|
self.ports[gpio] = command
|
||||||
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
|
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
|
||||||
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)
|
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)
|
||||||
|
@@ -44,9 +44,15 @@ class GPS(plugins.Plugin):
|
|||||||
self.coordinates = info["gps"]
|
self.coordinates = info["gps"]
|
||||||
gps_filename = filename.replace(".pcap", ".gps.json")
|
gps_filename = filename.replace(".pcap", ".gps.json")
|
||||||
|
|
||||||
logging.info(f"saving GPS to {gps_filename} ({self.coordinates})")
|
if self.coordinates and all([
|
||||||
with open(gps_filename, "w+t") as fp:
|
# avoid 0.000... measurements
|
||||||
json.dump(self.coordinates, fp)
|
self.coordinates["Latitude"], self.coordinates["Longitude"]
|
||||||
|
]):
|
||||||
|
logging.info(f"saving GPS to {gps_filename} ({self.coordinates})")
|
||||||
|
with open(gps_filename, "w+t") as fp:
|
||||||
|
json.dump(self.coordinates, fp)
|
||||||
|
else:
|
||||||
|
logging.info("not saving GPS. Couldn't find location.")
|
||||||
|
|
||||||
def on_ui_setup(self, ui):
|
def on_ui_setup(self, ui):
|
||||||
# add coordinates for other displays
|
# add coordinates for other displays
|
||||||
@@ -54,11 +60,19 @@ class GPS(plugins.Plugin):
|
|||||||
lat_pos = (127, 75)
|
lat_pos = (127, 75)
|
||||||
lon_pos = (122, 84)
|
lon_pos = (122, 84)
|
||||||
alt_pos = (127, 94)
|
alt_pos = (127, 94)
|
||||||
|
elif ui.is_waveshare_v1():
|
||||||
|
lat_pos = (130, 70)
|
||||||
|
lon_pos = (125, 80)
|
||||||
|
alt_pos = (130, 90)
|
||||||
elif ui.is_inky():
|
elif ui.is_inky():
|
||||||
|
lat_pos = (127, 60)
|
||||||
|
lon_pos = (127, 70)
|
||||||
|
alt_pos = (127, 80)
|
||||||
|
elif ui.is_waveshare144lcd():
|
||||||
# guessed values, add tested ones if you can
|
# guessed values, add tested ones if you can
|
||||||
lat_pos = (112, 30)
|
lat_pos = (67, 73)
|
||||||
lon_pos = (112, 49)
|
lon_pos = (62, 83)
|
||||||
alt_pos = (87, 63)
|
alt_pos = (67, 93)
|
||||||
else:
|
else:
|
||||||
# guessed values, add tested ones if you can
|
# guessed values, add tested ones if you can
|
||||||
lat_pos = (127, 51)
|
lat_pos = (127, 51)
|
||||||
@@ -104,6 +118,13 @@ class GPS(plugins.Plugin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def on_unload(self, ui):
|
||||||
|
with ui._lock:
|
||||||
|
ui.remove_element('latitude')
|
||||||
|
ui.remove_element('longitude')
|
||||||
|
ui.remove_element('altitude')
|
||||||
|
|
||||||
def on_ui_update(self, ui):
|
def on_ui_update(self, ui):
|
||||||
if self.coordinates and all([
|
if self.coordinates and all([
|
||||||
# avoid 0.000... measurements
|
# avoid 0.000... measurements
|
||||||
|
@@ -7,6 +7,7 @@ import re
|
|||||||
import pwnagotchi.grid as grid
|
import pwnagotchi.grid as grid
|
||||||
import pwnagotchi.plugins as plugins
|
import pwnagotchi.plugins as plugins
|
||||||
from pwnagotchi.utils import StatusFile, WifiInfo, extract_from_pcap
|
from pwnagotchi.utils import StatusFile, WifiInfo, extract_from_pcap
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
|
||||||
def parse_pcap(filename):
|
def parse_pcap(filename):
|
||||||
@@ -54,6 +55,7 @@ class Grid(plugins.Plugin):
|
|||||||
|
|
||||||
self.unread_messages = 0
|
self.unread_messages = 0
|
||||||
self.total_messages = 0
|
self.total_messages = 0
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
def is_excluded(self, what):
|
def is_excluded(self, what):
|
||||||
for skip in self.options['exclude']:
|
for skip in self.options['exclude']:
|
||||||
@@ -67,7 +69,8 @@ class Grid(plugins.Plugin):
|
|||||||
logging.info("grid plugin loaded.")
|
logging.info("grid plugin loaded.")
|
||||||
|
|
||||||
def set_reported(self, reported, net_id):
|
def set_reported(self, reported, net_id):
|
||||||
reported.append(net_id)
|
if net_id not in reported:
|
||||||
|
reported.append(net_id)
|
||||||
self.report.update(data={'reported': reported})
|
self.report.update(data={'reported': reported})
|
||||||
|
|
||||||
def check_inbox(self, agent):
|
def check_inbox(self, agent):
|
||||||
@@ -121,21 +124,25 @@ class Grid(plugins.Plugin):
|
|||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
logging.debug("internet available")
|
logging.debug("internet available")
|
||||||
|
|
||||||
try:
|
if self.lock.locked():
|
||||||
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
|
return
|
||||||
|
|
||||||
try:
|
with self.lock:
|
||||||
self.check_inbox(agent)
|
try:
|
||||||
except Exception as e:
|
grid.update_data(agent.last_session)
|
||||||
logging.error("[grid] error while checking inbox: %s" % e)
|
except Exception as e:
|
||||||
logging.debug(e, exc_info=True)
|
logging.error("error connecting to the pwngrid-peer service: %s" % e)
|
||||||
|
logging.debug(e, exc_info=True)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.check_handshakes(agent)
|
self.check_inbox(agent)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("[grid] error while checking pcaps: %s" % e)
|
logging.error("[grid] error while checking inbox: %s" % e)
|
||||||
logging.debug(e, exc_info=True)
|
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)
|
||||||
|
@@ -44,12 +44,12 @@ class Led(plugins.Plugin):
|
|||||||
logging.debug("[led] using pattern '%s' ..." % pattern)
|
logging.debug("[led] using pattern '%s' ..." % pattern)
|
||||||
for c in pattern:
|
for c in pattern:
|
||||||
if c == ' ':
|
if c == ' ':
|
||||||
self._led(0)
|
|
||||||
else:
|
|
||||||
self._led(1)
|
self._led(1)
|
||||||
|
else:
|
||||||
|
self._led(0)
|
||||||
time.sleep(self._delay / 1000.0)
|
time.sleep(self._delay / 1000.0)
|
||||||
# reset
|
# reset
|
||||||
self._led(1)
|
self._led(0)
|
||||||
|
|
||||||
def _worker(self):
|
def _worker(self):
|
||||||
while True:
|
while True:
|
||||||
|
282
pwnagotchi/plugins/default/logtail.py
Normal file
282
pwnagotchi/plugins/default/logtail.py
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from time import sleep
|
||||||
|
from datetime import datetime,timedelta
|
||||||
|
from pwnagotchi import plugins
|
||||||
|
from pwnagotchi.utils import StatusFile
|
||||||
|
from flask import render_template_string
|
||||||
|
from flask import jsonify
|
||||||
|
from flask import abort
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE = """
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "plugins" %}
|
||||||
|
{% block title %}
|
||||||
|
Logtail
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
#filter {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 20px 12px 40px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px;
|
||||||
|
width: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
td:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
thead, tr:hover {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
div.sticky {
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.sticky > * {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
div.sticky > span {
|
||||||
|
width: 1%;
|
||||||
|
}
|
||||||
|
div.sticky > input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
tr.default {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
tr.info {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
tr.warning {
|
||||||
|
color: darkorange;
|
||||||
|
}
|
||||||
|
tr.error {
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
tr.debug {
|
||||||
|
color: blueviolet;
|
||||||
|
}
|
||||||
|
.ui-mobile .ui-page-active {
|
||||||
|
overflow: visible;
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
var content = document.getElementById('content');
|
||||||
|
var filter = document.getElementById('filter');
|
||||||
|
var filterVal = filter.value.toUpperCase();
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', '{{ url_for('plugins') }}/logtail/stream');
|
||||||
|
xhr.send();
|
||||||
|
var position = 0;
|
||||||
|
var data;
|
||||||
|
var time;
|
||||||
|
var level;
|
||||||
|
var msg;
|
||||||
|
var colorClass;
|
||||||
|
|
||||||
|
function handleNewData() {
|
||||||
|
var messages = xhr.responseText.split('\\n');
|
||||||
|
filterVal = filter.value.toUpperCase();
|
||||||
|
messages.slice(position, -1).forEach(function(value) {
|
||||||
|
|
||||||
|
if (value.charAt(0) != '[') {
|
||||||
|
msg = value;
|
||||||
|
time = '';
|
||||||
|
level = '';
|
||||||
|
} else {
|
||||||
|
data = value.split(']');
|
||||||
|
time = data.shift() + ']';
|
||||||
|
level = data.shift() + ']';
|
||||||
|
msg = data.join(']');
|
||||||
|
|
||||||
|
switch(level) {
|
||||||
|
case ' [INFO]':
|
||||||
|
colorClass = 'info';
|
||||||
|
break;
|
||||||
|
case ' [WARNING]':
|
||||||
|
colorClass = 'warning';
|
||||||
|
break;
|
||||||
|
case ' [ERROR]':
|
||||||
|
colorClass = 'error';
|
||||||
|
break;
|
||||||
|
case ' [DEBUG]':
|
||||||
|
colorClass = 'debug';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
colorClass = 'default';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
var td1 = document.createElement('td');
|
||||||
|
var td2 = document.createElement('td');
|
||||||
|
var td3 = document.createElement('td');
|
||||||
|
|
||||||
|
td1.textContent = time;
|
||||||
|
td2.textContent = level;
|
||||||
|
td3.textContent = msg;
|
||||||
|
|
||||||
|
tr.appendChild(td1);
|
||||||
|
tr.appendChild(td2);
|
||||||
|
tr.appendChild(td3);
|
||||||
|
|
||||||
|
tr.className = colorClass;
|
||||||
|
|
||||||
|
if (filterVal.length > 0 && value.toUpperCase().indexOf(filterVal) == -1) {
|
||||||
|
tr.style.visibility = "collapse";
|
||||||
|
}
|
||||||
|
|
||||||
|
content.appendChild(tr);
|
||||||
|
});
|
||||||
|
position = messages.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollingElement = (document.scrollingElement || document.body)
|
||||||
|
function scrollToBottom () {
|
||||||
|
scrollingElement.scrollTop = scrollingElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timer;
|
||||||
|
var scrollElm = document.getElementById('autoscroll');
|
||||||
|
timer = setInterval(function() {
|
||||||
|
handleNewData();
|
||||||
|
if (scrollElm.checked) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
var typingTimer;
|
||||||
|
var doneTypingInterval = 1000;
|
||||||
|
|
||||||
|
filter.onkeyup = function() {
|
||||||
|
clearTimeout(typingTimer);
|
||||||
|
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.onkeydown = function() {
|
||||||
|
clearTimeout(typingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doneTyping() {
|
||||||
|
document.body.style.cursor = 'progress';
|
||||||
|
var table, tr, tds, td, i, txtValue;
|
||||||
|
filterVal = filter.value.toUpperCase();
|
||||||
|
table = document.getElementById("content");
|
||||||
|
tr = table.getElementsByTagName("tr");
|
||||||
|
for (i = 0; i < tr.length; i++) {
|
||||||
|
tds = tr[i].getElementsByTagName("td");
|
||||||
|
if (tds) {
|
||||||
|
for (l = 0; l < tds.length; l++) {
|
||||||
|
td = tds[l];
|
||||||
|
if (td) {
|
||||||
|
txtValue = td.textContent || td.innerText;
|
||||||
|
if (txtValue.toUpperCase().indexOf(filterVal) > -1) {
|
||||||
|
tr[i].style.visibility = "visible";
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
tr[i].style.visibility = "collapse";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.body.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="sticky">
|
||||||
|
<input type="text" id="filter" placeholder="Search for ..." title="Type in a filter">
|
||||||
|
<span><input checked type="checkbox" id="autoscroll"></span>
|
||||||
|
<span><label for="autoscroll"> Autoscroll to bottom</label><br></span>
|
||||||
|
</div>
|
||||||
|
<table id="content">
|
||||||
|
<thead>
|
||||||
|
<th>
|
||||||
|
Time
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Level
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Message
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Logtail(plugins.Plugin):
|
||||||
|
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||||
|
__version__ = '0.1.0'
|
||||||
|
__license__ = 'GPL3'
|
||||||
|
__description__ = 'This plugin tails the logfile.'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.options = dict()
|
||||||
|
self.ready = False
|
||||||
|
|
||||||
|
def on_config_changed(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.ready = True
|
||||||
|
|
||||||
|
def on_loaded(self):
|
||||||
|
"""
|
||||||
|
Gets called when the plugin gets loaded
|
||||||
|
"""
|
||||||
|
logging.info("Logtail plugin loaded.")
|
||||||
|
|
||||||
|
def on_webhook(self, path, request):
|
||||||
|
if not self.ready:
|
||||||
|
return "Plugin not ready"
|
||||||
|
|
||||||
|
if not path or path == "/":
|
||||||
|
return render_template_string(TEMPLATE)
|
||||||
|
|
||||||
|
if path == 'stream':
|
||||||
|
def generate():
|
||||||
|
with open(self.config['main']['log']['path']) as f:
|
||||||
|
yield f.read()
|
||||||
|
while True:
|
||||||
|
yield f.readline()
|
||||||
|
|
||||||
|
return Response(generate(), mimetype='text/plain')
|
||||||
|
|
||||||
|
abort(404)
|
@@ -44,9 +44,18 @@ class MemTemp(plugins.Plugin):
|
|||||||
if ui.is_waveshare_v2():
|
if ui.is_waveshare_v2():
|
||||||
h_pos = (180, 80)
|
h_pos = (180, 80)
|
||||||
v_pos = (180, 61)
|
v_pos = (180, 61)
|
||||||
|
elif ui.is_waveshare_v1():
|
||||||
|
h_pos = (170, 80)
|
||||||
|
v_pos = (170, 61)
|
||||||
|
elif ui.is_waveshare144lcd():
|
||||||
|
h_pos = (53, 77)
|
||||||
|
v_pos = (78, 67)
|
||||||
elif ui.is_inky():
|
elif ui.is_inky():
|
||||||
h_pos = (140, 68)
|
h_pos = (140, 68)
|
||||||
v_pos = (165, 54)
|
v_pos = (165, 54)
|
||||||
|
elif ui.is_waveshare27inch():
|
||||||
|
h_pos = (192, 138)
|
||||||
|
v_pos = (216, 122)
|
||||||
else:
|
else:
|
||||||
h_pos = (155, 76)
|
h_pos = (155, 76)
|
||||||
v_pos = (180, 61)
|
v_pos = (180, 61)
|
||||||
@@ -61,6 +70,10 @@ class MemTemp(plugins.Plugin):
|
|||||||
position=h_pos,
|
position=h_pos,
|
||||||
label_font=fonts.Small, text_font=fonts.Small))
|
label_font=fonts.Small, text_font=fonts.Small))
|
||||||
|
|
||||||
|
def on_unload(self, ui):
|
||||||
|
with ui._lock:
|
||||||
|
ui.remove_element('memtemp')
|
||||||
|
|
||||||
def on_ui_update(self, ui):
|
def on_ui_update(self, ui):
|
||||||
if self.options['scale'] == "fahrenheit":
|
if self.options['scale'] == "fahrenheit":
|
||||||
temp = (pwnagotchi.temperature() * 9 / 5) + 32
|
temp = (pwnagotchi.temperature() * 9 / 5) + 32
|
||||||
@@ -69,7 +82,7 @@ class MemTemp(plugins.Plugin):
|
|||||||
temp = pwnagotchi.temperature() + 273.15
|
temp = pwnagotchi.temperature() + 273.15
|
||||||
symbol = "k"
|
symbol = "k"
|
||||||
else:
|
else:
|
||||||
# default to celsius
|
# default to celsius
|
||||||
temp = pwnagotchi.temperature()
|
temp = pwnagotchi.temperature()
|
||||||
symbol = "c"
|
symbol = "c"
|
||||||
|
|
||||||
|
@@ -1,35 +1,39 @@
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
import pwnagotchi.plugins as plugins
|
import pwnagotchi.plugins as plugins
|
||||||
from pwnagotchi.utils import StatusFile
|
from pwnagotchi.utils import StatusFile
|
||||||
|
|
||||||
MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
|
||||||
|
|
||||||
|
|
||||||
class NetPos(plugins.Plugin):
|
class NetPos(plugins.Plugin):
|
||||||
__author__ = 'zenzen san'
|
__author__ = 'zenzen san'
|
||||||
__version__ = '2.0.1'
|
__version__ = '2.0.3'
|
||||||
__license__ = 'GPL3'
|
__license__ = 'GPL3'
|
||||||
__description__ = """Saves a json file with the access points with more signal
|
__description__ = """Saves a json file with the access points with more signal
|
||||||
whenever a handshake is captured.
|
whenever a handshake is captured.
|
||||||
When internet is available the files are converted in geo locations
|
When internet is available the files are converted in geo locations
|
||||||
using Mozilla LocationService """
|
using Mozilla LocationService """
|
||||||
|
|
||||||
|
API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
|
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
|
||||||
self.skip = list()
|
self.skip = list()
|
||||||
self.ready = False
|
self.ready = False
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
if 'api_key' not in self.options or ('api_key' in self.options and not self.options['api_key']):
|
||||||
logging.error("NET-POS: api_key isn't set. Can't use mozilla's api.")
|
logging.error("NET-POS: api_key isn't set. Can't use mozilla's api.")
|
||||||
return
|
return
|
||||||
|
if 'api_url' in self.options:
|
||||||
|
self.API_URL = self.options['api_url']
|
||||||
self.ready = True
|
self.ready = True
|
||||||
logging.info("net-pos plugin loaded.")
|
logging.info("net-pos plugin loaded.")
|
||||||
|
logging.debug(f"net-pos: use api_url: {self.API_URL}");
|
||||||
|
|
||||||
def _append_saved(self, path):
|
def _append_saved(self, path):
|
||||||
to_save = list()
|
to_save = list()
|
||||||
@@ -45,60 +49,66 @@ class NetPos(plugins.Plugin):
|
|||||||
saved_file.write(x + "\n")
|
saved_file.write(x + "\n")
|
||||||
|
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
if self.ready:
|
if self.lock.locked():
|
||||||
config = agent.config()
|
return
|
||||||
display = agent.view()
|
with self.lock:
|
||||||
reported = self.report.data_field_or('reported', default=list())
|
if self.ready:
|
||||||
handshake_dir = config['bettercap']['handshakes']
|
config = agent.config()
|
||||||
|
display = agent.view()
|
||||||
|
reported = self.report.data_field_or('reported', default=list())
|
||||||
|
handshake_dir = config['bettercap']['handshakes']
|
||||||
|
|
||||||
all_files = os.listdir(handshake_dir)
|
all_files = os.listdir(handshake_dir)
|
||||||
all_np_files = [os.path.join(handshake_dir, filename)
|
all_np_files = [os.path.join(handshake_dir, filename)
|
||||||
for filename in all_files
|
for filename in all_files
|
||||||
if filename.endswith('.net-pos.json')]
|
if filename.endswith('.net-pos.json')]
|
||||||
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
|
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
|
||||||
|
|
||||||
if new_np_files:
|
if new_np_files:
|
||||||
logging.info("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
|
logging.debug("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
|
||||||
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
|
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
|
||||||
display.update(force=True)
|
display.update(force=True)
|
||||||
for idx, np_file in enumerate(new_np_files):
|
for idx, np_file in enumerate(new_np_files):
|
||||||
|
|
||||||
|
geo_file = np_file.replace('.net-pos.json', '.geo.json')
|
||||||
|
if os.path.exists(geo_file):
|
||||||
|
# got already the position
|
||||||
|
reported.append(np_file)
|
||||||
|
self.report.update(data={'reported': reported})
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
geo_data = self._get_geo_data(np_file) # returns json obj
|
||||||
|
except requests.exceptions.RequestException as req_e:
|
||||||
|
logging.error("NET-POS: %s - RequestException: %s", np_file, req_e)
|
||||||
|
self.skip += np_file
|
||||||
|
continue
|
||||||
|
except json.JSONDecodeError as js_e:
|
||||||
|
logging.error("NET-POS: %s - JSONDecodeError: %s, removing it...", np_file, js_e)
|
||||||
|
os.remove(np_file)
|
||||||
|
continue
|
||||||
|
except OSError as os_e:
|
||||||
|
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
|
||||||
|
self.skip += np_file
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(geo_file, 'w+t') as sf:
|
||||||
|
json.dump(geo_data, sf)
|
||||||
|
|
||||||
geo_file = np_file.replace('.net-pos.json', '.geo.json')
|
|
||||||
if os.path.exists(geo_file):
|
|
||||||
# got already the position
|
|
||||||
reported.append(np_file)
|
reported.append(np_file)
|
||||||
self.report.update(data={'reported': reported})
|
self.report.update(data={'reported': reported})
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
|
||||||
geo_data = self._get_geo_data(np_file) # returns json obj
|
display.update(force=True)
|
||||||
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
|
|
||||||
|
|
||||||
with open(geo_file, 'w+t') as sf:
|
|
||||||
json.dump(geo_data, sf)
|
|
||||||
|
|
||||||
reported.append(np_file)
|
|
||||||
self.report.update(data={'reported': reported})
|
|
||||||
|
|
||||||
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
|
|
||||||
display.update(force=True)
|
|
||||||
|
|
||||||
def on_handshake(self, agent, filename, access_point, client_station):
|
def on_handshake(self, agent, filename, access_point, client_station):
|
||||||
netpos = self._get_netpos(agent)
|
netpos = self._get_netpos(agent)
|
||||||
|
if not netpos['wifiAccessPoints']:
|
||||||
|
return
|
||||||
|
|
||||||
netpos["ts"] = int("%.0f" % time.time())
|
netpos["ts"] = int("%.0f" % time.time())
|
||||||
netpos_filename = filename.replace('.pcap', '.net-pos.json')
|
netpos_filename = filename.replace('.pcap', '.net-pos.json')
|
||||||
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
|
logging.debug("NET-POS: Saving net-location to %s", netpos_filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(netpos_filename, 'w+t') as net_pos_file:
|
with open(netpos_filename, 'w+t') as net_pos_file:
|
||||||
@@ -106,6 +116,7 @@ class NetPos(plugins.Plugin):
|
|||||||
except OSError as os_e:
|
except OSError as os_e:
|
||||||
logging.error("NET-POS: %s", os_e)
|
logging.error("NET-POS: %s", os_e)
|
||||||
|
|
||||||
|
|
||||||
def _get_netpos(self, agent):
|
def _get_netpos(self, agent):
|
||||||
aps = agent.get_access_points()
|
aps = agent.get_access_points()
|
||||||
netpos = dict()
|
netpos = dict()
|
||||||
@@ -117,7 +128,7 @@ class NetPos(plugins.Plugin):
|
|||||||
return netpos
|
return netpos
|
||||||
|
|
||||||
def _get_geo_data(self, path, timeout=30):
|
def _get_geo_data(self, path, timeout=30):
|
||||||
geourl = MOZILLA_API_URL.format(api=self.options['api_key'])
|
geourl = self.API_URL.format(api=self.options['api_key'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(path, "r") as json_file:
|
with open(path, "r") as json_file:
|
||||||
|
@@ -1,49 +1,45 @@
|
|||||||
import os
|
import os
|
||||||
|
import csv
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
from pwnagotchi.utils import StatusFile
|
from datetime import datetime
|
||||||
|
from threading import Lock
|
||||||
|
from pwnagotchi.utils import StatusFile, remove_whitelisted
|
||||||
import pwnagotchi.plugins as plugins
|
import pwnagotchi.plugins as plugins
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
|
|
||||||
class OnlineHashCrack(plugins.Plugin):
|
class OnlineHashCrack(plugins.Plugin):
|
||||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||||
__version__ = '2.0.0'
|
__version__ = '2.0.1'
|
||||||
__license__ = 'GPL3'
|
__license__ = 'GPL3'
|
||||||
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
|
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ready = False
|
self.ready = False
|
||||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
try:
|
||||||
|
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||||
|
except JSONDecodeError:
|
||||||
|
os.remove('/root/.ohc_uploads')
|
||||||
|
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||||
self.skip = list()
|
self.skip = list()
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
"""
|
"""
|
||||||
Gets called when the plugin gets loaded
|
Gets called when the plugin gets loaded
|
||||||
"""
|
"""
|
||||||
if 'email' not in self.options or ('email' in self.options and self.options['email'] is None):
|
if 'email' not in self.options or ('email' in self.options and not self.options['email']):
|
||||||
logging.error("OHC: Email isn't set. Can't upload to onlinehashcrack.com")
|
logging.error("OHC: Email isn't set. Can't upload to onlinehashcrack.com")
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'whitelist' not in self.options:
|
if 'whitelist' not in self.options:
|
||||||
self.options['whitelist'] = []
|
self.options['whitelist'] = list()
|
||||||
|
|
||||||
# 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
|
self.ready = True
|
||||||
|
logging.info("OHC: OnlineHashCrack plugin loaded.")
|
||||||
|
|
||||||
def _filter_handshake_file(self, handshake_filename):
|
|
||||||
try:
|
|
||||||
basename = os.path.basename(handshake_filename)
|
|
||||||
ssid, bssid = basename.split('_')
|
|
||||||
# remove the ".pcap" from the bssid (which is really just the end of the filename)
|
|
||||||
bssid = bssid[:-5]
|
|
||||||
except:
|
|
||||||
# something failed in our parsing of the filename. let the file through
|
|
||||||
return True
|
|
||||||
|
|
||||||
return ssid not in self.options['whitelist'] and bssid not in self.options['whitelist']
|
|
||||||
|
|
||||||
def _upload_to_ohc(self, path, timeout=30):
|
def _upload_to_ohc(self, path, timeout=30):
|
||||||
"""
|
"""
|
||||||
@@ -64,37 +60,55 @@ class OnlineHashCrack(plugins.Plugin):
|
|||||||
logging.error(f"OHC: Got an exception while uploading {path} -> {e}")
|
logging.error(f"OHC: Got an exception while uploading {path} -> {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
def _download_cracked(self, save_file, timeout=120):
|
||||||
|
"""
|
||||||
|
Downloads the cracked passwords and saves them
|
||||||
|
|
||||||
|
returns the number of downloaded passwords
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
s = requests.Session()
|
||||||
|
dashboard = s.get(self.options['dashboard'], timeout=timeout)
|
||||||
|
result = s.get('https://www.onlinehashcrack.com/wpa-exportcsv', timeout=timeout)
|
||||||
|
result.raise_for_status()
|
||||||
|
with open(save_file, 'wb') as output_file:
|
||||||
|
output_file.write(result.content)
|
||||||
|
except requests.exceptions.RequestException as req_e:
|
||||||
|
raise req_e
|
||||||
|
except OSError as os_e:
|
||||||
|
raise os_e
|
||||||
|
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
"""
|
"""
|
||||||
Called in manual mode when there's internet connectivity
|
Called in manual mode when there's internet connectivity
|
||||||
"""
|
"""
|
||||||
if self.ready:
|
|
||||||
|
if not self.ready or self.lock.locked():
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
display = agent.view()
|
display = agent.view()
|
||||||
config = agent.config()
|
config = agent.config()
|
||||||
reported = self.report.data_field_or('reported', default=list())
|
reported = self.report.data_field_or('reported', default=list())
|
||||||
|
|
||||||
handshake_dir = config['bettercap']['handshakes']
|
handshake_dir = config['bettercap']['handshakes']
|
||||||
handshake_filenames = os.listdir(handshake_dir)
|
handshake_filenames = os.listdir(handshake_dir)
|
||||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||||
filename.endswith('.pcap')]
|
filename.endswith('.pcap')]
|
||||||
|
|
||||||
# pull out whitelisted APs
|
# pull out whitelisted APs
|
||||||
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
|
handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist'])
|
||||||
|
|
||||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||||
|
|
||||||
if handshake_new:
|
if handshake_new:
|
||||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onlinehashcrack.com")
|
||||||
|
|
||||||
for idx, handshake in enumerate(handshake_new):
|
for idx, handshake in enumerate(handshake_new):
|
||||||
display.set('status',
|
display.set('status',
|
||||||
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||||
display.update(force=True)
|
display.update(force=True)
|
||||||
try:
|
try:
|
||||||
self._upload_to_ohc(handshake)
|
self._upload_to_ohc(handshake)
|
||||||
reported.append(handshake)
|
if handshake not in reported:
|
||||||
self.report.update(data={'reported': reported})
|
reported.append(handshake)
|
||||||
logging.info(f"OHC: Successfully uploaded {handshake}")
|
self.report.update(data={'reported': reported})
|
||||||
|
logging.info(f"OHC: Successfully uploaded {handshake}")
|
||||||
except requests.exceptions.RequestException as req_e:
|
except requests.exceptions.RequestException as req_e:
|
||||||
self.skip.append(handshake)
|
self.skip.append(handshake)
|
||||||
logging.error("OHC: %s", req_e)
|
logging.error("OHC: %s", req_e)
|
||||||
@@ -103,3 +117,24 @@ class OnlineHashCrack(plugins.Plugin):
|
|||||||
self.skip.append(handshake)
|
self.skip.append(handshake)
|
||||||
logging.error("OHC: %s", os_e)
|
logging.error("OHC: %s", os_e)
|
||||||
continue
|
continue
|
||||||
|
if 'dashboard' in self.options and self.options['dashboard']:
|
||||||
|
cracked_file = os.path.join(handshake_dir, 'onlinehashcrack.cracked')
|
||||||
|
if os.path.exists(cracked_file):
|
||||||
|
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
|
||||||
|
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._download_cracked(cracked_file)
|
||||||
|
logging.info("OHC: Downloaded cracked passwords.")
|
||||||
|
except requests.exceptions.RequestException as req_e:
|
||||||
|
logging.debug("OHC: %s", req_e)
|
||||||
|
except OSError as os_e:
|
||||||
|
logging.debug("OHC: %s", os_e)
|
||||||
|
if 'single_files' in self.options and self.options['single_files']:
|
||||||
|
with open(cracked_file, 'r') as cracked_list:
|
||||||
|
for row in csv.DictReader(cracked_list):
|
||||||
|
if row['password']:
|
||||||
|
filename = re.sub(r'[^a-zA-Z0-9]', '', row['ESSID']) + '_' + row['BSSID'].replace(':','')
|
||||||
|
if os.path.exists( os.path.join(handshake_dir, filename+'.pcap') ):
|
||||||
|
with open(os.path.join(handshake_dir, filename+'.pcap.cracked'), 'w') as f:
|
||||||
|
f.write(row['password'])
|
||||||
|
@@ -4,10 +4,7 @@ import pwnagotchi.plugins as plugins
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
|
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
|
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
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +18,7 @@ class PawGPS(plugins.Plugin):
|
|||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
logging.info("PAW-GPS loaded")
|
logging.info("PAW-GPS loaded")
|
||||||
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
|
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)")
|
logging.info("PAW-GPS: No IP Address in the config file is defined, it uses the default (192.168.44.1:8080)")
|
||||||
|
|
||||||
def on_handshake(self, agent, filename, access_point, client_station):
|
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):
|
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
|
||||||
@@ -30,7 +27,7 @@ class PawGPS(plugins.Plugin):
|
|||||||
ip = self.options['ip']
|
ip = self.options['ip']
|
||||||
|
|
||||||
gps = requests.get('http://' + ip + '/gps.xhtml')
|
gps = requests.get('http://' + ip + '/gps.xhtml')
|
||||||
gps_filename = filename.replace('.pcap', '.gps.json')
|
gps_filename = filename.replace('.pcap', '.paw-gps.json')
|
||||||
|
|
||||||
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
|
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
|
||||||
with open(gps_filename, 'w+t') as f:
|
with open(gps_filename, 'w+t') as f:
|
||||||
|
263
pwnagotchi/plugins/default/session-stats.py
Normal file
263
pwnagotchi/plugins/default/session-stats.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from time import sleep
|
||||||
|
from datetime import datetime,timedelta
|
||||||
|
from pwnagotchi import plugins
|
||||||
|
from pwnagotchi.utils import StatusFile
|
||||||
|
from flask import render_template_string
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
TEMPLATE = """
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "plugins" %}
|
||||||
|
{% block title %}
|
||||||
|
Session stats
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/css/jquery.jqplot.min.css"/>
|
||||||
|
<link rel="stylesheet" href="/css/jquery.jqplot.css"/>
|
||||||
|
<style>
|
||||||
|
div.chart {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div#session {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script type="text/javascript" src="/js/jquery.jqplot.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/jquery.jqplot.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/plugins/jqplot.mobile.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/plugins/jqplot.json2.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/plugins/jqplot.dateAxisRenderer.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/plugins/jqplot.highlighter.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/plugins/jqplot.cursor.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/plugins/jqplot.enhancedLegendRenderer.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
$(document).ready(function(){
|
||||||
|
var ajaxDataRenderer = function(url, plot, options) {
|
||||||
|
var ret = null;
|
||||||
|
$.ajax({
|
||||||
|
async: false,
|
||||||
|
url: url,
|
||||||
|
dataType:"json",
|
||||||
|
success: function(data) {
|
||||||
|
ret = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadFiles(url, elm) {
|
||||||
|
var data = ajaxDataRenderer(url);
|
||||||
|
var x = document.getElementById(elm);
|
||||||
|
$.each(data['files'], function( index, value ) {
|
||||||
|
var option = document.createElement("option");
|
||||||
|
option.text = value;
|
||||||
|
x.add(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData(url, elm, title, fill) {
|
||||||
|
var data = ajaxDataRenderer(url);
|
||||||
|
var plot_os = $.jqplot(elm, data.values,{
|
||||||
|
title: title,
|
||||||
|
stackSeries: fill,
|
||||||
|
seriesDefaults: {
|
||||||
|
showMarker: !fill,
|
||||||
|
fill: fill,
|
||||||
|
fillAndStroke: fill
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: true,
|
||||||
|
renderer: $.jqplot.EnhancedLegendRenderer,
|
||||||
|
placement: 'outsideGrid',
|
||||||
|
labels: data.labels,
|
||||||
|
location: 's',
|
||||||
|
rendererOptions: {
|
||||||
|
numberRows: '2',
|
||||||
|
},
|
||||||
|
rowSpacing: '0px'
|
||||||
|
},
|
||||||
|
axes:{
|
||||||
|
xaxis:{
|
||||||
|
renderer:$.jqplot.DateAxisRenderer,
|
||||||
|
tickOptions:{formatString:'%H:%M:%S'}
|
||||||
|
},
|
||||||
|
yaxis:{
|
||||||
|
tickOptions:{formatString:'%.2f'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlighter: {
|
||||||
|
show: true,
|
||||||
|
sizeAdjust: 7.5
|
||||||
|
},
|
||||||
|
cursor:{
|
||||||
|
show: true,
|
||||||
|
tooltipLocation:'sw'
|
||||||
|
}
|
||||||
|
}).replot({
|
||||||
|
axes:{
|
||||||
|
xaxis:{
|
||||||
|
renderer:$.jqplot.DateAxisRenderer,
|
||||||
|
tickOptions:{formatString:'%H:%M:%S'}
|
||||||
|
},
|
||||||
|
yaxis:{
|
||||||
|
tickOptions:{formatString:'%.2f'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSessionFiles() {
|
||||||
|
loadFiles('/plugins/session-stats/session', 'session');
|
||||||
|
$("#session").change(function() {
|
||||||
|
loadSessionData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSessionData() {
|
||||||
|
var x = document.getElementById("session");
|
||||||
|
var session = x.options[x.selectedIndex].text;
|
||||||
|
loadData('/plugins/session-stats/os' + '?session=' + session, 'chart_os', 'OS', false)
|
||||||
|
loadData('/plugins/session-stats/temp' + '?session=' + session, 'chart_temp', 'Temp', false)
|
||||||
|
loadData('/plugins/session-stats/wifi' + '?session=' + session, 'chart_wifi', 'Wifi', true)
|
||||||
|
loadData('/plugins/session-stats/duration' + '?session=' + session, 'chart_duration', 'Sleeping', true)
|
||||||
|
loadData('/plugins/session-stats/reward' + '?session=' + session, 'chart_reward', 'Reward', false)
|
||||||
|
loadData('/plugins/session-stats/epoch' + '?session=' + session, 'chart_epoch', 'Epochs', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadSessionFiles();
|
||||||
|
loadSessionData();
|
||||||
|
setInterval(loadSessionData, 60000);
|
||||||
|
});
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<select id="session">
|
||||||
|
<option selected>Current</option>
|
||||||
|
</select>
|
||||||
|
<div id="chart_os" class="chart"></div>
|
||||||
|
<div id="chart_temp" class="chart"></div>
|
||||||
|
<div id="chart_wifi" class="chart"></div>
|
||||||
|
<div id="chart_duration" class="chart"></div>
|
||||||
|
<div id="chart_reward" class="chart"></div>
|
||||||
|
<div id="chart_epoch" class="chart"></div>
|
||||||
|
{% endblock %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class GhettoClock:
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self._track = datetime.now()
|
||||||
|
self._counter_thread = threading.Thread(target=self.counter)
|
||||||
|
self._counter_thread.daemon = True
|
||||||
|
self._counter_thread.start()
|
||||||
|
|
||||||
|
def counter(self):
|
||||||
|
while True:
|
||||||
|
with self.lock:
|
||||||
|
self._track += timedelta(seconds=1)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
def now(self):
|
||||||
|
with self.lock:
|
||||||
|
return self._track
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStats(plugins.Plugin):
|
||||||
|
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||||
|
__version__ = '0.1.0'
|
||||||
|
__license__ = 'GPL3'
|
||||||
|
__description__ = 'This plugin displays stats of the current session.'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.options = dict()
|
||||||
|
self.stats = dict()
|
||||||
|
self.clock = GhettoClock()
|
||||||
|
|
||||||
|
def on_loaded(self):
|
||||||
|
"""
|
||||||
|
Gets called when the plugin gets loaded
|
||||||
|
"""
|
||||||
|
# this has to happen in "loaded" because the options are not yet
|
||||||
|
# available in the __init__
|
||||||
|
os.makedirs(self.options['save_directory'], exist_ok=True)
|
||||||
|
self.session_name = "stats_{}.json".format(self.clock.now().strftime("%Y_%m_%d_%H_%M"))
|
||||||
|
self.session = StatusFile(os.path.join(self.options['save_directory'],
|
||||||
|
self.session_name),
|
||||||
|
data_format='json')
|
||||||
|
logging.info("Session-stats plugin loaded.")
|
||||||
|
|
||||||
|
def on_epoch(self, agent, epoch, epoch_data):
|
||||||
|
"""
|
||||||
|
Save the epoch_data to self.stats
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
self.stats[self.clock.now().strftime("%H:%M:%S")] = epoch_data
|
||||||
|
self.session.update(data={'data': self.stats})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_key_values(data, subkeys):
|
||||||
|
result = dict()
|
||||||
|
result['values'] = list()
|
||||||
|
result['labels'] = subkeys
|
||||||
|
for plot_key in subkeys:
|
||||||
|
v = [ [ts,d[plot_key]] for ts, d in data.items()]
|
||||||
|
result['values'].append(v)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def on_webhook(self, path, request):
|
||||||
|
if not path or path == "/":
|
||||||
|
return render_template_string(TEMPLATE)
|
||||||
|
|
||||||
|
session_param = request.args.get('session')
|
||||||
|
|
||||||
|
if path == "os":
|
||||||
|
extract_keys = ['cpu_load','mem_usage',]
|
||||||
|
elif path == "temp":
|
||||||
|
extract_keys = ['temperature']
|
||||||
|
elif path == "wifi":
|
||||||
|
extract_keys = [
|
||||||
|
'missed_interactions',
|
||||||
|
'num_hops',
|
||||||
|
'num_peers',
|
||||||
|
'tot_bond',
|
||||||
|
'avg_bond',
|
||||||
|
'num_deauths',
|
||||||
|
'num_associations',
|
||||||
|
'num_handshakes',
|
||||||
|
]
|
||||||
|
elif path == "duration":
|
||||||
|
extract_keys = [
|
||||||
|
'duration_secs',
|
||||||
|
'slept_for_secs',
|
||||||
|
]
|
||||||
|
elif path == "reward":
|
||||||
|
extract_keys = [
|
||||||
|
'reward',
|
||||||
|
]
|
||||||
|
elif path == "epoch":
|
||||||
|
extract_keys = [
|
||||||
|
'active_for_epochs',
|
||||||
|
]
|
||||||
|
elif path == "session":
|
||||||
|
return jsonify({'files': os.listdir(self.options['save_directory'])})
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
data = self.stats
|
||||||
|
if session_param and session_param != 'Current':
|
||||||
|
file_stats = StatusFile(os.path.join(self.options['save_directory'], session_param), data_format='json')
|
||||||
|
data = file_stats.data_field_or('data', default=dict())
|
||||||
|
return jsonify(SessionStats.extract_key_values(data, extract_keys))
|
147
pwnagotchi/plugins/default/switcher.py
Normal file
147
pwnagotchi/plugins/default/switcher.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from threading import Lock
|
||||||
|
from functools import partial
|
||||||
|
from pwnagotchi import plugins
|
||||||
|
from pwnagotchi import reboot
|
||||||
|
|
||||||
|
|
||||||
|
def systemd_dropin(name, content):
|
||||||
|
if not name.endswith('.service'):
|
||||||
|
name = '%s.service' % name
|
||||||
|
|
||||||
|
dropin_dir = "/etc/systemd/system/%s.d/" % name
|
||||||
|
os.makedirs(dropin_dir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(os.path.join(dropin_dir, "switcher.conf"), "wt") as dropin:
|
||||||
|
dropin.write(content)
|
||||||
|
|
||||||
|
systemctl("daemon-reload")
|
||||||
|
|
||||||
|
def systemctl(command, unit=None):
|
||||||
|
if unit:
|
||||||
|
os.system("/bin/systemctl %s %s" % (command, unit))
|
||||||
|
else:
|
||||||
|
os.system("/bin/systemctl %s" % command)
|
||||||
|
|
||||||
|
def run_task(name, options):
|
||||||
|
task_service_name = "switcher-%s-task.service" % name
|
||||||
|
# save all the commands to a shell script
|
||||||
|
script_dir = '/usr/local/bin/'
|
||||||
|
script_path = os.path.join(script_dir, 'switcher-%s.sh' % name)
|
||||||
|
os.makedirs(script_dir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(script_path, 'wt') as script_file:
|
||||||
|
script_file.write('#!/bin/bash\n')
|
||||||
|
for cmd in options['commands']:
|
||||||
|
script_file.write('%s\n' % cmd)
|
||||||
|
|
||||||
|
os.system("chmod a+x %s" % script_path)
|
||||||
|
|
||||||
|
# here we create the service which runs the tasks
|
||||||
|
with open('/etc/systemd/system/%s' % task_service_name, 'wt') as task_service:
|
||||||
|
task_service.write("""
|
||||||
|
[Unit]
|
||||||
|
Description=Executes the tasks of the pwnagotchi switcher plugin
|
||||||
|
After=pwnagotchi.service bettercap.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=-/usr/local/bin/switcher-%s.sh
|
||||||
|
ExecStart=-/bin/rm /etc/systemd/system/%s
|
||||||
|
ExecStart=-/bin/rm /usr/local/bin/switcher-%s.sh
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
""" % (name, task_service_name, name))
|
||||||
|
|
||||||
|
if 'reboot' in options and options['reboot']:
|
||||||
|
# create a indication file!
|
||||||
|
# if this file is set, we want the switcher-tasks to run
|
||||||
|
open('/root/.switcher', 'a').close()
|
||||||
|
|
||||||
|
# add condition
|
||||||
|
systemd_dropin("pwnagotchi.service", """
|
||||||
|
[Unit]
|
||||||
|
ConditionPathExists=!/root/.switcher""")
|
||||||
|
|
||||||
|
systemd_dropin("bettercap.service", """
|
||||||
|
[Unit]
|
||||||
|
ConditionPathExists=!/root/.switcher""")
|
||||||
|
|
||||||
|
systemd_dropin(task_service_name, """
|
||||||
|
[Service]
|
||||||
|
ExecStart=-/bin/rm /root/.switcher
|
||||||
|
ExecStart=-/bin/rm /etc/systemd/system/switcher-reboot.timer""")
|
||||||
|
|
||||||
|
with open('/etc/systemd/system/switcher-reboot.timer', 'wt') as reboot_timer:
|
||||||
|
reboot_timer.write("""
|
||||||
|
[Unit]
|
||||||
|
Description=Reboot when time is up
|
||||||
|
ConditionPathExists=/root/.switcher
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=%sm
|
||||||
|
Unit=reboot.target
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
""" % options['stopwatch'])
|
||||||
|
|
||||||
|
systemctl("daemon-reload")
|
||||||
|
systemctl("enable", "switcher-reboot.timer")
|
||||||
|
systemctl("enable", task_service_name)
|
||||||
|
reboot()
|
||||||
|
return
|
||||||
|
|
||||||
|
systemctl("daemon-reload")
|
||||||
|
systemctl("start", task_service_name)
|
||||||
|
|
||||||
|
class Switcher(plugins.Plugin):
|
||||||
|
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||||
|
__version__ = '0.0.1'
|
||||||
|
__name__ = 'switcher'
|
||||||
|
__license__ = 'GPL3'
|
||||||
|
__description__ = 'This plugin is a generic task scheduler.'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ready = False
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
|
def trigger(self, name, *args, **kwargs):
|
||||||
|
with self.lock:
|
||||||
|
function_name = name.lstrip('on_')
|
||||||
|
if function_name in self.tasks:
|
||||||
|
task = self.tasks[function_name]
|
||||||
|
|
||||||
|
# is this task enabled?
|
||||||
|
if 'enabled' not in task or ('enabled' in task and not task['enabled']):
|
||||||
|
return
|
||||||
|
|
||||||
|
run_task(function_name, task)
|
||||||
|
|
||||||
|
def on_loaded(self):
|
||||||
|
if 'tasks' in self.options and self.options['tasks']:
|
||||||
|
self.tasks = self.options['tasks']
|
||||||
|
else:
|
||||||
|
logging.debug('[switcher] No tasks found...')
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info("[switcher] is loaded.")
|
||||||
|
|
||||||
|
# create hooks
|
||||||
|
logging.debug("[switcher] creating hooks...")
|
||||||
|
methods = ['webhook', 'internet_available', 'ui_setup', 'ui_update',
|
||||||
|
'unload', 'display_setup', 'ready', 'ai_ready', 'ai_policy',
|
||||||
|
'ai_training_start', 'ai_training_step', 'ai_training_end',
|
||||||
|
'ai_best_reward', 'ai_worst_reward', 'free_channel',
|
||||||
|
'bored', 'sad', 'excited', 'lonely', 'rebooting', 'wait',
|
||||||
|
'sleep', 'wifi_update', 'unfiltered_ap_list', 'association',
|
||||||
|
'deauthentication', 'channel_hop', 'handshake', 'epoch',
|
||||||
|
'peer_detected', 'peer_lost', 'config_changed']
|
||||||
|
|
||||||
|
for m in methods:
|
||||||
|
setattr(Switcher, 'on_%s' % m, partial(self.trigger, m))
|
||||||
|
|
||||||
|
logging.debug("[switcher] triggers are ready to fire...")
|
@@ -7,12 +7,14 @@
|
|||||||
# For Raspberry Pi Zero Ups Power Expansion Board with Integrated Serial Port S3U4
|
# 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.ebay.de/itm/For-Raspberry-Pi-Zero-Ups-Power-Expansion-Board-with-Integrated-Serial-Port-S3U4/323873804310
|
||||||
# https://www.aliexpress.com/item/32888533624.html
|
# https://www.aliexpress.com/item/32888533624.html
|
||||||
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from pwnagotchi.ui.components import LabeledValue
|
from pwnagotchi.ui.components import LabeledValue
|
||||||
from pwnagotchi.ui.view import BLACK
|
from pwnagotchi.ui.view import BLACK
|
||||||
import pwnagotchi.ui.fonts as fonts
|
import pwnagotchi.ui.fonts as fonts
|
||||||
import pwnagotchi.plugins as plugins
|
import pwnagotchi.plugins as plugins
|
||||||
|
import pwnagotchi
|
||||||
|
|
||||||
|
|
||||||
# TODO: add enable switch in config.yml an cleanup all to the best place
|
# TODO: add enable switch in config.yml an cleanup all to the best place
|
||||||
@@ -58,5 +60,14 @@ class UPSLite(plugins.Plugin):
|
|||||||
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0),
|
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0),
|
||||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||||
|
|
||||||
|
def on_unload(self, ui):
|
||||||
|
with ui._lock:
|
||||||
|
ui.remove_element('ups')
|
||||||
|
|
||||||
def on_ui_update(self, ui):
|
def on_ui_update(self, ui):
|
||||||
ui.set('ups', "%2i%%" % self.ups.capacity())
|
capacity = self.ups.capacity()
|
||||||
|
ui.set('ups', "%2i%%" % capacity)
|
||||||
|
if capacity <= self.options['shutdown']:
|
||||||
|
logging.info('[ups_lite] Empty battery (<= %s%%): shuting down' % self.options['shutdown'])
|
||||||
|
ui.update(force=True, new_data={'status': 'Battery exhausted, bye ...'})
|
||||||
|
pwnagotchi.shutdown()
|
||||||
|
@@ -1,182 +1,190 @@
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import yaml
|
import toml
|
||||||
import _thread
|
import _thread
|
||||||
import pwnagotchi.plugins as plugins
|
from pwnagotchi import restart, plugins
|
||||||
from pwnagotchi import restart
|
from pwnagotchi.utils import save_config
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from flask import render_template_string
|
from flask import render_template_string
|
||||||
|
|
||||||
|
|
||||||
INDEX = """
|
INDEX = """
|
||||||
<html>
|
{% extends "base.html" %}
|
||||||
<head>
|
{% set active_page = "plugins" %}
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=0" />
|
{% block title %}
|
||||||
<title>
|
Webcfg
|
||||||
webcfg
|
{% endblock %}
|
||||||
</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 {
|
{% block meta %}
|
||||||
width: 100%;
|
<meta charset="utf-8">
|
||||||
}
|
<meta name="viewport" content="width=device-width, user-scalable=0" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
table {
|
{% block styles %}
|
||||||
table-layout: auto;
|
{{ super() }}
|
||||||
width: 100%;
|
<style>
|
||||||
}
|
#divTop {
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
table, th, td {
|
#searchText {
|
||||||
border: 1px solid black;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
table {
|
||||||
padding: 15px;
|
table-layout: auto;
|
||||||
text-align: left;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table tr:nth-child(even) {
|
table, th, td {
|
||||||
background-color: #eee;
|
border: 1px solid black;
|
||||||
}
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
table tr:nth-child(odd) {
|
th, td {
|
||||||
background-color: #fff;
|
padding: 15px;
|
||||||
}
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
table th {
|
table tr:nth-child(even) {
|
||||||
background-color: black;
|
background-color: #eee;
|
||||||
color: white;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.remove {
|
table tr:nth-child(odd) {
|
||||||
background-color: #f44336;
|
background-color: #fff;
|
||||||
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 {
|
table th {
|
||||||
background-color: white;
|
background-color: black;
|
||||||
color: black;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#btnSave {
|
.remove {
|
||||||
position: -webkit-sticky;
|
background-color: #f44336;
|
||||||
position: sticky;
|
color: white;
|
||||||
bottom: 0px;
|
border: 2px solid #f44336;
|
||||||
width: 100%;
|
padding: 4px 8px;
|
||||||
background-color: #0061b0;
|
text-align: center;
|
||||||
border: none;
|
text-decoration: none;
|
||||||
color: white;
|
display: inline-block;
|
||||||
padding: 15px 32px;
|
font-size: 12px;
|
||||||
text-align: center;
|
margin: 4px 2px;
|
||||||
text-decoration: none;
|
-webkit-transition-duration: 0.4s; /* Safari */
|
||||||
display: inline-block;
|
transition-duration: 0.4s;
|
||||||
font-size: 16px;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
}
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divTop {
|
.remove:hover {
|
||||||
display: table;
|
background-color: white;
|
||||||
width: 100%;
|
color: black;
|
||||||
}
|
}
|
||||||
#divTop > * {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
#divTop > span {
|
|
||||||
width: 1%;
|
|
||||||
}
|
|
||||||
#divTop > input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width:700px) {
|
#btnSave {
|
||||||
table, tr, td {
|
position: -webkit-sticky;
|
||||||
padding:0;
|
position: sticky;
|
||||||
border:1px solid black;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
#divTop {
|
||||||
border:none;
|
display: table;
|
||||||
}
|
width: 100%;
|
||||||
|
}
|
||||||
|
#divTop > * {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
#divTop > span {
|
||||||
|
width: 1%;
|
||||||
|
}
|
||||||
|
#divTop > input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
tr:first-child, thead, th {
|
@media screen and (max-width:700px) {
|
||||||
display:none;
|
table, tr, td {
|
||||||
border:none;
|
padding:0;
|
||||||
}
|
border:1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
tr {
|
table {
|
||||||
float: left;
|
border:none;
|
||||||
width: 100%;
|
}
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table tr:nth-child(odd) {
|
tr:first-child, thead, th {
|
||||||
background-color: #eee;
|
display:none;
|
||||||
}
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
td {
|
tr {
|
||||||
float: left;
|
float: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding:1em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
td::before {
|
table tr:nth-child(odd) {
|
||||||
content:attr(data-label);
|
background-color: #eee;
|
||||||
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 {
|
td {
|
||||||
content:attr(data-label);
|
float: left;
|
||||||
word-wrap: break-word;
|
width: 100%;
|
||||||
background: #eee;
|
padding:1em;
|
||||||
border-right:2px solid black;
|
}
|
||||||
width: 20%;
|
|
||||||
float:left;
|
td::before {
|
||||||
padding:1em;
|
content:attr(data-label);
|
||||||
font-weight: bold;
|
word-wrap: break-word;
|
||||||
margin:-1em 1em -1em -1em;
|
background: #eee;
|
||||||
}
|
border-right:2px solid black;
|
||||||
}
|
width: 20%;
|
||||||
</style>
|
float:left;
|
||||||
</head>
|
padding:1em;
|
||||||
<body>
|
font-weight: bold;
|
||||||
<div id="divTop">
|
margin:-1em 1em -1em -1em;
|
||||||
<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>
|
.del_btn_wrapper {
|
||||||
</div>
|
content:attr(data-label);
|
||||||
<div id="content"></div>
|
word-wrap: break-word;
|
||||||
<button id="btnSave" type="button" onclick="saveConfig()">Save</button>
|
background: #eee;
|
||||||
</body>
|
border-right:2px solid black;
|
||||||
<script type="text/javascript">
|
width: 20%;
|
||||||
|
float:left;
|
||||||
|
padding:1em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin:-1em 1em -1em -1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="divTop">
|
||||||
|
<input type="text" id="searchText" 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>
|
||||||
|
<button id="btnSave" type="button" onclick="saveConfig()">Save and restart</button>
|
||||||
|
<div id="content"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
function addOption() {
|
function addOption() {
|
||||||
var input, table, tr, td, divDelBtn, btnDel, selType, selTypeVal;
|
var input, table, tr, td, divDelBtn, btnDel, selType, selTypeVal;
|
||||||
input = document.getElementById("searchText");
|
input = document.getElementById("searchText");
|
||||||
@@ -232,11 +240,10 @@ INDEX = """
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var searchInput = document.getElementById("searchText");
|
||||||
function filterTable(){
|
searchInput.onkeyup = function() {
|
||||||
var input, filter, table, tr, td, i, txtValue;
|
var filter, table, tr, td, i, txtValue;
|
||||||
input = document.getElementById("searchText");
|
filter = searchInput.value.toUpperCase();
|
||||||
filter = input.value.toUpperCase();
|
|
||||||
table = document.getElementById("tableOptions");
|
table = document.getElementById("tableOptions");
|
||||||
if (table) {
|
if (table) {
|
||||||
tr = table.getElementsByTagName("tr");
|
tr = table.getElementsByTagName("tr");
|
||||||
@@ -447,8 +454,7 @@ INDEX = """
|
|||||||
divContent.innerHTML = "";
|
divContent.innerHTML = "";
|
||||||
divContent.appendChild(table);
|
divContent.appendChild(table);
|
||||||
});
|
});
|
||||||
</script>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def serializer(obj):
|
def serializer(obj):
|
||||||
@@ -464,16 +470,17 @@ class WebConfig(plugins.Plugin):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ready = False
|
self.ready = False
|
||||||
|
self.mode = 'MANU'
|
||||||
|
|
||||||
|
def on_config_changed(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.ready = True
|
||||||
|
|
||||||
def on_ready(self, agent):
|
def on_ready(self, agent):
|
||||||
self.config = agent.config()
|
self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'
|
||||||
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
|
|
||||||
self.ready = True
|
|
||||||
|
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
self.config = agent.config()
|
self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'
|
||||||
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
|
|
||||||
self.ready = True
|
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
"""
|
"""
|
||||||
@@ -500,12 +507,10 @@ class WebConfig(plugins.Plugin):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
if path == "save-config":
|
if path == "save-config":
|
||||||
try:
|
try:
|
||||||
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
|
save_config(request.get_json(), '/etc/pwnagotchi/config.toml') # test
|
||||||
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,))
|
_thread.start_new_thread(restart, (self.mode,))
|
||||||
return "success"
|
return "success"
|
||||||
except yaml.YAMLError as yaml_ex:
|
except Exception as ex:
|
||||||
return "config error"
|
logging.error(ex)
|
||||||
|
return "config error", 500
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@@ -3,13 +3,13 @@
|
|||||||
<meta http-equiv="Content-Type" content="text/xml; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/xml; charset=utf-8" />
|
||||||
<title>GPS MAP</title>
|
<title>GPS MAP</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"/>
|
<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="https://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" />
|
<link rel="stylesheet" type="text/css" href="https://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="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>
|
<script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js'></script>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
/* for map */
|
/* for map */
|
||||||
html, body, #mapdiv { height: 100%; width: 100%; margin:0; background-color:#000;}
|
html, body { height: 100%; width: 100%; margin:0; background-color:#000;}
|
||||||
.pwnAPPin path {
|
.pwnAPPin path {
|
||||||
fill: #ce7575;
|
fill: #ce7575;
|
||||||
}
|
}
|
||||||
@@ -85,11 +85,19 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#loading .face { font-size:8vw; }
|
#loading .face { font-size:8vw; }
|
||||||
#loading .text {position:absolute;bottom:0;text-align:center; font-size: 1vw;color:#a0a0a0;}
|
#loading .text { position:absolute;bottom:0;text-align:center; font-size: 2vw;color:#a0a0a0;}
|
||||||
|
#filterbox { position: fixed;top:0px;left:0px;z-index:999999;margin-left:55px;width:100%;height:20px;border-bottom:2px solid #303030;display: grid;grid-template-columns: 1fr 0.1fr;grid-template-rows: 1fr;grid-template-areas: ". .";}
|
||||||
|
#search { grid-area: 1 / 1 / 2 / 2;height:30px;padding:3px;background-color:#000;color:#e0e0e0;border:none;}
|
||||||
|
#matchcount { grid-area: 1 / 2 / 2 / 3;height:30px;margin-right:55px;padding-right:5px;background-color:#000;color:#a0a0a0;font-weight:bold;}
|
||||||
|
#mapdiv { width:100%; height: 100%; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mapdiv"></div>
|
<div id="mapdiv"></div>
|
||||||
|
<div id="filterbox">
|
||||||
|
<input type="text" id="search" placeholder="filter: #cracked #notcracked AA:BB:CC aabbcc AndroidAP ..."/>
|
||||||
|
<div id="matchcount">0 APs</div>
|
||||||
|
</div>
|
||||||
<div id="loading"><div class="face"><nobr>(⌐■ <span id="loading_ap_img"></span> ■)</nobr></div><div class="text" id="loading_infotext">loading positions...</div></div>
|
<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">
|
<script type="text/javascript">
|
||||||
function loadJSON(url, callback) {
|
function loadJSON(url, callback) {
|
||||||
@@ -133,11 +141,9 @@
|
|||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||||
subdomains: 'abcd',
|
subdomains: 'abcd',
|
||||||
opacity:0.8,
|
opacity:0.8,
|
||||||
maxZoom: 19
|
// maxZoom: 19
|
||||||
});
|
});
|
||||||
var mymap = L.map('mapdiv');
|
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 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>';
|
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>';
|
||||||
@@ -157,54 +163,88 @@
|
|||||||
popupAnchor : [0, -30],
|
popupAnchor : [0, -30],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var positionsLoaded = false;
|
||||||
|
var positions = [];
|
||||||
var accuracys = [];
|
var accuracys = [];
|
||||||
var markers = [];
|
var markers = [];
|
||||||
var marker_pos = [];
|
var marker_pos = [];
|
||||||
var markerClusters = L.markerClusterGroup();
|
var markerClusters = L.markerClusterGroup();
|
||||||
|
|
||||||
loadJSON("/plugins/webgpsmap/all", function(response) {
|
function drawPositions() {
|
||||||
var positions = JSON.parse(response);
|
|
||||||
count = 0;
|
count = 0;
|
||||||
|
//mymap.removeLayer(markerClusters);
|
||||||
|
mymap.eachLayer(function (layer) {
|
||||||
|
mymap.removeLayer(layer);
|
||||||
|
});
|
||||||
|
Esri_WorldImagery.addTo(mymap);
|
||||||
|
CartoDB_DarkMatter.addTo(mymap);
|
||||||
|
markerClusters = L.markerClusterGroup();
|
||||||
|
accuracys = [];
|
||||||
|
markers = [];
|
||||||
|
marker_pos = [];
|
||||||
|
filterText = document.getElementById("search").value;
|
||||||
|
//console.log(filterText);
|
||||||
Object.keys(positions).forEach(function(key) {
|
Object.keys(positions).forEach(function(key) {
|
||||||
count++;
|
|
||||||
if(positions[key].lng){
|
if(positions[key].lng){
|
||||||
new_marker_pos = [positions[key].lat, positions[key].lng];
|
filterPattern =
|
||||||
if (positions[key].acc) {
|
positions[key].ssid + ' ' +
|
||||||
radius = Math.round(Math.min(positions[key].acc, 500));
|
formatMacAddress(positions[key].mac) + ' ' +
|
||||||
markerColor = 'red';
|
positions[key].mac
|
||||||
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) {
|
if (positions[key].pass) {
|
||||||
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
|
filterPattern += positions[key].pass + ' #cracked';
|
||||||
} else {
|
} else {
|
||||||
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
|
filterPattern += ' #notcracked';
|
||||||
}
|
}
|
||||||
passInfo = '';
|
filterPattern = filterPattern.toLowerCase();
|
||||||
if (positions[key].pass) {
|
//console.log(filterPattern);
|
||||||
|
var matched = true;
|
||||||
|
if (filterText) {
|
||||||
|
filterText.split(" ").forEach(function (item) {
|
||||||
|
if (!filterPattern.includes(item.toLowerCase())) {
|
||||||
|
matched = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (matched) {
|
||||||
|
count++;
|
||||||
|
new_marker_pos = [positions[key].lat, positions[key].lng];
|
||||||
|
if (positions[key].acc) {
|
||||||
|
radius = Math.round(Math.min(positions[key].acc, 500));
|
||||||
|
markerColor = 'red';
|
||||||
|
markerColorCode = '#f03';
|
||||||
|
fillOpacity = 0.002;
|
||||||
|
if (positions[key].pass) {
|
||||||
|
markerColor = 'green';
|
||||||
|
markerColorCode = '#1aff00';
|
||||||
|
fillOpacity = 0.1;
|
||||||
|
}
|
||||||
|
accuracys.push(
|
||||||
|
L.circle(new_marker_pos, {
|
||||||
|
color: markerColor,
|
||||||
|
fillColor: markerColorCode,
|
||||||
|
fillOpacity: fillOpacity,
|
||||||
|
weight: 1,
|
||||||
|
opacity: 0.1,
|
||||||
|
radius: Math.min(positions[key].acc, 500),
|
||||||
|
}).setStyle({'className': 'radar'}).addTo(mymap)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
passInfo = '';
|
||||||
|
if (positions[key].pass) {
|
||||||
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
|
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
|
||||||
|
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
|
||||||
|
} else {
|
||||||
|
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
|
||||||
|
}
|
||||||
|
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
|
||||||
|
markers.push(newMarker);
|
||||||
|
marker_pos.push(new_marker_pos);
|
||||||
|
markerClusters.addLayer( newMarker );
|
||||||
}
|
}
|
||||||
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
|
|
||||||
markers.push(newMarker);
|
|
||||||
marker_pos.push(new_marker_pos);
|
|
||||||
markerClusters.addLayer( newMarker );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.getElementById("matchcount").innerHTML = count + " APs";
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
mymap.addLayer( markerClusters );
|
mymap.addLayer( markerClusters );
|
||||||
var bounds = new L.LatLngBounds(marker_pos);
|
var bounds = new L.LatLngBounds(marker_pos);
|
||||||
@@ -213,6 +253,35 @@
|
|||||||
} else {
|
} else {
|
||||||
document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
|
document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw map on Enter in FilterInputField
|
||||||
|
const node = document.getElementById("search").addEventListener("keyup", function(event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
if (positionsLoaded) {
|
||||||
|
drawPositions();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// load positions
|
||||||
|
loadJSON("/plugins/webgpsmap/all", function(response) {
|
||||||
|
positions = JSON.parse(response);
|
||||||
|
positionsLoaded = true;
|
||||||
|
drawPositions();
|
||||||
|
});
|
||||||
|
// get current position and set marker in interval if https request
|
||||||
|
if (location.protocol === 'https:') {
|
||||||
|
var myLocationMarker = {};
|
||||||
|
function onLocationFound(e) {
|
||||||
|
if (myLocationMarker != undefined) {
|
||||||
|
mymap.removeLayer(myLocationMarker);
|
||||||
|
};
|
||||||
|
myLocationMarker = L.marker(e.latlng).addTo(mymap);
|
||||||
|
setTimeout(function(){ mymap.locate(); }, 30000);
|
||||||
|
}
|
||||||
|
mymap.on('locationfound', onLocationFound);
|
||||||
|
mymap.locate({setView: true});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
@@ -6,32 +6,28 @@ import re
|
|||||||
import datetime
|
import datetime
|
||||||
from flask import Response
|
from flask import Response
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from dateutil.parser import parse
|
||||||
|
|
||||||
'''
|
'''
|
||||||
2do:
|
webgpsmap shows existing position data stored in your /handshakes/ directory
|
||||||
- make the cache handling multiple clients
|
|
||||||
- cleanup the javascript in a class and handle "/newest" additions
|
the plugin does the following:
|
||||||
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
|
- search for *.pcap files in your /handshakes/ dir
|
||||||
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
|
- for every found .pcap file it looks for a .geo.json or .gps.json or .paw-gps.json file with
|
||||||
https://gis.stackexchange.com/questions/312737/filtering-interactive-leaflet-map-with-dropdown-menu
|
latitude+longitude data inside and shows this position on the map
|
||||||
https://blogs.kent.ac.uk/websolutions/2015/01/29/filtering-map-markers-with-leaflet-js-a-brief-technical-overview/
|
- if also an .cracked file with a plaintext password inside exist, it reads the content and shows the
|
||||||
http://www.digital-geography.com/filter-leaflet-maps-slider/
|
position as green instead of red and the password inside the infopox of the position
|
||||||
http://bl.ocks.org/zross/47760925fcb1643b4225
|
special:
|
||||||
-
|
you can save the html-map as one file for offline use or host on your own webspace with "/plugins/webgpsmap/offlinemap"
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
class Webgpsmap(plugins.Plugin):
|
class Webgpsmap(plugins.Plugin):
|
||||||
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
|
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
|
||||||
__version__ = '1.2.2'
|
__version__ = '1.4.0'
|
||||||
__name__ = 'webgpsmap'
|
__name__ = 'webgpsmap'
|
||||||
__license__ = 'GPL3'
|
__license__ = 'GPL3'
|
||||||
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
|
__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()
|
ALREADY_SENT = list()
|
||||||
SKIP = list()
|
SKIP = list()
|
||||||
@@ -39,15 +35,15 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ready = False
|
self.ready = False
|
||||||
|
|
||||||
def on_ready(self, agent):
|
def on_config_changed(self, config):
|
||||||
self.config = agent.config()
|
self.config = config
|
||||||
self.ready = True
|
self.ready = True
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
"""
|
"""
|
||||||
Plugin got loaded
|
Plugin got loaded
|
||||||
"""
|
"""
|
||||||
logging.info("webgpsmap plugin loaded")
|
logging.info("[webgpsmap]: plugin loaded")
|
||||||
|
|
||||||
def on_webhook(self, path, request):
|
def on_webhook(self, path, request):
|
||||||
"""
|
"""
|
||||||
@@ -55,6 +51,7 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
"""
|
"""
|
||||||
# defaults:
|
# defaults:
|
||||||
response_header_contenttype = None
|
response_header_contenttype = None
|
||||||
|
response_header_contentdisposition = None
|
||||||
response_mimetype = "application/xhtml+xml"
|
response_mimetype = "application/xhtml+xml"
|
||||||
if not self.ready:
|
if not self.ready:
|
||||||
try:
|
try:
|
||||||
@@ -68,8 +65,8 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
response_status = 500
|
response_status = 500
|
||||||
response_mimetype = "application/xhtml+xml"
|
response_mimetype = "application/xhtml+xml"
|
||||||
response_header_contenttype = 'text/html'
|
response_header_contenttype = 'text/html'
|
||||||
except Exception as ex:
|
except Exception as error:
|
||||||
logging.error(ex)
|
logging.error(f"[webgpsmap] on_webhook NOT_READY error: {error}")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
@@ -78,8 +75,8 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
self.ALREADY_SENT = list()
|
self.ALREADY_SENT = list()
|
||||||
try:
|
try:
|
||||||
response_data = bytes(self.get_html(), "utf-8")
|
response_data = bytes(self.get_html(), "utf-8")
|
||||||
except Exception as ex:
|
except Exception as error:
|
||||||
logging.error(ex)
|
logging.error(f"[webgpsmap] on_webhook / error: {error}")
|
||||||
return
|
return
|
||||||
response_status = 200
|
response_status = 200
|
||||||
response_mimetype = "application/xhtml+xml"
|
response_mimetype = "application/xhtml+xml"
|
||||||
@@ -92,8 +89,23 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
response_status = 200
|
response_status = 200
|
||||||
response_mimetype = "application/json"
|
response_mimetype = "application/json"
|
||||||
response_header_contenttype = 'application/json'
|
response_header_contenttype = 'application/json'
|
||||||
except Exception as ex:
|
except Exception as error:
|
||||||
logging.error(ex)
|
logging.error(f"[webgpsmap] on_webhook all error: {error}")
|
||||||
|
return
|
||||||
|
elif path.startswith('offlinemap'):
|
||||||
|
# for download an all-in-one html file with positions.json inside
|
||||||
|
try:
|
||||||
|
self.ALREADY_SENT = list()
|
||||||
|
json_data = json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes']))
|
||||||
|
html_data = self.get_html()
|
||||||
|
html_data = html_data.replace('var positions = [];', 'var positions = ' + json_data + ';positionsLoaded=true;drawPositions();')
|
||||||
|
response_data = bytes(html_data, "utf-8")
|
||||||
|
response_status = 200
|
||||||
|
response_mimetype = "application/xhtml+xml"
|
||||||
|
response_header_contenttype = 'text/html'
|
||||||
|
response_header_contentdisposition = 'attachment; filename=webgpsmap.html';
|
||||||
|
except Exception as error:
|
||||||
|
logging.error(f"[webgpsmap] on_webhook offlinemap: error: {error}")
|
||||||
return
|
return
|
||||||
# elif path.startswith('/newest'):
|
# elif path.startswith('/newest'):
|
||||||
# # returns all positions newer then timestamp
|
# # returns all positions newer then timestamp
|
||||||
@@ -118,20 +130,22 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<style>body{font-size:1000%;}</style>
|
<style>body{font-size:1000%;}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>4😋4</body>
|
<body>4😋4 for bad boys</body>
|
||||||
</html>''', "utf-8")
|
</html>''', "utf-8")
|
||||||
response_status = 404
|
response_status = 404
|
||||||
try:
|
try:
|
||||||
r = Response(response=response_data, status=response_status, mimetype=response_mimetype)
|
r = Response(response=response_data, status=response_status, mimetype=response_mimetype)
|
||||||
if response_header_contenttype is not None:
|
if response_header_contenttype is not None:
|
||||||
r.headers["Content-Type"] = response_header_contenttype
|
r.headers["Content-Type"] = response_header_contenttype
|
||||||
|
if response_header_contentdisposition is not None:
|
||||||
|
r.headers["Content-Disposition"] = response_header_contentdisposition
|
||||||
return r
|
return r
|
||||||
except Exception as ex:
|
except Exception as error:
|
||||||
logging.error(ex)
|
logging.error(f"[webgpsmap] on_webhook CREATING_RESPONSE error: {error}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# cache 1024 items
|
# cache 2048 items
|
||||||
@lru_cache(maxsize=1024, typed=False)
|
@lru_cache(maxsize=2048, typed=False)
|
||||||
def _get_pos_from_file(self, path):
|
def _get_pos_from_file(self, path):
|
||||||
return PositionFile(path)
|
return PositionFile(path)
|
||||||
|
|
||||||
@@ -144,7 +158,7 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
handshake_dir = gpsdir
|
handshake_dir = gpsdir
|
||||||
gps_data = dict()
|
gps_data = dict()
|
||||||
|
|
||||||
logging.info("webgpsmap: scanning %s", handshake_dir)
|
logging.info(f"[webgpsmap] scanning {handshake_dir}")
|
||||||
|
|
||||||
|
|
||||||
all_files = os.listdir(handshake_dir)
|
all_files = os.listdir(handshake_dir)
|
||||||
@@ -156,33 +170,40 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
all_geo_or_gps_files = []
|
all_geo_or_gps_files = []
|
||||||
for filename_pcap in all_pcap_files:
|
for filename_pcap in all_pcap_files:
|
||||||
filename_base = filename_pcap[:-5] # remove ".pcap"
|
filename_base = filename_pcap[:-5] # remove ".pcap"
|
||||||
logging.debug("webgpsmap: found: " + filename_base)
|
logging.debug(f"[webgpsmap] found: {filename_base}")
|
||||||
filename_position = None
|
filename_position = None
|
||||||
|
|
||||||
|
logging.debug("[webgpsmap] search for .gps.json")
|
||||||
check_for = os.path.basename(filename_base) + ".gps.json"
|
check_for = os.path.basename(filename_base) + ".gps.json"
|
||||||
if check_for in all_files:
|
if check_for in all_files:
|
||||||
filename_position = str(os.path.join(handshake_dir, check_for))
|
filename_position = str(os.path.join(handshake_dir, check_for))
|
||||||
|
|
||||||
|
logging.debug("[webgpsmap] search for .geo.json")
|
||||||
check_for = os.path.basename(filename_base) + ".geo.json"
|
check_for = os.path.basename(filename_base) + ".geo.json"
|
||||||
if check_for in all_files:
|
if check_for in all_files:
|
||||||
filename_position = str(os.path.join(handshake_dir, check_for))
|
filename_position = str(os.path.join(handshake_dir, check_for))
|
||||||
|
|
||||||
|
logging.debug("[webgpsmap] search for .paw-gps.json")
|
||||||
|
check_for = os.path.basename(filename_base) + ".paw-gps.json"
|
||||||
|
if check_for in all_files:
|
||||||
|
filename_position = str(os.path.join(handshake_dir, check_for))
|
||||||
|
|
||||||
|
logging.debug(f"[webgpsmap] end search for position data files and use {filename_position}")
|
||||||
|
|
||||||
if filename_position is not None:
|
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.append(filename_position)
|
||||||
|
|
||||||
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skiped networks? No!
|
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skipped networks? No!
|
||||||
|
|
||||||
if newest_only:
|
if newest_only:
|
||||||
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
|
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 ...",
|
logging.info(f"[webgpsmap] Found {len(all_geo_or_gps_files)} position-data files from {len(all_pcap_files)} handshakes. Fetching positions ...")
|
||||||
len(all_geo_or_gps_files), len(all_pcap_files))
|
|
||||||
|
|
||||||
for pos_file in all_geo_or_gps_files:
|
for pos_file in all_geo_or_gps_files:
|
||||||
try:
|
try:
|
||||||
pos = self._get_pos_from_file(pos_file)
|
pos = self._get_pos_from_file(pos_file)
|
||||||
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO:
|
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO and not pos.type() == PositionFile.PAWGPS:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ssid, mac = pos.ssid(), pos.mac()
|
ssid, mac = pos.ssid(), pos.mac()
|
||||||
@@ -190,10 +211,17 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
# invalid mac is strange and should abort; ssid is ok
|
# invalid mac is strange and should abort; ssid is ok
|
||||||
if not mac:
|
if not mac:
|
||||||
raise ValueError("Mac can't be parsed from filename")
|
raise ValueError("Mac can't be parsed from filename")
|
||||||
|
pos_type = 'unknown'
|
||||||
|
if pos.type() == PositionFile.GPS:
|
||||||
|
pos_type = 'gps'
|
||||||
|
elif pos.type() == PositionFile.GEO:
|
||||||
|
pos_type = 'geo'
|
||||||
|
elif pos.type() == PositionFile.PAWGPS:
|
||||||
|
pos_type = 'paw'
|
||||||
gps_data[ssid+"_"+mac] = {
|
gps_data[ssid+"_"+mac] = {
|
||||||
'ssid': ssid,
|
'ssid': ssid,
|
||||||
'mac': mac,
|
'mac': mac,
|
||||||
'type': 'gps' if pos.type() == PositionFile.GPS else 'geo',
|
'type': pos_type,
|
||||||
'lng': pos.lng(),
|
'lng': pos.lng(),
|
||||||
'lat': pos.lat(),
|
'lat': pos.lat(),
|
||||||
'acc': pos.accuracy(),
|
'acc': pos.accuracy(),
|
||||||
@@ -201,24 +229,25 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
'ts_last': pos.timestamp_last(),
|
'ts_last': pos.timestamp_last(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# get ap password if exist
|
||||||
check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
|
check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
|
||||||
if check_for in all_files:
|
if check_for in all_files:
|
||||||
gps_data[ssid + "_" + mac]["pass"] = pos.password()
|
gps_data[ssid + "_" + mac]["pass"] = pos.password()
|
||||||
|
|
||||||
self.ALREADY_SENT += pos_file
|
self.ALREADY_SENT += pos_file
|
||||||
except json.JSONDecodeError as js_e:
|
except json.JSONDecodeError as error:
|
||||||
self.SKIP += pos_file
|
self.SKIP += pos_file
|
||||||
logging.error(js_e)
|
logging.error(f"[webgpsmap] JSONDecodeError in: {pos_file} - error: {error}")
|
||||||
continue
|
continue
|
||||||
except ValueError as v_e:
|
except ValueError as error:
|
||||||
self.SKIP += pos_file
|
self.SKIP += pos_file
|
||||||
logging.error(v_e)
|
logging.error(f"[webgpsmap] ValueError: {pos_file} - error: {error}")
|
||||||
continue
|
continue
|
||||||
except OSError as os_e:
|
except OSError as error:
|
||||||
self.SKIP += pos_file
|
self.SKIP += pos_file
|
||||||
logging.error(os_e)
|
logging.error(f"[webgpsmap] OSError: {pos_file} - error: {error}")
|
||||||
continue
|
continue
|
||||||
logging.info("webgpsmap loaded %d positions", len(gps_data))
|
logging.info(f"[webgpsmap] loaded {len(gps_data)} positions")
|
||||||
return gps_data
|
return gps_data
|
||||||
|
|
||||||
def get_html(self):
|
def get_html(self):
|
||||||
@@ -226,11 +255,10 @@ class Webgpsmap(plugins.Plugin):
|
|||||||
Returns the html page
|
Returns the html page
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
template_file = os.path.dirname(os.path.realpath(__file__))+"/"+"webgpsmap.html"
|
template_file = os.path.dirname(os.path.realpath(__file__)) + "/" + "webgpsmap.html"
|
||||||
html_data = open(template_file, "r").read()
|
html_data = open(template_file, "r").read()
|
||||||
except Exception as ex:
|
except Exception as error:
|
||||||
logging.error("error loading template file: %s", template_file)
|
logging.error(f"[webgpsmap] error loading template file {template_file} - error: {error}")
|
||||||
logging.error(ex)
|
|
||||||
return html_data
|
return html_data
|
||||||
|
|
||||||
|
|
||||||
@@ -238,15 +266,18 @@ class PositionFile:
|
|||||||
"""
|
"""
|
||||||
Wraps gps / net-pos files
|
Wraps gps / net-pos files
|
||||||
"""
|
"""
|
||||||
GPS = 0
|
GPS = 1
|
||||||
GEO = 1
|
GEO = 2
|
||||||
|
PAWGPS = 3
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self._file = path
|
self._file = path
|
||||||
self._filename = os.path.basename(path)
|
self._filename = os.path.basename(path)
|
||||||
try:
|
try:
|
||||||
|
logging.debug(f"[webgpsmap] loading {path}")
|
||||||
with open(path, 'r') as json_file:
|
with open(path, 'r') as json_file:
|
||||||
self._json = json.load(json_file)
|
self._json = json.load(json_file)
|
||||||
|
logging.debug(f"[webgpsmap] loaded {path}")
|
||||||
except json.JSONDecodeError as js_e:
|
except json.JSONDecodeError as js_e:
|
||||||
raise js_e
|
raise js_e
|
||||||
|
|
||||||
@@ -254,7 +285,7 @@ class PositionFile:
|
|||||||
"""
|
"""
|
||||||
Returns the mac from filename
|
Returns the mac from filename
|
||||||
"""
|
"""
|
||||||
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo)\.json', self._filename)
|
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo|paw-gps)\.json', self._filename)
|
||||||
if parsed_mac:
|
if parsed_mac:
|
||||||
mac = parsed_mac.groups()[0]
|
mac = parsed_mac.groups()[0]
|
||||||
return mac
|
return mac
|
||||||
@@ -264,7 +295,7 @@ class PositionFile:
|
|||||||
"""
|
"""
|
||||||
Returns the ssid from filename
|
Returns the ssid from filename
|
||||||
"""
|
"""
|
||||||
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo)\.json', self._filename)
|
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo|paw-gps)\.json', self._filename)
|
||||||
if parsed_ssid:
|
if parsed_ssid:
|
||||||
return parsed_ssid.groups()[0]
|
return parsed_ssid.groups()[0]
|
||||||
return None
|
return None
|
||||||
@@ -292,33 +323,30 @@ class PositionFile:
|
|||||||
return_ts = self._json['ts']
|
return_ts = self._json['ts']
|
||||||
elif 'Updated' in self._json:
|
elif 'Updated' in self._json:
|
||||||
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
|
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
|
||||||
date_iso_formated = self._json['Updated']
|
dateObj = parse(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())
|
return_ts = int("%.0f" % dateObj.timestamp())
|
||||||
else:
|
else:
|
||||||
# use file timestamp last modification of the pcap file
|
# use file timestamp last modification of the json file
|
||||||
return_ts = int("%.0f" % os.path.getmtime(self._file))
|
return_ts = int("%.0f" % os.path.getmtime(self._file))
|
||||||
return return_ts
|
return return_ts
|
||||||
|
|
||||||
def password(self):
|
def password(self):
|
||||||
"""
|
"""
|
||||||
returns the password from file.pcap.cracked od None
|
returns the password from file.pcap.cracked or None
|
||||||
"""
|
"""
|
||||||
return_pass = None
|
return_pass = None
|
||||||
password_file_path = self._file[:-9] + ".pcap.cracked"
|
# 2do: make better filename split/remove extension because this one has problems with "." in path
|
||||||
|
base_filename, ext1, ext2 = re.split('\.', self._file)
|
||||||
|
password_file_path = base_filename + ".pcap.cracked"
|
||||||
if os.path.isfile(password_file_path):
|
if os.path.isfile(password_file_path):
|
||||||
try:
|
try:
|
||||||
password_file = open(password_file_path, 'r')
|
password_file = open(password_file_path, 'r')
|
||||||
return_pass = password_file.read()
|
return_pass = password_file.read()
|
||||||
password_file.close()
|
password_file.close()
|
||||||
except OSError as err:
|
except OSError as error:
|
||||||
print("OS error: {0}".format(err))
|
logging.error(f"[webgpsmap] OS error loading password: {password_file_path} - error: {format(error)}")
|
||||||
except:
|
except:
|
||||||
print("Unexpected error:", sys.exc_info()[0])
|
logging.error(f"[webgpsmap] Unexpected error loading password: {password_file_path} - error: {sys.exc_info()[0]}")
|
||||||
raise
|
raise
|
||||||
return return_pass
|
return return_pass
|
||||||
|
|
||||||
@@ -330,37 +358,57 @@ class PositionFile:
|
|||||||
return PositionFile.GPS
|
return PositionFile.GPS
|
||||||
if self._file.endswith('.geo.json'):
|
if self._file.endswith('.geo.json'):
|
||||||
return PositionFile.GEO
|
return PositionFile.GEO
|
||||||
|
if self._file.endswith('.paw-gps.json'):
|
||||||
|
return PositionFile.PAWGPS
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def lat(self):
|
def lat(self):
|
||||||
try:
|
try:
|
||||||
if self.type() == PositionFile.GPS:
|
lat = None
|
||||||
|
# try to get value from known formats
|
||||||
|
if 'Latitude' in self._json:
|
||||||
lat = self._json['Latitude']
|
lat = self._json['Latitude']
|
||||||
if self.type() == PositionFile.GEO:
|
if 'lat' in self._json:
|
||||||
lat = self._json['location']['lat']
|
lat = self._json['lat'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
|
||||||
if lat != 0:
|
if 'location' in self._json:
|
||||||
return lat
|
if 'lat' in self._json['location']:
|
||||||
raise ValueError("Lat is 0")
|
lat = self._json['location']['lat']
|
||||||
|
# check value
|
||||||
|
if lat is None:
|
||||||
|
raise ValueError(f"Lat is None in {self._filename}")
|
||||||
|
if lat == 0:
|
||||||
|
raise ValueError(f"Lat is 0 in {self._filename}")
|
||||||
|
return lat
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def lng(self):
|
def lng(self):
|
||||||
try:
|
try:
|
||||||
if self.type() == PositionFile.GPS:
|
lng = None
|
||||||
|
# try to get value from known formats
|
||||||
|
if 'Longitude' in self._json:
|
||||||
lng = self._json['Longitude']
|
lng = self._json['Longitude']
|
||||||
if self.type() == PositionFile.GEO:
|
if 'long' in self._json:
|
||||||
lng = self._json['location']['lng']
|
lng = self._json['long'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
|
||||||
if lng != 0:
|
if 'location' in self._json:
|
||||||
return lng
|
if 'lng' in self._json['location']:
|
||||||
raise ValueError("Lng is 0")
|
lng = self._json['location']['lng']
|
||||||
|
# check value
|
||||||
|
if lng is None:
|
||||||
|
raise ValueError(f"Lng is None in {self._filename}")
|
||||||
|
if lng == 0:
|
||||||
|
raise ValueError(f"Lng is 0 in {self._filename}")
|
||||||
|
return lng
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def accuracy(self):
|
def accuracy(self):
|
||||||
if self.type() == PositionFile.GPS:
|
if self.type() == PositionFile.GPS:
|
||||||
return 50.0
|
return 50.0 # a default
|
||||||
|
if self.type() == PositionFile.PAWGPS:
|
||||||
|
return 50.0 # a default
|
||||||
if self.type() == PositionFile.GEO:
|
if self.type() == PositionFile.GEO:
|
||||||
try:
|
try:
|
||||||
return self._json['accuracy']
|
return self._json['accuracy']
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from io import StringIO
|
|
||||||
import csv
|
import csv
|
||||||
from datetime import datetime
|
|
||||||
import requests
|
import requests
|
||||||
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile
|
|
||||||
import pwnagotchi.plugins as plugins
|
from io import StringIO
|
||||||
|
from datetime import datetime
|
||||||
|
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile, remove_whitelisted
|
||||||
|
from threading import Lock
|
||||||
|
from pwnagotchi import plugins
|
||||||
|
|
||||||
|
|
||||||
def _extract_gps_data(path):
|
def _extract_gps_data(path):
|
||||||
@@ -100,90 +102,90 @@ class Wigle(plugins.Plugin):
|
|||||||
self.ready = False
|
self.ready = False
|
||||||
self.report = StatusFile('/root/.wigle_uploads', data_format='json')
|
self.report = StatusFile('/root/.wigle_uploads', data_format='json')
|
||||||
self.skip = list()
|
self.skip = list()
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
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")
|
logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not 'whitelist' in self.options:
|
||||||
|
self.options['whitelist'] = list()
|
||||||
|
|
||||||
self.ready = True
|
self.ready = True
|
||||||
|
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
from scapy.all import Scapy_Exception
|
|
||||||
"""
|
"""
|
||||||
Called in manual mode when there's internet connectivity
|
Called in manual mode when there's internet connectivity
|
||||||
"""
|
"""
|
||||||
if self.ready:
|
if not self.ready or self.lock.locked():
|
||||||
config = agent.config()
|
return
|
||||||
display = agent.view()
|
|
||||||
reported = self.report.data_field_or('reported', default=list())
|
|
||||||
|
|
||||||
handshake_dir = config['bettercap']['handshakes']
|
from scapy.all import Scapy_Exception
|
||||||
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)
|
|
||||||
|
|
||||||
if new_gps_files:
|
config = agent.config()
|
||||||
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
display = agent.view()
|
||||||
|
reported = self.report.data_field_or('reported', default=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')]
|
||||||
|
|
||||||
csv_entries = list()
|
all_gps_files = remove_whitelisted(all_gps_files, self.options['whitelist'])
|
||||||
no_err_entries = list()
|
new_gps_files = set(all_gps_files) - set(reported) - set(self.skip)
|
||||||
|
if new_gps_files:
|
||||||
for gps_file in new_gps_files:
|
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
||||||
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
csv_entries = list()
|
||||||
|
no_err_entries = list()
|
||||||
if not os.path.exists(pcap_filename):
|
for gps_file in new_gps_files:
|
||||||
logging.error("WIGLE: Can't find pcap for %s", gps_file)
|
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
||||||
self.skip.append(gps_file)
|
if not os.path.exists(pcap_filename):
|
||||||
continue
|
logging.error("WIGLE: Can't find pcap for %s", gps_file)
|
||||||
|
self.skip.append(gps_file)
|
||||||
try:
|
continue
|
||||||
gps_data = _extract_gps_data(gps_file)
|
try:
|
||||||
except OSError as os_err:
|
gps_data = _extract_gps_data(gps_file)
|
||||||
logging.error("WIGLE: %s", os_err)
|
except OSError as os_err:
|
||||||
self.skip.append(gps_file)
|
logging.error("WIGLE: %s", os_err)
|
||||||
continue
|
self.skip.append(gps_file)
|
||||||
except json.JSONDecodeError as json_err:
|
continue
|
||||||
logging.error("WIGLE: %s", json_err)
|
except json.JSONDecodeError as json_err:
|
||||||
self.skip.append(gps_file)
|
logging.error("WIGLE: %s", json_err)
|
||||||
continue
|
self.skip.append(gps_file)
|
||||||
|
continue
|
||||||
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
|
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)
|
logging.warning("WIGLE: Not enough gps-information for %s. Trying again next time.", gps_file)
|
||||||
self.skip.append(gps_file)
|
self.skip.append(gps_file)
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
try:
|
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
|
||||||
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
|
WifiInfo.ESSID,
|
||||||
WifiInfo.ESSID,
|
WifiInfo.ENCRYPTION,
|
||||||
WifiInfo.ENCRYPTION,
|
WifiInfo.CHANNEL,
|
||||||
WifiInfo.CHANNEL,
|
WifiInfo.RSSI])
|
||||||
WifiInfo.RSSI])
|
except FieldNotFoundError:
|
||||||
except FieldNotFoundError:
|
logging.error("WIGLE: Could not extract all information. Skip %s", gps_file)
|
||||||
logging.error("WIGLE: Could not extract all information. Skip %s", gps_file)
|
self.skip.append(gps_file)
|
||||||
self.skip.append(gps_file)
|
continue
|
||||||
continue
|
except Scapy_Exception as sc_e:
|
||||||
except Scapy_Exception as sc_e:
|
logging.error("WIGLE: %s", sc_e)
|
||||||
logging.error("WIGLE: %s", sc_e)
|
self.skip.append(gps_file)
|
||||||
self.skip.append(gps_file)
|
continue
|
||||||
continue
|
new_entry = _transform_wigle_entry(gps_data, pcap_data)
|
||||||
|
csv_entries.append(new_entry)
|
||||||
new_entry = _transform_wigle_entry(gps_data, pcap_data)
|
no_err_entries.append(gps_file)
|
||||||
csv_entries.append(new_entry)
|
if csv_entries:
|
||||||
no_err_entries.append(gps_file)
|
display.set('status', "Uploading gps-data to wigle.net ...")
|
||||||
|
display.update(force=True)
|
||||||
if csv_entries:
|
try:
|
||||||
display.set('status', "Uploading gps-data to wigle.net ...")
|
_send_to_wigle(csv_entries, self.options['api_key'])
|
||||||
display.update(force=True)
|
reported += no_err_entries
|
||||||
try:
|
self.report.update(data={'reported': reported})
|
||||||
_send_to_wigle(csv_entries, self.options['api_key'])
|
logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries))
|
||||||
reported += no_err_entries
|
except requests.exceptions.RequestException as re_e:
|
||||||
self.report.update(data={'reported': reported})
|
self.skip += no_err_entries
|
||||||
logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries))
|
logging.error("WIGLE: Got an exception while uploading %s", re_e)
|
||||||
except requests.exceptions.RequestException as re_e:
|
except OSError as os_e:
|
||||||
self.skip += no_err_entries
|
self.skip += no_err_entries
|
||||||
logging.error("WIGLE: Got an exception while uploading %s", re_e)
|
logging.error("WIGLE: Got the following error: %s", os_e)
|
||||||
except OSError as os_e:
|
|
||||||
self.skip += no_err_entries
|
|
||||||
logging.error("WIGLE: Got the following error: %s", os_e)
|
|
||||||
|
@@ -1,19 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
from pwnagotchi.utils import StatusFile
|
from datetime import datetime
|
||||||
import pwnagotchi.plugins as plugins
|
from threading import Lock
|
||||||
|
from pwnagotchi.utils import StatusFile, remove_whitelisted
|
||||||
|
from pwnagotchi import plugins
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
|
|
||||||
class WpaSec(plugins.Plugin):
|
class WpaSec(plugins.Plugin):
|
||||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||||
__version__ = '2.0.1'
|
__version__ = '2.1.0'
|
||||||
__license__ = 'GPL3'
|
__license__ = 'GPL3'
|
||||||
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
|
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ready = False
|
self.ready = False
|
||||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
self.lock = Lock()
|
||||||
|
try:
|
||||||
|
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||||
|
except JSONDecodeError:
|
||||||
|
os.remove("/root/.wpa_sec_uploads")
|
||||||
|
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||||
self.options = dict()
|
self.options = dict()
|
||||||
self.skip = list()
|
self.skip = list()
|
||||||
|
|
||||||
@@ -35,38 +43,66 @@ class WpaSec(plugins.Plugin):
|
|||||||
except requests.exceptions.RequestException as req_e:
|
except requests.exceptions.RequestException as req_e:
|
||||||
raise req_e
|
raise req_e
|
||||||
|
|
||||||
|
|
||||||
|
def _download_from_wpasec(self, output, timeout=30):
|
||||||
|
"""
|
||||||
|
Downloads the results from wpasec and safes them to output
|
||||||
|
|
||||||
|
Output-Format: bssid, station_mac, ssid, password
|
||||||
|
"""
|
||||||
|
api_url = self.options['api_url']
|
||||||
|
if not api_url.endswith('/'):
|
||||||
|
api_url = f"{api_url}/"
|
||||||
|
api_url = f"{api_url}?api&dl=1"
|
||||||
|
|
||||||
|
cookie = {'key': self.options['api_key']}
|
||||||
|
try:
|
||||||
|
result = requests.get(api_url, cookies=cookie, timeout=timeout)
|
||||||
|
with open(output, 'wb') as output_file:
|
||||||
|
output_file.write(result.content)
|
||||||
|
except requests.exceptions.RequestException as req_e:
|
||||||
|
raise req_e
|
||||||
|
except OSError as os_e:
|
||||||
|
raise os_e
|
||||||
|
|
||||||
|
|
||||||
def on_loaded(self):
|
def on_loaded(self):
|
||||||
"""
|
"""
|
||||||
Gets called when the plugin gets loaded
|
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):
|
if 'api_key' not in self.options or ('api_key' in self.options and not self.options['api_key']):
|
||||||
logging.error("WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org")
|
logging.error("WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org")
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'api_url' not in self.options or ('api_url' in self.options and self.options['api_url'] is None):
|
if 'api_url' not in self.options or ('api_url' in self.options and not self.options['api_url']):
|
||||||
logging.error("WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured.")
|
logging.error("WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if 'whitelist' not in self.options:
|
||||||
|
self.options['whitelist'] = list()
|
||||||
|
|
||||||
self.ready = True
|
self.ready = True
|
||||||
|
|
||||||
def on_internet_available(self, agent):
|
def on_internet_available(self, agent):
|
||||||
"""
|
"""
|
||||||
Called in manual mode when there's internet connectivity
|
Called in manual mode when there's internet connectivity
|
||||||
"""
|
"""
|
||||||
if self.ready:
|
if not self.ready or self.lock.locked():
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
config = agent.config()
|
config = agent.config()
|
||||||
display = agent.view()
|
display = agent.view()
|
||||||
reported = self.report.data_field_or('reported', default=list())
|
reported = self.report.data_field_or('reported', default=list())
|
||||||
|
|
||||||
handshake_dir = config['bettercap']['handshakes']
|
handshake_dir = config['bettercap']['handshakes']
|
||||||
handshake_filenames = os.listdir(handshake_dir)
|
handshake_filenames = os.listdir(handshake_dir)
|
||||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||||
filename.endswith('.pcap')]
|
filename.endswith('.pcap')]
|
||||||
|
handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist'])
|
||||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||||
|
|
||||||
if handshake_new:
|
if handshake_new:
|
||||||
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
|
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
|
||||||
|
|
||||||
for idx, handshake in enumerate(handshake_new):
|
for idx, handshake in enumerate(handshake_new):
|
||||||
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
||||||
display.update(force=True)
|
display.update(force=True)
|
||||||
@@ -82,3 +118,17 @@ class WpaSec(plugins.Plugin):
|
|||||||
except OSError as os_e:
|
except OSError as os_e:
|
||||||
logging.error("WPA_SEC: %s", os_e)
|
logging.error("WPA_SEC: %s", os_e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if 'download_results' in self.options and self.options['download_results']:
|
||||||
|
cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
|
||||||
|
if os.path.exists(cracked_file):
|
||||||
|
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
|
||||||
|
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile'))
|
||||||
|
logging.info("WPA_SEC: Downloaded cracked passwords.")
|
||||||
|
except requests.exceptions.RequestException as req_e:
|
||||||
|
logging.debug("WPA_SEC: %s", req_e)
|
||||||
|
except OSError as os_e:
|
||||||
|
logging.debug("WPA_SEC: %s", os_e)
|
||||||
|
@@ -52,12 +52,18 @@ class Display(View):
|
|||||||
def is_dfrobot(self):
|
def is_dfrobot(self):
|
||||||
return self._implementation.name == 'dfrobot'
|
return self._implementation.name == 'dfrobot'
|
||||||
|
|
||||||
|
def is_waveshare144lcd(self):
|
||||||
|
return self._implementation.name == 'waveshare144lcd'
|
||||||
|
|
||||||
def is_waveshare154inch(self):
|
def is_waveshare154inch(self):
|
||||||
return self._implementation.name == 'waveshare154inch'
|
return self._implementation.name == 'waveshare154inch'
|
||||||
|
|
||||||
def is_waveshare213d(self):
|
def is_waveshare213d(self):
|
||||||
return self._implementation.name == 'waveshare213d'
|
return self._implementation.name == 'waveshare213d'
|
||||||
|
|
||||||
|
def is_waveshare213bc(self):
|
||||||
|
return self._implementation.name == 'waveshare213bc'
|
||||||
|
|
||||||
def is_spotpear24inch(self):
|
def is_spotpear24inch(self):
|
||||||
return self._implementation.name == 'spotpear24inch'
|
return self._implementation.name == 'spotpear24inch'
|
||||||
|
|
||||||
|
@@ -1,18 +1,38 @@
|
|||||||
from PIL import ImageFont
|
from PIL import ImageFont
|
||||||
|
|
||||||
PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSansMono'
|
# should not be changed
|
||||||
|
FONT_NAME = 'DejaVuSansMono'
|
||||||
|
|
||||||
Bold = ImageFont.truetype("%s-Bold.ttf" % PATH, 10)
|
# can be changed
|
||||||
BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, 8)
|
STATUS_FONT_NAME = None
|
||||||
BoldBig = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
|
SIZE_OFFSET = 0
|
||||||
Medium = ImageFont.truetype("%s.ttf" % PATH, 10)
|
|
||||||
Small = ImageFont.truetype("%s.ttf" % PATH, 9)
|
Bold = None
|
||||||
Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
|
BoldSmall = None
|
||||||
|
BoldBig = None
|
||||||
|
Medium = None
|
||||||
|
Small = None
|
||||||
|
Huge = None
|
||||||
|
|
||||||
|
|
||||||
def setup(bold, bold_small, medium, huge):
|
def init(config):
|
||||||
global PATH, Bold, BoldSmall, Medium, Huge
|
global STATUS_FONT_NAME, SIZE_OFFSET
|
||||||
Bold = ImageFont.truetype("%s-Bold.ttf" % PATH, bold)
|
STATUS_FONT_NAME = config['ui']['font']['name']
|
||||||
BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, bold_small)
|
SIZE_OFFSET = config['ui']['font']['size_offset']
|
||||||
Medium = ImageFont.truetype("%s.ttf" % PATH, medium)
|
setup(10, 8, 10, 25, 25, 9)
|
||||||
Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, huge)
|
|
||||||
|
|
||||||
|
def status_font(old_font):
|
||||||
|
global STATUS_FONT_NAME, SIZE_OFFSET
|
||||||
|
return ImageFont.truetype(STATUS_FONT_NAME, size=old_font.size + SIZE_OFFSET)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bold, bold_small, medium, huge, bold_big, small):
|
||||||
|
global Bold, BoldSmall, Medium, Huge, BoldBig, Small, FONT_NAME
|
||||||
|
|
||||||
|
Small = ImageFont.truetype(FONT_NAME, small)
|
||||||
|
Medium = ImageFont.truetype(FONT_NAME, medium)
|
||||||
|
BoldSmall = ImageFont.truetype("%s-Bold" % FONT_NAME, bold_small)
|
||||||
|
Bold = ImageFont.truetype("%s-Bold" % FONT_NAME, bold)
|
||||||
|
BoldBig = ImageFont.truetype("%s-Bold" % FONT_NAME, bold_big)
|
||||||
|
Huge = ImageFont.truetype("%s-Bold" % FONT_NAME, huge)
|
||||||
|
@@ -7,8 +7,10 @@ from pwnagotchi.ui.hw.waveshare1 import WaveshareV1
|
|||||||
from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
|
from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
|
||||||
from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
|
from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
|
||||||
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
|
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
|
||||||
|
from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd
|
||||||
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
|
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
|
||||||
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
|
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
|
||||||
|
from pwnagotchi.ui.hw.waveshare213bc import Waveshare213bc
|
||||||
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
|
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
|
||||||
|
|
||||||
def display_for(config):
|
def display_for(config):
|
||||||
@@ -36,15 +38,21 @@ def display_for(config):
|
|||||||
|
|
||||||
elif config['ui']['display']['type'] == 'waveshare27inch':
|
elif config['ui']['display']['type'] == 'waveshare27inch':
|
||||||
return Waveshare27inch(config)
|
return Waveshare27inch(config)
|
||||||
|
|
||||||
elif config['ui']['display']['type'] == 'waveshare29inch':
|
elif config['ui']['display']['type'] == 'waveshare29inch':
|
||||||
return Waveshare29inch(config)
|
return Waveshare29inch(config)
|
||||||
|
|
||||||
|
elif config['ui']['display']['type'] == 'waveshare144lcd':
|
||||||
|
return Waveshare144lcd(config)
|
||||||
|
|
||||||
elif config['ui']['display']['type'] == 'waveshare154inch':
|
elif config['ui']['display']['type'] == 'waveshare154inch':
|
||||||
return Waveshare154inch(config)
|
return Waveshare154inch(config)
|
||||||
|
|
||||||
elif config['ui']['display']['type'] == 'waveshare213d':
|
elif config['ui']['display']['type'] == 'waveshare213d':
|
||||||
return Waveshare213d(config)
|
return Waveshare213d(config)
|
||||||
|
|
||||||
|
elif config['ui']['display']['type'] == 'waveshare213bc':
|
||||||
|
return Waveshare213bc(config)
|
||||||
|
|
||||||
elif config['ui']['display']['type'] == 'spotpear24inch':
|
elif config['ui']['display']['type'] == 'spotpear24inch':
|
||||||
return Spotpear24inch(config)
|
return Spotpear24inch(config)
|
||||||
|
@@ -22,7 +22,7 @@ class DisplayImpl(object):
|
|||||||
# status is special :D
|
# status is special :D
|
||||||
'status': {
|
'status': {
|
||||||
'pos': (0, 0),
|
'pos': (0, 0),
|
||||||
'font': fonts.Medium,
|
'font': fonts.status_font(fonts.Medium),
|
||||||
'max': 20
|
'max': 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ class DFRobot(DisplayImpl):
|
|||||||
self._display = None
|
self._display = None
|
||||||
|
|
||||||
def layout(self):
|
def layout(self):
|
||||||
fonts.setup(10, 9, 10, 35)
|
fonts.setup(10, 9, 10, 35, 25, 9)
|
||||||
self._layout['width'] = 250
|
self._layout['width'] = 250
|
||||||
self._layout['height'] = 122
|
self._layout['height'] = 122
|
||||||
self._layout['face'] = (0, 40)
|
self._layout['face'] = (0, 40)
|
||||||
@@ -25,7 +25,7 @@ class DFRobot(DisplayImpl):
|
|||||||
self._layout['mode'] = (225, 109)
|
self._layout['mode'] = (225, 109)
|
||||||
self._layout['status'] = {
|
self._layout['status'] = {
|
||||||
'pos': (125, 20),
|
'pos': (125, 20),
|
||||||
'font': fonts.Medium,
|
'font': fonts.status_font(fonts.Medium),
|
||||||
'max': 20
|
'max': 20
|
||||||
}
|
}
|
||||||
return self._layout
|
return self._layout
|
||||||
@@ -40,4 +40,4 @@ class DFRobot(DisplayImpl):
|
|||||||
self._display.display(buf)
|
self._display.display(buf)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self._display.Clear(0xFF)
|
self._display.Clear(0xFF)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user