Compare commits
183 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 | ||
|
f8ffab426b | ||
|
f563d71477 | ||
|
7b219fd139 | ||
|
42ed698583 | ||
|
6df7bcd885 | ||
|
1c299832ae | ||
|
11476433ca | ||
|
b1ad247e11 | ||
|
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
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{*.yml,*.yaml,config.yml,defaults.yml}]
|
||||
[*]
|
||||
indent_style = space
|
||||
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
|
||||
*backup*.tgz
|
||||
*backup*.gz
|
||||
.vscode
|
||||
|
3
Makefile
3
Makefile
@@ -1,10 +1,11 @@
|
||||
PACKER_VERSION=1.5.2
|
||||
PWN_HOSTNAME=pwnagotchi
|
||||
PWN_VERSION=master
|
||||
|
||||
all: clean install image
|
||||
|
||||
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
|
||||
sudo mv /tmp/packer /usr/bin/packer
|
||||
git clone https://github.com/solo-io/packer-builder-arm-image /tmp/packer-builder-arm-image
|
||||
|
@@ -2,24 +2,23 @@
|
||||
import logging
|
||||
import argparse
|
||||
import time
|
||||
import yaml
|
||||
import signal
|
||||
import sys
|
||||
import toml
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
from pwnagotchi.identity import KeyPair
|
||||
from pwnagotchi.agent import Agent
|
||||
from pwnagotchi.ui.display import Display
|
||||
from pwnagotchi import utils
|
||||
from pwnagotchi.plugins import cmd as plugins_cmd
|
||||
from pwnagotchi import log
|
||||
from pwnagotchi import restart
|
||||
from pwnagotchi import fs
|
||||
from pwnagotchi.utils import DottedTomlEncoder
|
||||
|
||||
|
||||
def do_clear(display):
|
||||
logging.info("clearing the display ...")
|
||||
display.clear()
|
||||
exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def do_manual_mode(agent):
|
||||
@@ -84,15 +83,16 @@ def do_auto_mode(agent):
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("main loop exception")
|
||||
logging.exception("main loop exception (%s)", e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
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.')
|
||||
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.')
|
||||
|
||||
parser.add_argument('--manual', dest="do_manual", action="store_true", default=False, help="Manual mode.")
|
||||
@@ -113,16 +113,34 @@ if __name__ == '__main__':
|
||||
|
||||
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:
|
||||
print(pwnagotchi.version)
|
||||
exit(0)
|
||||
print(pwnagotchi.__version__)
|
||||
sys.exit(0)
|
||||
|
||||
config = utils.load_config(args)
|
||||
if args.print_config:
|
||||
print(yaml.dump(config, default_flow_style=False))
|
||||
exit(0)
|
||||
|
||||
utils.setup_logging(args, config)
|
||||
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'])
|
||||
|
||||
@@ -132,7 +150,7 @@ if __name__ == '__main__':
|
||||
|
||||
if args.do_clear:
|
||||
do_clear(display)
|
||||
exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
|
||||
|
||||
|
@@ -6,10 +6,15 @@ After=pwngrid-peer.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/tmp
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwnagotchi-launcher
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
TasksMax=infinity
|
||||
LimitNPROC=infinity
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -1,6 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
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_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
|
||||
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_led 10 &
|
||||
|
||||
@@ -8,4 +16,4 @@ if is_auto_mode; then
|
||||
/usr/local/bin/pwnagotchi
|
||||
else
|
||||
/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
|
||||
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",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
|
@@ -35,7 +35,7 @@
|
||||
- ifup@wlan0.service
|
||||
packages:
|
||||
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"
|
||||
pwngrid:
|
||||
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-realtek
|
||||
remove:
|
||||
- rasberrypi-net-mods
|
||||
- raspberrypi-net-mods
|
||||
- dhcpcd5
|
||||
- triggerhappy
|
||||
- wpa_supplicant
|
||||
- nfs-common
|
||||
install:
|
||||
- rsync
|
||||
- vim
|
||||
- screen
|
||||
- golang
|
||||
@@ -100,9 +101,9 @@
|
||||
- bc
|
||||
- fonts-freefont-ttf
|
||||
- fbi
|
||||
- python3-flask
|
||||
- python3-flask-cors
|
||||
- python3-flaskext.wtf
|
||||
- fonts-ipaexfont-gothic
|
||||
- cryptsetup
|
||||
- dnsmasq
|
||||
|
||||
tasks:
|
||||
- name: change hostname
|
||||
@@ -216,9 +217,19 @@
|
||||
dest: /usr/local/src/pwnagotchi
|
||||
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
|
||||
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
|
||||
debug:
|
||||
@@ -228,7 +239,7 @@
|
||||
command: "python3 setup.py sdist bdist_wheel"
|
||||
args:
|
||||
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
|
||||
pip:
|
||||
@@ -246,7 +257,7 @@
|
||||
pip:
|
||||
name: "{{ lookup('fileglob', '/usr/local/src/pwnagotchi/dist/pwnagotchi*.whl') }}"
|
||||
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
|
||||
unarchive:
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import pwnagotchi.ui.view as view
|
||||
import pwnagotchi
|
||||
|
||||
version = '1.4.1'
|
||||
|
||||
|
||||
from pwnagotchi._version import __version__
|
||||
|
||||
_name = None
|
||||
config = None
|
||||
|
||||
|
||||
def set_name(new_name):
|
||||
@@ -27,17 +27,17 @@ def set_name(new_name):
|
||||
if new_name != current:
|
||||
global _name
|
||||
|
||||
logging.info("setting unit hostname '%s' -> '%s'" % (current, new_name))
|
||||
logging.info("setting unit hostname '%s' -> '%s'", current, new_name)
|
||||
with open('/etc/hostname', 'wt') as fp:
|
||||
fp.write(new_name)
|
||||
|
||||
with open('/etc/hosts', 'rt') as fp:
|
||||
prev = fp.read()
|
||||
logging.debug("old hosts:\n%s\n" % prev)
|
||||
logging.debug("old hosts:\n%s\n", prev)
|
||||
|
||||
with open('/etc/hosts', 'wt') as fp:
|
||||
patched = prev.replace(current, new_name, -1)
|
||||
logging.debug("new hosts:\n%s\n" % patched)
|
||||
logging.debug("new hosts:\n%s\n", patched)
|
||||
fp.write(patched)
|
||||
|
||||
os.system("hostname '%s'" % new_name)
|
||||
@@ -65,8 +65,6 @@ def mem_usage():
|
||||
kb_mem_total = int(line.split()[1])
|
||||
if line.startswith("MemFree:"):
|
||||
kb_mem_free = int(line.split()[1])
|
||||
if line.startswith("MemAvailable:"):
|
||||
kb_mem_available = int(line.split()[1])
|
||||
if line.startswith("Buffers:"):
|
||||
kb_main_buffers = int(line.split()[1])
|
||||
if line.startswith("Cached:"):
|
||||
@@ -77,18 +75,27 @@ def mem_usage():
|
||||
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:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith('cpu '):
|
||||
parts = list(map(int, line.split()[1:]))
|
||||
user_n = parts[0]
|
||||
sys_n = parts[2]
|
||||
idle_n = parts[3]
|
||||
tot = user_n + sys_n + idle_n
|
||||
return (user_n + sys_n) / tot
|
||||
return 0
|
||||
return list(map(int,fp.readline().split()[1:]))
|
||||
|
||||
|
||||
def cpu_load():
|
||||
"""
|
||||
Returns the current cpuload
|
||||
"""
|
||||
parts0 = _cpu_stat()
|
||||
time.sleep(0.1)
|
||||
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):
|
||||
@@ -99,7 +106,15 @@ def temperature(celsius=True):
|
||||
|
||||
|
||||
def shutdown():
|
||||
logging.warning("syncing...")
|
||||
|
||||
from pwnagotchi import fs
|
||||
for m in fs.mounts:
|
||||
m.sync()
|
||||
|
||||
logging.warning("shutting down ...")
|
||||
|
||||
from pwnagotchi.ui import view
|
||||
if view.ROOT:
|
||||
view.ROOT.on_shutdown()
|
||||
# give it some time to refresh the ui
|
||||
@@ -109,7 +124,7 @@ def shutdown():
|
||||
|
||||
|
||||
def restart(mode):
|
||||
logging.warning("restarting in %s mode ..." % mode)
|
||||
logging.warning("restarting in %s mode ...", mode)
|
||||
|
||||
if mode == 'AUTO':
|
||||
os.system("touch /root/.pwnagotchi-auto")
|
||||
@@ -123,10 +138,11 @@ def restart(mode):
|
||||
def reboot(mode=None):
|
||||
if mode is not None:
|
||||
mode = mode.upper()
|
||||
logging.warning("rebooting in %s mode ..." % mode)
|
||||
logging.warning("rebooting in %s mode ...", mode)
|
||||
else:
|
||||
logging.warning("rebooting ...")
|
||||
|
||||
from pwnagotchi.ui import view
|
||||
if view.ROOT:
|
||||
view.ROOT.on_rebooting()
|
||||
# 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 re
|
||||
import logging
|
||||
import asyncio
|
||||
import _thread
|
||||
|
||||
import pwnagotchi
|
||||
@@ -30,7 +31,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
AsyncTrainer.__init__(self, config)
|
||||
|
||||
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._tot_aps = 0
|
||||
self._aps_on_channel = 0
|
||||
@@ -49,7 +50,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
if not os.path.exists(config['bettercap']['handshakes']):
|
||||
os.makedirs(config['bettercap']['handshakes'])
|
||||
|
||||
logging.info("%s@%s (v%s)", pwnagotchi.name(), self.fingerprint(), pwnagotchi.version)
|
||||
logging.info("%s@%s (v%s)", pwnagotchi.name(), self.fingerprint(), pwnagotchi.__version__)
|
||||
for _, plugin in plugins.loaded.items():
|
||||
logging.debug("plugin '%s' v%s", plugin.__class__.__name__, plugin.__version__)
|
||||
|
||||
@@ -68,7 +69,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
for tag in self._config['bettercap']['silence']:
|
||||
try:
|
||||
self.run('events.ignore %s' % tag, verbose_errors=False)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _reset_wifi_settings(self):
|
||||
@@ -121,9 +122,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
def _wait_bettercap(self):
|
||||
while True:
|
||||
try:
|
||||
s = self.session()
|
||||
_s = self.session()
|
||||
return
|
||||
except:
|
||||
except Exception:
|
||||
logging.info("waiting for bettercap API to be available ...")
|
||||
time.sleep(1)
|
||||
|
||||
@@ -134,6 +135,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
self.set_starting()
|
||||
self.start_monitor_mode()
|
||||
self.start_event_polling()
|
||||
self.start_session_fetcher()
|
||||
# print initial stats
|
||||
self.next_epoch()
|
||||
self.set_ready()
|
||||
@@ -158,7 +160,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
try:
|
||||
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
logging.exception("Error while setting wifi.recon.channels (%s)", e)
|
||||
|
||||
self.wait_for(recon_time, sleeping=False)
|
||||
|
||||
@@ -188,7 +190,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
if self._filter_included(ap):
|
||||
aps.append(ap)
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
logging.exception("Error while getting acces points (%s)", e)
|
||||
|
||||
aps.sort(key=lambda ap: ap['channel'])
|
||||
return self.set_access_points(aps)
|
||||
@@ -212,7 +214,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
ch = ap['channel']
|
||||
# if we're sticking to a channel, skip anything
|
||||
# which is not on that channel
|
||||
if not channels and ch not in channels:
|
||||
if channels and ch not in channels:
|
||||
continue
|
||||
|
||||
if ch not in grouped:
|
||||
@@ -303,60 +305,67 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
if not no_exceptions:
|
||||
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')
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
new_shakes = 0
|
||||
|
||||
logging.debug("polling events ...")
|
||||
|
||||
try:
|
||||
s = self.session()
|
||||
self._update_uptime(s)
|
||||
|
||||
self._update_advertisement(s)
|
||||
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)
|
||||
loop.create_task(self.start_websocket(self._on_event))
|
||||
loop.run_forever()
|
||||
except Exception as ex:
|
||||
logging.debug("Error while polling via websocket (%s)", ex)
|
||||
|
||||
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):
|
||||
s = self.session()
|
||||
@@ -465,4 +474,4 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
plugins.on('channel_hop', self, channel)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("error: %s", e)
|
||||
logging.error("Error while setting channel (%s)", e)
|
||||
|
@@ -1,12 +1,9 @@
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
# 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'}
|
||||
# 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):
|
||||
@@ -59,7 +56,7 @@ def load(config, agent, epoch, from_disk=True):
|
||||
|
||||
return a2c
|
||||
except Exception as e:
|
||||
logging.exception("error while starting AI")
|
||||
logging.exception("error while starting AI (%s)", e)
|
||||
|
||||
logging.warning("[ai] AI not loaded!")
|
||||
return False
|
||||
|
@@ -19,6 +19,10 @@ class Epoch(object):
|
||||
self.active_for = 0
|
||||
# number of epochs with no visible access points
|
||||
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?
|
||||
self.did_deauth = False
|
||||
# number of deauths in this epoch
|
||||
@@ -99,13 +103,13 @@ class Epoch(object):
|
||||
try:
|
||||
aps_per_chan[ch_idx] += 1.0
|
||||
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))
|
||||
|
||||
for peer in peers:
|
||||
try:
|
||||
peers_per_chan[peer.last_channel - 1] += 1.0
|
||||
except IndexError as e:
|
||||
except IndexError:
|
||||
logging.error(
|
||||
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
|
||||
|
||||
@@ -157,6 +161,20 @@ class Epoch(object):
|
||||
else:
|
||||
self.active_for += 1
|
||||
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()
|
||||
cpu = pwnagotchi.cpu_load()
|
||||
@@ -172,6 +190,8 @@ class Epoch(object):
|
||||
'blind_for_epochs': self.blind_for,
|
||||
'inactive_for_epochs': self.inactive_for,
|
||||
'active_for_epochs': self.active_for,
|
||||
'sad_for_epochs': self.sad_for,
|
||||
'bored_for_epochs': self.bored_for,
|
||||
'missed_interactions': self.num_missed,
|
||||
'num_hops': self.num_hops,
|
||||
'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_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%% "
|
||||
"temperature=%dC reward=%s" % (
|
||||
self.epoch,
|
||||
utils.secs_to_hhmmss(self.epoch_duration),
|
||||
utils.secs_to_hhmmss(self.num_slept),
|
||||
self.blind_for,
|
||||
self.sad_for,
|
||||
self.bored_for,
|
||||
self.inactive_for,
|
||||
self.active_for,
|
||||
self.num_peers,
|
||||
|
@@ -18,4 +18,10 @@ class RewardFunction(object):
|
||||
m = -.3 * (state['missed_interactions'] / tot_interactions)
|
||||
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._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
|
||||
except Exception as e:
|
||||
logging.exception("[ai] error while training")
|
||||
logging.exception("[ai] error while training (%s)", e)
|
||||
finally:
|
||||
self.set_training(False)
|
||||
obs = self._model.env.reset()
|
||||
|
@@ -12,19 +12,18 @@ class Automata(object):
|
||||
self._epoch = Epoch(config)
|
||||
|
||||
def _on_miss(self, who):
|
||||
logging.info("it looks like %s is not in range anymore :/" % who)
|
||||
logging.info("it looks like %s is not in range anymore :/", who)
|
||||
self._epoch.track(miss=True)
|
||||
self._view.on_miss(who)
|
||||
|
||||
def _on_error(self, who, e):
|
||||
error = "%s" % e
|
||||
# when we're trying to associate or deauth something that is not in range anymore
|
||||
# (if we are moving), we get the following error from bettercap:
|
||||
# error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list.
|
||||
if 'is an unknown BSSID' in error:
|
||||
if 'is an unknown BSSID' in str(e):
|
||||
self._on_miss(who)
|
||||
else:
|
||||
logging.error("%s" % e)
|
||||
logging.error(e)
|
||||
|
||||
def set_starting(self):
|
||||
self._view.on_starting()
|
||||
@@ -58,7 +57,7 @@ class Automata(object):
|
||||
def set_bored(self):
|
||||
factor = self._epoch.inactive_for / self._config['personality']['bored_num_epochs']
|
||||
if not self._has_support_network_for(factor):
|
||||
logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for)
|
||||
logging.warning("%d epochs with no activity -> bored", self._epoch.inactive_for)
|
||||
self._view.on_bored()
|
||||
plugins.on('bored', self)
|
||||
else:
|
||||
@@ -68,7 +67,7 @@ class Automata(object):
|
||||
def set_sad(self):
|
||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||
if not self._has_support_network_for(factor):
|
||||
logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for)
|
||||
logging.warning("%d epochs with no activity -> sad", self._epoch.inactive_for)
|
||||
self._view.on_sad()
|
||||
plugins.on('sad', self)
|
||||
else:
|
||||
@@ -77,7 +76,7 @@ class Automata(object):
|
||||
|
||||
def set_angry(self, factor):
|
||||
if not self._has_support_network_for(factor):
|
||||
logging.warning("%d epochs with no activity -> angry" % self._epoch.inactive_for)
|
||||
logging.warning("%d epochs with no activity -> angry", self._epoch.inactive_for)
|
||||
self._view.on_angry()
|
||||
plugins.on('angry', self)
|
||||
else:
|
||||
@@ -85,7 +84,7 @@ class Automata(object):
|
||||
self.set_grateful()
|
||||
|
||||
def set_excited(self):
|
||||
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
|
||||
logging.warning("%d epochs with activity -> excited", self._epoch.active_for)
|
||||
self._view.on_excited()
|
||||
plugins.on('excited', self)
|
||||
|
||||
@@ -118,17 +117,17 @@ class Automata(object):
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
logging.warning("agent missed %d interactions -> lonely" % did_miss)
|
||||
logging.warning("agent missed %d interactions -> lonely", did_miss)
|
||||
self.set_lonely()
|
||||
# after X times being bored, the status is set to sad or angry
|
||||
elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
|
||||
elif self._epoch.sad_for:
|
||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
self.set_sad()
|
||||
# after X times being inactive, the status is set to bored
|
||||
elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']:
|
||||
elif self._epoch.bored_for:
|
||||
self.set_bored()
|
||||
# after X times being active, the status is set to happy / excited
|
||||
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())
|
||||
|
||||
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
|
||||
logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for)
|
||||
logging.critical("%d epochs without visible access points -> rebooting ...", self._epoch.blind_for)
|
||||
self._reboot()
|
||||
self._epoch.blind_for = 0
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import websockets
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
|
||||
@@ -25,15 +28,25 @@ class Client(object):
|
||||
self.username = username
|
||||
self.password = password
|
||||
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)
|
||||
|
||||
def session(self):
|
||||
r = requests.get("%s/session" % self.url, auth=self.auth)
|
||||
return decode(r)
|
||||
|
||||
def events(self):
|
||||
r = requests.get("%s/events" % self.url, auth=self.auth)
|
||||
return decode(r)
|
||||
async def start_websocket(self, consumer):
|
||||
s = "%s/events" % self.websocket
|
||||
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):
|
||||
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,296 +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"
|
||||
download_results: false
|
||||
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
|
||||
session-stats:
|
||||
enabled: true
|
||||
# monitor interface to use
|
||||
iface: mon0
|
||||
# command to run to bring the mon interface up in case it's not up already
|
||||
mon_start_cmd: /usr/bin/monstart
|
||||
mon_stop_cmd: /usr/bin/monstop
|
||||
mon_max_blind_epochs: 50
|
||||
# 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, waveshare144lcd/ws144inch
|
||||
type: 'waveshare_2'
|
||||
# Possible options red/yellow/black (black used for monocromatic displays)
|
||||
# Waveshare tri-color 2.13in display can be over-driven with color set as 'fastAndFurious'
|
||||
# THIS IS POTENTIALLY DANGEROUS. DO NOT USE UNLESS YOU UNDERSTAND THAT IT COULD KILL YOUR DISPLAY
|
||||
color: 'black'
|
||||
|
||||
# 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"),
|
||||
'brain': brain,
|
||||
'version': pwnagotchi.version
|
||||
'version': pwnagotchi.__version__
|
||||
}
|
||||
|
||||
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}!"
|
||||
msgstr "Имате {count} нови съобщения!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Упс, нещо се обърка ... Рестартиране ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -177,7 +177,7 @@ msgstr "太酷了, 我们抓到了{num}新的猎物{plural}!"
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "主人,有{count}新消息{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "行动,额等等有点小问题... 重启ing ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -26,7 +26,7 @@ msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack den Planet!"
|
||||
msgstr "Hack den Planeten!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "KI bereit."
|
||||
@@ -35,7 +35,7 @@ msgid "The neural network is ready."
|
||||
msgstr "Das neurale Netz ist bereit."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Generiere Keys, nicht ausschalten..."
|
||||
msgstr "Generiere Schlüssel, nicht ausschalten..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
@@ -83,7 +83,7 @@ msgid "I pwn therefore I am."
|
||||
msgstr "Ich pwne, also bin ich."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "So viele Netwerke!!!"
|
||||
msgstr "So viele Netzwerke!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Ich habe sooo viel Spaß!"
|
||||
@@ -93,7 +93,7 @@ msgstr "Mein Verbrechen ist das der Neugier..."
|
||||
|
||||
#, python-brace-format
|
||||
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?"
|
||||
@@ -198,16 +198,16 @@ msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
||||
msgid "You have {count} new message{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 was schief gelaufen... Starte neu..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} Stationen gekicked\n"
|
||||
msgstr "{num} Stationen gekickt\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} Freunde gefunden\n"
|
||||
msgstr "{num} neue Freunde gefunden\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
@@ -247,3 +247,4 @@ msgstr "Minute"
|
||||
|
||||
msgid "second"
|
||||
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}!"
|
||||
msgstr "Τέλεια δικέ μου, πήραμε {num} νέες χειραψίες!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ουπς, κάτιπήγε λάθος ... Επανεκκινούμαι ..."
|
||||
|
||||
#, 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}!"
|
||||
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 ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -72,13 +72,13 @@ msgstr "Je suis triste"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Je me sens si seul..."
|
||||
msgstr "Lache moi..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Je t'en veux !"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Je vis la vie !"
|
||||
msgstr "Je vis la belle vie !"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Je pwn donc je suis."
|
||||
@@ -114,7 +114,7 @@ msgstr "Hum... au revoir {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} est part ..."
|
||||
msgstr "{name} est parti ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
@@ -144,7 +144,7 @@ msgstr "Où est tout le monde ?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Fais la sieste pendant {secs}s..."
|
||||
msgstr "Je fais la sieste pendant {secs}s..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
@@ -161,11 +161,11 @@ msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Attends pendant {secs}s..."
|
||||
msgstr "J'attends pendant {secs}s..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Regarde autour ({secs}s)"
|
||||
msgstr "J'observe ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
@@ -193,13 +193,13 @@ msgstr "Je kick et je bannis {mac} !"
|
||||
|
||||
#, python-brace-format
|
||||
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
|
||||
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..."
|
||||
|
||||
#, python-brace-format
|
||||
@@ -208,18 +208,18 @@ msgstr "{num} stations kick\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "A {num} nouveaux amis\n"
|
||||
msgstr "A fait {num} nouve(l/aux) ami(s)\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "A {num} handshakes\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 pair rencontré"
|
||||
msgstr "1 camarade rencontré"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} pairs recontrés"
|
||||
msgstr "{num} camarades recontrés"
|
||||
|
||||
#, python-brace-format
|
||||
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}!"
|
||||
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..."
|
||||
|
||||
#, 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}!"
|
||||
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 ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -3,12 +3,11 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR 24534649+wytshadow@users.noreply.github.com, 2019.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\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"
|
||||
"Last-Translator: wytshadow <24534649+wytshadow@users.noreply.github.com>\n"
|
||||
"Language-Team: pwnagotchi <24534649+wytshadow@users.noreply.github.com>\n"
|
||||
@@ -21,170 +20,207 @@ msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "すやすや〜"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "こんにちは、ポウナゴッチです!始めている。。。"
|
||||
msgstr "僕、 ポーナゴッチです!"
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr ""
|
||||
msgstr "ポーンしようよ。"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "ハックザプラネット!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "人工知能の準備ができました。"
|
||||
msgstr "AIの準備ができました。"
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "ニューラルネットワークの準備ができました。"
|
||||
msgstr "ニューラルネットワークの\n準備ができました。"
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "鍵生成をしてます。\n電源を落とさないでね。"
|
||||
|
||||
#, python-brace-format
|
||||
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 ..."
|
||||
msgstr "退屈です。。。"
|
||||
msgstr "退屈だぁ。。。"
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "散歩に行きましょう!"
|
||||
msgstr "散歩に行こうよ!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "今日は私の人生で最高の日です!"
|
||||
msgstr "人生最高の日だよ!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr ""
|
||||
msgstr "がっかりな日だよ。orz"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "とても退屈です。"
|
||||
msgstr "退屈だね。"
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "とても悲しいです。。。"
|
||||
msgstr "あ~悲しいよぉ。"
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "悲しいです。"
|
||||
msgstr "悲しいね。"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "ひとりぼっちだよ。"
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "怒っちゃうよ。"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "人生を生きている!"
|
||||
msgstr "わくわくするね。"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr ""
|
||||
msgstr "ポーンしてこそのオレ。"
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "たくさんネットワークがある!!!"
|
||||
msgstr "たくさん\nWiFiが飛んでるよ!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "とても楽しんでいます!"
|
||||
msgstr "楽しいよぉ!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr ""
|
||||
msgstr "APに興味津々..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "こんにちは{name}!初めまして。{name}"
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "こんにちは{name}!\n初めまして。{name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr ""
|
||||
msgid "Yo {name}! Sup?"
|
||||
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
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "ええと。。。さようなら{name}"
|
||||
msgstr "じゃあね、さようなら {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name}がなくなった。。。"
|
||||
msgstr "{name}\nがいなくなったよ。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "おっと。。。{name}がなくなった。"
|
||||
msgstr "あらら、\n{name}\nがいなくなったね。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name}逃した!"
|
||||
msgstr "{name} が逃げた!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "逃した!"
|
||||
msgstr "残念、逃した!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "良い仲間にめぐりあえたよ。"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "友達は大好きだよ。"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "誰も僕と一緒にプレーしたくない。。。"
|
||||
msgstr "誰も僕と一緒に\nあそんでくれない。"
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "僕は孤独を感じる。。。"
|
||||
msgstr "ひとりぼっちだよ。"
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "みんなどこ?!"
|
||||
msgstr "みんなどこにいるの?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "{secs}寝ている。"
|
||||
msgstr "{secs}秒 寝ます。"
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "すや〜"
|
||||
msgstr "ぐぅ〜"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "すやすや〜 ({secs})"
|
||||
msgstr "すやすや〜 ({secs}秒)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "お休みなさい。"
|
||||
msgstr "おやすみなさい。"
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "す〜"
|
||||
msgstr "ぐぅ~"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "{secs}を待っている。。。"
|
||||
msgstr "{secs}秒 待ちです。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "{secs}を探している。"
|
||||
msgstr "{secs}秒 探してます。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "ちょっと{what}友だちになりましょう!"
|
||||
msgstr "ねぇねぇ\n{what} \n友だちになろうよ。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr ""
|
||||
msgstr "{what} \nとつながるかな?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "よー{what}!"
|
||||
msgstr "ねぇねぇ\n{what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr ""
|
||||
msgstr "{mac}\nはWiFiじゃないのね。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr ""
|
||||
msgstr "{mac}\nの認証取得中..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr ""
|
||||
msgstr "{mac}\nに拒否られた。"
|
||||
|
||||
#, python-brace-format
|
||||
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 ..."
|
||||
msgstr "おっと!何かが間違っていた。。。リブートしている。。。"
|
||||
msgstr "何か間違った。\nリブートしている。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr ""
|
||||
msgstr "{num}回拒否された。\n"
|
||||
|
||||
msgid "Made >999 new friends\n"
|
||||
msgstr "1000人以上友達ができた。\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num}人の新しい友達を作りました\n"
|
||||
msgstr "{num}人友達ができた。\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "{num}ハンドシェイクがある。\n"
|
||||
msgstr "{num}回ハンドシェイクした。\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1人の仲間を会いました。"
|
||||
msgstr "1人 仲間に会いました。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num}人の仲間を会いました。"
|
||||
msgstr "{num}人 仲間に会いました。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -192,6 +228,9 @@ msgid ""
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"{duration}中{deauthed}のAPに拒否されたけど、{associated}回チャンスがあって"
|
||||
"{handshakes}回ハンドシェイクがあったよ。。 #pwnagotchi #pwnlog #pwnlife "
|
||||
"#hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "時間"
|
||||
@@ -203,7 +242,7 @@ msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "時間"
|
||||
msgstr "時"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "分"
|
||||
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Кикбан {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Кул, фативме {num} нови ракувања!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Упс, нешто не еко што треба ... Рестартирам ..."
|
||||
|
||||
#, 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}!"
|
||||
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 ..."
|
||||
|
||||
#, 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}!"
|
||||
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 ..."
|
||||
|
||||
#, 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}!"
|
||||
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ę ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Kickbanning {mac}"
|
||||
msgid "Cool, we got {num} new handshake{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 ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -164,7 +164,7 @@ msgstr "A chutar {mac}!"
|
||||
msgid "Cool, we got {num} new 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 ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
# 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"
|
||||
@@ -199,7 +199,7 @@ msgstr "Șmecher, avem {num} de handshake-uri noi!"
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Ai {count} mesaj(e) nou/noi!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -5,17 +5,21 @@
|
||||
# Second author <https://github.com/mbgroot>, 2019
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.2"
|
||||
"Report-Msgid-Bugs-To: m-b-g@yandex.ru"
|
||||
"POT-Creation-Date: 2019-11-27 16:47+0200"
|
||||
"PO-Revision-Date: 2019-11-27 18:50+0300"
|
||||
"Last-Translator: Evgeny Zelenin <m-b-g@yandex.ru>"
|
||||
"Language-Team: ru"
|
||||
"Language: ru"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Project-Id-Version: Pwnagotchi Russian translation v 0.0.2\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\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"
|
||||
"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"
|
||||
msgstr "Хрррр..."
|
||||
@@ -34,12 +38,14 @@ msgstr "A.I. готов."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Нейронная сеть готова."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Генерация ключей, не выключайте..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Чтение логов последнего сеанса..."
|
||||
|
||||
@@ -67,6 +73,7 @@ msgstr "Мне очень грустно …"
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Мне грустно"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Оставь меня в покое..."
|
||||
|
||||
@@ -95,9 +102,10 @@ msgstr "Привет, {name}! Рад встрече с тобой!"
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Цель {name} близко!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Хэй {nume}! Как дела?"
|
||||
msgstr "Хэй {name}! Как дела?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
@@ -121,6 +129,7 @@ msgstr "{name} упустил!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Промахнулся!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Хорошие друзья - это благословение!"
|
||||
|
||||
@@ -146,6 +155,7 @@ msgstr "Хррр..."
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "Хррррр.. ({secs}c)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Доброй ночи."
|
||||
|
||||
@@ -188,7 +198,7 @@ msgstr "Кикаю {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Круто, мы получили {num} новое рукопожатие!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ой, что-то пошло не так … Перезагружаюсь …"
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr ""
|
||||
msgid "Cool, we got {num} new handshake{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 ..."
|
||||
|
||||
#, 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}!"
|
||||
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 ..."
|
||||
|
||||
#, 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}!"
|
||||
msgstr "Tienes {count} nuevos mensajes{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oops, algo salio mal ... Reiniciando ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -177,7 +177,7 @@ msgstr "Отакої, у нас є {num} нових рукостискань!"
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Нових повідомлень: {count}"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ой, щось пішло не так ... Перезавантажуюсь ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
@@ -198,7 +198,7 @@ msgstr ""
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
|
@@ -3,6 +3,9 @@ import time
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import gzip
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
|
||||
from pwnagotchi.voice import Voice
|
||||
@@ -209,3 +212,98 @@ class LastSession(object):
|
||||
|
||||
def is_new(self):
|
||||
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._advertisement = {
|
||||
'name': pwnagotchi.name(),
|
||||
'version': pwnagotchi.version,
|
||||
'version': pwnagotchi.__version__,
|
||||
'identity': self._keypair.fingerprint,
|
||||
'face': faces.FRIEND,
|
||||
'pwnd_run': 0,
|
||||
|
@@ -1,40 +1,64 @@
|
||||
import os
|
||||
import glob
|
||||
import _thread
|
||||
import threading
|
||||
import importlib, importlib.util
|
||||
import logging
|
||||
from pwnagotchi.ui import view
|
||||
|
||||
|
||||
|
||||
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
|
||||
loaded = {}
|
||||
database = {}
|
||||
locks = {}
|
||||
|
||||
|
||||
class Plugin:
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
global loaded
|
||||
global loaded, locks
|
||||
|
||||
plugin_name = cls.__module__.split('.')[0]
|
||||
plugin_instance = cls()
|
||||
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
|
||||
loaded[plugin_name] = plugin_instance
|
||||
|
||||
for attr_name in plugin_instance.__dir__():
|
||||
if attr_name.startswith('on_'):
|
||||
cb = getattr(plugin_instance, attr_name, None)
|
||||
if cb is not None and callable(cb):
|
||||
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
|
||||
|
||||
|
||||
def toggle_plugin(name, enable=True):
|
||||
"""
|
||||
Load or unload a plugin
|
||||
|
||||
returns True if changed, otherwise False
|
||||
"""
|
||||
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
|
||||
@@ -43,19 +67,32 @@ def toggle_plugin(name, enable=True):
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
global loaded
|
||||
|
||||
if plugin_name in loaded:
|
||||
plugin = loaded[plugin_name]
|
||||
cb_name = 'on_%s' % event_name
|
||||
callback = getattr(plugin, cb_name, None)
|
||||
if callback is not None and callable(callback):
|
||||
try:
|
||||
_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:
|
||||
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
|
||||
logging.error(e, exc_info=True)
|
||||
@@ -103,3 +140,4 @@ def load(config):
|
||||
plugin.options = config['main']['plugins'][name]
|
||||
|
||||
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 shutil
|
||||
import glob
|
||||
import pkg_resources
|
||||
from threading import Lock
|
||||
|
||||
import pwnagotchi
|
||||
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):
|
||||
@@ -29,8 +29,8 @@ def check(version, repo, native=True):
|
||||
info['available'] = latest_ver = latest['tag_name'].replace('v', '')
|
||||
is_arm = info['arch'].startswith('arm')
|
||||
|
||||
local = pkg_resources.parse_version(info['current'])
|
||||
remote = pkg_resources.parse_version(latest_ver)
|
||||
local = version_to_tuple(info['current'])
|
||||
remote = version_to_tuple(latest_ver)
|
||||
if remote > local:
|
||||
if not native:
|
||||
info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name'])
|
||||
@@ -150,69 +150,74 @@ class AutoUpdate(plugins.Plugin):
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.status = StatusFile('/root/.auto-update')
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
if 'interval' not in self.options or ('interval' in self.options and self.options['interval'] is None):
|
||||
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")
|
||||
return
|
||||
self.ready = True
|
||||
logging.info("[update] plugin loaded.")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||
|
||||
if not self.ready:
|
||||
if self.lock.locked():
|
||||
return
|
||||
|
||||
if self.status.newer_then_hours(self.options['interval']):
|
||||
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
|
||||
return
|
||||
with self.lock:
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||
|
||||
logging.info("[update] checking for updates ...")
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
display = agent.view()
|
||||
prev_status = display.get('status')
|
||||
if self.status.newer_then_hours(self.options['interval']):
|
||||
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
|
||||
return
|
||||
|
||||
try:
|
||||
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
||||
logging.info("[update] checking for updates ...")
|
||||
|
||||
to_install = []
|
||||
to_check = [
|
||||
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
||||
('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
|
||||
]
|
||||
display = agent.view()
|
||||
prev_status = display.get('status')
|
||||
|
||||
for repo, local_version, is_native, svc_name in to_check:
|
||||
info = check(local_version, repo, is_native)
|
||||
if info['url'] is not None:
|
||||
logging.warning(
|
||||
"update for %s available (local version is '%s'): %s" % (
|
||||
repo, info['current'], info['url']))
|
||||
info['service'] = svc_name
|
||||
to_install.append(info)
|
||||
try:
|
||||
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
||||
|
||||
num_updates = len(to_install)
|
||||
num_installed = 0
|
||||
to_install = []
|
||||
to_check = [
|
||||
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
||||
('evilsocket/pwnagotchi', pwnagotchi.__version__, False, 'pwnagotchi')
|
||||
]
|
||||
|
||||
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 '')
|
||||
for repo, local_version, is_native, svc_name in to_check:
|
||||
info = check(local_version, repo, is_native)
|
||||
if info['url'] is not None:
|
||||
logging.warning(
|
||||
"update for %s available (local version is '%s'): %s" % (
|
||||
repo, info['current'], info['url']))
|
||||
info['service'] = svc_name
|
||||
to_install.append(info)
|
||||
|
||||
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:
|
||||
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
||||
pwnagotchi.reboot()
|
||||
logging.info("[update] done")
|
||||
|
||||
except Exception as e:
|
||||
logging.error("[update] %s" % e)
|
||||
self.status.update()
|
||||
|
||||
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 ''})
|
||||
|
@@ -437,7 +437,7 @@ class BTTether(plugins.Plugin):
|
||||
for device_opt in ['enabled', 'priority', 'scantime', 'search_order',
|
||||
'max_tries', 'share_internet', 'mac', 'ip',
|
||||
'netmask', 'interval']:
|
||||
if device_opt not in options or (device_opt in options and options[device_opt] is None):
|
||||
if device_opt not in options or options[device_opt] is None:
|
||||
logging.error("BT-TETHER: Please specify the %s for device %s.",
|
||||
device_opt, device)
|
||||
break
|
||||
@@ -448,7 +448,7 @@ class BTTether(plugins.Plugin):
|
||||
# legacy
|
||||
if 'mac' in self.options:
|
||||
for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
|
||||
if opt not in self.options or (opt in self.options and self.options[opt] is None):
|
||||
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)
|
||||
return
|
||||
|
||||
|
@@ -44,9 +44,15 @@ class GPS(plugins.Plugin):
|
||||
self.coordinates = info["gps"]
|
||||
gps_filename = filename.replace(".pcap", ".gps.json")
|
||||
|
||||
logging.info(f"saving GPS to {gps_filename} ({self.coordinates})")
|
||||
with open(gps_filename, "w+t") as fp:
|
||||
json.dump(self.coordinates, fp)
|
||||
if self.coordinates and all([
|
||||
# avoid 0.000... measurements
|
||||
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):
|
||||
# add coordinates for other displays
|
||||
@@ -54,11 +60,14 @@ class GPS(plugins.Plugin):
|
||||
lat_pos = (127, 75)
|
||||
lon_pos = (122, 84)
|
||||
alt_pos = (127, 94)
|
||||
elif ui.is_waveshare_v1():
|
||||
lat_pos = (130, 70)
|
||||
lon_pos = (125, 80)
|
||||
alt_pos = (130, 90)
|
||||
elif ui.is_inky():
|
||||
# guessed values, add tested ones if you can
|
||||
lat_pos = (112, 30)
|
||||
lon_pos = (112, 49)
|
||||
alt_pos = (87, 63)
|
||||
lat_pos = (127, 60)
|
||||
lon_pos = (127, 70)
|
||||
alt_pos = (127, 80)
|
||||
elif ui.is_waveshare144lcd():
|
||||
# guessed values, add tested ones if you can
|
||||
lat_pos = (67, 73)
|
||||
@@ -109,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):
|
||||
if self.coordinates and all([
|
||||
# avoid 0.000... measurements
|
||||
|
@@ -7,6 +7,7 @@ import re
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile, WifiInfo, extract_from_pcap
|
||||
from threading import Lock
|
||||
|
||||
|
||||
def parse_pcap(filename):
|
||||
@@ -54,6 +55,7 @@ class Grid(plugins.Plugin):
|
||||
|
||||
self.unread_messages = 0
|
||||
self.total_messages = 0
|
||||
self.lock = Lock()
|
||||
|
||||
def is_excluded(self, what):
|
||||
for skip in self.options['exclude']:
|
||||
@@ -122,21 +124,25 @@ class Grid(plugins.Plugin):
|
||||
def on_internet_available(self, agent):
|
||||
logging.debug("internet available")
|
||||
|
||||
try:
|
||||
grid.update_data(agent.last_session)
|
||||
except Exception as e:
|
||||
logging.error("error connecting to the pwngrid-peer service: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
if self.lock.locked():
|
||||
return
|
||||
|
||||
try:
|
||||
self.check_inbox(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking inbox: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
with self.lock:
|
||||
try:
|
||||
grid.update_data(agent.last_session)
|
||||
except Exception as e:
|
||||
logging.error("error connecting to the pwngrid-peer service: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
return
|
||||
|
||||
try:
|
||||
self.check_handshakes(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking pcaps: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
try:
|
||||
self.check_inbox(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking inbox: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
|
||||
try:
|
||||
self.check_handshakes(agent)
|
||||
except Exception as e:
|
||||
logging.error("[grid] error while checking pcaps: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
|
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,12 +44,18 @@ class MemTemp(plugins.Plugin):
|
||||
if ui.is_waveshare_v2():
|
||||
h_pos = (180, 80)
|
||||
v_pos = (180, 61)
|
||||
elif ui.is_waveshare_v1():
|
||||
h_pos = (170, 80)
|
||||
v_pos = (170, 61)
|
||||
elif ui.is_waveshare144lcd():
|
||||
h_pos = (53, 77)
|
||||
v_pos = (78, 67)
|
||||
elif ui.is_inky():
|
||||
h_pos = (140, 68)
|
||||
v_pos = (165, 54)
|
||||
elif ui.is_waveshare27inch():
|
||||
h_pos = (192, 138)
|
||||
v_pos = (216, 122)
|
||||
else:
|
||||
h_pos = (155, 76)
|
||||
v_pos = (180, 61)
|
||||
@@ -64,6 +70,10 @@ class MemTemp(plugins.Plugin):
|
||||
position=h_pos,
|
||||
label_font=fonts.Small, text_font=fonts.Small))
|
||||
|
||||
def on_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element('memtemp')
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if self.options['scale'] == "fahrenheit":
|
||||
temp = (pwnagotchi.temperature() * 9 / 5) + 32
|
||||
@@ -72,7 +82,7 @@ class MemTemp(plugins.Plugin):
|
||||
temp = pwnagotchi.temperature() + 273.15
|
||||
symbol = "k"
|
||||
else:
|
||||
# default to celsius
|
||||
# default to celsius
|
||||
temp = pwnagotchi.temperature()
|
||||
symbol = "c"
|
||||
|
||||
|
@@ -7,18 +7,18 @@ import time
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
||||
|
||||
|
||||
class NetPos(plugins.Plugin):
|
||||
__author__ = 'zenzen san'
|
||||
__version__ = '2.0.2'
|
||||
__version__ = '2.0.3'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = """Saves a json file with the access points with more signal
|
||||
whenever a handshake is captured.
|
||||
When internet is available the files are converted in geo locations
|
||||
using Mozilla LocationService """
|
||||
|
||||
API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
||||
|
||||
def __init__(self):
|
||||
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
|
||||
self.skip = list()
|
||||
@@ -26,12 +26,14 @@ class NetPos(plugins.Plugin):
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
||||
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.")
|
||||
return
|
||||
|
||||
if 'api_url' in self.options:
|
||||
self.API_URL = self.options['api_url']
|
||||
self.ready = True
|
||||
logging.info("net-pos plugin loaded.")
|
||||
logging.debug(f"net-pos: use api_url: {self.API_URL}");
|
||||
|
||||
def _append_saved(self, path):
|
||||
to_save = list()
|
||||
@@ -47,6 +49,8 @@ class NetPos(plugins.Plugin):
|
||||
saved_file.write(x + "\n")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
if self.lock.locked():
|
||||
return
|
||||
with self.lock:
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
@@ -124,7 +128,7 @@ class NetPos(plugins.Plugin):
|
||||
return netpos
|
||||
|
||||
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:
|
||||
with open(path, "r") as json_file:
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
import csv
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile
|
||||
from pwnagotchi.utils import StatusFile, remove_whitelisted
|
||||
import pwnagotchi.plugins as plugins
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -18,7 +20,7 @@ class OnlineHashCrack(plugins.Plugin):
|
||||
self.ready = False
|
||||
try:
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
except JSONDecodeError as json_err:
|
||||
except JSONDecodeError:
|
||||
os.remove('/root/.ohc_uploads')
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
@@ -28,29 +30,16 @@ class OnlineHashCrack(plugins.Plugin):
|
||||
"""
|
||||
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")
|
||||
return
|
||||
|
||||
if 'whitelist' not in self.options:
|
||||
self.options['whitelist'] = []
|
||||
|
||||
# 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.options['whitelist'] = list()
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -71,44 +60,81 @@ class OnlineHashCrack(plugins.Plugin):
|
||||
logging.error(f"OHC: Got an exception while uploading {path} -> {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):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
|
||||
if not self.ready or self.lock.locked():
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
if self.ready:
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
|
||||
# pull out whitelisted APs
|
||||
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
|
||||
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status',
|
||||
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_ohc(handshake)
|
||||
if handshake not in reported:
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info(f"OHC: Successfully uploaded {handshake}")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", os_e)
|
||||
continue
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
# pull out whitelisted APs
|
||||
handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist'])
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onlinehashcrack.com")
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status',
|
||||
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_ohc(handshake)
|
||||
if handshake not in reported:
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info(f"OHC: Successfully uploaded {handshake}")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("OHC: %s", os_e)
|
||||
continue
|
||||
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'])
|
||||
|
@@ -18,7 +18,7 @@ class PawGPS(plugins.Plugin):
|
||||
def on_loaded(self):
|
||||
logging.info("PAW-GPS loaded")
|
||||
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
|
||||
logging.info("PAW-GPS: No IP Address in the config file is defined, it uses the default (192.168.44.1)")
|
||||
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):
|
||||
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
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
|
||||
|
||||
@@ -14,11 +16,22 @@ TEMPLATE = """
|
||||
{% 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>
|
||||
@@ -31,94 +44,137 @@ TEMPLATE = """
|
||||
|
||||
{% block script %}
|
||||
$(document).ready(function(){
|
||||
var ajaxDataRenderer = function(url, plot, options) {
|
||||
var ret = null;
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: url,
|
||||
dataType:"json",
|
||||
success: function(data) {
|
||||
ret = data;
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
function loadData(url, elm, title) {
|
||||
var data = ajaxDataRenderer(url);
|
||||
var plot_os = $.jqplot(elm, data.values,{
|
||||
title: title,
|
||||
stackSeries: true,
|
||||
seriesDefaults: {
|
||||
showMarker: false,
|
||||
fill: true,
|
||||
fillAndStroke: true
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
renderer: $.jqplot.EnhancedLegendRenderer,
|
||||
placement: 'outsideGrid',
|
||||
labels: data.labels,
|
||||
location: 's',
|
||||
rendererOptions: {
|
||||
numberRows: '2',
|
||||
},
|
||||
rowSpacing: '0px'
|
||||
},
|
||||
axes:{
|
||||
xaxis:{
|
||||
renderer:$.jqplot.DateAxisRenderer,
|
||||
tickOptions:{formatString:'%H:%M:%S'}
|
||||
},
|
||||
yaxis:{
|
||||
min: 0,
|
||||
tickOptions:{formatString:'%.2f'}
|
||||
}
|
||||
},
|
||||
highlighter: {
|
||||
show: true,
|
||||
sizeAdjust: 7.5
|
||||
},
|
||||
cursor:{
|
||||
show: true,
|
||||
tooltipLocation:'sw'
|
||||
var ajaxDataRenderer = function(url, plot, options) {
|
||||
var ret = null;
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: url,
|
||||
dataType:"json",
|
||||
success: function(data) {
|
||||
ret = data;
|
||||
}
|
||||
}).replot({
|
||||
axes:{
|
||||
xaxis:{
|
||||
renderer:$.jqplot.DateAxisRenderer,
|
||||
tickOptions:{formatString:'%H:%M:%S'}
|
||||
},
|
||||
yaxis:{
|
||||
min: 0,
|
||||
tickOptions:{formatString:'%.2f'}
|
||||
}
|
||||
});
|
||||
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 loadAll() {
|
||||
loadData('/plugins/session-stats/os', 'chart_os', 'OS')
|
||||
loadData('/plugins/session-stats/temp', 'chart_temp', 'Temp')
|
||||
loadData('/plugins/session-stats/nums', 'chart_nums', 'Wifi')
|
||||
loadData('/plugins/session-stats/duration', 'chart_duration', 'Sleeping')
|
||||
loadData('/plugins/session-stats/epoch', 'chart_epoch', 'Epochs')
|
||||
}
|
||||
function loadSessionFiles() {
|
||||
loadFiles('/plugins/session-stats/session', 'session');
|
||||
$("#session").change(function() {
|
||||
loadSessionData();
|
||||
});
|
||||
}
|
||||
|
||||
loadAll();
|
||||
setInterval(loadAll, 60000);
|
||||
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 %}
|
||||
<div id="chart_os" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_temp" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_nums" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_duration" style="height:400px;width:100%; "></div>
|
||||
<div id="chart_epoch" style="height:400px;width:100%; "></div>
|
||||
<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'
|
||||
@@ -126,27 +182,31 @@ class SessionStats(plugins.Plugin):
|
||||
__description__ = 'This plugin displays stats of the current session.'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.lock = threading.Lock()
|
||||
self.options = dict()
|
||||
self.stats = dict()
|
||||
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.")
|
||||
self.ready = True
|
||||
|
||||
def on_unloaded(self, ui):
|
||||
pass
|
||||
|
||||
def on_epoch(self, agent, epoch, epoch_data):
|
||||
"""
|
||||
Save the epoch_data to self.stats
|
||||
"""
|
||||
with self.lock:
|
||||
self.stats[datetime.now().strftime("%H:%M:%S")] = epoch_data
|
||||
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):
|
||||
@@ -162,11 +222,13 @@ class SessionStats(plugins.Plugin):
|
||||
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 == "nums":
|
||||
elif path == "wifi":
|
||||
extract_keys = [
|
||||
'missed_interactions',
|
||||
'num_hops',
|
||||
@@ -182,14 +244,20 @@ class SessionStats(plugins.Plugin):
|
||||
'duration_secs',
|
||||
'slept_for_secs',
|
||||
]
|
||||
elif path == "reward":
|
||||
extract_keys = [
|
||||
'reward',
|
||||
]
|
||||
elif path == "epoch":
|
||||
extract_keys = [
|
||||
'blind_for_epochs',
|
||||
'inactive_for_epochs',
|
||||
'active_for_epochs',
|
||||
]
|
||||
|
||||
|
||||
elif path == "session":
|
||||
return jsonify({'files': os.listdir(self.options['save_directory'])})
|
||||
|
||||
with self.lock:
|
||||
return jsonify(SessionStats.extract_key_values(self.stats, extract_keys))
|
||||
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
|
||||
# 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
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi
|
||||
|
||||
|
||||
# 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),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
def on_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element('ups')
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
ui.set('ups', "%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 json
|
||||
import yaml
|
||||
import toml
|
||||
import _thread
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi import restart
|
||||
from pwnagotchi import restart, plugins
|
||||
from pwnagotchi.utils import save_config
|
||||
from flask import abort
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
INDEX = """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=0" />
|
||||
<title>
|
||||
webcfg
|
||||
</title>
|
||||
<style>
|
||||
#divTop {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "plugins" %}
|
||||
{% block title %}
|
||||
Webcfg
|
||||
{% endblock %}
|
||||
|
||||
#searchText {
|
||||
width: 100%;
|
||||
}
|
||||
{% block meta %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=0" />
|
||||
{% endblock %}
|
||||
|
||||
table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<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 {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#searchText {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #eee;
|
||||
}
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #fff;
|
||||
}
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
table tr:nth-child(even) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.remove {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: 2px solid #f44336;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
margin: 4px 2px;
|
||||
-webkit-transition-duration: 0.4s; /* Safari */
|
||||
transition-duration: 0.4s;
|
||||
cursor: pointer;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
table th {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#btnSave {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
background-color: #0061b0;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
.remove {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: 2px solid #f44336;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
margin: 4px 2px;
|
||||
-webkit-transition-duration: 0.4s; /* Safari */
|
||||
transition-duration: 0.4s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#divTop {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
#divTop > * {
|
||||
display: table-cell;
|
||||
}
|
||||
#divTop > span {
|
||||
width: 1%;
|
||||
}
|
||||
#divTop > input {
|
||||
width: 100%;
|
||||
}
|
||||
.remove:hover {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media screen and (max-width:700px) {
|
||||
table, tr, td {
|
||||
padding:0;
|
||||
border:1px solid black;
|
||||
}
|
||||
#btnSave {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
background-color: #0061b0;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
|
||||
table {
|
||||
border:none;
|
||||
}
|
||||
#divTop {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
#divTop > * {
|
||||
display: table-cell;
|
||||
}
|
||||
#divTop > span {
|
||||
width: 1%;
|
||||
}
|
||||
#divTop > input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tr:first-child, thead, th {
|
||||
display:none;
|
||||
border:none;
|
||||
}
|
||||
@media screen and (max-width:700px) {
|
||||
table, tr, td {
|
||||
padding:0;
|
||||
border:1px solid black;
|
||||
}
|
||||
|
||||
tr {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
table {
|
||||
border:none;
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #eee;
|
||||
}
|
||||
tr:first-child, thead, th {
|
||||
display:none;
|
||||
border:none;
|
||||
}
|
||||
|
||||
td {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding:1em;
|
||||
}
|
||||
tr {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
td::before {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
background: #eee;
|
||||
border-right:2px solid black;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.del_btn_wrapper {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
background: #eee;
|
||||
border-right:2px solid black;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="divTop">
|
||||
<input type="text" id="searchText" onkeyup="filterTable()" placeholder="Search for options ..." title="Type an option name">
|
||||
<span><select id="selAddType"><option value="text">Text</option><option value="number">Number</option></select></span>
|
||||
<span><button id="btnAdd" type="button" onclick="addOption()">+</button></span>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
<button id="btnSave" type="button" onclick="saveConfig()">Save</button>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
td {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding:1em;
|
||||
}
|
||||
|
||||
td::before {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
background: #eee;
|
||||
border-right:2px solid black;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
|
||||
.del_btn_wrapper {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
background: #eee;
|
||||
border-right:2px solid black;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% 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() {
|
||||
var input, table, tr, td, divDelBtn, btnDel, selType, selTypeVal;
|
||||
input = document.getElementById("searchText");
|
||||
@@ -232,11 +240,10 @@ INDEX = """
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function filterTable(){
|
||||
var input, filter, table, tr, td, i, txtValue;
|
||||
input = document.getElementById("searchText");
|
||||
filter = input.value.toUpperCase();
|
||||
var searchInput = document.getElementById("searchText");
|
||||
searchInput.onkeyup = function() {
|
||||
var filter, table, tr, td, i, txtValue;
|
||||
filter = searchInput.value.toUpperCase();
|
||||
table = document.getElementById("tableOptions");
|
||||
if (table) {
|
||||
tr = table.getElementsByTagName("tr");
|
||||
@@ -447,8 +454,7 @@ INDEX = """
|
||||
divContent.innerHTML = "";
|
||||
divContent.appendChild(table);
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
def serializer(obj):
|
||||
@@ -464,16 +470,17 @@ class WebConfig(plugins.Plugin):
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.mode = 'MANU'
|
||||
|
||||
def on_config_changed(self, config):
|
||||
self.config = config
|
||||
self.ready = True
|
||||
|
||||
def on_ready(self, agent):
|
||||
self.config = agent.config()
|
||||
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
|
||||
self.ready = True
|
||||
self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
self.config = agent.config()
|
||||
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
|
||||
self.ready = True
|
||||
self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
@@ -500,13 +507,10 @@ class WebConfig(plugins.Plugin):
|
||||
elif request.method == "POST":
|
||||
if path == "save-config":
|
||||
try:
|
||||
parsed_yaml = yaml.safe_load(str(request.get_json()))
|
||||
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
|
||||
yaml.safe_dump(parsed_yaml, config_file, encoding='utf-8',
|
||||
allow_unicode=True, default_flow_style=False)
|
||||
|
||||
save_config(request.get_json(), '/etc/pwnagotchi/config.toml') # test
|
||||
_thread.start_new_thread(restart, (self.mode,))
|
||||
return "success"
|
||||
except yaml.YAMLError as yaml_ex:
|
||||
return "config error"
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
return "config error", 500
|
||||
abort(404)
|
||||
|
@@ -3,10 +3,10 @@
|
||||
<meta http-equiv="Content-Type" content="text/xml; charset=utf-8" />
|
||||
<title>GPS MAP</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" />
|
||||
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" />
|
||||
<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="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='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">
|
||||
/* for map */
|
||||
html, body { height: 100%; width: 100%; margin:0; background-color:#000;}
|
||||
@@ -270,5 +270,18 @@
|
||||
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>
|
||||
</body></html>
|
||||
|
@@ -6,23 +6,25 @@ import re
|
||||
import datetime
|
||||
from flask import Response
|
||||
from functools import lru_cache
|
||||
from dateutil.parser import parse
|
||||
|
||||
'''
|
||||
2do:
|
||||
- make+test the cache handling multiple clients
|
||||
- cleanup the javascript in a class and handle "/newest" additions
|
||||
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
|
||||
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
|
||||
https://gis.stackexchange.com/questions/312737/filtering-interactive-leaflet-map-with-dropdown-menu
|
||||
https://blogs.kent.ac.uk/websolutions/2015/01/29/filtering-map-markers-with-leaflet-js-a-brief-technical-overview/
|
||||
http://www.digital-geography.com/filter-leaflet-maps-slider/
|
||||
http://bl.ocks.org/zross/47760925fcb1643b4225
|
||||
-
|
||||
webgpsmap shows existing position data stored in your /handshakes/ directory
|
||||
|
||||
the plugin does the following:
|
||||
- search for *.pcap files in your /handshakes/ dir
|
||||
- for every found .pcap file it looks for a .geo.json or .gps.json or .paw-gps.json file with
|
||||
latitude+longitude data inside and shows this position on the map
|
||||
- if also an .cracked file with a plaintext password inside exist, it reads the content and shows the
|
||||
position as green instead of red and the password inside the infopox of the position
|
||||
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):
|
||||
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
|
||||
__version__ = '1.3.0'
|
||||
__version__ = '1.4.0'
|
||||
__name__ = 'webgpsmap'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
|
||||
@@ -33,8 +35,8 @@ class Webgpsmap(plugins.Plugin):
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
|
||||
def on_ready(self, agent):
|
||||
self.config = agent.config()
|
||||
def on_config_changed(self, config):
|
||||
self.config = config
|
||||
self.ready = True
|
||||
|
||||
def on_loaded(self):
|
||||
@@ -49,6 +51,7 @@ class Webgpsmap(plugins.Plugin):
|
||||
"""
|
||||
# defaults:
|
||||
response_header_contenttype = None
|
||||
response_header_contentdisposition = None
|
||||
response_mimetype = "application/xhtml+xml"
|
||||
if not self.ready:
|
||||
try:
|
||||
@@ -63,7 +66,7 @@ class Webgpsmap(plugins.Plugin):
|
||||
response_mimetype = "application/xhtml+xml"
|
||||
response_header_contenttype = 'text/html'
|
||||
except Exception as error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
logging.error(f"[webgpsmap] on_webhook NOT_READY error: {error}")
|
||||
return
|
||||
else:
|
||||
if request.method == "GET":
|
||||
@@ -73,7 +76,7 @@ class Webgpsmap(plugins.Plugin):
|
||||
try:
|
||||
response_data = bytes(self.get_html(), "utf-8")
|
||||
except Exception as error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
logging.error(f"[webgpsmap] on_webhook / error: {error}")
|
||||
return
|
||||
response_status = 200
|
||||
response_mimetype = "application/xhtml+xml"
|
||||
@@ -87,7 +90,22 @@ class Webgpsmap(plugins.Plugin):
|
||||
response_mimetype = "application/json"
|
||||
response_header_contenttype = 'application/json'
|
||||
except Exception as error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
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
|
||||
# elif path.startswith('/newest'):
|
||||
# # returns all positions newer then timestamp
|
||||
@@ -119,9 +137,11 @@ class Webgpsmap(plugins.Plugin):
|
||||
r = Response(response=response_data, status=response_status, mimetype=response_mimetype)
|
||||
if response_header_contenttype is not None:
|
||||
r.headers["Content-Type"] = response_header_contenttype
|
||||
if response_header_contentdisposition is not None:
|
||||
r.headers["Content-Disposition"] = response_header_contentdisposition
|
||||
return r
|
||||
except Exception as error:
|
||||
logging.error(f"[webgpsmap] error: {error}")
|
||||
logging.error(f"[webgpsmap] on_webhook CREATING_RESPONSE error: {error}")
|
||||
return
|
||||
|
||||
# cache 2048 items
|
||||
@@ -217,15 +237,15 @@ class Webgpsmap(plugins.Plugin):
|
||||
self.ALREADY_SENT += pos_file
|
||||
except json.JSONDecodeError as error:
|
||||
self.SKIP += pos_file
|
||||
logging.error(f"[webgpsmap] JSONDecodeError in: {error}")
|
||||
logging.error(f"[webgpsmap] JSONDecodeError in: {pos_file} - error: {error}")
|
||||
continue
|
||||
except ValueError as error:
|
||||
self.SKIP += pos_file
|
||||
logging.error(f"[webgpsmap] ValueError: {error}")
|
||||
logging.error(f"[webgpsmap] ValueError: {pos_file} - error: {error}")
|
||||
continue
|
||||
except OSError as error:
|
||||
self.SKIP += pos_file
|
||||
logging.error(f"[webgpsmap] OSError: {error}")
|
||||
logging.error(f"[webgpsmap] OSError: {pos_file} - error: {error}")
|
||||
continue
|
||||
logging.info(f"[webgpsmap] loaded {len(gps_data)} positions")
|
||||
return gps_data
|
||||
@@ -303,15 +323,7 @@ class PositionFile:
|
||||
return_ts = self._json['ts']
|
||||
elif 'Updated' in self._json:
|
||||
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
|
||||
date_iso_formated = self._json['Updated']
|
||||
# bad microseconds fix: fill/cut microseconds to 6 numbers
|
||||
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
|
||||
part2 = part2.ljust(6, '0')[:6]
|
||||
# timezone fix: 0200 >>> 02:00
|
||||
if len(part3) == 4:
|
||||
part3 = part3[1:2].rjust(2, '0') + ':' + part3[3:4].rjust(2, '0')
|
||||
date_iso_formated = part1 + "." + part2 + "+" + part3
|
||||
dateObj = datetime.datetime.fromisoformat(date_iso_formated)
|
||||
dateObj = parse(self._json['Updated'])
|
||||
return_ts = int("%.0f" % dateObj.timestamp())
|
||||
else:
|
||||
# use file timestamp last modification of the json file
|
||||
@@ -332,9 +344,9 @@ class PositionFile:
|
||||
return_pass = password_file.read()
|
||||
password_file.close()
|
||||
except OSError as error:
|
||||
logging.error(f"[webgpsmap] OS error: {format(error)}")
|
||||
logging.error(f"[webgpsmap] OS error loading password: {password_file_path} - error: {format(error)}")
|
||||
except:
|
||||
logging.error(f"[webgpsmap] Unexpected error: {sys.exc_info()[0]}")
|
||||
logging.error(f"[webgpsmap] Unexpected error loading password: {password_file_path} - error: {sys.exc_info()[0]}")
|
||||
raise
|
||||
return return_pass
|
||||
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
from io import StringIO
|
||||
import csv
|
||||
from datetime import datetime
|
||||
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):
|
||||
@@ -100,90 +102,90 @@ class Wigle(plugins.Plugin):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.wigle_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
|
||||
logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
|
||||
return
|
||||
|
||||
if not 'whitelist' in self.options:
|
||||
self.options['whitelist'] = list()
|
||||
|
||||
self.ready = True
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
from scapy.all import Scapy_Exception
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
if not self.ready or self.lock.locked():
|
||||
return
|
||||
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
all_files = os.listdir(handshake_dir)
|
||||
all_gps_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.gps.json')]
|
||||
new_gps_files = set(all_gps_files) - set(reported) - set(self.skip)
|
||||
from scapy.all import Scapy_Exception
|
||||
|
||||
if new_gps_files:
|
||||
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
||||
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_gps_files = [os.path.join(handshake_dir, filename)
|
||||
for filename in all_files
|
||||
if filename.endswith('.gps.json')]
|
||||
|
||||
csv_entries = list()
|
||||
no_err_entries = list()
|
||||
|
||||
for gps_file in new_gps_files:
|
||||
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
||||
|
||||
if not os.path.exists(pcap_filename):
|
||||
logging.error("WIGLE: Can't find pcap for %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
try:
|
||||
gps_data = _extract_gps_data(gps_file)
|
||||
except OSError as os_err:
|
||||
logging.error("WIGLE: %s", os_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except json.JSONDecodeError as json_err:
|
||||
logging.error("WIGLE: %s", json_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
|
||||
logging.warning("WIGLE: Not enough gps-information for %s. Trying again next time.", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
try:
|
||||
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
|
||||
WifiInfo.ESSID,
|
||||
WifiInfo.ENCRYPTION,
|
||||
WifiInfo.CHANNEL,
|
||||
WifiInfo.RSSI])
|
||||
except FieldNotFoundError:
|
||||
logging.error("WIGLE: Could not extract all information. Skip %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except Scapy_Exception as sc_e:
|
||||
logging.error("WIGLE: %s", sc_e)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
|
||||
new_entry = _transform_wigle_entry(gps_data, pcap_data)
|
||||
csv_entries.append(new_entry)
|
||||
no_err_entries.append(gps_file)
|
||||
|
||||
if csv_entries:
|
||||
display.set('status', "Uploading gps-data to wigle.net ...")
|
||||
display.update(force=True)
|
||||
try:
|
||||
_send_to_wigle(csv_entries, self.options['api_key'])
|
||||
reported += no_err_entries
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries))
|
||||
except requests.exceptions.RequestException as re_e:
|
||||
self.skip += no_err_entries
|
||||
logging.error("WIGLE: Got an exception while uploading %s", re_e)
|
||||
except OSError as os_e:
|
||||
self.skip += no_err_entries
|
||||
logging.error("WIGLE: Got the following error: %s", os_e)
|
||||
all_gps_files = remove_whitelisted(all_gps_files, self.options['whitelist'])
|
||||
new_gps_files = set(all_gps_files) - set(reported) - set(self.skip)
|
||||
if new_gps_files:
|
||||
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
||||
csv_entries = list()
|
||||
no_err_entries = list()
|
||||
for gps_file in new_gps_files:
|
||||
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
||||
if not os.path.exists(pcap_filename):
|
||||
logging.error("WIGLE: Can't find pcap for %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
try:
|
||||
gps_data = _extract_gps_data(gps_file)
|
||||
except OSError as os_err:
|
||||
logging.error("WIGLE: %s", os_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except json.JSONDecodeError as json_err:
|
||||
logging.error("WIGLE: %s", json_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
|
||||
logging.warning("WIGLE: Not enough gps-information for %s. Trying again next time.", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
try:
|
||||
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
|
||||
WifiInfo.ESSID,
|
||||
WifiInfo.ENCRYPTION,
|
||||
WifiInfo.CHANNEL,
|
||||
WifiInfo.RSSI])
|
||||
except FieldNotFoundError:
|
||||
logging.error("WIGLE: Could not extract all information. Skip %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except Scapy_Exception as sc_e:
|
||||
logging.error("WIGLE: %s", sc_e)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
new_entry = _transform_wigle_entry(gps_data, pcap_data)
|
||||
csv_entries.append(new_entry)
|
||||
no_err_entries.append(gps_file)
|
||||
if csv_entries:
|
||||
display.set('status', "Uploading gps-data to wigle.net ...")
|
||||
display.update(force=True)
|
||||
try:
|
||||
_send_to_wigle(csv_entries, self.options['api_key'])
|
||||
reported += no_err_entries
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WIGLE: Successfully uploaded %d files", len(no_err_entries))
|
||||
except requests.exceptions.RequestException as re_e:
|
||||
self.skip += no_err_entries
|
||||
logging.error("WIGLE: Got an exception while uploading %s", re_e)
|
||||
except OSError as os_e:
|
||||
self.skip += no_err_entries
|
||||
logging.error("WIGLE: Got the following error: %s", os_e)
|
||||
|
@@ -3,7 +3,7 @@ import logging
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile
|
||||
from pwnagotchi.utils import StatusFile, remove_whitelisted
|
||||
from pwnagotchi import plugins
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -19,7 +19,7 @@ class WpaSec(plugins.Plugin):
|
||||
self.lock = Lock()
|
||||
try:
|
||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
except JSONDecodeError as json_err:
|
||||
except JSONDecodeError:
|
||||
os.remove("/root/.wpa_sec_uploads")
|
||||
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
self.options = dict()
|
||||
@@ -70,62 +70,65 @@ class WpaSec(plugins.Plugin):
|
||||
"""
|
||||
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")
|
||||
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.")
|
||||
return
|
||||
|
||||
if 'whitelist' not in self.options:
|
||||
self.options['whitelist'] = list()
|
||||
|
||||
self.ready = True
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
if not self.ready or self.lock.locked():
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
if self.ready:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._upload_to_wpasec(handshake)
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.error("WPA_SEC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("WPA_SEC: %s", os_e)
|
||||
continue
|
||||
|
||||
if 'download_results' in self.options and self.options['download_results']:
|
||||
cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
|
||||
if os.path.exists(cracked_file):
|
||||
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
|
||||
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
|
||||
return
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = self.report.data_field_or('reported', default=list())
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
|
||||
filename.endswith('.pcap')]
|
||||
handshake_paths = remove_whitelisted(handshake_paths, self.options['whitelist'])
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile'))
|
||||
logging.info("WPA_SEC: Downloaded cracked passwords.")
|
||||
self._upload_to_wpasec(handshake)
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
logging.debug("WPA_SEC: %s", req_e)
|
||||
self.skip.append(handshake)
|
||||
logging.error("WPA_SEC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.debug("WPA_SEC: %s", os_e)
|
||||
logging.error("WPA_SEC: %s", os_e)
|
||||
continue
|
||||
|
||||
if 'download_results' in self.options and self.options['download_results']:
|
||||
cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
|
||||
if os.path.exists(cracked_file):
|
||||
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
|
||||
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
|
||||
return
|
||||
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)
|
||||
|
@@ -61,6 +61,9 @@ class Display(View):
|
||||
def is_waveshare213d(self):
|
||||
return self._implementation.name == 'waveshare213d'
|
||||
|
||||
def is_waveshare213bc(self):
|
||||
return self._implementation.name == 'waveshare213bc'
|
||||
|
||||
def is_spotpear24inch(self):
|
||||
return self._implementation.name == 'spotpear24inch'
|
||||
|
||||
|
@@ -1,18 +1,38 @@
|
||||
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)
|
||||
BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, 8)
|
||||
BoldBig = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
|
||||
Medium = ImageFont.truetype("%s.ttf" % PATH, 10)
|
||||
Small = ImageFont.truetype("%s.ttf" % PATH, 9)
|
||||
Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
|
||||
# can be changed
|
||||
STATUS_FONT_NAME = None
|
||||
SIZE_OFFSET = 0
|
||||
|
||||
Bold = None
|
||||
BoldSmall = None
|
||||
BoldBig = None
|
||||
Medium = None
|
||||
Small = None
|
||||
Huge = None
|
||||
|
||||
|
||||
def setup(bold, bold_small, medium, huge):
|
||||
global PATH, Bold, BoldSmall, Medium, Huge
|
||||
Bold = ImageFont.truetype("%s-Bold.ttf" % PATH, bold)
|
||||
BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, bold_small)
|
||||
Medium = ImageFont.truetype("%s.ttf" % PATH, medium)
|
||||
Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, huge)
|
||||
def init(config):
|
||||
global STATUS_FONT_NAME, SIZE_OFFSET
|
||||
STATUS_FONT_NAME = config['ui']['font']['name']
|
||||
SIZE_OFFSET = config['ui']['font']['size_offset']
|
||||
setup(10, 8, 10, 25, 25, 9)
|
||||
|
||||
|
||||
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)
|
||||
|
@@ -10,6 +10,7 @@ from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
|
||||
from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd
|
||||
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
|
||||
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
|
||||
from pwnagotchi.ui.hw.waveshare213bc import Waveshare213bc
|
||||
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
|
||||
|
||||
def display_for(config):
|
||||
@@ -37,10 +38,10 @@ def display_for(config):
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare27inch':
|
||||
return Waveshare27inch(config)
|
||||
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare29inch':
|
||||
return Waveshare29inch(config)
|
||||
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare144lcd':
|
||||
return Waveshare144lcd(config)
|
||||
|
||||
@@ -50,5 +51,8 @@ def display_for(config):
|
||||
elif config['ui']['display']['type'] == 'waveshare213d':
|
||||
return Waveshare213d(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare213bc':
|
||||
return Waveshare213bc(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'spotpear24inch':
|
||||
return Spotpear24inch(config)
|
||||
return Spotpear24inch(config)
|
||||
|
@@ -22,7 +22,7 @@ class DisplayImpl(object):
|
||||
# status is special :D
|
||||
'status': {
|
||||
'pos': (0, 0),
|
||||
'font': fonts.Medium,
|
||||
'font': fonts.status_font(fonts.Medium),
|
||||
'max': 20
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ class DFRobot(DisplayImpl):
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 9, 10, 35)
|
||||
fonts.setup(10, 9, 10, 35, 25, 9)
|
||||
self._layout['width'] = 250
|
||||
self._layout['height'] = 122
|
||||
self._layout['face'] = (0, 40)
|
||||
@@ -25,7 +25,7 @@ class DFRobot(DisplayImpl):
|
||||
self._layout['mode'] = (225, 109)
|
||||
self._layout['status'] = {
|
||||
'pos': (125, 20),
|
||||
'font': fonts.Medium,
|
||||
'font': fonts.status_font(fonts.Medium),
|
||||
'max': 20
|
||||
}
|
||||
return self._layout
|
||||
@@ -40,4 +40,4 @@ class DFRobot(DisplayImpl):
|
||||
self._display.display(buf)
|
||||
|
||||
def clear(self):
|
||||
self._display.Clear(0xFF)
|
||||
self._display.Clear(0xFF)
|
||||
|
@@ -10,7 +10,7 @@ class Inky(DisplayImpl):
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 8, 10, 28)
|
||||
fonts.setup(10, 8, 10, 28, 25, 9)
|
||||
self._layout['width'] = 212
|
||||
self._layout['height'] = 104
|
||||
self._layout['face'] = (0, 37)
|
||||
@@ -26,7 +26,7 @@ class Inky(DisplayImpl):
|
||||
self._layout['mode'] = (187, 93)
|
||||
self._layout['status'] = {
|
||||
'pos': (102, 18),
|
||||
'font': fonts.Small,
|
||||
'font': fonts.status_font(fonts.Small),
|
||||
'max': 20
|
||||
}
|
||||
return self._layout
|
||||
|
@@ -10,7 +10,7 @@ class LcdHat(DisplayImpl):
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 9, 10, 35)
|
||||
fonts.setup(10, 9, 10, 35, 25, 9)
|
||||
self._layout['width'] = 240
|
||||
self._layout['height'] = 240
|
||||
self._layout['face'] = (0, 40)
|
||||
@@ -26,7 +26,7 @@ class LcdHat(DisplayImpl):
|
||||
self._layout['mode'] = (215, 109)
|
||||
self._layout['status'] = {
|
||||
'pos': (125, 20),
|
||||
'font': fonts.Medium,
|
||||
'font': fonts.status_font(fonts.Medium),
|
||||
'max': 20
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ try:
|
||||
from .gpio import GPIO
|
||||
except:
|
||||
print("unknown platform")
|
||||
exit()
|
||||
sys.exit()
|
||||
|
||||
CONFIG_IL0376F = {
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user