mirror of
https://github.com/jayofelony/pwnagotchi.git
synced 2025-07-01 18:37:27 -04:00
Compare commits
136 Commits
Author | SHA1 | Date | |
---|---|---|---|
c3df982870 | |||
6f7b07f21f | |||
4c583cf184 | |||
9a8616df51 | |||
28c2ef294c | |||
255ca31837 | |||
661687e180 | |||
fb8ee9dbee | |||
d98be99c6b | |||
08e9656bb5 | |||
dbcc488900 | |||
94b01b2fc7 | |||
9b4b239deb | |||
4fc029d638 | |||
9d0ada2a33 | |||
da156fde4b | |||
edaadfdbd4 | |||
0341ac0202 | |||
60832e788d | |||
87a51d1de7 | |||
db85c68e63 | |||
c98745cd1c | |||
d91fe8e17e | |||
8da625e7e2 | |||
6bab0b36d1 | |||
19de2a2d8f | |||
5d4c2e3b3a | |||
e7eba208bd | |||
b47371cfe6 | |||
56b70fcc83 | |||
d698c35fef | |||
1f5785f823 | |||
d78220b9ba | |||
faca5b2904 | |||
234b38e4e4 | |||
27b9ec49da | |||
87730543d1 | |||
67e28fa7ab | |||
7285863c00 | |||
23eae9c65a | |||
89a85bed78 | |||
393981e0ba | |||
31e542f45a | |||
c433a6c2d5 | |||
7dd809afe0 | |||
d7f7dac0d7 | |||
2dc45bc4b4 | |||
29d1ca6728 | |||
1d635d955b | |||
f20d4017bd | |||
5d668ae34e | |||
9d0df49fb6 | |||
e1f22cd6a0 | |||
f4a7588a48 | |||
c66728f03e | |||
20ddc9425c | |||
e1bcea681a | |||
43d58b77c9 | |||
40918029e7 | |||
54986ef831 | |||
1400e8aac8 | |||
5a398c70bb | |||
81d377f491 | |||
1135ec3df1 | |||
d3231a11ce | |||
a00d94b520 | |||
beb8308969 | |||
2893632ee5 | |||
0b3b38bb44 | |||
d4874a18e1 | |||
5a113a2163 | |||
e82621c80b | |||
1337494e74 | |||
675e275f34 | |||
7a0301b57f | |||
4774a983f9 | |||
83b9077e09 | |||
ef0680a140 | |||
a54579cf3d | |||
c08f33c2c5 | |||
fe7e7ec31a | |||
10d387afcf | |||
4748b671bc | |||
17f04f7506 | |||
76c4888e88 | |||
d44b8d02b6 | |||
02454597b2 | |||
cb207d4d70 | |||
7b9150af6b | |||
8787c1bdd3 | |||
ec93838b7a | |||
f6d5a481bb | |||
eee3eb962b | |||
847e9f5908 | |||
af1544535c | |||
0fbf209881 | |||
cf14f3f663 | |||
948fe89ce6 | |||
89af46c6fc | |||
8d1a5babe8 | |||
042d5ba765 | |||
58857058a4 | |||
5e6443ae58 | |||
0e06d3bd76 | |||
64f7c6e1e5 | |||
8442ce93be | |||
730fa7dc8e | |||
0959140098 | |||
7c4764bff8 | |||
e179165850 | |||
0140a1fc97 | |||
cb0c6c6e44 | |||
0ceeb94111 | |||
6bf50a36dd | |||
fb0fda4d0d | |||
51f86d0286 | |||
614e844a51 | |||
fd16b2c94d | |||
a4d5930477 | |||
f9b2a76665 | |||
80279ca02b | |||
c7a06a4e43 | |||
9b969375b7 | |||
61eff05227 | |||
21151dd9ef | |||
cd3eb0963b | |||
a41a8a277f | |||
4b7a3bc138 | |||
6fa6ca8a67 | |||
6b5631373a | |||
de5cd8705f | |||
a317528aeb | |||
1ec6931740 | |||
45138c7fae | |||
dcd91268a1 | |||
27c8a113de |
14
.github/ISSUE_TEMPLATE.yml
vendored
14
.github/ISSUE_TEMPLATE.yml
vendored
@ -33,12 +33,20 @@ body:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- 2.9.3
|
||||
- 2.9.3-2
|
||||
- 2.9.4
|
||||
- 2.9.5.2
|
||||
- 2.9.5.3
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: 3rd-party-hardware
|
||||
attributes:
|
||||
label: 3rd Party Hardware
|
||||
description: Are you using any 3rd party hardware? By selecting "Yes", you agree that you have tested the issue without the 3rd party hardware. And acknowledge that the issue may be related to the 3rd party hardware, for which we cannot provide full support.
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
default: 1
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
|
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -33,12 +33,20 @@ body:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- 2.9.3
|
||||
- 2.9.3-2
|
||||
- 2.9.4
|
||||
- 2.9.5.2
|
||||
- 2.9.5.3
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: 3rd-party-hardware
|
||||
attributes:
|
||||
label: 3rd Party Hardware
|
||||
description: Are you using any 3rd party hardware? By selecting "Yes", you agree that you have tested the issue without the 3rd party hardware. And acknowledge that the issue may be related to the 3rd party hardware, for which we cannot provide full support.
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
default: 1
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
@ -49,7 +57,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/jayofelony/pwnagotchi/blob/noai/CODE_OF_CONDUCT.md).
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/jayofelony/pwnagotchi/blob/noai/CODE_OF_CONDUCT.md).
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
*.pyc
|
||||
*.pyc
|
||||
.vscode
|
||||
|
@ -39,4 +39,4 @@ https://pwnagotchi.org
|
||||
|
||||
## License
|
||||
|
||||
`pwnagotchi` created by [@evilsocket](https://twitter.com/evilsocket) and updated by [us](https://github.com/jayofelony/pwnagotchi/graphs/contributors). It is released under the GPL3 license.
|
||||
`pwnagotchi` created by [@evilsocket](https://x.com/evilsocket) and updated by [us](https://github.com/jayofelony/pwnagotchi/graphs/contributors). It is released under the GPL3 license.
|
||||
|
@ -1 +1 @@
|
||||
__version__ = '2.9.4-2'
|
||||
__version__ = '2.9.5.4'
|
||||
|
@ -3,7 +3,7 @@ import argparse
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
import toml
|
||||
import tomlkit
|
||||
import requests
|
||||
import os
|
||||
import re
|
||||
@ -14,7 +14,7 @@ from pwnagotchi.google import cmd as google_cmd
|
||||
from pwnagotchi.plugins import cmd as plugins_cmd
|
||||
from pwnagotchi import log
|
||||
from pwnagotchi import fs
|
||||
from pwnagotchi.utils import DottedTomlEncoder, parse_version as version_to_tuple
|
||||
from pwnagotchi.utils import parse_version as version_to_tuple
|
||||
|
||||
|
||||
def pwnagotchi_cli():
|
||||
@ -187,12 +187,14 @@ def pwnagotchi_cli():
|
||||
if pwn_name == "":
|
||||
pwn_name = "Pwnagotchi"
|
||||
print("I shall go by Pwnagotchi from now on!")
|
||||
pwn_name = f"main.name = \"{pwn_name}\"\n"
|
||||
pwn_name = (f"[main]\n"
|
||||
f"name = \"{pwn_name}\"\n")
|
||||
f.write(pwn_name)
|
||||
else:
|
||||
if is_valid_hostname(pwn_name):
|
||||
print(f"I shall go by {pwn_name} from now on!")
|
||||
pwn_name = f"main.name = \"{pwn_name}\"\n"
|
||||
pwn_name = (f"[main]\n"
|
||||
f"name = \"{pwn_name}\"\n")
|
||||
f.write(pwn_name)
|
||||
else:
|
||||
print("You have chosen an invalid name. Please start over.")
|
||||
@ -203,53 +205,57 @@ def pwnagotchi_cli():
|
||||
"Be sure to use digits as your answer.\n\n"
|
||||
"Amount of networks: ")
|
||||
if int(pwn_whitelist) > 0:
|
||||
f.write("main.whitelist = [\n")
|
||||
f.write("whitelist = [\n")
|
||||
for x in range(int(pwn_whitelist)):
|
||||
ssid = input("SSID (Name): ")
|
||||
bssid = input("BSSID (MAC): ")
|
||||
f.write(f"\t\"{ssid}\",\n")
|
||||
f.write(f"\t\"{bssid}\",\n")
|
||||
if bssid != "":
|
||||
f.write(f"\t\"{bssid}\",\n")
|
||||
f.write("]\n")
|
||||
# set bluetooth tether
|
||||
pwn_bluetooth = input("Do you want to enable BT-Tether?\n\n"
|
||||
"[Y/N] ")
|
||||
if pwn_bluetooth.lower() in ('y', 'yes'):
|
||||
f.write("main.plugins.bt-tether.enabled = true\n\n")
|
||||
f.write("[main.plugins.bt-tether]\n"
|
||||
"enabled = true\n\n")
|
||||
pwn_bluetooth_phone_name = input("What name uses your phone, check settings?\n\n")
|
||||
if pwn_bluetooth_phone_name != "":
|
||||
f.write(f"main.plugins.bt-tether.phone-name = \"{pwn_bluetooth_phone_name}\"\n")
|
||||
f.write(f"phone-name = \"{pwn_bluetooth_phone_name}\"\n")
|
||||
pwn_bluetooth_device = input("What device do you use? android or ios?\n\n"
|
||||
"Device: ")
|
||||
if pwn_bluetooth_device != "":
|
||||
if pwn_bluetooth_device != "android" and pwn_bluetooth_device != "ios":
|
||||
print("You have chosen an invalid device. Please start over.")
|
||||
exit()
|
||||
f.write(f"main.plugins.bt-tether.phone = \"{pwn_bluetooth_device.lower()}\"\n")
|
||||
f.write(f"phone = \"{pwn_bluetooth_device.lower()}\"\n")
|
||||
if pwn_bluetooth_device == "android":
|
||||
f.write("main.plugins.bt-tether.ip = \"192.168.44.44\"\n")
|
||||
f.write("ip = \"192.168.44.44\"\n")
|
||||
elif pwn_bluetooth_device == "ios":
|
||||
f.write("main.plugins.bt-tether.ip = \"172.20.10.6\"\n")
|
||||
f.write("ip = \"172.20.10.6\"\n")
|
||||
pwn_bluetooth_mac = input("What is the bluetooth MAC of your device?\n\n"
|
||||
"MAC: ")
|
||||
if pwn_bluetooth_mac != "":
|
||||
f.write(f"main.plugins.bt-tether.mac = \"{pwn_bluetooth_mac}\"\n")
|
||||
f.write(f"mac = \"{pwn_bluetooth_mac}\"\n")
|
||||
# set up display settings
|
||||
pwn_display_enabled = input("Do you want to enable a display?\n\n"
|
||||
"[Y/N]: ")
|
||||
if pwn_display_enabled.lower() in ('y', 'yes'):
|
||||
f.write("ui.display.enabled = true\n")
|
||||
f.write("[ui.display]\n"
|
||||
"enabled = true\n")
|
||||
pwn_display_type = input("What display do you use?\n\n"
|
||||
"Be sure to check for the correct display type @ \n"
|
||||
"https://github.com/jayofelony/pwnagotchi/blob/master/pwnagotchi/utils.py#L240-L501\n\n"
|
||||
"Display type: ")
|
||||
if pwn_display_type != "":
|
||||
f.write(f"ui.display.type = \"{pwn_display_type}\"\n")
|
||||
f.write(f"type = \"{pwn_display_type}\"\n")
|
||||
pwn_display_invert = input("Do you want to invert the display colors?\n"
|
||||
"N = Black background\n"
|
||||
"Y = White background\n\n"
|
||||
"[Y/N]: ")
|
||||
if pwn_display_invert.lower() in ('y', 'yes'):
|
||||
f.write("ui.invert = true\n")
|
||||
f.write("[ui]\n"
|
||||
"invert = true\n")
|
||||
f.close()
|
||||
if pwn_bluetooth.lower() in ('y', 'yes'):
|
||||
if pwn_bluetooth_device.lower == "android":
|
||||
@ -299,7 +305,7 @@ def pwnagotchi_cli():
|
||||
config = utils.load_config(args)
|
||||
|
||||
if args.print_config:
|
||||
print(toml.dumps(config, encoder=DottedTomlEncoder()))
|
||||
print(tomlkit.dumps(config))
|
||||
sys.exit(0)
|
||||
|
||||
from pwnagotchi.identity import KeyPair
|
||||
|
@ -1,181 +1,257 @@
|
||||
main.name = "pwnagotchi"
|
||||
main.lang = "en"
|
||||
main.whitelist = [
|
||||
[main]
|
||||
name = "pwnagotchi"
|
||||
lang = "en"
|
||||
iface = "wlan0mon"
|
||||
mon_start_cmd = "/usr/bin/monstart"
|
||||
mon_stop_cmd = "/usr/bin/monstop"
|
||||
mon_max_blind_epochs = 5
|
||||
no_restart = false
|
||||
whitelist = [
|
||||
"EXAMPLE_NETWORK",
|
||||
"ANOTHER_EXAMPLE_NETWORK",
|
||||
"fo:od:ba:be:fo:od",
|
||||
"fo:od:ba"
|
||||
]
|
||||
main.confd = "/etc/pwnagotchi/conf.d/"
|
||||
main.custom_plugin_repos = [
|
||||
confd = "/etc/pwnagotchi/conf.d/"
|
||||
custom_plugin_repos = [
|
||||
"https://github.com/jayofelony/pwnagotchi-torch-plugins/archive/master.zip",
|
||||
"https://github.com/Sniffleupagus/pwnagotchi_plugins/archive/master.zip",
|
||||
"https://github.com/NeonLightning/pwny/archive/master.zip",
|
||||
"https://github.com/marbasec/UPSLite_Plugin_1_3/archive/master.zip",
|
||||
"https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip"
|
||||
"https://github.com/wpa-2/Pwnagotchi-Plugins/archive/master.zip",
|
||||
"https://github.com/cyberartemio/wardriver-pwnagotchi-plugin/archive/main.zip"
|
||||
]
|
||||
custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/"
|
||||
|
||||
main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/"
|
||||
[main.plugins.auto-tune]
|
||||
enabled = true
|
||||
|
||||
main.plugins.auto-tune.enabled = true
|
||||
[main.plugins.auto_backup]
|
||||
enabled = false
|
||||
interval = "daily" # or "hourly", or a number (minutes)
|
||||
max_tries = 0
|
||||
backup_location = "/home/pi/"
|
||||
files = [
|
||||
"/root/settings.yaml",
|
||||
"/root/client_secrets.json",
|
||||
"/root/.api-report.json",
|
||||
"/root/.ssh",
|
||||
"/root/.bashrc",
|
||||
"/root/.profile",
|
||||
"/home/pi/handshakes",
|
||||
"/root/peers",
|
||||
"/etc/pwnagotchi/",
|
||||
"/usr/local/share/pwnagotchi/custom-plugins",
|
||||
"/etc/ssh/",
|
||||
"/home/pi/.bashrc",
|
||||
"/home/pi/.profile",
|
||||
"/home/pi/.wpa_sec_uploads"
|
||||
]
|
||||
exclude = [ "/etc/pwnagotchi/logs/*"]
|
||||
commands = [ "tar cf {backup_file} {files}"]
|
||||
|
||||
main.plugins.auto-update.enabled = false
|
||||
main.plugins.auto-update.install = false
|
||||
main.plugins.auto-update.interval = 1
|
||||
[main.plugins.auto-update]
|
||||
enabled = true
|
||||
install = true
|
||||
interval = 1
|
||||
token = "" # Create a personal access token (classic) with scope set to public_repo to use the GitHub API
|
||||
|
||||
main.plugins.bt-tether.enabled = false
|
||||
main.plugins.bt-tether.phone-name = "" # name as shown on the phone i.e. "Pwnagotchi's Phone"
|
||||
main.plugins.bt-tether.mac = ""
|
||||
main.plugins.bt-tether.phone = "" # android or ios
|
||||
main.plugins.bt-tether.ip = "" # 192.168.44.2 android / 172.20.10.2 ios
|
||||
[main.plugins.bt-tether]
|
||||
enabled = false
|
||||
phone-name = "" # name as shown on the phone i.e. "Pwnagotchi's Phone"
|
||||
mac = ""
|
||||
phone = "" # android or ios
|
||||
ip = "" # optional, default : 192.168.44.2 if android or 172.20.10.2 if ios
|
||||
dns = "8.8.8.8 1.1.1.1" # optional, default (google): "8.8.8.8 1.1.1.1". Consider using anonymous DNS like OpenNic :-)
|
||||
|
||||
main.plugins.fix_services.enabled = true
|
||||
[main.plugins.fix_services]
|
||||
enabled = true
|
||||
|
||||
main.plugins.gdrivesync.enabled = false
|
||||
main.plugins.gdrivesync.backupfiles = ['']
|
||||
main.plugins.gdrivesync.backup_folder = "PwnagotchiBackups"
|
||||
main.plugin.gdrivesync.interval = 1
|
||||
[main.plugins.cache]
|
||||
enabled = true
|
||||
|
||||
main.plugins.gpio_buttons.enabled = false
|
||||
[main.plugins.gdrivesync]
|
||||
enabled = false
|
||||
backupfiles = [""]
|
||||
backup_folder = "PwnagotchiBackups"
|
||||
interval = 1
|
||||
|
||||
main.plugins.gps.enabled = false
|
||||
main.plugins.gps.speed = 19200
|
||||
main.plugins.gps.device = "/dev/ttyUSB0" # for GPSD: "localhost:2947"
|
||||
[main.plugins.gpio_buttons]
|
||||
enabled = false
|
||||
|
||||
main.plugins.gps_listener.enabled = false
|
||||
[main.plugins.gps]
|
||||
enabled = false
|
||||
speed = 19200
|
||||
device = "/dev/ttyUSB0" # for GPSD: "localhost:2947"
|
||||
|
||||
main.plugins.grid.enabled = true
|
||||
main.plugins.grid.report = true
|
||||
[main.plugins.gps_listener]
|
||||
enabled = false
|
||||
|
||||
main.plugins.logtail.enabled = false
|
||||
main.plugins.logtail.max-lines = 10000
|
||||
[main.plugins.grid]
|
||||
enabled = true
|
||||
report = true
|
||||
|
||||
main.plugins.memtemp.enabled = false
|
||||
main.plugins.memtemp.scale = "celsius"
|
||||
main.plugins.memtemp.orientation = "horizontal"
|
||||
[main.plugins.logtail]
|
||||
enabled = false
|
||||
max-lines = 10000
|
||||
|
||||
main.plugins.onlinehashcrack.enabled = false
|
||||
main.plugins.onlinehashcrack.email = ""
|
||||
main.plugins.onlinehashcrack.dashboard = ""
|
||||
main.plugins.onlinehashcrack.single_files = false
|
||||
[main.plugins.memtemp]
|
||||
enabled = false
|
||||
scale = "celsius"
|
||||
orientation = "horizontal"
|
||||
|
||||
main.plugins.pisugarx.enabled = false
|
||||
main.plugins.pisugarx.rotation = false
|
||||
main.plugins.pisugarx.default_display = "percentage"
|
||||
[main.plugins.ohcapi]
|
||||
enabled = false
|
||||
api_key = "sk_your_api_key_here"
|
||||
receive_email = "yes"
|
||||
|
||||
main.plugins.session-stats.enabled = false
|
||||
main.plugins.session-stats.save_directory = "/var/tmp/pwnagotchi/sessions/"
|
||||
[main.plugins.pwndroid]
|
||||
enabled = false
|
||||
display = false # show coords on display
|
||||
display_altitude = false # show altitude on display
|
||||
|
||||
main.plugins.ups_hat_c.enabled = false
|
||||
main.plugins.ups_hat_c.label_on = true # show BAT label or just percentage
|
||||
main.plugins.ups_hat_c.shutdown = 5 # battery percent at which the device will turn off
|
||||
main.plugins.ups_hat_c.bat_x_coord = 140
|
||||
main.plugins.ups_hat_c.bat_y_coord = 0
|
||||
[main.plugins.pisugarx]
|
||||
enabled = false
|
||||
rotation = false
|
||||
default_display = "percentage"
|
||||
lowpower_shutdown = true
|
||||
lowpower_shutdown_level = 10 # battery percent at which the device will turn off
|
||||
max_charge_voltage_protection = false #It will limit the battery voltage to about 80% to extend battery life
|
||||
|
||||
main.plugins.ups_lite.enabled = false
|
||||
main.plugins.ups_lite.shutdown = 2
|
||||
[main.plugins.pwncrack]
|
||||
enabled = false
|
||||
key = ""
|
||||
|
||||
main.plugins.webcfg.enabled = true
|
||||
[main.plugins.session-stats]
|
||||
enabled = false
|
||||
save_directory = "/var/tmp/pwnagotchi/sessions/"
|
||||
|
||||
main.plugins.webgpsmap.enabled = false
|
||||
[main.plugins.ups_hat_c]
|
||||
enabled = false
|
||||
label_on = true # show BAT label or just percentage
|
||||
shutdown = 5 # battery percent at which the device will turn off
|
||||
bat_x_coord = 140
|
||||
bat_y_coord = 0
|
||||
|
||||
main.plugins.wigle.enabled = false
|
||||
main.plugins.wigle.api_key = ""
|
||||
main.plugins.wigle.donate = false
|
||||
[main.plugins.ups_lite]
|
||||
enabled = false
|
||||
shutdown = 2
|
||||
|
||||
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.show_pwd = false
|
||||
[main.plugins.webcfg]
|
||||
enabled = true
|
||||
|
||||
main.iface = "wlan0mon"
|
||||
main.mon_start_cmd = "/usr/bin/monstart"
|
||||
main.mon_stop_cmd = "/usr/bin/monstop"
|
||||
main.mon_max_blind_epochs = 5
|
||||
main.no_restart = false
|
||||
[main.plugins.webgpsmap]
|
||||
enabled = false
|
||||
|
||||
main.log.path = "/etc/pwnagotchi/log/pwnagotchi.log"
|
||||
main.log.path-debug = "/etc/pwnagotchi/log/pwnagotchi-debug.log"
|
||||
main.log.rotation.enabled = true
|
||||
main.log.rotation.size = "10M"
|
||||
[main.plugins.wigle]
|
||||
enabled = false
|
||||
api_key = "" # mandatory
|
||||
cvs_dir = "/tmp" # optionnal, is set, the CVS is written to this directory
|
||||
donate = false # default: off
|
||||
timeout = 30 # default: 30
|
||||
position = [7, 85] # optionnal
|
||||
|
||||
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
|
||||
personality.throttle_a = 0.4
|
||||
personality.throttle_d = 0.9
|
||||
[main.plugins.wpa-sec]
|
||||
enabled = false
|
||||
api_key = ""
|
||||
api_url = "https://wpa-sec.stanev.org"
|
||||
download_results = false
|
||||
show_pwd = false
|
||||
|
||||
ui.invert = false # false = black background, true = white background
|
||||
ui.cursor = true
|
||||
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
|
||||
[main.log]
|
||||
path = "/etc/pwnagotchi/log/pwnagotchi.log"
|
||||
path-debug = "/etc/pwnagotchi/log/pwnagotchi-debug.log"
|
||||
|
||||
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.faces.upload = "(1__0)"
|
||||
ui.faces.upload1 = "(1__1)"
|
||||
ui.faces.upload2 = "(0__1)"
|
||||
ui.faces.png = false
|
||||
ui.faces.position_x = 0
|
||||
ui.faces.position_y = 34
|
||||
[main.log.rotation]
|
||||
enabled = true
|
||||
size = "10M"
|
||||
|
||||
ui.web.enabled = true
|
||||
ui.web.address = "::" # listening on both ipv4 and ipv6 - switch to 0.0.0.0 to listen on just ipv4
|
||||
ui.web.username = "changeme"
|
||||
ui.web.password = "changeme"
|
||||
ui.web.origin = ""
|
||||
ui.web.port = 8080
|
||||
ui.web.on_frame = ""
|
||||
[personality]
|
||||
advertise = true
|
||||
deauth = true
|
||||
associate = true
|
||||
channels = []
|
||||
min_rssi = -200
|
||||
ap_ttl = 120
|
||||
sta_ttl = 300
|
||||
recon_time = 30
|
||||
max_inactive_scale = 2
|
||||
recon_inactive_multiplier = 2
|
||||
hop_recon_time = 10
|
||||
min_recon_time = 5
|
||||
max_interactions = 3
|
||||
max_misses_for_recon = 5
|
||||
excited_num_epochs = 10
|
||||
bored_num_epochs = 15
|
||||
sad_num_epochs = 25
|
||||
bond_encounters_factor = 20000
|
||||
throttle_a = 0.4
|
||||
throttle_d = 0.9
|
||||
|
||||
ui.display.enabled = false
|
||||
ui.display.rotation = 180
|
||||
ui.display.type = "waveshare_4"
|
||||
[ui]
|
||||
invert = false # false = black background, true = white background
|
||||
cursor = true
|
||||
fps = 0.0
|
||||
|
||||
bettercap.handshakes = "/home/pi/handshakes"
|
||||
bettercap.silence = [
|
||||
[ui.font]
|
||||
name = "DejaVuSansMono" # for japanese: fonts-japanese-gothic
|
||||
size_offset = 0 # will be added to the font size
|
||||
|
||||
[ui.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 = "(#__#)"
|
||||
upload = "(1__0)"
|
||||
upload1 = "(1__1)"
|
||||
upload2 = "(0__1)"
|
||||
png = false
|
||||
position_x = 0
|
||||
position_y = 34
|
||||
|
||||
[ui.web]
|
||||
enabled = true
|
||||
address = "::" # listening on both ipv4 and ipv6 - switch to 0.0.0.0 to listen on just ipv4
|
||||
auth = false
|
||||
username = "changeme" # if auth is true
|
||||
password = "changeme" # if auth is true
|
||||
origin = ""
|
||||
port = 8080
|
||||
on_frame = ""
|
||||
|
||||
[ui.display]
|
||||
enabled = false
|
||||
rotation = 180
|
||||
type = "waveshare_4"
|
||||
|
||||
[bettercap]
|
||||
handshakes = "/home/pi/handshakes"
|
||||
silence = [
|
||||
"ble.device.new",
|
||||
"ble.device.lost",
|
||||
"ble.device.disconnected",
|
||||
"ble.device.connected",
|
||||
"ble.device.service.discovered",
|
||||
"ble.device.characteristic.discovered",
|
||||
"ble.device.disconnected",
|
||||
"ble.device.connected",
|
||||
"ble.connection.timeout",
|
||||
"wifi.client.new",
|
||||
"wifi.client.lost",
|
||||
"wifi.client.probe",
|
||||
@ -184,17 +260,21 @@ bettercap.silence = [
|
||||
"mod.started"
|
||||
]
|
||||
|
||||
fs.memory.enabled = true
|
||||
fs.memory.mounts.log.enabled = true
|
||||
fs.memory.mounts.log.mount = "/etc/pwnagotchi/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]
|
||||
enabled = true
|
||||
|
||||
fs.memory.mounts.data.enabled = true
|
||||
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 = true
|
||||
fs.memory.mounts.data.rsync = true
|
||||
[fs.memory.mounts.log]
|
||||
enabled = true
|
||||
mount = "/etc/pwnagotchi/log/"
|
||||
size = "50M"
|
||||
sync = 60
|
||||
zram = true
|
||||
rsync = true
|
||||
|
||||
[fs.memory.mounts.data]
|
||||
enabled = true
|
||||
mount = "/var/tmp/pwnagotchi"
|
||||
size = "10M"
|
||||
sync = 3600
|
||||
zram = true
|
||||
rsync = true
|
||||
|
@ -268,6 +268,7 @@ def setup_logging(args, config):
|
||||
requests_log.addHandler(logging.NullHandler())
|
||||
requests_log.prpagate = False
|
||||
|
||||
logging.info("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- Pwnagotchi Re|Started -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
|
||||
|
||||
|
||||
|
||||
|
@ -16,8 +16,10 @@ def freq_to_channel(freq: float) -> int:
|
||||
return 14
|
||||
# 5 GHz Wi-Fi channels
|
||||
elif 5150 <= freq <= 5850: # 5 GHz Wi-Fi
|
||||
if freq < 5270: # Channels 36-48
|
||||
if 5150 <= freq <= 5350: # Channels 36-64
|
||||
return int(((freq - 5180) / 20) + 36)
|
||||
elif 5470 <= freq <= 5725: # Channels 100-144
|
||||
return int(((freq - 5500) / 20) + 100)
|
||||
else: # Channels 149-165
|
||||
return int(((freq - 5745) / 20) + 149)
|
||||
# 6 GHz Wi-Fi channels
|
||||
|
@ -1,10 +1,9 @@
|
||||
# Handles the commandline stuff
|
||||
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import re
|
||||
import shutil
|
||||
import socket # <-- Added for DNS check
|
||||
from fnmatch import fnmatch
|
||||
from pwnagotchi.utils import download_file, unzip, save_config, parse_version, md5
|
||||
from pwnagotchi.plugins import default_path
|
||||
@ -107,20 +106,19 @@ def edit(args, config):
|
||||
|
||||
plugin_config = {'main': {'plugins': {plugin: config['main']['plugins'][plugin]}}}
|
||||
|
||||
import toml
|
||||
import tomlkit
|
||||
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.write(tomlkit.dumps(plugin_config))
|
||||
tmp.flush()
|
||||
rc = call([editor, tmp.name])
|
||||
if rc != 0:
|
||||
return rc
|
||||
tmp.seek(0)
|
||||
new_plugin_config = toml.load(tmp)
|
||||
new_plugin_config = tomlkit.load(tmp)
|
||||
|
||||
config['main']['plugins'][plugin] = new_plugin_config['main']['plugins'][plugin]
|
||||
save_config(config, args.user_config)
|
||||
@ -164,8 +162,8 @@ def upgrade(args, config, pattern='*'):
|
||||
installed_version = _extract_version(filename)
|
||||
|
||||
if installed_version and available_version:
|
||||
if available_version <= installed_version:
|
||||
continue
|
||||
if available_version <= installed_version:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
@ -348,12 +346,34 @@ def _analyse_dir(path):
|
||||
return results
|
||||
|
||||
|
||||
def _check_internet():
|
||||
"""
|
||||
Simple DNS check to verify that we can resolve a common hostname.
|
||||
Returns True if DNS resolution succeeds, False otherwise.
|
||||
"""
|
||||
try:
|
||||
socket.gethostbyname('google.com')
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def update(config):
|
||||
"""
|
||||
Updates the database
|
||||
"""
|
||||
global SAVE_DIR
|
||||
|
||||
if not _check_internet():
|
||||
logging.error("No internet connection or DNS not working. Please follow these instructions:")
|
||||
logging.error("https://github.com/jayofelony/pwnagotchi/wiki/Step-2-Connecting")
|
||||
print("No internet/DNS. Please follow these instructions:")
|
||||
print("https://github.com/jayofelony/pwnagotchi/wiki/Step-2-Connecting")
|
||||
return 1
|
||||
else:
|
||||
logging.info("Internet detected - Please run sudo pwnagotchi plugins list")
|
||||
print("Internet detected - Please run sudo pwnagotchi plugins list")
|
||||
|
||||
urls = config['main']['custom_plugin_repos']
|
||||
if not urls:
|
||||
logging.info('No plugin repositories configured.')
|
||||
@ -393,3 +413,4 @@ def update(config):
|
||||
logging.error('Error while updating plugins: %s', ex)
|
||||
rc = 1
|
||||
return rc
|
||||
|
||||
|
@ -374,7 +374,7 @@ class auto_tune(plugins.Plugin):
|
||||
try:
|
||||
defaults = {'show_hidden': False,
|
||||
'reset_history': True,
|
||||
'extra_channels': 3,
|
||||
'extra_channels': 15,
|
||||
}
|
||||
|
||||
for d in defaults:
|
||||
|
@ -14,7 +14,7 @@ import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile, parse_version as version_to_tuple
|
||||
|
||||
|
||||
def check(version, repo, native=True):
|
||||
def check(version, repo, native=True, token=""):
|
||||
logging.debug("checking remote version for %s, local is %s" % (repo, version))
|
||||
info = {
|
||||
'repo': repo,
|
||||
@ -25,7 +25,21 @@ def check(version, repo, native=True):
|
||||
'arch': platform.machine()
|
||||
}
|
||||
|
||||
resp = requests.get("https://api.github.com/repos/%s/releases/latest" % repo)
|
||||
headers = {}
|
||||
if token != "":
|
||||
headers['Authorization'] = f'token {token}'
|
||||
resp = requests.get(f"https://api.github.com/repos/{repo}/releases/latest", headers=headers)
|
||||
else:
|
||||
resp = requests.get(f"https://api.github.com/repos/{repo}/releases/latest")
|
||||
|
||||
|
||||
if resp.status_code != 200:
|
||||
logging.error(f"[Auto-Update] Failed to get latest release for {repo}: {resp.status_code}")
|
||||
return info
|
||||
|
||||
remaining_requests = resp.headers.get('X-RateLimit-Remaining')
|
||||
logging.debug(f"[Auto-Update] Requests remaining: {remaining_requests}")
|
||||
|
||||
latest = resp.json()
|
||||
info['available'] = latest_ver = latest['tag_name'].replace('v', '')
|
||||
is_armhf = info['arch'].startswith('arm')
|
||||
@ -208,13 +222,13 @@ class AutoUpdate(plugins.Plugin):
|
||||
|
||||
to_install = []
|
||||
to_check = [
|
||||
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||
('jayofelony/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
|
||||
('jayofelony/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
|
||||
('jayofelony/pwnagotchi', pwnagotchi.__version__, False, 'pwnagotchi')
|
||||
]
|
||||
|
||||
for repo, local_version, is_native, svc_name in to_check:
|
||||
info = check(local_version, repo, is_native)
|
||||
info = check(local_version, repo, is_native, self.options['token'])
|
||||
if info['url'] is not None:
|
||||
|
||||
logging.warning(
|
||||
|
152
pwnagotchi/plugins/default/auto_backup.py
Normal file
152
pwnagotchi/plugins/default/auto_backup.py
Normal file
@ -0,0 +1,152 @@
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import socket
|
||||
|
||||
class AutoBackup(plugins.Plugin):
|
||||
__author__ = 'WPA2'
|
||||
__version__ = '1.1.3'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Backs up files when internet is available, with support for excludes.'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.tries = 0
|
||||
# Used to throttle repeated log messages for "backup not due yet"
|
||||
self.last_not_due_logged = 0
|
||||
# Store the status file path separately.
|
||||
self.status_file = '/root/.auto-backup'
|
||||
self.status = StatusFile(self.status_file)
|
||||
|
||||
def on_loaded(self):
|
||||
required_options = ['files', 'interval', 'backup_location', 'max_tries']
|
||||
for opt in required_options:
|
||||
if opt not in self.options or self.options[opt] is None:
|
||||
logging.error(f"AUTO-BACKUP: Option '{opt}' is not set.")
|
||||
return
|
||||
|
||||
# If no custom command(s) are provided, use the default plain tar command.
|
||||
# The command includes a placeholder for {excludes} so that if no excludes are set, it will be empty.
|
||||
if 'commands' not in self.options or not self.options['commands']:
|
||||
self.options['commands'] = ["tar cf {backup_file} {excludes} {files}"]
|
||||
self.ready = True
|
||||
logging.info("AUTO-BACKUP: Successfully loaded.")
|
||||
|
||||
def get_interval_seconds(self):
|
||||
"""
|
||||
Convert the interval option into seconds.
|
||||
Supports:
|
||||
- "daily" for 24 hours,
|
||||
- "hourly" for 60 minutes,
|
||||
- or a numeric value (interpreted as minutes).
|
||||
"""
|
||||
interval = self.options['interval']
|
||||
if isinstance(interval, str):
|
||||
if interval.lower() == "daily":
|
||||
return 24 * 60 * 60
|
||||
elif interval.lower() == "hourly":
|
||||
return 60 * 60
|
||||
else:
|
||||
try:
|
||||
minutes = float(interval)
|
||||
return minutes * 60
|
||||
except ValueError:
|
||||
logging.error("AUTO-BACKUP: Invalid interval format. Defaulting to daily interval.")
|
||||
return 24 * 60 * 60
|
||||
elif isinstance(interval, (int, float)):
|
||||
return float(interval) * 60
|
||||
else:
|
||||
logging.error("AUTO-BACKUP: Unrecognized type for interval. Defaulting to daily interval.")
|
||||
return 24 * 60 * 60
|
||||
|
||||
def is_backup_due(self):
|
||||
"""
|
||||
Determines if enough time has passed since the last backup.
|
||||
If the status file does not exist, a backup is due.
|
||||
"""
|
||||
interval_sec = self.get_interval_seconds()
|
||||
try:
|
||||
last_backup = os.path.getmtime(self.status_file)
|
||||
except OSError:
|
||||
# Status file doesn't exist—backup is due.
|
||||
return True
|
||||
now = time.time()
|
||||
return (now - last_backup) >= interval_sec
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
if self.options['max_tries'] and self.tries >= self.options['max_tries']:
|
||||
logging.info("AUTO-BACKUP: Maximum tries reached, skipping backup.")
|
||||
return
|
||||
|
||||
if not self.is_backup_due():
|
||||
now = time.time()
|
||||
# Log "backup not due" only once every 600 seconds.
|
||||
if now - self.last_not_due_logged > 600:
|
||||
logging.info("AUTO-BACKUP: Backup not due yet based on the interval.")
|
||||
self.last_not_due_logged = now
|
||||
return
|
||||
|
||||
# Only include files/directories that exist to prevent errors.
|
||||
existing_files = list(filter(lambda f: os.path.exists(f), self.options['files']))
|
||||
if not existing_files:
|
||||
logging.warning("AUTO-BACKUP: No files found to backup.")
|
||||
return
|
||||
files_to_backup = " ".join(existing_files)
|
||||
|
||||
# Build excludes string if configured.
|
||||
# Use get() so that if 'exclude' is missing or empty, we default to an empty list.
|
||||
excludes = ""
|
||||
exclude_list = self.options.get('exclude', [])
|
||||
if exclude_list:
|
||||
for pattern in exclude_list:
|
||||
excludes += f" --exclude='{pattern}'"
|
||||
|
||||
# Get the backup location from config.
|
||||
backup_location = self.options['backup_location']
|
||||
|
||||
# Retrieve the global config from agent. If agent.config is callable, call it.
|
||||
global_config = getattr(agent, 'config', None)
|
||||
if callable(global_config):
|
||||
global_config = global_config()
|
||||
if global_config is None:
|
||||
global_config = {}
|
||||
pwnagotchi_name = global_config.get('main', {}).get('name', socket.gethostname())
|
||||
backup_file = os.path.join(backup_location, f"{pwnagotchi_name}-backup.tar")
|
||||
|
||||
try:
|
||||
display = agent.view()
|
||||
logging.info("AUTO-BACKUP: Starting backup process...")
|
||||
display.set('status', 'Backing up ...')
|
||||
display.update()
|
||||
|
||||
# Execute each backup command.
|
||||
for cmd in self.options['commands']:
|
||||
formatted_cmd = cmd.format(backup_file=backup_file, files=files_to_backup, excludes=excludes)
|
||||
logging.info(f"AUTO-BACKUP: Running command: {formatted_cmd}")
|
||||
process = subprocess.Popen(
|
||||
formatted_cmd,
|
||||
shell=True,
|
||||
stdin=None,
|
||||
stdout=open("/dev/null", "w"),
|
||||
stderr=subprocess.STDOUT,
|
||||
executable="/bin/bash"
|
||||
)
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
raise OSError(f"Command failed with return code: {process.returncode}")
|
||||
|
||||
logging.info(f"AUTO-BACKUP: Backup completed successfully. File created at {backup_file}")
|
||||
display.set('status', 'Backup done!')
|
||||
display.update()
|
||||
self.status.update()
|
||||
except OSError as os_e:
|
||||
self.tries += 1
|
||||
logging.error(f"AUTO-BACKUP: Backup error: {os_e}")
|
||||
display.set('status', 'Backup failed!')
|
||||
display.update()
|
@ -1,84 +1,328 @@
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
import time
|
||||
from flask import abort, render_template_string
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
|
||||
TEMPLATE = """
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "bt-tether" %}
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
{% block meta %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=0" />
|
||||
{% endblock %}
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
#searchText {
|
||||
width: 100%;
|
||||
}
|
||||
table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
table, th, td {
|
||||
border: 1px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
@media screen and (max-width:700px) {
|
||||
table, tr, td {
|
||||
padding:0;
|
||||
border:1px solid;
|
||||
}
|
||||
table {
|
||||
border:none;
|
||||
}
|
||||
tr:first-child, thead, th {
|
||||
display:none;
|
||||
border:none;
|
||||
}
|
||||
tr {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
td {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding:1em;
|
||||
}
|
||||
td::before {
|
||||
content:attr(data-label);
|
||||
word-wrap: break-word;
|
||||
color: white;
|
||||
border-right:2px solid;
|
||||
width: 20%;
|
||||
float:left;
|
||||
padding:1em;
|
||||
font-weight: bold;
|
||||
margin:-1em 1em -1em -1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
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");
|
||||
|
||||
for (i = 0; i < tr.length; i++) {
|
||||
td = tr[i].getElementsByTagName("td")[0];
|
||||
if (td) {
|
||||
txtValue = td.textContent || td.innerText;
|
||||
if (txtValue.toUpperCase().indexOf(filter) > -1) {
|
||||
tr[i].style.display = "";
|
||||
}else{
|
||||
tr[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<input type="text" id="searchText" placeholder="Search for ..." title="Type in a filter">
|
||||
<table id="tableOptions">
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Configuration</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="bluetooth">Bluetooth</td>
|
||||
<td>{{bluetooth|safe}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="device">Device</td>
|
||||
<td>{{device|safe}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="connection">Connection</td>
|
||||
<td>{{connection|safe}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
# We all love crazy regex patterns
|
||||
MAC_PTTRN = r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
|
||||
IP_PTTRN = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"
|
||||
DNS_PTTRN = r"^\s*((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*[ ,;]\s*)+((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*[ ,;]?\s*)$"
|
||||
|
||||
|
||||
class BTTether(plugins.Plugin):
|
||||
__author__ = 'Jayofelony'
|
||||
__version__ = '1.2'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A new BT-Tether plugin'
|
||||
__author__ = "Jayofelony, modified my fmatray"
|
||||
__version__ = "1.4"
|
||||
__license__ = "GPL3"
|
||||
__description__ = "A new BT-Tether plugin"
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.options = dict()
|
||||
self.status = '-'
|
||||
self.phone_name = None
|
||||
self.mac = None
|
||||
|
||||
@staticmethod
|
||||
def exec_cmd(cmd, args, pattern=None):
|
||||
try:
|
||||
result = subprocess.run([cmd] + args, check=True, capture_output=True, text=True)
|
||||
if pattern:
|
||||
return result.stdout.find(pattern)
|
||||
return result
|
||||
except Exception as exp:
|
||||
logging.error(f"[BT-Tether] Error with {cmd}")
|
||||
logging.error(f"[BT-Tether] Exception : {exp}")
|
||||
raise exp
|
||||
|
||||
def bluetoothctl(self, args, pattern=None):
|
||||
return self.exec_cmd("bluetoothctl", args, pattern)
|
||||
|
||||
def nmcli(self, args, pattern=None):
|
||||
return self.exec_cmd("nmcli", args, pattern)
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("[BT-Tether] plugin loaded.")
|
||||
|
||||
def on_config_changed(self, config):
|
||||
ip = self.options['ip']
|
||||
mac = self.options['mac']
|
||||
phone_name = self.options['phone-name'] + ' Network'
|
||||
if self.options['phone'].lower() == 'android':
|
||||
address = f'{ip}'
|
||||
gateway = '192.168.44.1'
|
||||
elif self.options['phone'].lower() == 'ios':
|
||||
address = f'{ip}'
|
||||
gateway = '172.20.10.1'
|
||||
else:
|
||||
logging.error("[BT-Tether] Phone type not supported.")
|
||||
if "phone-name" not in self.options:
|
||||
logging.error("[BT-Tether] Phone name not provided")
|
||||
return
|
||||
if not ("mac" in self.options and re.match(MAC_PTTRN, self.options["mac"])):
|
||||
logging.error("[BT-Tether] Error with mac address")
|
||||
return
|
||||
|
||||
if not ("phone" in self.options and self.options["phone"].lower() in ["android", "ios"]):
|
||||
logging.error("[BT-Tether] Phone type not supported")
|
||||
return
|
||||
if self.options["phone"].lower() == "android":
|
||||
address = self.options.get("ip", "192.168.44.2")
|
||||
gateway = "192.168.44.1"
|
||||
elif self.options["phone"].lower() == "ios":
|
||||
address = self.options.get("ip", "172.20.10.2")
|
||||
gateway = "172.20.10.1"
|
||||
if not re.match(IP_PTTRN, address):
|
||||
logging.error(f"[BT-Tether] IP error: {address}")
|
||||
return
|
||||
|
||||
self.phone_name = self.options["phone-name"] + " Network"
|
||||
self.mac = self.options["mac"]
|
||||
dns = self.options.get("dns", "8.8.8.8 1.1.1.1")
|
||||
if not re.match(DNS_PTTRN, dns):
|
||||
if dns == "":
|
||||
logging.error(f"[BT-Tether] Empty DNS setting")
|
||||
else:
|
||||
logging.error(f"[BT-Tether] Wrong DNS setting: '{dns}'")
|
||||
return
|
||||
dns = re.sub("[\s,;]+", " ", dns).strip() # DNS cleaning
|
||||
|
||||
try:
|
||||
# Configure connection. Metric is set to 200 to prefer connection over USB
|
||||
self.nmcli(
|
||||
[
|
||||
"connection", "modify", f"{self.phone_name}",
|
||||
"connection.type", "bluetooth",
|
||||
"bluetooth.type", "panu",
|
||||
"bluetooth.bdaddr", f"{self.mac}",
|
||||
"connection.autoconnect", "yes",
|
||||
"connection.autoconnect-retries", "0",
|
||||
"ipv4.method", "manual",
|
||||
"ipv4.dns", f"{dns}",
|
||||
"ipv4.addresses", f"{address}/24",
|
||||
"ipv4.gateway", f"{gateway}",
|
||||
"ipv4.route-metric", "200",
|
||||
]
|
||||
)
|
||||
# Configure Device to autoconnect
|
||||
self.nmcli([
|
||||
"device", "set", f"{self.mac}",
|
||||
"autoconnect", "yes",
|
||||
"managed", "yes"
|
||||
])
|
||||
self.nmcli(["connection", "reload"])
|
||||
self.ready = True
|
||||
logging.info(f"[BT-Tether] Connection {self.phone_name} configured")
|
||||
except Exception as e:
|
||||
logging.error(f"[BT-Tether] Error while configuring: {e}")
|
||||
return
|
||||
try:
|
||||
subprocess.run([
|
||||
'nmcli', 'connection', 'modify', f'{phone_name}',
|
||||
'connection.type', 'bluetooth',
|
||||
'bluetooth.type', 'panu',
|
||||
'bluetooth.bdaddr', f'{mac}',
|
||||
'ipv4.method', 'manual',
|
||||
'ipv4.addresses', f'{address}',
|
||||
'ipv4.gateway', f'{gateway}',
|
||||
'ipv4.route-metric', '100'
|
||||
], check=True)
|
||||
subprocess.run(['nmcli', 'connection', 'reload'], check=True)
|
||||
subprocess.run(['nmcli', 'connection', 'up', f'{phone_name}'], check=True)
|
||||
time.sleep(5) # Give some delay to configure before going up
|
||||
self.nmcli(["connection", "up", f"{self.phone_name}"])
|
||||
except Exception as e:
|
||||
logging.error(f"[BT-Tether] Failed to connect to device: {e}")
|
||||
self.ready = True
|
||||
logging.error(
|
||||
f"[BT-Tether] Failed to connect to device: have you enabled bluetooth tethering on your phone?"
|
||||
)
|
||||
|
||||
def on_ready(self, agent):
|
||||
self.ready = True
|
||||
try:
|
||||
logging.info(f"[BT-Tether] Disabling bettercap's BLE module")
|
||||
agent.run("ble.recon off", verbose_errors=False)
|
||||
except Exception as e:
|
||||
logging.info(f"[BT-Tether] Bettercap BLE was already off.")
|
||||
|
||||
def on_unload(self, ui):
|
||||
with ui._lock:
|
||||
ui.remove_element("bluetooth")
|
||||
try:
|
||||
self.nmcli(["connection", "down", f"{self.phone_name}"])
|
||||
except Exception as e:
|
||||
logging.error(f"[BT-Tether] Failed to disconnect from device: {e}")
|
||||
|
||||
def on_ui_setup(self, ui):
|
||||
with ui._lock:
|
||||
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-',
|
||||
position=(ui.width() / 2 - 10, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
ui.add_element(
|
||||
"bluetooth",
|
||||
LabeledValue(
|
||||
color=BLACK,
|
||||
label="BT",
|
||||
value="-",
|
||||
position=(ui.width() / 2 - 10, 0),
|
||||
label_font=fonts.Bold,
|
||||
text_font=fonts.Medium,
|
||||
),
|
||||
)
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
phone_name = self.options['phone-name'] + ' Network'
|
||||
if (subprocess.run(['bluetoothctl', 'info'], capture_output=True, text=True)).stdout.find('Connected: yes') != -1:
|
||||
self.status = 'C'
|
||||
else:
|
||||
self.status = '-'
|
||||
try:
|
||||
subprocess.run(['nmcli', 'connection', 'up', f'{phone_name}'], check=True)
|
||||
except Exception as e:
|
||||
logging.error(f"[BT-Tether] Failed to connect to device: {e}")
|
||||
ui.set('bluetooth', self.status)
|
||||
|
||||
def on_unload(self, ui):
|
||||
phone_name = self.options['phone-name'] + ' Network'
|
||||
if not self.ready:
|
||||
return
|
||||
with ui._lock:
|
||||
ui.remove_element('bluetooth')
|
||||
try:
|
||||
if (subprocess.run(['bluetoothctl', 'info'], capture_output=True, text=True)).stdout.find('Connected: yes') != -1:
|
||||
subprocess.run(['nmcli', 'connection', 'down', f'{phone_name}'], check=True)
|
||||
logging.info(f"[BT-Tether] Disconnected from device with name: {phone_name}")
|
||||
else:
|
||||
logging.info(f"[BT-Tether] Device with name {phone_name} is not connected, not disconnecting")
|
||||
except Exception as e:
|
||||
logging.error(f"[BT-Tether] Failed to disconnect from device: {e}")
|
||||
status = ""
|
||||
try:
|
||||
# Checking connection
|
||||
if (
|
||||
self.nmcli(["-w", "0", "-g", "GENERAL.STATE", "connection", "show", self.phone_name],
|
||||
"activated",
|
||||
)
|
||||
!= -1
|
||||
):
|
||||
ui.set("bluetooth", "U")
|
||||
return
|
||||
else:
|
||||
ui.set("bluetooth", "D")
|
||||
status = "BT Conn. down"
|
||||
|
||||
# Checking device
|
||||
if (
|
||||
self.nmcli(
|
||||
["-w", "0", "-g", "GENERAL.STATE", "device", "show", self.mac],
|
||||
"(connected)",
|
||||
)
|
||||
!= -1
|
||||
):
|
||||
ui.set("bluetooth", "C")
|
||||
status += "\nBT dev conn."
|
||||
else:
|
||||
ui.set("bluetooth", "-")
|
||||
status += "\nBT dev disconn."
|
||||
ui.set("status", status)
|
||||
except Exception as e:
|
||||
logging.error(f"[BT-Tether] Error on update: {e}")
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
if not self.ready:
|
||||
return """<html>
|
||||
<head><title>BT-tether: Error</title></head>
|
||||
<body><code>Plugin not ready</code></body>
|
||||
</html>"""
|
||||
if path == "/" or not path:
|
||||
try:
|
||||
bluetooth = self.bluetoothctl(["info", self.mac])
|
||||
bluetooth = bluetooth.stdout.replace("\n", "<br>")
|
||||
except Exception as e:
|
||||
bluetooth = "Error while checking bluetoothctl"
|
||||
|
||||
try:
|
||||
device = self.nmcli(["-w", "0", "device", "show", self.mac])
|
||||
device = device.stdout.replace("\n", "<br>")
|
||||
except Exception as e:
|
||||
device = "Error while checking nmcli device"
|
||||
|
||||
try:
|
||||
connection = self.nmcli(["-w", "0", "connection", "show", self.phone_name])
|
||||
connection = connection.stdout.replace("\n", "<br>")
|
||||
except Exception as e:
|
||||
connection = "Error while checking nmcli connection"
|
||||
|
||||
logging.debug(device)
|
||||
return render_template_string(
|
||||
TEMPLATE,
|
||||
title="BT-Tether",
|
||||
bluetooth=bluetooth,
|
||||
device=device,
|
||||
connection=connection,
|
||||
)
|
||||
abort(404)
|
||||
|
119
pwnagotchi/plugins/default/cache.py
Normal file
119
pwnagotchi/plugins/default/cache.py
Normal file
@ -0,0 +1,119 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import pathlib
|
||||
import pwnagotchi.plugins as plugins
|
||||
from datetime import datetime, UTC
|
||||
from threading import Lock
|
||||
|
||||
|
||||
def read_ap_cache(cache_dir, file):
|
||||
cache_filename = os.path.basename(re.sub(r"\.(pcap|gps\.json|geo\.json)$", ".cache", file))
|
||||
cache_filename = os.path.join(cache_dir, cache_filename)
|
||||
if not os.path.exists(cache_filename):
|
||||
logging.info("Cache not exist")
|
||||
return None
|
||||
try:
|
||||
with open(cache_filename, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logging.info(f"Exception {e}")
|
||||
return None
|
||||
|
||||
|
||||
class Cache(plugins.Plugin):
|
||||
__author__ = "fmatray"
|
||||
__version__ = "1.0.0"
|
||||
__license__ = "GPL3"
|
||||
__description__ = "A simple plugin to cache AP informations"
|
||||
|
||||
def __init__(self):
|
||||
self.options = dict()
|
||||
self.ready = False
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info("[CACHE] plugin loaded.")
|
||||
|
||||
def on_config_changed(self, config):
|
||||
try:
|
||||
handshake_dir = config["bettercap"].get("handshakes")
|
||||
self.cache_dir = os.path.join(handshake_dir, "cache")
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
except Exception:
|
||||
logging.info(f"[CACHE] Cannot access to the cache directory")
|
||||
return
|
||||
self.last_clean = datetime.now(tz=UTC)
|
||||
self.ready = True
|
||||
logging.info(f"[CACHE] Cache plugin configured")
|
||||
self.clean_ap_cache()
|
||||
|
||||
def on_unload(self, ui):
|
||||
self.clean_ap_cache()
|
||||
|
||||
def clean_ap_cache(self):
|
||||
if not self.ready:
|
||||
return
|
||||
with self.lock:
|
||||
ctime = datetime.now(tz=UTC)
|
||||
cache_to_delete = list()
|
||||
for cache_file in pathlib.Path(self.cache_dir).glob("*.apcache"):
|
||||
try:
|
||||
mtime = datetime.fromtimestamp(cache_file.lstat().st_mtime, tz=UTC)
|
||||
if (ctime - mtime).total_seconds() > 60 * 5:
|
||||
cache_to_delete.append(cache_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if cache_to_delete:
|
||||
logging.info(f"[CACHE] Cleaning {len(cache_to_delete)} files")
|
||||
for cache_file in cache_to_delete:
|
||||
try:
|
||||
cache_file.unlink()
|
||||
except FileNotFoundError as e:
|
||||
pass
|
||||
|
||||
def write_ap_cache(self, access_point):
|
||||
with self.lock:
|
||||
try:
|
||||
mac = access_point["mac"].replace(":", "")
|
||||
hostname = re.sub(r"[^a-zA-Z0-9]", "", access_point["hostname"])
|
||||
except KeyError:
|
||||
return
|
||||
cache_file = os.path.join(self.cache_dir, f"{hostname}_{mac}.apcache")
|
||||
try:
|
||||
with open(cache_file, "w") as f:
|
||||
json.dump(access_point, f)
|
||||
except Exception as e:
|
||||
logging.error(f"[CACHE] Cannot write {cache_file}: {e}")
|
||||
pass
|
||||
|
||||
def on_wifi_update(self, agent, access_points):
|
||||
if self.ready:
|
||||
for ap in filter(lambda ap: ap["hostname"] not in ["", "<hidden>"], access_points):
|
||||
self.write_ap_cache(ap)
|
||||
|
||||
def on_unfiltered_ap_list(self, agent, aps):
|
||||
if self.ready:
|
||||
for ap in filter(lambda ap: ap["hostname"] not in ["", "<hidden>"], aps):
|
||||
self.write_ap_cache(ap)
|
||||
|
||||
def on_association(self, agent, access_point):
|
||||
if self.ready:
|
||||
self.write_ap_cache(access_point)
|
||||
|
||||
def on_deauthentication(self, agent, access_point, client_station):
|
||||
if self.ready:
|
||||
self.write_ap_cache(access_point)
|
||||
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
if self.ready:
|
||||
self.write_ap_cache(access_point)
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
if not self.ready:
|
||||
return
|
||||
current_time = datetime.now(tz=UTC)
|
||||
if (current_time - self.last_clean).total_seconds() > 60:
|
||||
self.clean_ap_cache()
|
||||
self.last_clean = current_time
|
@ -13,8 +13,8 @@ import zipfile
|
||||
|
||||
|
||||
class GdriveSync(plugins.Plugin):
|
||||
__author__ = '@jayofelony'
|
||||
__version__ = '1.2'
|
||||
__author__ = '@jayofelony & Moist'
|
||||
__version__ = '1.4'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin to backup various pwnagotchi files and folders to Google Drive. Once every hour from loading plugin.'
|
||||
|
||||
@ -26,12 +26,15 @@ class GdriveSync(plugins.Plugin):
|
||||
self.status = StatusFile('/root/.gdrive-backup')
|
||||
self.backup = True
|
||||
self.backupfiles = [
|
||||
'/root/brain.nn',
|
||||
'/root/brain.json',
|
||||
'/root/.api-report.json',
|
||||
'/root/handshakes',
|
||||
'/home/pi/handshakes',
|
||||
'/root/peers',
|
||||
'/etc/pwnagotchi'
|
||||
'/etc/pwnagotchi',
|
||||
'.etc/profile/',
|
||||
'/usr/local/share/pwnagotchi/custom-plugins',
|
||||
'/boot/firmware/config.txt',
|
||||
'/boot/firmware/cmdline.txt'
|
||||
]
|
||||
|
||||
def on_loaded(self):
|
||||
@ -168,7 +171,7 @@ class GdriveSync(plugins.Plugin):
|
||||
"""
|
||||
self.internet = True
|
||||
|
||||
def on_handshake(self, agent):
|
||||
def on_handshake(self, agent, filename, access_point, client_station):
|
||||
display = agent.view()
|
||||
if not self.ready and not self.internet:
|
||||
return
|
||||
|
@ -44,7 +44,7 @@ def parse_pcap(filename):
|
||||
|
||||
class Grid(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.1'
|
||||
__version__ = '1.1.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned ' \
|
||||
'networks to opwngrid.xyz '
|
||||
@ -69,6 +69,11 @@ class Grid(plugins.Plugin):
|
||||
def on_loaded(self):
|
||||
logging.info("grid plugin loaded.")
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
from flask import make_response, redirect
|
||||
response = make_response(redirect("https://opwngrid.xyz", code=302))
|
||||
return response
|
||||
|
||||
def set_reported(self, reported, net_id):
|
||||
if net_id not in reported:
|
||||
reported.append(net_id)
|
||||
|
229
pwnagotchi/plugins/default/ohcapi.py
Normal file
229
pwnagotchi/plugins/default/ohcapi.py
Normal file
@ -0,0 +1,229 @@
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import pwnagotchi.plugins as plugins
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
class ohcapi(plugins.Plugin):
|
||||
__author__ = 'Rohan Dayaram'
|
||||
__version__ = '1.1.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Uploads WPA/WPA2 handshakes to OnlineHashCrack.com using the new API (V2), no dashboard.'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.lock = Lock()
|
||||
try:
|
||||
self.report = StatusFile('/root/handshakes/.ohc_uploads', data_format='json')
|
||||
except JSONDecodeError:
|
||||
os.remove('/root/.ohc_newapi_uploads')
|
||||
self.report = StatusFile('/root/handshakes/.ohc_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
self.last_run = 0 # Track last time periodic tasks were run
|
||||
self.internet_active = False # Track whether internet is currently available
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Called when the plugin is loaded.
|
||||
"""
|
||||
required_fields = ['api_key']
|
||||
missing = [field for field in required_fields if field not in self.options or not self.options[field]]
|
||||
if missing:
|
||||
logging.error(f"OHC NewAPI: Missing required config fields: {missing}")
|
||||
return
|
||||
|
||||
if 'receive_email' not in self.options:
|
||||
self.options['receive_email'] = 'yes' # default
|
||||
|
||||
if 'sleep' not in self.options:
|
||||
self.options['sleep'] = 60*60 # default to 1 hour
|
||||
|
||||
self.ready = True
|
||||
logging.info("OHC NewAPI: Plugin loaded and ready.")
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
from flask import make_response, redirect
|
||||
response = make_response(redirect("https://www.onlinehashcrack.com", code=302))
|
||||
return response
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
"""
|
||||
Called once when the internet becomes available.
|
||||
Run upload/download tasks immediately.
|
||||
"""
|
||||
if not self.ready or self.lock.locked():
|
||||
return
|
||||
|
||||
self.internet_active = True
|
||||
self._run_tasks(agent) # Run immediately when internet is detected
|
||||
self.last_run = time.time() # Record the time of this run
|
||||
|
||||
def on_ui_update(self, ui):
|
||||
"""
|
||||
Called periodically by the UI. We will use this event to run tasks every 60 seconds if internet is still available.
|
||||
"""
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
# Attempt to get agent from ui
|
||||
agent = getattr(ui, '_agent', None)
|
||||
if agent is None:
|
||||
return
|
||||
|
||||
# Check if the internet is still available by pinging Google
|
||||
self.internet_active = False
|
||||
try:
|
||||
response = requests.get('https://www.google.com', timeout=5)
|
||||
if response.status_code == 200:
|
||||
self.internet_active = True
|
||||
except requests.ConnectionError:
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
if self.internet_active and current_time - self.last_run >= self.options['sleep']:
|
||||
self._run_tasks(agent)
|
||||
self.last_run = current_time
|
||||
|
||||
def _extract_essid_bssid_from_hash(self, hash_line):
|
||||
parts = hash_line.strip().split('*')
|
||||
essid = 'unknown_ESSID'
|
||||
bssid = '00:00:00:00:00:00'
|
||||
|
||||
if len(parts) > 5:
|
||||
essid_hex = parts[5]
|
||||
try:
|
||||
essid = bytes.fromhex(essid_hex).decode('utf-8', errors='replace')
|
||||
except:
|
||||
essid = 'unknown_ESSID'
|
||||
|
||||
if len(parts) > 3:
|
||||
apmac = parts[3]
|
||||
if len(apmac) == 12:
|
||||
bssid = ':'.join(apmac[i:i+2] for i in range(0, 12, 2))
|
||||
|
||||
if essid == 'unknown_ESSID' or bssid == '00:00:00:00:00:00':
|
||||
logging.debug(f"OHC NewAPI: Failed to extract ESSID/BSSID from hash -> {hash_line}")
|
||||
|
||||
return essid, bssid
|
||||
|
||||
def _run_tasks(self, agent):
|
||||
"""
|
||||
Encapsulates the logic of extracting, uploading, and updating tasks.
|
||||
"""
|
||||
with self.lock:
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = self.report.data_field_or('reported', default=[])
|
||||
processed_stations = self.report.data_field_or('processed_stations', default=[])
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
|
||||
# Find .pcap files
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename)
|
||||
for filename in handshake_filenames if filename.endswith('.pcap')]
|
||||
|
||||
# If the corresponding .22000 file exists, skip re-upload
|
||||
handshake_paths = [p for p in handshake_paths if not os.path.exists(p.replace('.pcap', '.22000'))]
|
||||
|
||||
# Filter out already reported and skipped .pcap files
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
|
||||
|
||||
if handshake_new:
|
||||
logging.info(f"OHC NewAPI: Processing {len(handshake_new)} new PCAP handshakes.")
|
||||
|
||||
all_hashes = []
|
||||
successfully_extracted = []
|
||||
essid_bssid_map = {}
|
||||
|
||||
for idx, pcap_path in enumerate(handshake_new):
|
||||
hashes = self._extract_hashes_from_handshake(pcap_path)
|
||||
if hashes:
|
||||
# Extract ESSID and BSSID from the first hash line
|
||||
essid, bssid = self._extract_essid_bssid_from_hash(hashes[0])
|
||||
if (essid, bssid) in processed_stations:
|
||||
logging.debug(f"OHC NewAPI: Station {essid}/{bssid} already processed, skipping {pcap_path}.")
|
||||
self.skip.append(pcap_path)
|
||||
continue
|
||||
|
||||
all_hashes.extend(hashes)
|
||||
successfully_extracted.append(pcap_path)
|
||||
essid_bssid_map[pcap_path] = (essid, bssid)
|
||||
else:
|
||||
logging.debug(f"OHC NewAPI: No hashes extracted from {pcap_path}, skipping.")
|
||||
self.skip.append(pcap_path)
|
||||
|
||||
# Now upload all extracted hashes
|
||||
if all_hashes:
|
||||
batches = [all_hashes[i:i+50] for i in range(0, len(all_hashes), 50)]
|
||||
upload_success = True
|
||||
for batch_idx, batch in enumerate(batches):
|
||||
display.on_uploading(f"onlinehashcrack.com ({(batch_idx+1)*50}/{len(all_hashes)})")
|
||||
if not self._add_tasks(batch):
|
||||
upload_success = False
|
||||
break
|
||||
|
||||
if upload_success:
|
||||
# Mark all successfully extracted pcaps as reported
|
||||
for pcap_path in successfully_extracted:
|
||||
reported.append(pcap_path)
|
||||
essid, bssid = essid_bssid_map[pcap_path]
|
||||
processed_stations.append((essid, bssid))
|
||||
self.report.update(data={'reported': reported, 'processed_stations': processed_stations})
|
||||
logging.debug("OHC NewAPI: Successfully reported all new handshakes.")
|
||||
else:
|
||||
# Upload failed, skip these pcaps for future attempts
|
||||
for pcap_path in successfully_extracted:
|
||||
self.skip.append(pcap_path)
|
||||
logging.debug("OHC NewAPI: Failed to upload tasks, added to skip list.")
|
||||
else:
|
||||
logging.debug("OHC NewAPI: No hashes were extracted from the new pcaps. Nothing to upload.")
|
||||
|
||||
display.on_normal()
|
||||
else:
|
||||
logging.debug("OHC NewAPI: No new PCAP files to process.")
|
||||
|
||||
def _add_tasks(self, hashes, timeout=30):
|
||||
clean_hashes = [h.strip() for h in hashes if h.strip()]
|
||||
if not clean_hashes:
|
||||
return True # No hashes to add is success
|
||||
|
||||
payload = {
|
||||
'api_key': self.options['api_key'],
|
||||
'agree_terms': "yes",
|
||||
'action': 'add_tasks',
|
||||
'algo_mode': 22000,
|
||||
'hashes': clean_hashes,
|
||||
'receive_email': self.options['receive_email']
|
||||
}
|
||||
|
||||
try:
|
||||
result = requests.post('https://api.onlinehashcrack.com/v2',
|
||||
json=payload,
|
||||
timeout=timeout)
|
||||
result.raise_for_status()
|
||||
data = result.json()
|
||||
logging.info(f"OHC NewAPI: Add tasks response: {data}")
|
||||
return True
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.debug(f"OHC NewAPI: Exception while adding tasks -> {e}")
|
||||
return False
|
||||
|
||||
def _extract_hashes_from_handshake(self, pcap_path):
|
||||
hashes = []
|
||||
hcxpcapngtool = '/usr/bin/hcxpcapngtool'
|
||||
hccapx_path = pcap_path.replace('.pcap', '.22000')
|
||||
hcxpcapngtool_cmd = f"{hcxpcapngtool} -o {hccapx_path} {pcap_path}"
|
||||
os.popen(hcxpcapngtool_cmd).read()
|
||||
if os.path.exists(hccapx_path) and os.path.getsize(hccapx_path) > 0:
|
||||
logging.debug(f"OHC NewAPI: Extracted hashes from {pcap_path}")
|
||||
with open(hccapx_path, 'r') as hccapx_file:
|
||||
hashes = hccapx_file.readlines()
|
||||
else:
|
||||
logging.debug(f"OHC NewAPI: Failed to extract hashes from {pcap_path}")
|
||||
if os.path.exists(hccapx_path):
|
||||
os.remove(hccapx_path)
|
||||
return hashes
|
@ -1,147 +0,0 @@
|
||||
import os
|
||||
import csv
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile, remove_whitelisted
|
||||
import pwnagotchi.plugins as plugins
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
|
||||
class OnlineHashCrack(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '2.1.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
try:
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
except JSONDecodeError:
|
||||
os.remove('/root/.ohc_uploads')
|
||||
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
self.skip = list()
|
||||
self.lock = Lock()
|
||||
self.options = dict()
|
||||
|
||||
def on_loaded(self):
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
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
|
||||
|
||||
self.ready = True
|
||||
logging.info("OHC: OnlineHashCrack plugin loaded.")
|
||||
|
||||
def _upload_to_ohc(self, path, timeout=30):
|
||||
"""
|
||||
Uploads the file to onlinehashcrack.com
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
data = {'email': self.options['email']}
|
||||
payload = {'file': file_to_upload}
|
||||
|
||||
try:
|
||||
result = requests.post('https://api.onlinehashcrack.com',
|
||||
data=data,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if 'already been sent' in result.text:
|
||||
logging.debug(f"{path} was already uploaded.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.debug(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_webhook(self, path, request):
|
||||
import requests
|
||||
from flask import redirect
|
||||
s = requests.Session()
|
||||
s.get('https://www.onlinehashcrack.com/dashboard')
|
||||
r = s.post('https://www.onlinehashcrack.com/dashboard', data={'emailTasks': self.options['email'], 'submit': ''})
|
||||
return redirect(r.url, code=302)
|
||||
|
||||
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:
|
||||
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, config['main']['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.on_uploading(f"onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||
|
||||
try:
|
||||
self._upload_to_ohc(handshake)
|
||||
if handshake not in reported:
|
||||
reported.append(handshake)
|
||||
self.report.update(data={'reported': reported})
|
||||
logging.debug(f"OHC: Successfully uploaded {handshake}")
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
self.skip.append(handshake)
|
||||
logging.debug("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
self.skip.append(handshake)
|
||||
logging.debug("OHC: %s", os_e)
|
||||
continue
|
||||
|
||||
display.on_normal()
|
||||
|
||||
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'])
|
@ -6,9 +6,530 @@ import pwnagotchi.ui.fonts as fonts
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi
|
||||
import time
|
||||
from pisugar import *
|
||||
import smbus
|
||||
from flask import abort
|
||||
from flask import render_template_string
|
||||
from collections import deque
|
||||
|
||||
import threading
|
||||
PiSugar_addresses = {
|
||||
"PiSugar2": 0x75, # PiSugar2\2Plus
|
||||
"PiSugar3": 0x57, # PiSugar3\3Plus
|
||||
"PiSugar2 RTC": 0x32 # PiSugar2\2Plus RTC
|
||||
}
|
||||
|
||||
# Use the same battery level curve as pisugar-power-manager
|
||||
curve5312 = [
|
||||
(4.10, 100.0),
|
||||
(4.05, 95.0),
|
||||
(3.90, 88.0),
|
||||
(3.80, 77.0),
|
||||
(3.70, 65.0),
|
||||
(3.62, 55.0),
|
||||
(3.58, 49.0),
|
||||
(3.49, 25.6),
|
||||
(3.32, 4.5),
|
||||
(3.1, 0.0),
|
||||
]
|
||||
|
||||
curve5209 = [
|
||||
(4.16, 100.0),
|
||||
(4.05, 95.0),
|
||||
(4.00, 80.0),
|
||||
(3.92, 65.0),
|
||||
(3.86, 40.0),
|
||||
(3.79, 25.5),
|
||||
(3.66, 10.0),
|
||||
(3.52, 6.5),
|
||||
(3.49, 3.2),
|
||||
(3.1, 0.0),
|
||||
]
|
||||
|
||||
class PiSugarServer:
|
||||
def __init__(self):
|
||||
"""
|
||||
PiSugar initialization, if unable to connect to any version of PiSugar, return false
|
||||
"""
|
||||
self._bus = smbus.SMBus(1)
|
||||
self.ready = False
|
||||
self.model = None
|
||||
self.i2creg = []
|
||||
self.address = 0
|
||||
self.battery_voltage = 0.00
|
||||
self.voltage_history = deque(maxlen=10)
|
||||
self.battery_level = 0
|
||||
self.battery_charging = 0
|
||||
self.temperature = 0
|
||||
self.power_plugged = False
|
||||
self.allow_charging = True
|
||||
self.lowpower_shutdown = False
|
||||
self.lowpower_shutdown_level = 10
|
||||
self.max_charge_voltage_protection = False
|
||||
self.max_protection_level=80
|
||||
# Start the device connection in a background thread
|
||||
self.connection_thread = threading.Thread(
|
||||
target=self._connect_device, daemon=True)
|
||||
self.connection_thread.start()
|
||||
|
||||
def _connect_device(self):
|
||||
"""
|
||||
Attempt to connect to the PiSugar device in a background thread.
|
||||
"""
|
||||
while self.model is None:
|
||||
if self.check_device(PiSugar_addresses["PiSugar2"]) is not None:
|
||||
self.address = PiSugar_addresses["PiSugar2"]
|
||||
if self.check_device(PiSugar_addresses["PiSugar2"], 0xC2) != 0:
|
||||
self.model = "PiSugar2Plus"
|
||||
else:
|
||||
self.model = "PiSugar2"
|
||||
self.device_init()
|
||||
elif self.check_device(PiSugar_addresses["PiSugar3"]) is not None:
|
||||
self.model = 'PiSugar3'
|
||||
self.address = PiSugar_addresses["PiSugar3"]
|
||||
self.device_init()
|
||||
else:
|
||||
self.model = None
|
||||
logging.info(
|
||||
"No PiSugar device was found. Please check if the PiSugar device is powered on."
|
||||
)
|
||||
time.sleep(5)
|
||||
logging.info(f"{self.model} is connected")
|
||||
# Once connected, start the timer
|
||||
self.start_timer()
|
||||
while len(self.i2creg) < 256:
|
||||
time.sleep(1)
|
||||
self.ready = True
|
||||
logging.info(f"{self.model} is ready")
|
||||
|
||||
def start_timer(self):
|
||||
# Create a thread to execute the timer function
|
||||
timer_thread = threading.Thread(target=self.update_value)
|
||||
timer_thread.daemon = True # Set as a daemon thread, automatically ends when the main program exits
|
||||
timer_thread.start()
|
||||
|
||||
def update_value(self):
|
||||
"""Update PiSugar status every three seconds, including triggering auto shutdown"""
|
||||
while True:
|
||||
try:
|
||||
if self.model == 'PiSugar2' or self.model == 'PiSugar2Plus':
|
||||
self.set_battery_notallow_charging() # Temporarily disable charging to get accurate battery voltage
|
||||
time.sleep(0.05)
|
||||
self.i2creg = []
|
||||
for i in range(0, 256, 32):
|
||||
# Calculate the starting register address for the current read
|
||||
current_register = 0 + i
|
||||
# Calculate the length of the current read
|
||||
current_length = min(32, 256 - i)
|
||||
# Read data block
|
||||
chunk = self._bus.read_i2c_block_data(
|
||||
self.address, current_register, current_length)
|
||||
# Add the read data block to the result list
|
||||
self.i2creg.extend(chunk)
|
||||
time.sleep(0.1)
|
||||
logging.debug(f"Data length: {len(self.i2creg)}")
|
||||
logging.debug(f"Data: {self.i2creg}")
|
||||
if self.model == 'PiSugar3':
|
||||
low = self.i2creg[0x23]
|
||||
high = self.i2creg[0x22]
|
||||
self.battery_voltage = (((high << 8) + low) / 1000)
|
||||
self.temperature = self.i2creg[0x04] - 40
|
||||
ctr1 = self.i2creg[0x02] # Read control register 1
|
||||
self.power_plugged = (ctr1 & (1 << 7)) != 0 # Check if power is plugged in
|
||||
self.allow_charging = (ctr1 & (1 << 6)) != 0 # Check if charging is allowed
|
||||
if self.max_charge_voltage_protection:
|
||||
self._bus.write_byte_data(
|
||||
self.address, 0x0B, 0x29) # Disable write protection
|
||||
self._bus.write_byte_data(self.address, 0x20, self._bus.read_byte_data(
|
||||
self.address, 0x20) | 0b10000000)
|
||||
self._bus.write_byte_data(
|
||||
self.address, 0x0B, 0x00) # Enable write protection
|
||||
else:
|
||||
self._bus.write_byte_data(
|
||||
self.address, 0x0B, 0x29) # Disable write protection
|
||||
self._bus.write_byte_data(self.address, 0x20, self._bus.read_byte_data(
|
||||
self.address, 0x20) & 0b01111111)
|
||||
self._bus.write_byte_data(
|
||||
self.address, 0x0B, 0x00) # Enable write protection
|
||||
elif self.model == 'PiSugar2':
|
||||
high = self.i2creg[0xa3]
|
||||
low = self.i2creg[0xa2]
|
||||
self.battery_voltage = (2600.0 - (((high | 0b11000000) << 8) + low) * 0.26855) / \
|
||||
1000.0 if high & 0x20 else (
|
||||
2600.0 + (((high & 0x1f) << 8) + low) * 0.26855) / 1000.0
|
||||
self.power_plugged = (self.i2creg[0x55] & 0b00010000) != 0
|
||||
|
||||
if self.max_charge_voltage_protection:
|
||||
self.voltage_history.append(self.battery_voltage)
|
||||
self.battery_level = self.convert_battery_voltage_to_level()
|
||||
if (self.battery_level) > self.max_protection_level:
|
||||
self.set_battery_notallow_charging()
|
||||
else:
|
||||
self.set_battery_allow_charging()
|
||||
else:
|
||||
self.set_battery_allow_charging()
|
||||
|
||||
elif self.model == 'PiSugar2Plus':
|
||||
low = self.i2creg[0xd0]
|
||||
high = self.i2creg[0xd1]
|
||||
self.battery_voltage = (
|
||||
(((high & 0b00111111) << 8) + low) * 0.26855 + 2600.0)/1000
|
||||
self.power_plugged = self.i2creg[0xdd] == 0x1f
|
||||
if self.max_charge_voltage_protection:
|
||||
self.voltage_history.append(self.battery_voltage)
|
||||
self.battery_level = self.convert_battery_voltage_to_level()
|
||||
if (self.battery_level) > self.max_protection_level:
|
||||
self.set_battery_notallow_charging()
|
||||
else:
|
||||
self.set_battery_allow_charging()
|
||||
else:
|
||||
self.set_battery_allow_charging()
|
||||
|
||||
self.voltage_history.append(self.battery_voltage)
|
||||
self.battery_level = self.convert_battery_voltage_to_level()
|
||||
|
||||
if self.lowpower_shutdown:
|
||||
if self.battery_level < self.lowpower_shutdown_level:
|
||||
logging.info("[PiSugarX] low power shutdown now.")
|
||||
self.shutdown()
|
||||
pwnagotchi.shutdown()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
logging.error(f"read error{e}")
|
||||
time.sleep(3)
|
||||
|
||||
def shutdown(self):
|
||||
# logging.info("[PiSugarX] PiSugar set shutdown .")
|
||||
if self.model == 'PiSugar3':
|
||||
# Shutdown the power after 10 seconds
|
||||
self._bus.write_byte_data(self.address, 0x0B, 0x29) # Disable write protection
|
||||
self._bus.write_byte_data(self.address, 0x09, 10)
|
||||
self._bus.write_byte_data(self.address, 0x02, self._bus.read_byte_data(
|
||||
self.address, 0x02) & 0b11011111)
|
||||
self._bus.write_byte_data(self.address, 0x0B, 0x00) # Enable write protection
|
||||
logging.info("[PiSugarX] PiSugar shutdown in 10s.")
|
||||
elif self.model == 'PiSugar2':
|
||||
pass
|
||||
elif self.model == 'PiSugar2Plus':
|
||||
pass
|
||||
|
||||
def check_device(self, address, reg=0):
|
||||
"""Check if a device is present at the specified address"""
|
||||
try:
|
||||
return self._bus.read_byte_data(address, reg)
|
||||
except OSError as e:
|
||||
logging.debug(f"Device not found at address {address}: {e}")
|
||||
return None
|
||||
|
||||
def device_init(self):
|
||||
|
||||
if self.model == "PiSugar2Plus":
|
||||
'''Initialize GPIO'''
|
||||
self._bus.write_byte_data(self.address, 0x52, self._bus.read_byte_data(
|
||||
self.address, 0x52) | 0b00000010)
|
||||
self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data(
|
||||
self.address, 0x54) | 0b00000010)
|
||||
self._bus.write_byte_data(self.address, 0x52, self._bus.read_byte_data(
|
||||
self.address, 0x52) | 0b00000100)
|
||||
self._bus.write_byte_data(self.address, 0x29, self._bus.read_byte_data(
|
||||
self.address, 0x29) & 0b10111111)
|
||||
self._bus.write_byte_data(self.address, 0x52, self._bus.read_byte_data(
|
||||
self.address, 0x52) & 0b10011111 | 0b01000000)
|
||||
self._bus.write_byte_data(self.address, 0xc2, self._bus.read_byte_data(
|
||||
self.address, 0xc2) | 0b00010000)
|
||||
logging.debug(f"PiSugar2Plus GPIO initialization complete")
|
||||
'''Init boost intensity, 0x3f*50ma, 3A'''
|
||||
self._bus.write_byte_data(self.address, 0x30, self._bus.read_byte_data(
|
||||
self.address, 0x30) & 0b11000000 | 0x3f)
|
||||
logging.debug(f"PiSugar2Plus current setting complete")
|
||||
|
||||
elif self.model == "PiSugar2":
|
||||
'''Initialize GPIO'''
|
||||
self._bus.write_byte_data(self.address, 0x51, (self._bus.read_byte_data(
|
||||
self.address, 0x51) & 0b11110011) | 0b00000100)
|
||||
self._bus.write_byte_data(self.address, 0x53, self._bus.read_byte_data(
|
||||
self.address, 0x53) | 0b00000010)
|
||||
self._bus.write_byte_data(self.address, 0x51, (self._bus.read_byte_data(
|
||||
self.address, 0x51) & 0b11001111) | 0b00010000)
|
||||
self._bus.write_byte_data(self.address, 0x26, self._bus.read_byte_data(
|
||||
self.address, 0x26) & 0b10110000)
|
||||
self._bus.write_byte_data(self.address, 0x52, (self._bus.read_byte_data(
|
||||
self.address, 0x52) & 0b11110011) | 0b00000100)
|
||||
self._bus.write_byte_data(self.address, 0x53, (self._bus.read_byte_data(
|
||||
self.address, 0x53) & 0b11101111) | 0b00010000)
|
||||
logging.debug(f"PiSugar2 GPIO initialization complete")
|
||||
pass
|
||||
|
||||
def convert_battery_voltage_to_level(self):
|
||||
"""
|
||||
Convert battery voltage to battery percentage.
|
||||
|
||||
:param voltage: Current battery voltage
|
||||
:param curve: Battery threshold curve, format [(voltage1, percentage1), (voltage2, percentage2), ...]
|
||||
:return: Battery percentage
|
||||
"""
|
||||
if self.model == "PiSugar2Plus":
|
||||
curve = curve5312
|
||||
elif self.model == "PiSugar3Plus":
|
||||
curve = curve5312
|
||||
elif self.model == "PiSugar2":
|
||||
curve = curve5209
|
||||
elif self.model == "PiSugar3":
|
||||
curve = curve5312
|
||||
# Add the current voltage to the history
|
||||
|
||||
# If the history is less than 5 entries, return the average directly (to avoid truncation with no valid data)
|
||||
if len(self.voltage_history) < 5:
|
||||
avg_voltage = sum(self.voltage_history) / len(self.voltage_history)
|
||||
else:
|
||||
# Sort and remove the highest 2 and lowest 2
|
||||
sorted_history = sorted(self.voltage_history)
|
||||
trimmed_history = sorted_history[2:-2] # Remove the first two and last two
|
||||
avg_voltage = sum(trimmed_history) / len(trimmed_history) # Calculate truncated mean
|
||||
# Traverse each segment of the battery curve
|
||||
for (v1, p1), (v2, p2) in zip(curve, curve[1:]):
|
||||
# If the voltage is within the current interval
|
||||
if v2 <= avg_voltage <= v1:
|
||||
# Use linear interpolation to calculate the percentage
|
||||
return p2 + (p1 - p2) * (avg_voltage - v2) / (v1 - v2)
|
||||
|
||||
# If the voltage is out of the curve range, return the lowest or highest percentage
|
||||
return curve[-1][1] if avg_voltage < curve[-1][0] else curve[0][1]
|
||||
|
||||
def get_version(self):
|
||||
"""
|
||||
Get the firmware version of the PiSugar3.
|
||||
If not PiSugar3, return None
|
||||
:return: Version string or None
|
||||
"""
|
||||
if self.model == 'PiSugar3':
|
||||
try:
|
||||
return bytes(self.i2creg[0xe2:0xee]).decode('ascii')
|
||||
except OSError as e:
|
||||
logging.error(f"Failed to read version from PiSugar3: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_model(self):
|
||||
"""
|
||||
Get the model of the PiSugar hardware.
|
||||
|
||||
:return: Model string.
|
||||
"""
|
||||
return self.model
|
||||
|
||||
def get_battery_level(self):
|
||||
"""
|
||||
Get the current battery level in percentage.
|
||||
|
||||
:return: Battery level as a percentage (0-100).
|
||||
"""
|
||||
return self.battery_level
|
||||
|
||||
def get_battery_voltage(self):
|
||||
"""
|
||||
Get the current battery voltage.
|
||||
|
||||
:return: Battery voltage in volts.
|
||||
"""
|
||||
return self.battery_voltage
|
||||
|
||||
def get_battery_current(self):
|
||||
"""
|
||||
Get the current battery current.
|
||||
|
||||
:return: Battery current in amperes.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_battery_allow_charging(self):
|
||||
"""
|
||||
Check if battery charging is allowed.
|
||||
|
||||
:return: True if charging is allowed, False otherwise.
|
||||
"""
|
||||
return self.allow_charging
|
||||
|
||||
def set_battery_allow_charging(self):
|
||||
if self.model == 'PiSugar3':
|
||||
pass
|
||||
elif self.model == 'PiSugar2':
|
||||
# Disable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data(
|
||||
self.address, 0x54) & 0b11111011)
|
||||
# Enable charging
|
||||
self._bus.write_byte_data(self.address, 0x55, self._bus.read_byte_data(
|
||||
self.address, 0x55) & 0b11111011)
|
||||
# Enable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data(
|
||||
self.address, 0x54) | 0b00000100)
|
||||
elif self.model == 'PiSugar2Plus':
|
||||
# Disable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data(
|
||||
self.address, 0x56) & 0b11111011)
|
||||
# Enable charging
|
||||
self._bus.write_byte_data(self.address, 0x58, self._bus.read_byte_data(
|
||||
self.address, 0x58) & 0b11111011)
|
||||
# Enable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data(
|
||||
self.address, 0x56) | 0b00000100)
|
||||
|
||||
return
|
||||
|
||||
def set_battery_notallow_charging(self):
|
||||
if self.model == 'PiSugar3':
|
||||
pass
|
||||
elif self.model == 'PiSugar2':
|
||||
# Disable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data(
|
||||
self.address, 0x54) & 0b11111011)
|
||||
# Disable charging
|
||||
self._bus.write_byte_data(self.address, 0x55, self._bus.read_byte_data(
|
||||
self.address, 0x55) | 0b00000100)
|
||||
# Enable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x54, self._bus.read_byte_data(
|
||||
self.address, 0x54) | 0b00000100)
|
||||
elif self.model == 'PiSugar2Plus':
|
||||
# Disable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data(
|
||||
self.address, 0x56) & 0b11111011)
|
||||
# Disable charging
|
||||
self._bus.write_byte_data(self.address, 0x58, self._bus.read_byte_data(
|
||||
self.address, 0x58) | 0b00000100)
|
||||
# Enable gpio2 output
|
||||
self._bus.write_byte_data(self.address, 0x56, self._bus.read_byte_data(
|
||||
self.address, 0x56) | 0b00000100)
|
||||
|
||||
return
|
||||
|
||||
def get_battery_charging_range(self):
|
||||
"""
|
||||
Get the battery charging range.
|
||||
|
||||
:return: Charging range string.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_battery_full_charge_duration(self):
|
||||
"""
|
||||
Get the duration of keeping the battery charging when full.
|
||||
|
||||
:return: Duration in seconds.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_battery_safe_shutdown_level(self):
|
||||
"""
|
||||
Get the safe shutdown level for the battery.
|
||||
|
||||
:return: Safe shutdown level as a percentage.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_battery_safe_shutdown_delay(self):
|
||||
"""
|
||||
Get the safe shutdown delay.
|
||||
|
||||
:return: Delay in seconds.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_battery_auto_power_on(self):
|
||||
"""
|
||||
Check if auto power on is enabled.
|
||||
|
||||
:return: True if enabled, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_battery_soft_poweroff(self):
|
||||
"""
|
||||
Check if soft power off is enabled.
|
||||
|
||||
:return: True if enabled, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_system_time(self):
|
||||
"""
|
||||
Get the system time.
|
||||
|
||||
:return: System time string.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_rtc_adjust_ppm(self):
|
||||
"""
|
||||
Get the RTC adjust PPM.
|
||||
|
||||
:return: RTC adjust PPM value.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_rtc_alarm_repeat(self):
|
||||
"""
|
||||
Get the RTC alarm repeat setting.
|
||||
|
||||
:return: RTC alarm repeat string.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_tap_enable(self, tap):
|
||||
"""
|
||||
Check if a specific tap (single, double, long) is enabled.
|
||||
|
||||
:param tap: Type of tap ('single', 'double', 'long').
|
||||
:return: True if enabled, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_tap_shell(self, tap):
|
||||
"""
|
||||
Get the shell command associated with a specific tap.
|
||||
|
||||
:param tap: Type of tap ('single', 'double', 'long').
|
||||
:return: Shell command string.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_anti_mistouch(self):
|
||||
"""
|
||||
Check if anti-mistouch protection is enabled.
|
||||
|
||||
:return: True if enabled, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_temperature(self):
|
||||
"""
|
||||
Get the current temperature.
|
||||
|
||||
:return: Temperature in degrees Celsius.
|
||||
"""
|
||||
return self.temperature
|
||||
|
||||
def get_battery_power_plugged(self):
|
||||
"""
|
||||
Check if the battery is plugged in.
|
||||
|
||||
:return: True if plugged in, False otherwise.
|
||||
"""
|
||||
return self.power_plugged
|
||||
|
||||
def get_battery_charging(self):
|
||||
"""
|
||||
Check if the battery is currently charging.
|
||||
|
||||
:return: True if charging, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def rtc_web(self):
|
||||
"""
|
||||
Synchronize RTC with web time.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class PiSugar(plugins.Plugin):
|
||||
__author__ = "jayofelony"
|
||||
@ -22,15 +543,25 @@ class PiSugar(plugins.Plugin):
|
||||
|
||||
def __init__(self):
|
||||
self._agent = None
|
||||
self.is_new_model = False
|
||||
self.options = dict()
|
||||
"""
|
||||
self.options = {
|
||||
'enabled': True,
|
||||
'rotation': False,
|
||||
'default_display': 'percentage',
|
||||
'lowpower_shutdown': True,
|
||||
'lowpower_shutdown_level': 10,
|
||||
'max_charge_voltage_protection': True
|
||||
}
|
||||
"""
|
||||
self.ps = None
|
||||
# logging.debug(f"[PiSugarX] {self.options}")
|
||||
try:
|
||||
conn, event_conn = connect_tcp()
|
||||
self.ps = PiSugarServer(conn, event_conn)
|
||||
self.ps = PiSugarServer()
|
||||
except Exception as e:
|
||||
# Log at debug to avoid clutter since it might be a false positive
|
||||
logging.debug("[PiSugarX] Unable to establish connection: %s", repr(e))
|
||||
logging.debug(
|
||||
"[PiSugarX] Unable to establish connection: %s", repr(e))
|
||||
|
||||
self.ready = False
|
||||
self.lasttemp = 69
|
||||
@ -49,7 +580,8 @@ class PiSugar(plugins.Plugin):
|
||||
try:
|
||||
return func()
|
||||
except Exception as e:
|
||||
logging.debug("[PiSugarX] Failed to get data using %s: %s", func.__name__, e)
|
||||
logging.debug(
|
||||
"[PiSugarX] Failed to get data using %s: %s", func.__name__, e)
|
||||
return default
|
||||
|
||||
def on_loaded(self):
|
||||
@ -60,20 +592,25 @@ class PiSugar(plugins.Plugin):
|
||||
|
||||
valid_displays = ['voltage', 'percentage', 'temp']
|
||||
if self.default_display not in valid_displays:
|
||||
logging.warning(f"[PiSugarX] Invalid default_display '{self.default_display}'. Using 'voltage'.")
|
||||
logging.warning(
|
||||
f"[PiSugarX] Invalid default_display '{self.default_display}'. Using 'voltage'.")
|
||||
self.default_display = 'voltage'
|
||||
|
||||
logging.info(f"[PiSugarX] Rotation is {'enabled' if self.rotation_enabled else 'disabled'}.")
|
||||
logging.info(f"[PiSugarX] Default display (when rotation disabled): {self.default_display}")
|
||||
logging.info(
|
||||
f"[PiSugarX] Rotation is {'enabled' if self.rotation_enabled else 'disabled'}.")
|
||||
logging.info(
|
||||
f"[PiSugarX] Default display (when rotation disabled): {self.default_display}")
|
||||
self.ps.lowpower_shutdown = self.options['lowpower_shutdown']
|
||||
self.ps.lowpower_shutdown_level = self.options['lowpower_shutdown_level']
|
||||
self.ps.max_charge_voltage_protection = self.options['max_charge_voltage_protection']
|
||||
|
||||
def on_ready(self, agent):
|
||||
self.ready = True
|
||||
self._agent = agent
|
||||
led_amount = self.safe_get(self.ps.get_battery_led_amount, default=0)
|
||||
if led_amount == 2:
|
||||
self.is_new_model = True
|
||||
else:
|
||||
self.is_new_model = False
|
||||
try:
|
||||
self.ready = self.ps.ready
|
||||
except Exception as e:
|
||||
# Log at debug to avoid clutter since it might be a false positive
|
||||
logging.warning(f"[PiSugarX] {e}")
|
||||
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
self._agent = agent
|
||||
@ -87,31 +624,52 @@ class PiSugar(plugins.Plugin):
|
||||
try:
|
||||
if request.method == "GET":
|
||||
if path == "/" or not path:
|
||||
version = self.safe_get(self.ps.get_version, default='Unknown')
|
||||
version = self.safe_get(
|
||||
self.ps.get_version, default='Unknown')
|
||||
model = self.safe_get(self.ps.get_model, default='Unknown')
|
||||
battery_level = self.safe_get(self.ps.get_battery_level, default='N/A')
|
||||
battery_voltage = self.safe_get(self.ps.get_battery_voltage, default='N/A')
|
||||
battery_current = self.safe_get(self.ps.get_battery_current, default='N/A')
|
||||
battery_led_amount = self.safe_get(self.ps.get_battery_led_amount, default='N/A') if model == 'Pisugar 2' else 'Not supported'
|
||||
battery_allow_charging = self.safe_get(self.ps.get_battery_allow_charging, default=False)
|
||||
battery_charging_range = self.safe_get(self.ps.get_battery_charging_range, default='N/A') if self.is_new_model or model == 'Pisugar 3' else 'Not supported'
|
||||
battery_full_charge_duration = getattr(self.ps, 'get_battery_full_charge_duration', lambda: 'N/A')()
|
||||
safe_shutdown_level = self.safe_get(self.ps.get_battery_safe_shutdown_level, default=None)
|
||||
battery_level = self.safe_get(
|
||||
self.ps.get_battery_level, default='N/A')
|
||||
battery_voltage = self.safe_get(
|
||||
self.ps.get_battery_voltage, default='N/A')
|
||||
battery_current = self.safe_get(
|
||||
self.ps.get_battery_current, default='N/A')
|
||||
battery_allow_charging = self.safe_get(
|
||||
self.ps.get_battery_allow_charging, default=False)
|
||||
battery_charging_range = self.safe_get(
|
||||
self.ps.get_battery_charging_range, default='N/A')
|
||||
battery_full_charge_duration = getattr(
|
||||
self.ps, 'get_battery_full_charge_duration', lambda: 'N/A')()
|
||||
safe_shutdown_level = self.safe_get(
|
||||
self.ps.get_battery_safe_shutdown_level, default=None)
|
||||
battery_safe_shutdown_level = f"{safe_shutdown_level}%" if safe_shutdown_level is not None else 'Not set'
|
||||
battery_safe_shutdown_delay = self.safe_get(self.ps.get_battery_safe_shutdown_delay, default='N/A')
|
||||
battery_auto_power_on = self.safe_get(self.ps.get_battery_auto_power_on, default=False)
|
||||
battery_soft_poweroff = self.safe_get(self.ps.get_battery_soft_poweroff, default=False) if model == 'Pisugar 3' else False
|
||||
system_time = self.safe_get(self.ps.get_system_time, default='N/A')
|
||||
rtc_adjust_ppm = self.safe_get(self.ps.get_rtc_adjust_ppm, default='Not supported') if model == 'Pisugar 3' else 'Not supported'
|
||||
rtc_alarm_repeat = self.safe_get(self.ps.get_rtc_alarm_repeat, default='N/A')
|
||||
single_tap_enabled = self.safe_get(lambda: self.ps.get_tap_enable(tap='single'), default=False)
|
||||
double_tap_enabled = self.safe_get(lambda: self.ps.get_tap_enable(tap='double'), default=False)
|
||||
long_tap_enabled = self.safe_get(lambda: self.ps.get_tap_enable(tap='long'), default=False)
|
||||
single_tap_shell = self.safe_get(lambda: self.ps.get_tap_shell(tap='single'), default='N/A')
|
||||
double_tap_shell = self.safe_get(lambda: self.ps.get_tap_shell(tap='double'), default='N/A')
|
||||
long_tap_shell = self.safe_get(lambda: self.ps.get_tap_shell(tap='long'), default='N/A')
|
||||
anti_mistouch = self.safe_get(self.ps.get_anti_mistouch, default=False) if model == 'Pisugar 3' else False
|
||||
temperature = self.safe_get(self.ps.get_temperature, default='N/A')
|
||||
battery_safe_shutdown_delay = self.safe_get(
|
||||
self.ps.get_battery_safe_shutdown_delay, default='N/A')
|
||||
battery_auto_power_on = self.safe_get(
|
||||
self.ps.get_battery_auto_power_on, default=False)
|
||||
battery_soft_poweroff = self.safe_get(
|
||||
self.ps.get_battery_soft_poweroff, default=False) if model == 'Pisugar 3' else False
|
||||
system_time = self.safe_get(
|
||||
self.ps.get_system_time, default='N/A')
|
||||
rtc_adjust_ppm = self.safe_get(
|
||||
self.ps.get_rtc_adjust_ppm, default='Not supported') if model == 'Pisugar 3' else 'Not supported'
|
||||
rtc_alarm_repeat = self.safe_get(
|
||||
self.ps.get_rtc_alarm_repeat, default='N/A')
|
||||
single_tap_enabled = self.safe_get(
|
||||
lambda: self.ps.get_tap_enable(tap='single'), default=False)
|
||||
double_tap_enabled = self.safe_get(
|
||||
lambda: self.ps.get_tap_enable(tap='double'), default=False)
|
||||
long_tap_enabled = self.safe_get(
|
||||
lambda: self.ps.get_tap_enable(tap='long'), default=False)
|
||||
single_tap_shell = self.safe_get(
|
||||
lambda: self.ps.get_tap_shell(tap='single'), default='N/A')
|
||||
double_tap_shell = self.safe_get(
|
||||
lambda: self.ps.get_tap_shell(tap='double'), default='N/A')
|
||||
long_tap_shell = self.safe_get(
|
||||
lambda: self.ps.get_tap_shell(tap='long'), default='N/A')
|
||||
anti_mistouch = self.safe_get(
|
||||
self.ps.get_anti_mistouch, default=False) if model == 'Pisugar 3' else False
|
||||
temperature = self.safe_get(
|
||||
self.ps.get_temperature, default='N/A')
|
||||
|
||||
ret = '''
|
||||
<!DOCTYPE html>
|
||||
@ -172,8 +730,7 @@ class PiSugar(plugins.Plugin):
|
||||
<tr><td>Battery Level</td><td>{battery_level}%</td></tr>
|
||||
<tr><td>Battery Voltage</td><td>{battery_voltage}V</td></tr>
|
||||
<tr><td>Battery Current</td><td>{battery_current}A</td></tr>
|
||||
<tr><td>Battery LED Amount</td><td>{battery_led_amount}</td></tr>
|
||||
<tr><td>Battery Allow Charging</td><td>{"Yes" if battery_allow_charging and self.is_new_model else "No"}</td></tr>
|
||||
<tr><td>Battery Allow Charging</td><td>{"Yes" if battery_allow_charging else "No"}</td></tr>
|
||||
<tr><td>Battery Charging Range</td><td>{battery_charging_range}</td></tr>
|
||||
<tr><td>Duration of Keep Charging When Full</td><td>{battery_full_charge_duration} seconds</td></tr>
|
||||
<tr><td>Battery Safe Shutdown Level</td><td>{battery_safe_shutdown_level}</td></tr>
|
||||
@ -240,13 +797,25 @@ class PiSugar(plugins.Plugin):
|
||||
# Make sure "bat" is in the UI state (guard to prevent KeyError)
|
||||
if 'bat' not in ui._state._state:
|
||||
return
|
||||
try:
|
||||
self.ready = self.ps.ready
|
||||
except Exception as e:
|
||||
# Log at debug to avoid clutter since it might be a false positive
|
||||
logging.warning(f"[PiSugarX] {e}")
|
||||
if self.ready:
|
||||
capacity = self.safe_get(self.ps.get_battery_level, default=0)
|
||||
voltage = self.safe_get(self.ps.get_battery_voltage, default=0.00)
|
||||
temp = self.safe_get(self.ps.get_temperature, default=0)
|
||||
|
||||
capacity = self.safe_get(self.ps.get_battery_level, default=0)
|
||||
voltage = self.safe_get(self.ps.get_battery_voltage, default=0.00)
|
||||
temp = self.safe_get(self.ps.get_temperature, default=0)
|
||||
else:
|
||||
capacity = 0
|
||||
voltage = 0.00
|
||||
temp = 0
|
||||
logging.info(f"[PiSugarX] PiSugar is not ready")
|
||||
|
||||
# Check if battery is plugged in
|
||||
battery_plugged = self.safe_get(self.ps.get_battery_power_plugged, default=False)
|
||||
battery_plugged = self.safe_get(
|
||||
self.ps.get_battery_power_plugged, default=False)
|
||||
|
||||
if battery_plugged:
|
||||
# If plugged in, display "CHG"
|
||||
@ -275,12 +844,3 @@ class PiSugar(plugins.Plugin):
|
||||
ui.set('bat', f"{capacity:.0f}%")
|
||||
elif self.default_display == 'temp':
|
||||
ui.set('bat', f"{temp}°C")
|
||||
|
||||
charging = self.safe_get(self.ps.get_battery_charging, default=None)
|
||||
safe_shutdown_level = self.safe_get(self.ps.get_battery_safe_shutdown_level, default=0)
|
||||
if charging is not None:
|
||||
if capacity <= safe_shutdown_level:
|
||||
logging.info(
|
||||
f"[PiSugarX] Empty battery (<= {safe_shutdown_level}%): shutting down"
|
||||
)
|
||||
ui.update(force=True, new_data={"status": "Battery exhausted, bye ..."})
|
||||
|
81
pwnagotchi/plugins/default/pwncrack.py
Normal file
81
pwnagotchi/plugins/default/pwncrack.py
Normal file
@ -0,0 +1,81 @@
|
||||
import time
|
||||
import os
|
||||
import subprocess
|
||||
import requests
|
||||
import logging
|
||||
import socket
|
||||
from pwnagotchi.plugins import Plugin
|
||||
|
||||
class UploadConvertPlugin(Plugin):
|
||||
__author__ = 'Terminatoror'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Converts .pcap files to .hc22000 and uploads them to pwncrack.org when internet is available.'
|
||||
|
||||
def __init__(self):
|
||||
self.server_url = 'http://pwncrack.org/upload_handshake' # Leave this as is
|
||||
self.potfile_url = 'http://pwncrack.org/download_potfile_script' # Leave this as is
|
||||
self.timewait = 600
|
||||
self.last_run_time = 0
|
||||
self.options = dict()
|
||||
|
||||
def on_loaded(self):
|
||||
logging.info('[pwncrack] loading')
|
||||
|
||||
def on_config_changed(self, config):
|
||||
self.handshake_dir = config["bettercap"].get("handshakes")
|
||||
self.key = self.options.get('key', "") # Change this to your key
|
||||
self.whitelist = config["main"].get("whitelist", [])
|
||||
self.combined_file = os.path.join(self.handshake_dir, 'combined.hc22000')
|
||||
self.potfile_path = os.path.join(self.handshake_dir, 'cracked.pwncrack.potfile')
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
current_time = time.time()
|
||||
remaining_wait_time = self.timewait - (current_time - self.last_run_time)
|
||||
if remaining_wait_time > 0:
|
||||
logging.debug(f"[pwncrack] Waiting {remaining_wait_time:.1f} more seconds before next run.")
|
||||
return
|
||||
self.last_run_time = current_time
|
||||
logging.info(f"[pwncrack] Running upload process. Key: {self.key}, waiting: {self.timewait} seconds.")
|
||||
try:
|
||||
self._convert_and_upload()
|
||||
self._download_potfile()
|
||||
except Exception as e:
|
||||
logging.error(f"[pwncrack] Error occurred during upload process: {e}", exc_info=True)
|
||||
|
||||
def _convert_and_upload(self):
|
||||
# Convert all .pcap files to .hc22000, excluding files matching whitelist items
|
||||
pcap_files = [f for f in os.listdir(self.handshake_dir)
|
||||
if f.endswith('.pcap') and not any(item in f for item in self.whitelist)]
|
||||
if pcap_files:
|
||||
for pcap_file in pcap_files:
|
||||
subprocess.run(['hcxpcapngtool', '-o', self.combined_file, os.path.join(self.handshake_dir, pcap_file)])
|
||||
|
||||
# Ensure the combined file is created
|
||||
if not os.path.exists(self.combined_file):
|
||||
open(self.combined_file, 'w').close()
|
||||
|
||||
# Upload the combined .hc22000 file
|
||||
with open(self.combined_file, 'rb') as file:
|
||||
files = {'handshake': file}
|
||||
data = {'key': self.key}
|
||||
response = requests.post(self.server_url, files=files, data=data)
|
||||
|
||||
# Log the response
|
||||
logging.info(f"[pwncrack] Upload response: {response.json()}")
|
||||
os.remove(self.combined_file) # Remove the combined.hc22000 file
|
||||
else:
|
||||
logging.info("[pwncrack] No .pcap files found to convert (or all files are whitelisted).")
|
||||
|
||||
def _download_potfile(self):
|
||||
response = requests.get(self.potfile_url, params={'key': self.key})
|
||||
if response.status_code == 200:
|
||||
with open(self.potfile_path, 'w') as file:
|
||||
file.write(response.text)
|
||||
logging.info(f"[pwncrack] Potfile downloaded to {self.potfile_path}")
|
||||
else:
|
||||
logging.error(f"[pwncrack] Failed to download potfile: {response.status_code}")
|
||||
logging.error(f"[pwncrack] {response.json()}") # Log the error message from the server
|
||||
|
||||
def on_unload(self, ui):
|
||||
logging.info('[pwncrack] unloading')
|
@ -4,207 +4,368 @@ import json
|
||||
import csv
|
||||
import requests
|
||||
import pwnagotchi
|
||||
|
||||
from io import StringIO
|
||||
from datetime import datetime
|
||||
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile, remove_whitelisted
|
||||
import re
|
||||
from glob import glob
|
||||
from threading import Lock
|
||||
from io import StringIO
|
||||
from datetime import datetime, UTC
|
||||
from dataclasses import dataclass
|
||||
|
||||
from flask import make_response, redirect
|
||||
from pwnagotchi.utils import (
|
||||
WifiInfo,
|
||||
FieldNotFoundError,
|
||||
extract_from_pcap,
|
||||
StatusFile,
|
||||
remove_whitelisted,
|
||||
)
|
||||
from pwnagotchi import plugins
|
||||
from pwnagotchi.plugins.default.cache import read_ap_cache
|
||||
from pwnagotchi._version import __version__ as __pwnagotchi_version__
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.components import Text
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
|
||||
def _extract_gps_data(path):
|
||||
"""
|
||||
Extract data from gps-file
|
||||
|
||||
return json-obj
|
||||
"""
|
||||
|
||||
try:
|
||||
if path.endswith('.geo.json'):
|
||||
with open(path, 'r') as json_file:
|
||||
tempJson = json.load(json_file)
|
||||
d = datetime.utcfromtimestamp(int(tempJson["ts"]))
|
||||
return {"Latitude": tempJson["location"]["lat"],
|
||||
"Longitude": tempJson["location"]["lng"],
|
||||
"Altitude": 10,
|
||||
"Accuracy": tempJson["accuracy"],
|
||||
"Updated": d.strftime('%Y-%m-%dT%H:%M:%S.%f')}
|
||||
else:
|
||||
with open(path, 'r') as json_file:
|
||||
return json.load(json_file)
|
||||
except OSError as os_err:
|
||||
raise os_err
|
||||
except json.JSONDecodeError as json_err:
|
||||
raise json_err
|
||||
from scapy.all import Scapy_Exception
|
||||
|
||||
|
||||
def _format_auth(data):
|
||||
out = ""
|
||||
for auth in data:
|
||||
out = f"{out}[{auth}]"
|
||||
return [f"{auth}" for auth in data]
|
||||
@dataclass
|
||||
class WigleStatistics:
|
||||
ready: bool = False
|
||||
username: str = None
|
||||
rank: int = None
|
||||
monthrank: int = None
|
||||
discoveredwiFi: int = None
|
||||
last: str = None
|
||||
groupID: str = None
|
||||
groupname: str = None
|
||||
grouprank: int = None
|
||||
|
||||
def update_user(self, json_res):
|
||||
self.ready = True
|
||||
self.username = json_res["user"]
|
||||
self.rank = json_res["rank"]
|
||||
self.monthrank = json_res["monthRank"]
|
||||
self.discoveredwiFi = json_res["statistics"]["discoveredWiFi"]
|
||||
last = json_res["statistics"]["last"]
|
||||
self.last = f"{last[6:8]}/{last[4:6]}/{last[0:4]}"
|
||||
|
||||
def _transform_wigle_entry(gps_data, pcap_data, plugin_version):
|
||||
"""
|
||||
Transform to wigle entry in file
|
||||
"""
|
||||
dummy = StringIO()
|
||||
# write kismet header
|
||||
dummy.write(f"WigleWifi-1.6,appRelease={plugin_version},model=pwnagotchi,release={__pwnagotchi_version__},"
|
||||
f"device={pwnagotchi.name()},display=kismet,board=RaspberryPi,brand=pwnagotchi,star=Sol,body=3,subBody=0\n")
|
||||
dummy.write(
|
||||
"MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type\n")
|
||||
def update_user_group(self, json_res):
|
||||
self.groupID = json_res["groupId"]
|
||||
self.groupname = json_res["groupName"]
|
||||
|
||||
writer = csv.writer(dummy, delimiter=",", quoting=csv.QUOTE_NONE, escapechar="\\")
|
||||
writer.writerow([
|
||||
pcap_data[WifiInfo.BSSID],
|
||||
pcap_data[WifiInfo.ESSID],
|
||||
_format_auth(pcap_data[WifiInfo.ENCRYPTION]),
|
||||
datetime.strptime(gps_data['Updated'].rsplit('.')[0],
|
||||
"%Y-%m-%dT%H:%M:%S").strftime('%Y-%m-%d %H:%M:%S'),
|
||||
pcap_data[WifiInfo.CHANNEL],
|
||||
pcap_data[WifiInfo.RSSI],
|
||||
gps_data['Latitude'],
|
||||
gps_data['Longitude'],
|
||||
gps_data['Altitude'],
|
||||
gps_data['Accuracy'],
|
||||
'WIFI'])
|
||||
return dummy.getvalue()
|
||||
|
||||
|
||||
def _send_to_wigle(lines, api_key, donate=True, timeout=30):
|
||||
"""
|
||||
Uploads the file to wigle-net
|
||||
"""
|
||||
|
||||
dummy = StringIO()
|
||||
|
||||
for line in lines:
|
||||
dummy.write(f"{line}")
|
||||
|
||||
dummy.seek(0)
|
||||
|
||||
headers = {"Authorization": f"Basic {api_key}",
|
||||
"Accept": "application/json",
|
||||
"HTTP_USER_AGENT": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1"}
|
||||
data = {"donate": "on" if donate else "false"}
|
||||
payload = {"file": (pwnagotchi.name() + ".csv", dummy, "multipart/form-data", {"Expires": "0"})}
|
||||
try:
|
||||
res = requests.post('https://api.wigle.net/api/v2/file/upload',
|
||||
data=data,
|
||||
headers=headers,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
json_res = res.json()
|
||||
if not json_res['success']:
|
||||
raise requests.exceptions.RequestException(json_res['message'])
|
||||
except requests.exceptions.RequestException as re_e:
|
||||
raise re_e
|
||||
def update_group(self, json_res):
|
||||
rank = 1
|
||||
for group in json_res["groups"]:
|
||||
if group["groupId"] == self.groupID:
|
||||
self.grouprank = rank
|
||||
rank += 1
|
||||
|
||||
|
||||
class Wigle(plugins.Plugin):
|
||||
__author__ = "Dadav and updated by Jayofelony"
|
||||
__version__ = "3.0.1"
|
||||
__author__ = "Dadav and updated by Jayofelony and fmatray"
|
||||
__version__ = "4.1.0"
|
||||
__license__ = "GPL3"
|
||||
__description__ = "This plugin automatically uploads collected WiFi to wigle.net"
|
||||
LABEL_SPACING = 0
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.report = StatusFile('/root/.wigle_uploads', data_format='json')
|
||||
self.report = None
|
||||
self.skip = list()
|
||||
self.lock = Lock()
|
||||
self.options = dict()
|
||||
self.statistics = WigleStatistics()
|
||||
self.last_stat = datetime.now(tz=UTC)
|
||||
self.ui_counter = 0
|
||||
|
||||
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.debug("WIGLE: api_key isn't set. Can't upload to wigle.net")
|
||||
logging.info("[WIGLE] plugin loaded.")
|
||||
|
||||
def on_config_changed(self, config):
|
||||
self.api_key = self.options.get("api_key", None)
|
||||
if not self.api_key:
|
||||
logging.info("[WIGLE] api_key must be set.")
|
||||
return
|
||||
|
||||
if 'donate' not in self.options:
|
||||
self.options['donate'] = False
|
||||
|
||||
self.donate = self.options.get("donate", False)
|
||||
self.handshake_dir = config["bettercap"].get("handshakes")
|
||||
report_filename = os.path.join(self.handshake_dir, ".wigle_uploads")
|
||||
self.report = StatusFile(report_filename, data_format="json")
|
||||
self.cache_dir = os.path.join(self.handshake_dir, "cache")
|
||||
self.cvs_dir = self.options.get("cvs_dir", None)
|
||||
self.whitelist = config["main"].get("whitelist", [])
|
||||
self.timeout = self.options.get("timeout", 30)
|
||||
self.position = self.options.get("position", (10, 10))
|
||||
self.ready = True
|
||||
logging.info("WIGLE: ready")
|
||||
logging.info("[WIGLE] Ready for wardriving!!!")
|
||||
self.get_statistics(force=True)
|
||||
|
||||
def on_webhook(self, path, request):
|
||||
return make_response(redirect("https://www.wigle.net/", code=302))
|
||||
|
||||
def get_new_gps_files(self, reported):
|
||||
all_gps_files = glob(os.path.join(self.handshake_dir, "*.gps.json"))
|
||||
all_gps_files += glob(os.path.join(self.handshake_dir, "*.geo.json"))
|
||||
all_gps_files = remove_whitelisted(all_gps_files, self.whitelist)
|
||||
return set(all_gps_files) - set(reported) - set(self.skip)
|
||||
|
||||
@staticmethod
|
||||
def get_pcap_filename(gps_file):
|
||||
pcap_filename = re.sub(r"\.(geo|gps)\.json$", ".pcap", gps_file)
|
||||
if not os.path.exists(pcap_filename):
|
||||
logging.debug("[WIGLE] Can't find pcap for %s", gps_file)
|
||||
return None
|
||||
return pcap_filename
|
||||
|
||||
@staticmethod
|
||||
def extract_gps_data(path):
|
||||
"""
|
||||
Extract data from gps-file
|
||||
return json-obj
|
||||
"""
|
||||
try:
|
||||
if path.endswith(".geo.json"):
|
||||
with open(path, "r") as json_file:
|
||||
tempJson = json.load(json_file)
|
||||
d = datetime.fromtimestamp(int(tempJson["ts"]), tz=UTC)
|
||||
return {
|
||||
"Latitude": tempJson["location"]["lat"],
|
||||
"Longitude": tempJson["location"]["lng"],
|
||||
"Altitude": 10,
|
||||
"Accuracy": tempJson["accuracy"],
|
||||
"Updated": d.strftime("%Y-%m-%dT%H:%M:%S.%f"),
|
||||
}
|
||||
with open(path, "r") as json_file:
|
||||
return json.load(json_file)
|
||||
except (OSError, json.JSONDecodeError) as exp:
|
||||
raise exp
|
||||
|
||||
def get_gps_data(self, gps_file):
|
||||
try:
|
||||
gps_data = self.extract_gps_data(gps_file)
|
||||
except (OSError, json.JSONDecodeError) as exp:
|
||||
logging.debug(f"[WIGLE] Error while extracting GPS data: {exp}")
|
||||
return None
|
||||
if gps_data["Latitude"] == 0 and gps_data["Longitude"] == 0:
|
||||
logging.debug(f"[WIGLE] Not enough gps data for {gps_file}. Next time.")
|
||||
return None
|
||||
return gps_data
|
||||
|
||||
def get_pcap_data(self, pcap_filename):
|
||||
try:
|
||||
if cache := read_ap_cache(self.cache_dir, self.pcap_filename):
|
||||
logging.info(f"[WIGLE] Using cache for {pcap_filename}")
|
||||
return {
|
||||
WifiInfo.BSSID: cache["mac"],
|
||||
WifiInfo.ESSID: cache["hostname"],
|
||||
WifiInfo.ENCRYPTION: cache["encryption"],
|
||||
WifiInfo.CHANNEL: cache["channel"],
|
||||
WifiInfo.FREQUENCY: cache["frequency"],
|
||||
WifiInfo.RSSI: cache["rssi"],
|
||||
}
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
try:
|
||||
pcap_data = extract_from_pcap(
|
||||
pcap_filename,
|
||||
[
|
||||
WifiInfo.BSSID,
|
||||
WifiInfo.ESSID,
|
||||
WifiInfo.ENCRYPTION,
|
||||
WifiInfo.CHANNEL,
|
||||
WifiInfo.FREQUENCY,
|
||||
WifiInfo.RSSI,
|
||||
],
|
||||
)
|
||||
logging.debug(f"[WIGLE] PCAP data for {pcap_filename}: {pcap_data}")
|
||||
except FieldNotFoundError:
|
||||
logging.debug(f"[WIGLE] Cannot extract all data: {pcap_filename} (skipped)")
|
||||
return None
|
||||
except Scapy_Exception as sc_e:
|
||||
logging.debug(f"[WIGLE] {sc_e}")
|
||||
return None
|
||||
return pcap_data
|
||||
|
||||
def generate_csv(self, data):
|
||||
date = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{pwnagotchi.name()}_{date}.csv"
|
||||
|
||||
content = StringIO()
|
||||
# write kismet header + header
|
||||
content.write(
|
||||
f"WigleWifi-1.6,appRelease={self.__version__},model=pwnagotchi,release={__pwnagotchi_version__},"
|
||||
f"device={pwnagotchi.name()},display=kismet,board=RaspberryPi,brand=pwnagotchi,star=Sol,body=3,subBody=0\n"
|
||||
f"MAC,SSID,AuthMode,FirstSeen,Channel,Frequency,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,RCOIs,MfgrId,Type\n"
|
||||
)
|
||||
writer = csv.writer(content, delimiter=",", quoting=csv.QUOTE_NONE, escapechar="\\")
|
||||
for gps_data, pcap_data in data: # write WIFIs
|
||||
try:
|
||||
timestamp = datetime.strptime(
|
||||
gps_data["Updated"].rsplit(".")[0], "%Y-%m-%dT%H:%M:%S"
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
timestamp = datetime.strptime(
|
||||
gps_data["Updated"].rsplit(".")[0], "%Y-%m-%d %H:%M:%S"
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
writer.writerow(
|
||||
[
|
||||
pcap_data[WifiInfo.BSSID],
|
||||
pcap_data[WifiInfo.ESSID],
|
||||
f"[{']['.join(pcap_data[WifiInfo.ENCRYPTION])}]",
|
||||
timestamp,
|
||||
pcap_data[WifiInfo.CHANNEL],
|
||||
pcap_data[WifiInfo.FREQUENCY],
|
||||
pcap_data[WifiInfo.RSSI],
|
||||
gps_data["Latitude"],
|
||||
gps_data["Longitude"],
|
||||
gps_data["Altitude"],
|
||||
gps_data["Accuracy"],
|
||||
"", # RCOIs to populate
|
||||
"", # MfgrId always empty
|
||||
"WIFI",
|
||||
]
|
||||
)
|
||||
content.seek(0)
|
||||
return filename, content
|
||||
|
||||
def save_to_file(self, cvs_filename, cvs_content):
|
||||
if not self.cvs_dir:
|
||||
return
|
||||
filename = os.path.join(self.cvs_dir, cvs_filename)
|
||||
logging.info(f"[WIGLE] Saving to file {filename}")
|
||||
try:
|
||||
with open(filename, mode="w") as f:
|
||||
f.write(cvs_content.getvalue())
|
||||
except Exception as exp:
|
||||
logging.error(f"[WIGLE] Error while writing CSV file(skipping): {exp}")
|
||||
|
||||
def post_wigle(self, reported, cvs_filename, cvs_content, no_err_entries):
|
||||
try:
|
||||
json_res = requests.post(
|
||||
"https://api.wigle.net/api/v2/file/upload",
|
||||
headers={
|
||||
"Authorization": f"Basic {self.api_key}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
data={"donate": "on" if self.donate else "false"},
|
||||
files=dict(file=(cvs_filename, cvs_content, "text/csv")),
|
||||
timeout=self.timeout,
|
||||
).json()
|
||||
if not json_res["success"]:
|
||||
raise requests.exceptions.RequestException(json_res["message"])
|
||||
reported += no_err_entries
|
||||
self.report.update(data={"reported": reported})
|
||||
logging.info(f"[WIGLE] Successfully uploaded {len(no_err_entries)} wifis")
|
||||
except (requests.exceptions.RequestException, OSError) as exp:
|
||||
self.skip += no_err_entries
|
||||
logging.debug(f"[WIGLE] Exception while uploading: {exp}")
|
||||
|
||||
def upload_new_handshakes(self, reported, new_gps_files, agent):
|
||||
logging.info("[WIGLE] Uploading new handshakes to wigle.net")
|
||||
csv_entries, no_err_entries = list(), list()
|
||||
for gps_file in new_gps_files:
|
||||
logging.info(f"[WIGLE] Processing {os.path.basename(gps_file)}")
|
||||
if (
|
||||
(pcap_filename := self.get_pcap_filename(gps_file))
|
||||
and (gps_data := self.get_gps_data(gps_file))
|
||||
and (pcap_data := self.get_pcap_data(pcap_filename))
|
||||
):
|
||||
csv_entries.append((gps_data, pcap_data))
|
||||
no_err_entries.append(gps_file)
|
||||
else:
|
||||
self.skip.append(gps_file)
|
||||
logging.info(f"[WIGLE] Wifi to upload: {len(csv_entries)}")
|
||||
if csv_entries:
|
||||
cvs_filename, cvs_content = self.generate_csv(csv_entries)
|
||||
self.save_to_file(cvs_filename, cvs_content)
|
||||
display = agent.view()
|
||||
display.on_uploading("wigle.net")
|
||||
self.post_wigle(reported, cvs_filename, cvs_content, no_err_entries)
|
||||
display.on_normal()
|
||||
|
||||
def request_statistics(self, url):
|
||||
try:
|
||||
return requests.get(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Basic {self.api_key}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
timeout=self.timeout,
|
||||
).json()
|
||||
except (requests.exceptions.RequestException, OSError) as exp:
|
||||
return None
|
||||
|
||||
def get_user_statistics(self):
|
||||
json_res = self.request_statistics(
|
||||
"https://api.wigle.net/api/v2/stats/user",
|
||||
)
|
||||
if json_res and json_res["success"]:
|
||||
self.statistics.update_user(json_res)
|
||||
|
||||
def get_usergroup_statistics(self):
|
||||
if not self.statistics.username or self.statistics.groupID:
|
||||
return
|
||||
url = f"https://api.wigle.net/api/v2/group/groupForUser/{self.statistics.username}"
|
||||
if json_res := self.request_statistics(url):
|
||||
self.statistics.update_user_group(json_res)
|
||||
|
||||
def get_group_statistics(self):
|
||||
if not self.statistics.groupID:
|
||||
return
|
||||
json_res = self.request_statistics("https://api.wigle.net/api/v2/stats/group")
|
||||
if json_res and json_res["success"]:
|
||||
self.statistics.update_group(json_res)
|
||||
|
||||
def get_statistics(self, force=False):
|
||||
if force or (datetime.now(tz=UTC) - self.last_stat).total_seconds() > 30:
|
||||
self.last_stat = datetime.now(tz=UTC)
|
||||
self.get_user_statistics()
|
||||
self.get_usergroup_statistics()
|
||||
self.get_group_statistics()
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
"""
|
||||
Called when there's internet connectivity
|
||||
"""
|
||||
if not self.ready or self.lock.locked():
|
||||
if not self.ready:
|
||||
return
|
||||
with self.lock:
|
||||
reported = self.report.data_field_or("reported", default=list())
|
||||
if new_gps_files := self.get_new_gps_files(reported):
|
||||
self.upload_new_handshakes(reported, new_gps_files, agent)
|
||||
else:
|
||||
self.get_statistics()
|
||||
|
||||
from scapy.all import Scapy_Exception
|
||||
def on_ui_setup(self, ui):
|
||||
with ui._lock:
|
||||
ui.add_element(
|
||||
"wigle",
|
||||
Text(value="-", position=self.position, font=fonts.Small, color=BLACK),
|
||||
)
|
||||
|
||||
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') or filename.endswith('.geo.json')]
|
||||
def on_unload(self, ui):
|
||||
with ui._lock:
|
||||
try:
|
||||
ui.remove_element("wigle")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
all_gps_files = remove_whitelisted(all_gps_files, config['main']['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:
|
||||
if gps_file.endswith('.gps.json'):
|
||||
pcap_filename = gps_file.replace('.gps.json', '.pcap')
|
||||
if gps_file.endswith('.geo.json'):
|
||||
pcap_filename = gps_file.replace('.geo.json', '.pcap')
|
||||
if not os.path.exists(pcap_filename):
|
||||
logging.debug("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.debug("WIGLE: %s", os_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except json.JSONDecodeError as json_err:
|
||||
logging.debug("WIGLE: %s", json_err)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
|
||||
logging.debug("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.debug("WIGLE: Could not extract all information. Skip %s", gps_file)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
except Scapy_Exception as sc_e:
|
||||
logging.debug("WIGLE: %s", sc_e)
|
||||
self.skip.append(gps_file)
|
||||
continue
|
||||
new_entry = _transform_wigle_entry(gps_data, pcap_data, self.__version__)
|
||||
csv_entries.append(new_entry)
|
||||
no_err_entries.append(gps_file)
|
||||
if csv_entries:
|
||||
display.on_uploading('wigle.net')
|
||||
|
||||
try:
|
||||
_send_to_wigle(csv_entries, self.options['api_key'], donate=self.options['donate'])
|
||||
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.debug("WIGLE: Got an exception while uploading %s", re_e)
|
||||
except OSError as os_e:
|
||||
self.skip += no_err_entries
|
||||
logging.debug("WIGLE: Got the following error: %s", os_e)
|
||||
|
||||
display.on_normal()
|
||||
def on_ui_update(self, ui):
|
||||
with ui._lock:
|
||||
if not (self.ready and self.statistics.ready):
|
||||
ui.set("wigle", "We Will Wait Wigle")
|
||||
return
|
||||
msg = "-"
|
||||
self.ui_counter = (self.ui_counter + 1) % 6
|
||||
if self.ui_counter == 0:
|
||||
msg = f"User:{self.statistics.username}"
|
||||
if self.ui_counter == 1:
|
||||
msg = f"Rank:{self.statistics.rank} Month:{self.statistics.monthrank}"
|
||||
elif self.ui_counter == 2:
|
||||
msg = f"{self.statistics.discoveredwiFi} discovered WiFis"
|
||||
elif self.ui_counter == 3:
|
||||
msg = f"Last upl.:{self.statistics.last}"
|
||||
elif self.ui_counter == 4:
|
||||
msg = f"Grp:{self.statistics.groupname}"
|
||||
elif self.ui_counter == 5:
|
||||
msg = f"Grp rank:{self.statistics.grouprank}"
|
||||
ui.set("wigle", msg)
|
||||
|
@ -1,7 +1,6 @@
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from pwnagotchi.utils import StatusFile, remove_whitelisted
|
||||
|
@ -319,7 +319,7 @@ class Display(View):
|
||||
def image(self):
|
||||
img = None
|
||||
if self._canvas is not None:
|
||||
img = self._canvas if self._rotation == 0 else self._canvas.rotate(-self._rotation)
|
||||
img = self._canvas if self._rotation == 0 else self._canvas.rotate(-self._rotation, expand=True)
|
||||
return img
|
||||
|
||||
def _render_thread(self):
|
||||
@ -338,7 +338,7 @@ class Display(View):
|
||||
logging.error("%s" % e)
|
||||
|
||||
if self._enabled:
|
||||
self._canvas = (img if self._rotation == 0 else img.rotate(self._rotation))
|
||||
self._canvas = (img if self._rotation == 0 else img.rotate(self._rotation, expand=True))
|
||||
if self._implementation is not None:
|
||||
self._canvas_next = self._canvas
|
||||
self._canvas_next_event.set()
|
||||
|
@ -7,6 +7,7 @@ from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
class DummyDisplay(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(DummyDisplay, self).__init__(config, 'DummyDisplay')
|
||||
self._display = self
|
||||
|
||||
def layout(self):
|
||||
width = 480 if 'width' not in self.config else self.config['width']
|
||||
@ -25,7 +26,7 @@ class DummyDisplay(DisplayImpl):
|
||||
self._layout['friend_name'] = (int(width/12), int(height/10))
|
||||
self._layout['shakes'] = (0, height-int(height/25))
|
||||
self._layout['mode'] = (width-int(width/8), height - int (height/25))
|
||||
lw, lh = fonts.Small.getbbox("W")
|
||||
lw, lh = fonts.Small.getsize("W")
|
||||
self._layout['status'] = {
|
||||
'pos': (int(width/48), int(height/3)),
|
||||
'font': fonts.status_font(fonts.Small),
|
||||
|
@ -49,8 +49,14 @@ class View(object):
|
||||
self._voice = Voice(lang=config['main']['lang'])
|
||||
self._implementation = impl
|
||||
self._layout = impl.layout()
|
||||
self._width = self._layout['width']
|
||||
self._height = self._layout['height']
|
||||
self._rotation = config['ui']['display'].get('rotation',0)
|
||||
if (self._rotation/90)%2 == 0:
|
||||
self._width = self._layout['width']
|
||||
self._height = self._layout['height']
|
||||
else:
|
||||
self._width = self._layout['height']
|
||||
self._height = self._layout['width']
|
||||
|
||||
self._state = State(state={
|
||||
'channel': LabeledValue(color=BLACK, label='CH', value='00', position=self._layout['channel'],
|
||||
label_font=fonts.Bold,
|
||||
|
@ -64,11 +64,14 @@ class Handler:
|
||||
def with_auth(self, f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
auth = request.authorization
|
||||
if not auth or not auth.username or not auth.password or not self._check_creds(auth.username,
|
||||
auth.password):
|
||||
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'})
|
||||
return f(*args, **kwargs)
|
||||
if not self._config['auth']:
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
auth = request.authorization
|
||||
if not auth or not auth.username or not auth.password or not self._check_creds(auth.username,
|
||||
auth.password):
|
||||
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'})
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
@ -5,61 +5,13 @@ import subprocess
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import toml
|
||||
import sys
|
||||
import re
|
||||
import tomlkit
|
||||
|
||||
from toml.encoder import TomlEncoder, _dump_str
|
||||
from zipfile import ZipFile
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DottedTomlEncoder(TomlEncoder):
|
||||
"""
|
||||
Dumps the toml into the dotted-key format
|
||||
"""
|
||||
|
||||
def __init__(self, _dict=dict):
|
||||
super(DottedTomlEncoder, self).__init__(_dict)
|
||||
|
||||
def dump_list(self, v):
|
||||
retval = "["
|
||||
# 1 line if its just 1 item; therefore no newline
|
||||
if len(v) > 1:
|
||||
retval += "\n"
|
||||
for u in v:
|
||||
retval += " " + str(self.dump_value(u)) + ",\n"
|
||||
# 1 line if its just 1 item; remove newline
|
||||
if len(v) <= 1:
|
||||
retval = retval.rstrip("\n")
|
||||
retval += "]"
|
||||
return retval
|
||||
|
||||
def dump_sections(self, o, sup):
|
||||
retstr = ""
|
||||
pre = ""
|
||||
|
||||
if sup:
|
||||
pre = sup + "."
|
||||
|
||||
for section, value in o.items():
|
||||
section = str(section)
|
||||
qsection = section
|
||||
if not re.match(r'^[A-Za-z0-9_-]+$', section):
|
||||
qsection = _dump_str(section)
|
||||
if value is not None:
|
||||
if isinstance(value, dict):
|
||||
toadd, _ = self.dump_sections(value, pre + qsection)
|
||||
retstr += toadd
|
||||
# separte sections
|
||||
if not retstr.endswith('\n\n'):
|
||||
retstr += '\n'
|
||||
else:
|
||||
retstr += (pre + qsection + " = " + str(self.dump_value(value)) + '\n')
|
||||
return retstr, self._dict()
|
||||
|
||||
|
||||
def parse_version(version):
|
||||
"""
|
||||
Converts a version str to tuple, so that versions can be compared
|
||||
@ -150,7 +102,8 @@ def keys_to_str(data):
|
||||
|
||||
def save_config(config, target):
|
||||
with open(target, 'wt') as fp:
|
||||
fp.write(toml.dumps(config, encoder=DottedTomlEncoder()))
|
||||
fp.write(tomlkit.dumps(config))
|
||||
#fp.write(toml.dumps(config, encoder=DottedTomlEncoder()))
|
||||
return True
|
||||
|
||||
|
||||
@ -198,9 +151,33 @@ def load_config(args):
|
||||
print("!!! file in %s is different than release defaults, overwriting !!!" % args.config)
|
||||
shutil.copy(ref_defaults_file, args.config)
|
||||
|
||||
|
||||
def load_toml_file(filename):
|
||||
"""Load toml data from a file. Use toml for dotted, tomlkit for nice formatted"""
|
||||
with open(filename) as fp:
|
||||
text = fp.read()
|
||||
# look for "[main]". if not there, then load
|
||||
# dotted toml with toml instead of tomlkit
|
||||
if text.find("[main]") != -1:
|
||||
return tomlkit.loads(text)
|
||||
else:
|
||||
print("Converting dotted toml %s: %s" % (filename, text[0:100]))
|
||||
import toml
|
||||
data = toml.loads(text)
|
||||
# save original as a backup
|
||||
try:
|
||||
backup = filename + ".ORIG"
|
||||
os.rename(filename, backup)
|
||||
with open(filename, "w") as fp2:
|
||||
tomlkit.dump(data, fp2)
|
||||
print("Converted to new format. Original saved at %s" % backup)
|
||||
except Exception as e:
|
||||
print("Unable to convert %s to new format: %s" % (backup, e))
|
||||
return data
|
||||
|
||||
# load the defaults
|
||||
with open(args.config) as fp:
|
||||
config = toml.load(fp)
|
||||
config = load_toml_file(args.config)
|
||||
|
||||
|
||||
# load the user config
|
||||
try:
|
||||
@ -216,10 +193,10 @@ def load_config(args):
|
||||
# convert int/float keys to str
|
||||
user_config = keys_to_str(user_config)
|
||||
# convert to toml but use loaded yaml
|
||||
toml.dump(user_config, toml_file)
|
||||
# toml.dump(user_config, toml_file)
|
||||
tomlkit.dump(user_config, toml_file)
|
||||
elif os.path.exists(args.user_config):
|
||||
with open(args.user_config) as toml_file:
|
||||
user_config = toml.load(toml_file)
|
||||
user_config = load_toml_file(args.user_config)
|
||||
|
||||
if user_config:
|
||||
config = merge_config(user_config, config)
|
||||
@ -232,9 +209,8 @@ def load_config(args):
|
||||
if dropin and os.path.isdir(dropin):
|
||||
dropin += '*.toml' if dropin.endswith('/') else '/*.toml' # only toml here; yaml is no more
|
||||
for conf in glob.glob(dropin):
|
||||
with open(conf) as toml_file:
|
||||
additional_config = toml.load(toml_file)
|
||||
config = merge_config(additional_config, config)
|
||||
additional_config = load_toml_file(conf)
|
||||
config = merge_config(additional_config, config)
|
||||
|
||||
# the very first step is to normalize the display name, so we don't need dozens of if/elif around
|
||||
# Dummy Display -------------------------------------------------------------------
|
||||
@ -564,7 +540,8 @@ class WifiInfo(Enum):
|
||||
ESSID = 1
|
||||
ENCRYPTION = 2
|
||||
CHANNEL = 3
|
||||
RSSI = 4
|
||||
FREQUENCY = 4
|
||||
RSSI = 5
|
||||
|
||||
|
||||
class FieldNotFoundError(Exception):
|
||||
@ -594,9 +571,6 @@ def extract_from_pcap(path, fields):
|
||||
"""
|
||||
results = dict()
|
||||
for field in fields:
|
||||
if not isinstance(field, WifiInfo):
|
||||
raise TypeError("Invalid field")
|
||||
|
||||
subtypes = set()
|
||||
|
||||
if field == WifiInfo.BSSID:
|
||||
@ -606,10 +580,9 @@ def extract_from_pcap(path, fields):
|
||||
packets = sniff(offline=path, filter=bpf_filter)
|
||||
try:
|
||||
for packet in packets:
|
||||
if packet.haslayer(Dot11Beacon):
|
||||
if hasattr(packet[Dot11], 'addr3'):
|
||||
results[field] = packet[Dot11].addr3
|
||||
break
|
||||
if packet.haslayer(Dot11Beacon) and hasattr(packet[Dot11], 'addr3'):
|
||||
results[field] = packet[Dot11].addr3
|
||||
break
|
||||
else: # magic
|
||||
raise FieldNotFoundError("Could not find field [BSSID]")
|
||||
except Exception:
|
||||
@ -654,6 +627,14 @@ def extract_from_pcap(path, fields):
|
||||
results[field] = freq_to_channel(packets[0][RadioTap].ChannelFrequency)
|
||||
except Exception:
|
||||
raise FieldNotFoundError("Could not find field [CHANNEL]")
|
||||
elif field == WifiInfo.FREQUENCY:
|
||||
from scapy.layers.dot11 import sniff, RadioTap
|
||||
from pwnagotchi.mesh.wifi import freq_to_channel
|
||||
packets = sniff(offline=path, count=1)
|
||||
try:
|
||||
results[field] = packets[0][RadioTap].ChannelFrequency
|
||||
except Exception:
|
||||
raise FieldNotFoundError("Could not find field [FREQUENCY]")
|
||||
elif field == WifiInfo.RSSI:
|
||||
from scapy.layers.dot11 import sniff, RadioTap
|
||||
from pwnagotchi.mesh.wifi import freq_to_channel
|
||||
@ -662,7 +643,8 @@ def extract_from_pcap(path, fields):
|
||||
results[field] = packets[0][RadioTap].dBm_AntSignal
|
||||
except Exception:
|
||||
raise FieldNotFoundError("Could not find field [RSSI]")
|
||||
|
||||
else:
|
||||
raise TypeError("Invalid field")
|
||||
return results
|
||||
|
||||
|
||||
|
@ -27,11 +27,19 @@ class Voice:
|
||||
self._('Hack the Planet!'),
|
||||
self._('No more mister Wi-Fi!!'),
|
||||
self._('Pretty fly 4 a Wi-Fi!'),
|
||||
self._('Good Pwning!'), # Battlestar Galactica
|
||||
self._('Ensign, Engage!'), # Star trek
|
||||
self._('Free your Wi-Fi!'), # Matrix
|
||||
self._('Chevron Seven, locked.'), # Stargate
|
||||
self._('May the Wi-fi be with you'), # Star wars
|
||||
])
|
||||
|
||||
def on_keys_generation(self):
|
||||
return random.choice([
|
||||
self._('Generating keys, do not turn off ...')])
|
||||
self._('Generating keys, do not turn off ...'),
|
||||
self._('Are you the keymaster?'), # Ghostbusters
|
||||
self._('I am the keymaster!'), # Ghostbusters
|
||||
])
|
||||
|
||||
def on_normal(self):
|
||||
return random.choice([
|
||||
@ -44,8 +52,7 @@ class Voice:
|
||||
def on_reading_logs(self, lines_so_far=0):
|
||||
if lines_so_far == 0:
|
||||
return self._('Reading last session logs ...')
|
||||
else:
|
||||
return self._('Read {lines_so_far} log lines so far ...').format(lines_so_far=lines_so_far)
|
||||
return self._('Read {lines_so_far} log lines so far ...').format(lines_so_far=lines_so_far)
|
||||
|
||||
def on_bored(self):
|
||||
return random.choice([
|
||||
@ -53,7 +60,11 @@ class Voice:
|
||||
self._('Let\'s go for a walk!')])
|
||||
|
||||
def on_motivated(self, reward):
|
||||
return self._('This is the best day of my life!')
|
||||
return random.choice([
|
||||
self._('This is the best day of my life!'),
|
||||
self._('All your base are belong to us'),
|
||||
self._('Fascinating!'), # Star trek
|
||||
])
|
||||
|
||||
def on_demotivated(self, reward):
|
||||
return self._('Shitty day :/')
|
||||
@ -63,6 +74,8 @@ class Voice:
|
||||
self._('I\'m extremely bored ...'),
|
||||
self._('I\'m very sad ...'),
|
||||
self._('I\'m sad'),
|
||||
self._('I\'m so happy ...'), # Marvin in H2G2
|
||||
self._('Life? Don\'t talk to me about life.'), # Also Marvin in H2G2
|
||||
'...'])
|
||||
|
||||
def on_angry(self):
|
||||
@ -78,17 +91,17 @@ class Voice:
|
||||
self._('I pwn therefore I am.'),
|
||||
self._('So many networks!!!'),
|
||||
self._('I\'m having so much fun!'),
|
||||
self._('It\'s a Wi-Fi system! I know this!'), # Jurassic park
|
||||
self._('My crime is that of curiosity ...')])
|
||||
|
||||
def on_new_peer(self, peer):
|
||||
if peer.first_encounter():
|
||||
return random.choice([
|
||||
self._('Hello {name}! Nice to meet you.').format(name=peer.name())])
|
||||
else:
|
||||
return random.choice([
|
||||
self._('Yo {name}! Sup?').format(name=peer.name()),
|
||||
self._('Hey {name} how are you doing?').format(name=peer.name()),
|
||||
self._('Unit {name} is nearby!').format(name=peer.name())])
|
||||
return random.choice([
|
||||
self._('Yo {name}! Sup?').format(name=peer.name()),
|
||||
self._('Hey {name} how are you doing?').format(name=peer.name()),
|
||||
self._('Unit {name} is nearby!').format(name=peer.name())])
|
||||
|
||||
def on_lost_peer(self, peer):
|
||||
return random.choice([
|
||||
@ -104,19 +117,23 @@ class Voice:
|
||||
def on_grateful(self):
|
||||
return random.choice([
|
||||
self._('Good friends are a blessing!'),
|
||||
self._('I love my friends!')])
|
||||
self._('I love my friends!')
|
||||
])
|
||||
|
||||
def on_lonely(self):
|
||||
return random.choice([
|
||||
self._('Nobody wants to play with me ...'),
|
||||
self._('I feel so alone ...'),
|
||||
self._('Let\'s find friends'),
|
||||
self._('Where\'s everybody?!')])
|
||||
|
||||
def on_napping(self, secs):
|
||||
return random.choice([
|
||||
self._('Napping for {secs}s ...').format(secs=secs),
|
||||
self._('Zzzzz'),
|
||||
self._('ZzzZzzz ({secs}s)').format(secs=secs)])
|
||||
self._('Snoring ...'),
|
||||
self._('ZzzZzzz ({secs}s)').format(secs=secs),
|
||||
])
|
||||
|
||||
def on_shutdown(self):
|
||||
return random.choice([
|
||||
@ -124,12 +141,17 @@ class Voice:
|
||||
self._('Zzz')])
|
||||
|
||||
def on_awakening(self):
|
||||
return random.choice(['...', '!'])
|
||||
return random.choice([
|
||||
'...',
|
||||
'!',
|
||||
'Hello World!',
|
||||
self._('I dreamed of electric sheep'),
|
||||
])
|
||||
|
||||
def on_waiting(self, secs):
|
||||
return random.choice([
|
||||
self._('Waiting for {secs}s ...').format(secs=secs),
|
||||
'...',
|
||||
self._('Waiting for {secs}s ...').format(secs=secs),
|
||||
self._('Looking around ({secs}s)').format(secs=secs)])
|
||||
|
||||
def on_assoc(self, ap):
|
||||
@ -138,12 +160,16 @@ class Voice:
|
||||
return random.choice([
|
||||
self._('Hey {what} let\'s be friends!').format(what=what),
|
||||
self._('Associating to {what}').format(what=what),
|
||||
self._('Yo {what}!').format(what=what)])
|
||||
self._('Yo {what}!').format(what=what),
|
||||
self._('Rise and Shine Mr. {what}!').format(what=what), # Half Life
|
||||
])
|
||||
|
||||
def on_deauth(self, sta):
|
||||
return random.choice([
|
||||
self._('Just decided that {mac} needs no WiFi!').format(mac=sta['mac']),
|
||||
self._('Just decided that {mac} needs no Wi-Fi!').format(mac=sta['mac']),
|
||||
self._('Deauthenticating {mac}').format(mac=sta['mac']),
|
||||
self._('No more Wi-Fi for {mac}').format(mac=sta['mac']),
|
||||
self._('It\'s a trap! {mac}').format(mac=sta['mac']), # Star wars
|
||||
self._('Kickbanning {mac}!').format(mac=sta['mac'])])
|
||||
|
||||
def on_handshakes(self, new_shakes):
|
||||
@ -155,10 +181,19 @@ class Voice:
|
||||
return self._('You have {count} new message{plural}!').format(count=count, plural=s)
|
||||
|
||||
def on_rebooting(self):
|
||||
return self._("Oops, something went wrong ... Rebooting ...")
|
||||
return random.choice([
|
||||
self._("Oops, something went wrong ... Rebooting ..."),
|
||||
self._("Have you tried turning it off and on again?"), # The IT crew
|
||||
self._("I\'m afraid Dave"), # 2001 Space Odyssey
|
||||
self._("I\'m dead, Jim!"), # Star Trek
|
||||
self._("I have a bad feeling about this"), # Star wars
|
||||
])
|
||||
|
||||
def on_uploading(self, to):
|
||||
return self._("Uploading data to {to} ...").format(to=to)
|
||||
return random.choice([
|
||||
self._("Uploading data to {to} ...").format(to=to),
|
||||
self._("Beam me up to {to}").format(to=to),
|
||||
])
|
||||
|
||||
def on_downloading(self, name):
|
||||
return self._("Downloading from {name} ...").format(name=name)
|
||||
|
158
pwndroid_privacy_policy
Normal file
158
pwndroid_privacy_policy
Normal file
@ -0,0 +1,158 @@
|
||||
Privacy Policy for PwnDroid
|
||||
Privacy Policy
|
||||
|
||||
Last updated: January 05, 2025
|
||||
|
||||
This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.
|
||||
|
||||
We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the Privacy Policy Generator.
|
||||
Interpretation and Definitions
|
||||
Interpretation
|
||||
|
||||
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
|
||||
Definitions
|
||||
|
||||
For the purposes of this Privacy Policy:
|
||||
|
||||
Account means a unique account created for You to access our Service or parts of our Service.
|
||||
|
||||
Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
|
||||
|
||||
Application refers to PwnDroid, the software program provided by the Company.
|
||||
|
||||
Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to PwnDroid.
|
||||
|
||||
Country refers to: Netherlands
|
||||
|
||||
Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.
|
||||
|
||||
Personal Data is any information that relates to an identified or identifiable individual.
|
||||
|
||||
Service refers to the Application.
|
||||
|
||||
Service Provider means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
|
||||
|
||||
Usage Data refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
|
||||
|
||||
You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
|
||||
|
||||
Collecting and Using Your Personal Data
|
||||
Types of Data Collected
|
||||
Personal Data
|
||||
|
||||
While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:
|
||||
|
||||
Usage Data
|
||||
|
||||
Usage Data
|
||||
|
||||
Usage Data is collected automatically when using the Service.
|
||||
|
||||
Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.
|
||||
|
||||
When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.
|
||||
|
||||
We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.
|
||||
Information Collected while Using the Application
|
||||
|
||||
While using Our Application, in order to provide features of Our Application, We may collect, with Your prior permission:
|
||||
|
||||
Information regarding your location
|
||||
|
||||
We use this information to provide features of Our Service, to improve and customize Our Service. The information will not be uploaded to the Company's servers and/or a Service Provider's server or it will only be simply stored on Your device.
|
||||
|
||||
You can enable or disable access to this information at any time, through Your Device settings.
|
||||
Use of Your Personal Data
|
||||
|
||||
The Company may use Personal Data for the following purposes:
|
||||
|
||||
To provide and maintain our Service, including to monitor the usage of our Service.
|
||||
|
||||
To manage Your Account: to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
|
||||
|
||||
For the performance of a contract: the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
|
||||
|
||||
To contact You: To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
|
||||
|
||||
To provide You with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.
|
||||
|
||||
To manage Your requests: To attend and manage Your requests to Us.
|
||||
|
||||
For business transfers: We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.
|
||||
|
||||
For other purposes: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.
|
||||
|
||||
We may share Your personal information in the following situations:
|
||||
|
||||
With Service Providers: We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.
|
||||
For business transfers: We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.
|
||||
With Affiliates: We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
|
||||
With business partners: We may share Your information with Our business partners to offer You certain products, services or promotions.
|
||||
With other users: when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside.
|
||||
With Your consent: We may disclose Your personal information for any other purpose with Your consent.
|
||||
|
||||
Retention of Your Personal Data
|
||||
|
||||
The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
|
||||
|
||||
The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
|
||||
Transfer of Your Personal Data
|
||||
|
||||
Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.
|
||||
|
||||
Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.
|
||||
|
||||
The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.
|
||||
Delete Your Personal Data
|
||||
|
||||
You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You.
|
||||
|
||||
Our Service may give You the ability to delete certain information about You from within the Service.
|
||||
|
||||
You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us.
|
||||
|
||||
Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so.
|
||||
Disclosure of Your Personal Data
|
||||
Business Transactions
|
||||
|
||||
If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.
|
||||
Law enforcement
|
||||
|
||||
Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).
|
||||
Other legal requirements
|
||||
|
||||
The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:
|
||||
|
||||
Comply with a legal obligation
|
||||
Protect and defend the rights or property of the Company
|
||||
Prevent or investigate possible wrongdoing in connection with the Service
|
||||
Protect the personal safety of Users of the Service or the public
|
||||
Protect against legal liability
|
||||
|
||||
Security of Your Personal Data
|
||||
|
||||
The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.
|
||||
Children's Privacy
|
||||
|
||||
Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
|
||||
|
||||
If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.
|
||||
Links to Other Websites
|
||||
|
||||
Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.
|
||||
|
||||
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
|
||||
Changes to this Privacy Policy
|
||||
|
||||
We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.
|
||||
|
||||
We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.
|
||||
|
||||
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
|
||||
Contact Us
|
||||
|
||||
If you have any questions about this Privacy Policy, You can contact us:
|
||||
|
||||
By email: oudshoorn.jeroen@gmail.com
|
||||
|
||||
Generated using Privacy Policies Generator
|
@ -9,7 +9,7 @@ dependencies = [
|
||||
"PyYAML", "dbus-python", "file-read-backwards", "flask", "flask-cors",
|
||||
"flask-wtf", "gast", "gpiozero", "inky", "numpy", "pycryptodome", "pydrive2", "python-dateutil",
|
||||
"requests", "rpi-lgpio", "rpi_hardware_pwm", "scapy", "setuptools", "shimmy", "smbus", "smbus2",
|
||||
"spidev", "toml", "tweepy", "websockets", "pisugar",
|
||||
"spidev", "tomlkit", "toml", "tweepy", "websockets", "pisugar",
|
||||
]
|
||||
|
||||
requires-python = ">=3.11"
|
||||
|
142
scripts/BR.bat
Normal file
142
scripts/BR.bat
Normal file
@ -0,0 +1,142 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
REM ---------------------------------------------------------------------------
|
||||
REM A simple .bat script that handles two subcommands: "backup" and "restore"
|
||||
REM USAGE:
|
||||
REM backup-restore.bat backup [ -n HOST ] [ -u USER ] [ -o OUTPUT ]
|
||||
REM backup-restore.bat restore [ -n HOST ] [ -u USER ] [ -b BACKUP_FILE ]
|
||||
REM ---------------------------------------------------------------------------
|
||||
|
||||
set "LOGFILE=%~dp0BR_GPT.log"
|
||||
echo [INFO] Script started at %DATE% %TIME% > "%LOGFILE%"
|
||||
|
||||
REM ---------------------------------------------------------------------------
|
||||
REM Check for Arguments
|
||||
if "%~1"=="" goto usage
|
||||
|
||||
set "SUBCOMMAND=%~1"
|
||||
shift
|
||||
|
||||
REM Default Values
|
||||
set "UNIT_HOSTNAME=10.0.0.2"
|
||||
set "UNIT_USERNAME=pi"
|
||||
set "OUTPUT="
|
||||
set "BACKUP_FILE="
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto done_args
|
||||
|
||||
if "%~1"=="-n" (
|
||||
set "UNIT_HOSTNAME=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
|
||||
if "%~1"=="-u" (
|
||||
set "UNIT_USERNAME=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
|
||||
if "%SUBCOMMAND%"=="backup" (
|
||||
if "%~1"=="-o" (
|
||||
set "OUTPUT=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
)
|
||||
|
||||
if "%SUBCOMMAND%"=="restore" (
|
||||
if "%~1"=="-b" (
|
||||
set "BACKUP_FILE=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
)
|
||||
|
||||
echo [ERROR] Unknown or invalid argument: %~1 >> "%LOGFILE%"
|
||||
goto usage
|
||||
|
||||
:done_args
|
||||
|
||||
REM ---------------------------------------------------------------------------
|
||||
REM Subcommand Routing
|
||||
if "%SUBCOMMAND%"=="backup" goto do_backup
|
||||
if "%SUBCOMMAND%"=="restore" goto do_restore
|
||||
goto usage
|
||||
|
||||
REM ===========================================================================
|
||||
:do_backup
|
||||
if "%OUTPUT%"=="" (
|
||||
set /a RAND=%random%
|
||||
set "OUTPUT=%UNIT_HOSTNAME%-backup-%RAND%.tgz"
|
||||
)
|
||||
|
||||
echo [INFO] Checking connectivity to %UNIT_HOSTNAME% >> "%LOGFILE%"
|
||||
ping -n 1 %UNIT_HOSTNAME% >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Device %UNIT_HOSTNAME% is unreachable. >> "%LOGFILE%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Backing up %UNIT_HOSTNAME% to %OUTPUT% ... >> "%LOGFILE%"
|
||||
set "FILES_TO_BACKUP=/root/settings.yaml /root/client_secrets.json /root/.ssh /root/.api-report.json /root/.bashrc /root/.profile /home/pi/handshakes /root/peers /etc/pwnagotchi/ /usr/local/share/pwnagotchi/custom-plugins /etc/ssh/ /home/pi/.bashrc /home/pi/.profile /home/pi/.wpa_sec_uploads"
|
||||
|
||||
ssh %UNIT_USERNAME%@%UNIT_HOSTNAME% "sudo tar -cf - %FILES_TO_BACKUP% | gzip -9" > "%OUTPUT%" 2>> "%LOGFILE%"
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Backup failed! >> "%LOGFILE%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Backup completed successfully: %OUTPUT% >> "%LOGFILE%"
|
||||
exit /b 0
|
||||
|
||||
REM ===========================================================================
|
||||
:do_restore
|
||||
if "%BACKUP_FILE%"=="" (
|
||||
for /f "delims=" %%A in ('dir /b /a-d /O-D "%UNIT_HOSTNAME%-backup-*.tgz" 2^>nul') do (
|
||||
set "BACKUP_FILE=%%A"
|
||||
goto found_file
|
||||
)
|
||||
echo [ERROR] No backup file found. >> "%LOGFILE%"
|
||||
exit /b 1
|
||||
|
||||
:found_file
|
||||
echo [INFO] Found backup file: %BACKUP_FILE% >> "%LOGFILE%"
|
||||
)
|
||||
|
||||
echo [INFO] Checking connectivity to %UNIT_HOSTNAME% >> "%LOGFILE%"
|
||||
ping -n 1 %UNIT_HOSTNAME% > nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Device %UNIT_HOSTNAME% is unreachable. >> "%LOGFILE%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Copying backup file to remote device... >> "%LOGFILE%"
|
||||
scp "%BACKUP_FILE%" %UNIT_USERNAME%@%UNIT_HOSTNAME%:/tmp/ 2>> "%LOGFILE%"
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Failed to copy backup file. >> "%LOGFILE%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Restoring %BACKUP_FILE% on %UNIT_HOSTNAME% >> "%LOGFILE%"
|
||||
ssh %UNIT_USERNAME%@%UNIT_HOSTNAME% "sudo tar xzvf /tmp/%BACKUP_FILE% -C /" 2>> "%LOGFILE%"
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Restore failed! >> "%LOGFILE%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Restore completed successfully. >> "%LOGFILE%"
|
||||
exit /b 0
|
||||
|
||||
REM ===========================================================================
|
||||
:usage
|
||||
echo Usage:
|
||||
echo BR_GPT.bat backup [ -n HOST ] [ -u USER ] [ -o OUTPUT ]
|
||||
echo BR_GPT.bat restore [ -n HOST ] [ -u USER ] [ -b BACKUP_FILE ]
|
||||
exit /b 1
|
197
scripts/BR.sh
Normal file
197
scripts/BR.sh
Normal file
@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# A Combined backup/restore script for linux
|
||||
# Usage:
|
||||
# backup-restore.sh backup [ -n HOST ] [ -u USER ] [ -o OUTPUT ]
|
||||
# backup-restore.sh restore [ -n HOST ] [ -u USER ] [ -b BACKUP_FILE ]
|
||||
#
|
||||
|
||||
###############################################################################
|
||||
# GLOBAL USAGE
|
||||
###############################################################################
|
||||
usage() {
|
||||
echo "Usage:"
|
||||
echo " $0 backup [ -n HOST ] [ -u USER ] [ -o OUTPUT ]"
|
||||
echo " $0 restore [ -n HOST ] [ -u USER ] [ -b BACKUP_FILE ]"
|
||||
echo
|
||||
echo "Subcommands:"
|
||||
echo " backup Backup files from the remote device."
|
||||
echo " restore Restore files to the remote device."
|
||||
echo
|
||||
echo "Common Options:"
|
||||
echo " -n HOST Hostname or IP of remote device (default: 10.0.0.2)"
|
||||
echo " -u USER Username for SSH (default: pi)"
|
||||
echo
|
||||
echo "Options for 'backup':"
|
||||
echo " -o OUTPUT Path/name of the output archive (default: \$HOST-backup-\$(date +%s).tgz)"
|
||||
echo
|
||||
echo "Options for 'restore':"
|
||||
echo " -b BACKUP Path to the local backup archive to restore."
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " $0 backup"
|
||||
echo " $0 backup -n 10.0.0.2 -u pi -o my-backup.tgz"
|
||||
echo " $0 restore -b 10.0.0.2-backup-123456789.tgz"
|
||||
echo
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# BACKUP FUNCTION
|
||||
###############################################################################
|
||||
do_backup() {
|
||||
# Defaults
|
||||
UNIT_HOSTNAME="10.0.0.2"
|
||||
UNIT_USERNAME="pi"
|
||||
OUTPUT=""
|
||||
|
||||
# Parse arguments specific to backup
|
||||
while getopts "hn:u:o:" arg; do
|
||||
case $arg in
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
n)
|
||||
UNIT_HOSTNAME=$OPTARG
|
||||
;;
|
||||
u)
|
||||
UNIT_USERNAME=$OPTARG
|
||||
;;
|
||||
o)
|
||||
OUTPUT=$OPTARG
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If OUTPUT was not specified, set a default
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT="${UNIT_HOSTNAME}-backup-$(date +%s).tgz"
|
||||
fi
|
||||
|
||||
# List of files/directories to backup
|
||||
# (the same list you used in your backup.sh script)
|
||||
FILES_TO_BACKUP="
|
||||
/root/settings.yaml
|
||||
/root/client_secrets.json
|
||||
/root/.api-report.json
|
||||
/root/.ssh
|
||||
/root/.bashrc
|
||||
/root/.profile
|
||||
/home/pi/handshakes
|
||||
/root/peers
|
||||
/etc/pwnagotchi/
|
||||
/usr/local/share/pwnagotchi/custom-plugins
|
||||
/etc/ssh/
|
||||
/home/pi/.bashrc
|
||||
/home/pi/.profile
|
||||
/home/pi/.wpa_sec_uploads
|
||||
"
|
||||
# Convert multiline variable into a space-separated list
|
||||
FILES_TO_BACKUP=$(echo "$FILES_TO_BACKUP" | tr '\n' ' ')
|
||||
|
||||
echo "@ Checking connectivity to $UNIT_HOSTNAME ..."
|
||||
if ! ping -c 1 "$UNIT_HOSTNAME" &>/dev/null; then
|
||||
echo "@ unit ${UNIT_HOSTNAME} can't be reached, check network or USB interface IP."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "@ Backing up ${UNIT_HOSTNAME} to ${OUTPUT} ..."
|
||||
# -n => do not read from stdin
|
||||
ssh -n "${UNIT_USERNAME}@${UNIT_HOSTNAME}" \
|
||||
"sudo tar -cf - ${FILES_TO_BACKUP}" \
|
||||
| gzip -9 > "${OUTPUT}"
|
||||
|
||||
echo "@ Backup finished. Archive: ${OUTPUT}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# RESTORE FUNCTION
|
||||
###############################################################################
|
||||
do_restore() {
|
||||
# Defaults
|
||||
UNIT_HOSTNAME="10.0.0.2"
|
||||
UNIT_USERNAME="pi"
|
||||
BACKUP_FILE=""
|
||||
|
||||
# Parse arguments specific to restore
|
||||
while getopts "hn:u:b:" arg; do
|
||||
case $arg in
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
n)
|
||||
UNIT_HOSTNAME=$OPTARG
|
||||
;;
|
||||
u)
|
||||
UNIT_USERNAME=$OPTARG
|
||||
;;
|
||||
b)
|
||||
BACKUP_FILE=$OPTARG
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If no backup file given, try to find the latest
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
BACKUP_FILE=$(ls -rt "${UNIT_HOSTNAME}"-backup-*.tgz 2>/dev/null | tail -n1)
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
echo "@ Can't find backup file. Please specify one with '-b'."
|
||||
exit 1
|
||||
fi
|
||||
echo "@ Found backup file: $BACKUP_FILE"
|
||||
echo -n "@ Continue restoring this file? (y/n) "
|
||||
read -r CONTINUE
|
||||
CONTINUE=$(echo "${CONTINUE}" | tr "[:upper:]" "[:lower:]")
|
||||
if [[ "${CONTINUE}" != "y" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "@ Checking connectivity to $UNIT_HOSTNAME ..."
|
||||
if ! ping -c 1 "$UNIT_HOSTNAME" &>/dev/null; then
|
||||
echo "@ unit ${UNIT_HOSTNAME} can't be reached, make sure it's connected and a static IP assigned."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "@ Restoring $BACKUP_FILE to $UNIT_HOSTNAME ..."
|
||||
cat "${BACKUP_FILE}" | ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo tar xzv -C /"
|
||||
|
||||
echo "@ Restore finished."
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# MAIN ENTRY POINT
|
||||
###############################################################################
|
||||
# We expect the first argument to be either 'backup', 'restore', or '-h'.
|
||||
SUBCOMMAND=$1
|
||||
if [[ -z "$SUBCOMMAND" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
shift # Shift past subcommand
|
||||
|
||||
case "$SUBCOMMAND" in
|
||||
backup)
|
||||
do_backup "$@"
|
||||
;;
|
||||
restore)
|
||||
do_restore "$@"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
@ -1,67 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
usage() {
|
||||
echo "Usage: backup.sh [-honu] [-h] [-u user] [-n host name or ip] [-o output]"
|
||||
}
|
||||
|
||||
while getopts "ho:n:u:" arg; do
|
||||
case $arg in
|
||||
h)
|
||||
usage
|
||||
exit
|
||||
;;
|
||||
n)
|
||||
UNIT_HOSTNAME=$OPTARG
|
||||
;;
|
||||
o)
|
||||
OUTPUT=$OPTARG
|
||||
;;
|
||||
u)
|
||||
UNIT_USERNAME=$OPTARG
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
esac
|
||||
done
|
||||
|
||||
# name of the ethernet gadget interface on the host
|
||||
UNIT_HOSTNAME=${UNIT_HOSTNAME:-10.0.0.2}
|
||||
# output backup tgz file
|
||||
OUTPUT=${OUTPUT:-${UNIT_HOSTNAME}-backup-$(date +%s).tgz}
|
||||
# username to use for ssh
|
||||
UNIT_USERNAME=${UNIT_USERNAME:-pi}
|
||||
# what to backup
|
||||
FILES_TO_BACKUP="/boot/firmware/cmdline.txt \
|
||||
/boot/firmware/config.txt \
|
||||
/root/settings.yaml \
|
||||
/root/client_secrets.json \
|
||||
/root/.api-report.json \
|
||||
/root/.ssh \
|
||||
/root/.bashrc \
|
||||
/root/.profile \
|
||||
/home/pi/handshakes \
|
||||
/root/peers \
|
||||
/etc/modprobe.d/g_ether.conf \
|
||||
/etc/pwnagotchi/ \
|
||||
/etc/ssh/ \
|
||||
/etc/pwnagotchi/log/pwnagotchi.log \
|
||||
/etc/pwnagotchi/log/pwnagotchi*.gz \
|
||||
/home/pi/.ssh \
|
||||
/home/pi/.bashrc \
|
||||
/home/pi/.profile \
|
||||
/root/.api-report.json \
|
||||
/root/.auto-update \
|
||||
/root/.bt-tether* \
|
||||
/root/.ohc_uploads \
|
||||
/root/.wigle_uploads \
|
||||
/home/pi/.wpa_sec_uploads"
|
||||
|
||||
ping -c 1 "${UNIT_HOSTNAME}" > /dev/null 2>&1 || {
|
||||
echo "@ unit ${UNIT_HOSTNAME} can't be reached, make sure it's connected and a static IP assigned to the USB interface."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "@ backing up $UNIT_HOSTNAME to $OUTPUT ..."
|
||||
# shellcheck disable=SC2029
|
||||
ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo find ${FILES_TO_BACKUP} -type f -print0 | xargs -0 sudo tar cv" | gzip -9 > "$OUTPUT"
|
@ -1,60 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
usage() {
|
||||
echo "Usage: restore.sh [-bhnu] [-h] [-b backup name] [-n host name] [-u user name]"
|
||||
}
|
||||
|
||||
while getopts "hb:n:u:" arg; do
|
||||
case $arg in
|
||||
b)
|
||||
BACKUP=$OPTARG
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit
|
||||
;;
|
||||
n)
|
||||
UNIT_HOSTNAME=$OPTARG
|
||||
;;
|
||||
u)
|
||||
UNIT_USERNAME=$OPTARG
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
esac
|
||||
done
|
||||
# name of the ethernet gadget interface on the host
|
||||
UNIT_HOSTNAME=${UNIT_HOSTNAME:-10.0.0.2}
|
||||
# output backup tgz file
|
||||
# shellcheck disable=SC2086
|
||||
if [ -z $BACKUP ]; then
|
||||
# shellcheck disable=SC2012
|
||||
BACKUP=$(ls -rt "${UNIT_HOSTNAME}"-backup-*.tgz 2>/dev/null | tail -n1)
|
||||
if [ -z "$BACKUP" ]; then
|
||||
echo "@ Can't find backup file. Please specify one with '-b'"
|
||||
exit 1
|
||||
fi
|
||||
echo "@ Found backup file:"
|
||||
# shellcheck disable=SC2028
|
||||
echo "\t${BACKUP}"
|
||||
# shellcheck disable=SC2039
|
||||
echo -n "@ continue restroring this file? (y/n) "
|
||||
# shellcheck disable=SC2162
|
||||
read CONTINUE
|
||||
CONTINUE=$(echo "${CONTINUE}" | tr "[:upper:]" "[:lower:]")
|
||||
if [ "${CONTINUE}" != "y" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# username to use for ssh
|
||||
UNIT_USERNAME=${UNIT_USERNAME:-pi}
|
||||
|
||||
ping -c 1 "${UNIT_HOSTNAME}" > /dev/null 2>&1 || {
|
||||
echo "@ unit ${UNIT_HOSTNAME} can't be reached, make sure it's connected and a static IP assigned to the USB interface."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "@ restoring $BACKUP to $UNIT_HOSTNAME ..."
|
||||
# shellcheck disable=SC2002
|
||||
cat "${BACKUP}" | ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo tar xzv -C /"
|
Reference in New Issue
Block a user