133 Commits

Author SHA1 Message Date
Simone Margaritelli
785d678e30 releasing v1.3.0 2019-11-18 14:12:13 +01:00
Simone Margaritelli
9c8784e533 fix: reload inbox every 15 seconds 2019-11-18 13:24:58 +01:00
Simone Margaritelli
fd288b4acd misc: small fix or general refactoring i did not bother commenting 2019-11-18 13:21:44 +01:00
evilsocket
b704614254 Merge pull request from Arttumiro/patch-2
Add link to better guide to paw-gps.py
2019-11-18 12:29:44 +01:00
Arttumiro
2dc36651df Add link to better guide to paw-gps.py
Got permission from LeonT for this
2019-11-18 13:27:30 +02:00
evilsocket
d8d6d52eda Merge pull request from andrewbeard/master
Fix for issue 
2019-11-16 10:09:49 +01:00
Andrew Beard
43c5ab7ecf - Fix the temperature symbol when using something other than celsius
- Add defaults so we don't throw an exception if an invalid scale is
selected. Bad things happen if you spell fahrenheit wrong.
2019-11-16 01:29:02 -05:00
evilsocket
de62424dbc Merge pull request from neutralinsomniac/fix_backup_script_default_username
fix: don't use $USERNAME as default username for backup/restore scripts, as this is usually defined on linux hosts as the current logged in user
2019-11-15 14:59:53 +01:00
Jeremy O'Brien
8c51936c13 fix: don't use $USERNAME as default username for backup/restore scripts, as this is usually defined on linux hosts as the current logged in user
Signed-off-by: Jeremy O'Brien <neutral@fastmail.com>
2019-11-15 07:59:32 -05:00
Simone Margaritelli
87e46610f9 fix: --clear does not start the agent anymore 2019-11-15 12:27:53 +01:00
evilsocket
a96dead519 Merge pull request from Arttumiro/master
Fixed Paw-Gps config, added a - mark
2019-11-15 12:06:06 +01:00
evilsocket
8ed2950eb5 Merge pull request from Arttumiro/patch-1
Fixes to paw-gps.py
2019-11-15 12:05:58 +01:00
evilsocket
6f8133b2b8 Merge pull request from benleb/add-spacing-to-gps
decrease spacing in gps ui
2019-11-15 12:05:07 +01:00
evilsocket
16afa87112 Merge branch 'master' into add-spacing-to-gps 2019-11-15 12:05:00 +01:00
evilsocket
ed22343877 Merge pull request from budd3993/fix-webgpsmap-negatives
webgpsmap plugin - fixed check for nonzero lat/long
2019-11-15 12:04:00 +01:00
evilsocket
c70f2c30e9 Merge pull request from wystans/master
Added signal strength to logging messages
2019-11-15 12:03:39 +01:00
Wystan Schmidt
5111490c70 Added rssi info to logging messages
Signed-off-by: Wystan Schmidt <wystans@gmail.com>
2019-11-14 20:13:50 -07:00
Josh Bauer
59ae35372e fixed check for nonzero lat/long 2019-11-14 21:15:12 -05:00
evilsocket
5f593a4231 Merge pull request from benleb/more-pythonic
f-strings and double quotes in gps plugin
2019-11-14 23:05:43 +01:00
Arttumiro
56cc872daa Update paw-gps.py 2019-11-14 22:29:58 +02:00
Arttumiro
6e1490da78 Fix on_handshake, whole plugin should work now
Add the else: so if the ip is set in the options the plugin actually works.
2019-11-14 22:29:43 +02:00
Ben Lebherz
69597103b5 use f-strings and double quotes in gps plugin 2019-11-14 15:57:43 +01:00
Ben Lebherz
96ca5dd8e3 decrease spacing in gps ui 2019-11-14 15:46:12 +01:00
evilsocket
3efa3a935a Merge pull request from benleb/add-gateway-option
add gateway option to bt-tether
2019-11-14 11:04:30 +01:00
evilsocket
39ccd141eb Merge pull request from benleb/add-gps-coords-to-ui
show gps coords of last handshake in ui
2019-11-14 11:04:12 +01:00
evilsocket
1a30b52a90 Merge pull request from benleb/make-label-spacing-dynamic
make label to value space configurable to better fit small fonts
2019-11-14 11:03:28 +01:00
Arttumiro
8965ad9272 change default ip of paw-gps, it needs port too.
Change default ip from 192.168.44.1 (which is the ip address of an android on bt tether) to 192.168.44.1:8080 so it has the default port paw-gps opens on, which is 8080.
2019-11-14 11:45:39 +02:00
Arttumiro
5dae0ce982 Fixed Paw-Gps, added a - mark to the config.
Before, paw-gps was set as pawgps in the config, this was different than the filename (paw-gps.py) so it didnt work, this request should fix that. (if this is the place that sets /etc/pwnagotchi/default.yml, this is my first PR so sorry im dumb)
2019-11-14 11:26:32 +02:00
Ben Lebherz
92266a783a make label to value space configurable to better fit small fonts 2019-11-13 21:11:21 +01:00
Ben Lebherz
9e656d4ea6 show gps coords of last handshake in ui 2019-11-13 15:38:21 +01:00
Ben Lebherz
1d255b577d add gateway option to bt-tether 2019-11-13 15:37:20 +01:00
Simone Margaritelli
1ff14c05a9 misc: small fix or general refactoring i did not bother commenting 2019-11-13 15:05:21 +01:00
Simone Margaritelli
ab63ecccd7 new: macos connection share script now autodetects the usb interface and uses en0 as default upstream (closes ) 2019-11-13 14:54:49 +01:00
Simone Margaritelli
a7e37115d9 misc: small fix or general refactoring i did not bother commenting 2019-11-13 14:38:39 +01:00
Simone Margaritelli
b1d8aa3ba1 misc: several improvements on the web ui 2019-11-13 14:37:13 +01:00
evilsocket
6e26463278 Merge pull request from ahsec/add_spanish_support
Adding support for Spanish language
2019-11-13 11:39:06 +01:00
evilsocket
7261073073 Merge pull request from lexusburn/patch-1
small typo fix
2019-11-13 11:38:54 +01:00
lexusburn
a02c1d6d92 small typo fix 2019-11-13 09:17:18 +01:00
Angel
40caf3f51a Adding support for Spanish language
Signed-off-by: Angel <ahsec>
2019-11-12 22:58:02 -08:00
Simone Margaritelli
21f1273bd8 Merge branch 'master' of github.com:evilsocket/pwnagotchi 2019-11-13 01:32:14 +01:00
Simone Margaritelli
a8c07ba997 fix: supporting channels greater than 140 for 5g (closes ) 2019-11-13 01:32:05 +01:00
evilsocket
6bd09c7f43 Merge pull request from qbit/macos_and_posix_fix
Remove '-w' as macOS doesn't have it. Fix var expansion in dash.
2019-11-13 01:17:58 +01:00
Aaron Bieber
1830a19b37 Remove '-w' as macOS doesn't have it. Fix var expansion in dash.
Signed-off-by: Aaron Bieber <aaron@bolddaemon.com>
2019-11-12 16:44:46 -07:00
Simone Margaritelli
9dcc647656 Merge branch 'master' of github.com:evilsocket/pwnagotchi 2019-11-13 00:11:59 +01:00
Simone Margaritelli
8fcfd4cafd fix: plugin events dispatch is now asynchronous (fixes ) 2019-11-13 00:11:50 +01:00
evilsocket
369d7a65a8 Merge pull request from michelep/master
Add support for SpotPear 2,4inch LCD display via framebuffer
2019-11-12 23:58:30 +01:00
evilsocket
d7ad8ee0d7 Merge pull request from dipsylala/additional_agent_properties
Providing APs/APs per channel and current channel as R/O from Agent
2019-11-12 23:57:46 +01:00
Simone Margaritelli
a5f9b9b2ee new: encountered units 2019-11-12 23:56:59 +01:00
Dispsylala
b266671864 Providing APs/APs per channel and current channel as R/O from Agent 2019-11-12 22:51:10 +00:00
Simone Margaritelli
c47b8f2d11 misc: small fix or general refactoring i did not bother commenting 2019-11-12 23:25:56 +01:00
Simone Margaritelli
8129fb7dd2 misc: small fix or general refactoring i did not bother commenting 2019-11-12 23:13:32 +01:00
Simone Margaritelli
c16cabc852 new: searchbox in the pwnmail 2019-11-12 23:08:39 +01:00
Simone Margaritelli
9a7de86057 new: fingerprint qrcode 2019-11-12 23:04:54 +01:00
Simone Margaritelli
440f2a470a new: added basic authentication to the web ui 2019-11-12 21:49:23 +01:00
Simone Margaritelli
81a89d43e0 misc: refactored ui.display.video as ui.web 2019-11-12 21:19:31 +01:00
Simone Margaritelli
91b409053b fix: using static assets 2019-11-12 21:03:21 +01:00
Simone Margaritelli
df01a03a4b new: pwngrid web client 2019-11-12 20:18:02 +01:00
evilsocket
e2be21004d Merge pull request from luclu7/patch-1
Fixed a small typo in bt-tether
2019-11-12 13:18:42 +01:00
Luclu7
0f3d9db01d Fixed a small typo in bt-tether 2019-11-12 11:46:04 +01:00
evilsocket
19abc17816 Merge pull request from dwi/patch-1
Small UPS Lite typo fix
2019-11-12 11:39:59 +01:00
evilsocket
4b9ebc2512 Merge pull request from benleb/patch-2
shift word
2019-11-12 11:39:16 +01:00
evilsocket
41a3fad43e Merge pull request from benleb/patch-3
fix baudrate option name in gps plugin
2019-11-12 11:39:00 +01:00
Ben Lebherz
f4b886cf7b fix baudrate option name 2019-11-12 08:37:36 +01:00
Ben Lebherz
0eb8e1829e shift word 2019-11-12 07:40:48 +01:00
dwi
52b40f049e Small UPS Lite typo fix
Fixing dfd534ac41 that fixes 
2019-11-11 17:43:20 +01:00
evilsocket
3df35ef03b Merge pull request from qbit/bkp_rstr_compat
Make backup / restore use POSIX shell for portability.
2019-11-11 17:37:11 +01:00
evilsocket
e87bcc4744 Merge pull request from benleb/patch-1
fix completely broken gps plugin
2019-11-11 17:36:12 +01:00
Simone Margaritelli
dfd534ac41 fix: fixed ups_lite plugin layout (closes ) 2019-11-11 17:35:45 +01:00
Aaron Bieber
0b2c156d29 Make backup / restore use POSIX shell for portability.
backup.sh:
  - Add use getopts for a more friendly user interface.
  - Add timeout to ping check.
  - Add unix epoch to backup file names.
  - Backup ssh information (/etc/ssh, $USER/.ssh).

restore.sh:
  - Add use getopts for a more friendly user interface.
  - Add timeout to ping check.
  - If user doesn't specify a backup file to restore, attempt to
    find the latest for the host name passed in.

Signed-off-by: Aaron Bieber <aaron@bolddaemon.com>
2019-11-11 07:49:24 -07:00
O-Zone
14064c3b5b Add support for SpotPear 2,4inch LCD display via framebuffer 2019-11-11 14:15:48 +01:00
Ben Lebherz
313fd66634 fix completely broken gps plugin :D 2019-11-11 14:12:38 +01:00
evilsocket
c939af4248 Merge pull request from xenDE/master
added webgpsmap plugin
2019-11-11 11:00:57 +01:00
xenDE
e934181606 webgpsmap default disabled
was enabled default by mistake (copy&paste)
2019-11-11 10:59:54 +01:00
evilsocket
2505cbf14c Merge pull request from DaniloNC/master
Add support to whitelist by MAC and MAC vendor
2019-11-11 10:51:57 +01:00
evilsocket
b2c6de72cd Merge pull request from budd3993/fix-memtemp-inky
fixed memtemp location for inky display
2019-11-11 10:51:22 +01:00
evilsocket
307b3890f1 Merge pull request from LaurentFough/master
Create .editorconfig
2019-11-11 10:50:26 +01:00
xenDE
b9a909de2b add handling for empty position data 2019-11-10 21:56:36 +00:00
Łaurent ʘ❢Ŧ Ŧough
b180f16aa6 Update .editorconfig
Removed; defaults that may not be used by everyone
2019-11-10 12:56:52 -08:00
Łaurent ʘ❢Ŧ Ŧough
2d517e3de5 Update .editorconfig
Removed *.py section
2019-11-10 12:55:28 -08:00
xenDE
a1746da7f1 comment default gpio button command examples 2019-11-10 19:45:43 +00:00
xenDE
1a1a70d6e8 added webgpsmap plugin 2019-11-10 19:34:49 +00:00
Josh Bauer
229e2671e8 fixed memtemp location for inky display
Signed-off-by: Josh Bauer <joshbauer3@gmail.com>
2019-11-10 14:08:33 -05:00
Simone Margaritelli
a2ac679499 new: pwnfile link in the web ui (closes ) 2019-11-10 13:24:56 +01:00
evilsocket
d7e1c59709 Merge pull request from danielhoherd/improve-backup
Ignore backup archives. Use 'find | xargs' to handle missing files.
2019-11-10 13:11:30 +01:00
evilsocket
a5ed09cd08 Merge pull request from xenDE/master
prevent double execution on gpio buttons
2019-11-10 13:10:49 +01:00
Simone Margaritelli
8c83f8129c fix: added memtemp defaults (fixes for PR ) 2019-11-10 13:10:16 +01:00
Daniel Hoherd
7ca1168fed Ignore backup archives. Use 'find | xargs' to handle missing files. Correct file type in comments.
Signed-off-by: Daniel Hoherd <daniel.hoherd@gmail.com>
2019-11-09 17:20:42 -08:00
Łaurent ʘ❢Ŧ Ŧough
d41e5c1152 Create .editorconfig
Reasonably tame .editorconfig for help with editting the .py & especially the YAML files
2019-11-09 14:18:03 -08:00
xenDE
25eee18e7b verhindern von doppelten ausführungen
i had with the old value 250 many double executions, despite short push switches. The new value 600 prevents this. Tested with volume buttons.
2019-11-09 19:50:47 +01:00
evilsocket
38144a7abb Merge pull request from mil1200/master
Added Slovak language
2019-11-09 10:30:49 +01:00
evilsocket
1f17d3cbbe Merge pull request from bensmith83/patch-1
Add Fahrenheit and Kelvin temperature scales to memtemp.py
2019-11-09 10:30:26 +01:00
bensmith83
1da59b50b4 Add Fahrenheit and Kelvin temperature scales to memtemp.py 2019-11-08 19:50:33 -05:00
danilonc
1130c72098 Add support to whitelist by MAC and MAC vendor 2019-11-08 16:18:42 -06:00
Milan Kyselica
6b99deb7bd Update voice.po 2019-11-08 20:13:05 +01:00
Milan Kyselica
c3ed3509e9 + SK voice.mo 2019-11-08 20:12:23 +01:00
Milan Kyselica
e3a2e8c811 Delete voice.po 2019-11-08 20:12:04 +01:00
Milan Kyselica
89046bf0c5 + SK voice.po 2019-11-08 20:11:51 +01:00
Milan Kyselica
4cc61322de + SK voice.po 2019-11-08 20:09:53 +01:00
evilsocket
94521f2174 Merge pull request from dadav/feature/webcfg-plugin
Add webcfg
2019-11-08 18:43:48 +01:00
dadav
b50acd364c Add webcfg 2019-11-08 17:55:48 +01:00
Simone Margaritelli
9bc7fcccb3 new: the grid plugin now triggers an on_unread_inbox event that other plugins can intercept (see led plugin) 2019-11-08 15:49:49 +01:00
Simone Margaritelli
bd61196c3c new: the auto-update plugin now triggers an on_updating event that other plugins can intercept (see led plugin) 2019-11-08 15:47:12 +01:00
Simone Margaritelli
186042aa20 misc: small fix or general refactoring i did not bother commenting 2019-11-08 15:43:57 +01:00
Simone Margaritelli
78bf801273 misc: small fix or general refactoring i did not bother commenting 2019-11-08 15:40:54 +01:00
Simone Margaritelli
89450ec1bd new: implemented led plugin (closes ) 2019-11-08 15:31:40 +01:00
Simone Margaritelli
09f80cc842 fix: fixed typo in the example plugin 2019-11-08 14:08:43 +01:00
Simone Margaritelli
fcb5c87ef0 fix: enabling fstrim.timer from setup.py for updating users 2019-11-08 13:36:27 +01:00
Simone Margaritelli
bf0e480266 Merge branch 'master' of github.com:evilsocket/pwnagotchi 2019-11-08 13:28:53 +01:00
Simone Margaritelli
fce57ad8eb misc: small fix or general refactoring i did not bother commenting 2019-11-08 13:28:37 +01:00
evilsocket
425fe7e55a Merge pull request from Spindel/devel
Enable fstrim.timer to increase SDCard lifetime
2019-11-08 13:26:49 +01:00
Simone Margaritelli
5760864495 misc: small fix or general refactoring i did not bother commenting 2019-11-08 13:25:55 +01:00
Simone Margaritelli
3d9c559cdb misc: small fix or general refactoring i did not bother commenting 2019-11-08 13:20:14 +01:00
Simone Margaritelli
97a019fe25 new: implemented log rotation (closes ) 2019-11-08 13:01:50 +01:00
Simone Margaritelli
8d5834232b fix: fixed exit after --version 2019-11-08 12:07:14 +01:00
evilsocket
ebc161e82f Merge pull request from uzerai/master
Add  norwegian locale.
2019-11-08 10:58:30 +01:00
Edvard Botten
9485e53484 Add norwegian translation files.
Signed-off-by: Edvard Botten <edvbot@gmail.com>
2019-11-07 22:02:28 +00:00
D.S. Ljungmark
0d66f93ef3 Enable fstrim.timer to increase SDCard lifetime
fstrim timer triggers weekly to issue TRIM/DISCARD on devices that support it,
which includes SD and MMC devices.

This marks all unused diskspace as available for internal garbage collection
and wear levelling on the card.  It's preferrable to use the timer to the
discard option, as it involves a one-off cost of latency over a global slowing
down of all write or erase requests that the `discard` mount option does.
2019-11-07 17:41:17 +01:00
evilsocket
ad80fab554 Merge pull request from dadav/fix/webhook_arguments
Changed webhook arguments, added exception handling, added logrotation
2019-11-07 11:06:14 +01:00
dadav
b7380018f1 Changed webhook arguments and add exception handling 2019-11-07 11:03:37 +01:00
Simone Margaritelli
2ea8e7fe6b misc: moved non-core and problematic plugins to separate repository (closes ) 2019-11-07 10:59:40 +01:00
evilsocket
e23e1affae Merge pull request from neutralinsomniac/normalize_waveshare29inch_config
normalize the waveshare29inch config string
2019-11-07 10:39:43 +01:00
evilsocket
8d8333b586 Merge pull request from dadav/fix/gpio-plugin
Fix options of gpio-plugin
2019-11-07 10:38:16 +01:00
dadav
5712f5cd51 Fix arguments 2019-11-07 07:37:41 +01:00
Jeremy O'Brien
9cc15403c3 normalize the waveshare29inch config string
Signed-off-by: Jeremy O'Brien <neutral@fastmail.com>
2019-11-06 09:07:27 -05:00
evilsocket
15fa7039e8 Merge pull request from deveth0/deveth0-523-webui
: Add some styling to webui
2019-11-06 11:31:40 +01:00
Alex Muthmann
f83c820b38 Fix missing js 2019-11-06 11:28:23 +01:00
evilsocket
323c9a74cc Merge pull request from deveth0/deveth0-netpos-logging
Additional Logging for net-pos plugin
2019-11-06 11:24:14 +01:00
evilsocket
8a2d6eac9d Merge pull request from FrixosTh/patch-3
Bug Fix on AircrackOnly Plugin preventing it to load
2019-11-06 11:23:36 +01:00
FrixosTh
61d8e28aad Bug Fix on AircrackOnly Plugin preventing it to load
Unnecessary/Wrong call to the super init method prevents AircrackOnly to load during startup
2019-11-06 01:46:48 +02:00
Alex Muthmann
337ebd6f9f Add some style 2019-11-06 00:06:19 +01:00
amuthmann
399dbf2b41 Fixes : Add some style 2019-11-06 00:03:29 +01:00
amuthmann
f952bcd298 Fixes : Add some style 2019-11-06 00:03:17 +01:00
Alex Muthmann
9dc7c92c86 Additional Logging for net-pos plugin to simplify search for broken files
I currently have some broken net-pos files on my device and the logging does not provide enough information to find the invalid files. I'd suggest to log the path.
2019-11-05 21:54:37 +01:00
283 changed files with 43809 additions and 633 deletions
.editorconfig.gitignore
bin
builder
pwnagotchi
__init__.pyagent.py
ai
defaults.ymlgrid.py
locale
no
LC_MESSAGES
sk
LC_MESSAGES
spa
LC_MESSAGES
log.py
mesh
plugins
ui
components.pydisplay.py
hw
view.py
web
handler.pyserver.py
static
css
js
jquery-1.12.4.min.jsjquery-qrcode-0.17.0.min.js
jquery.mobile
images
ajax-loader.gif
icons-png
action-black.pngaction-white.pngalert-black.pngalert-white.pngarrow-d-black.pngarrow-d-l-black.pngarrow-d-l-white.pngarrow-d-r-black.pngarrow-d-r-white.pngarrow-d-white.pngarrow-l-black.pngarrow-l-white.pngarrow-r-black.pngarrow-r-white.pngarrow-u-black.pngarrow-u-l-black.pngarrow-u-l-white.pngarrow-u-r-black.pngarrow-u-r-white.pngarrow-u-white.pngaudio-black.pngaudio-white.pngback-black.pngback-white.pngbars-black.pngbars-white.pngbullets-black.pngbullets-white.pngcalendar-black.pngcalendar-white.pngcamera-black.pngcamera-white.pngcarat-d-black.pngcarat-d-white.pngcarat-l-black.pngcarat-l-white.pngcarat-r-black.pngcarat-r-white.pngcarat-u-black.pngcarat-u-white.pngcheck-black.pngcheck-white.pngclock-black.pngclock-white.pngcloud-black.pngcloud-white.pngcomment-black.pngcomment-white.pngdelete-black.pngdelete-white.pngedit-black.pngedit-white.pngeye-black.pngeye-white.pngforbidden-black.pngforbidden-white.pngforward-black.pngforward-white.pnggear-black.pnggear-white.pnggrid-black.pnggrid-white.pngheart-black.pngheart-white.pnghome-black.pnghome-white.pnginfo-black.pnginfo-white.pnglocation-black.pnglocation-white.pnglock-black.pnglock-white.pngmail-black.pngmail-white.pngminus-black.pngminus-white.pngnavigation-black.pngnavigation-white.pngphone-black.pngphone-white.pngplus-black.pngplus-white.pngpower-black.pngpower-white.pngrecycle-black.pngrecycle-white.pngrefresh-black.pngrefresh-white.pngsearch-black.pngsearch-white.pngshop-black.pngshop-white.pngstar-black.pngstar-white.pngtag-black.pngtag-white.pnguser-black.pnguser-white.pngvideo-black.pngvideo-white.png
icons-svg
action-black.svgaction-white.svgalert-black.svgalert-white.svgarrow-d-black.svgarrow-d-l-black.svgarrow-d-l-white.svgarrow-d-r-black.svgarrow-d-r-white.svgarrow-d-white.svgarrow-l-black.svgarrow-l-white.svgarrow-r-black.svgarrow-r-white.svgarrow-u-black.svgarrow-u-l-black.svgarrow-u-l-white.svgarrow-u-r-black.svgarrow-u-r-white.svgarrow-u-white.svgaudio-black.svgaudio-white.svgback-black.svgback-white.svgbars-black.svgbars-white.svgbullets-black.svgbullets-white.svgcalendar-black.svgcalendar-white.svgcamera-black.svgcamera-white.svgcarat-d-black.svgcarat-d-white.svgcarat-l-black.svgcarat-l-white.svgcarat-r-black.svgcarat-r-white.svgcarat-u-black.svgcarat-u-white.svgcheck-black.svgcheck-white.svgclock-black.svgclock-white.svgcloud-black.svgcloud-white.svgcomment-black.svgcomment-white.svgdelete-black.svgdelete-white.svgedit-black.svgedit-white.svgeye-black.svgeye-white.svgforbidden-black.svgforbidden-white.svgforward-black.svgforward-white.svggear-black.svggear-white.svggrid-black.svggrid-white.svgheart-black.svgheart-white.svghome-black.svghome-white.svginfo-black.svginfo-white.svglocation-black.svglocation-white.svglock-black.svglock-white.svgmail-black.svgmail-white.svgminus-black.svgminus-white.svgnavigation-black.svgnavigation-white.svgphone-black.svgphone-white.svgplus-black.svgplus-white.svgpower-black.svgpower-white.svgrecycle-black.svgrecycle-white.svgrefresh-black.svgrefresh-white.svgsearch-black.svgsearch-white.svgshop-black.svgshop-white.svgstar-black.svgstar-white.svgtag-black.svgtag-white.svguser-black.svguser-white.svgvideo-black.svgvideo-white.svg
jquery.mobile-1.4.5.cssjquery.mobile-1.4.5.jsjquery.mobile-1.4.5.min.cssjquery.mobile-1.4.5.min.jsjquery.mobile-1.4.5.min.mapjquery.mobile.external-png-1.4.5.cssjquery.mobile.external-png-1.4.5.min.cssjquery.mobile.icons-1.4.5.cssjquery.mobile.icons-1.4.5.min.cssjquery.mobile.inline-png-1.4.5.cssjquery.mobile.inline-png-1.4.5.min.cssjquery.mobile.inline-svg-1.4.5.cssjquery.mobile.inline-svg-1.4.5.min.cssjquery.mobile.structure-1.4.5.cssjquery.mobile.structure-1.4.5.min.cssjquery.mobile.theme-1.4.5.cssjquery.mobile.theme-1.4.5.min.css
jquery.timeago.jsrefresh.jsviewportHeight.js
templates
utils.py
scripts
setup.py

7
.editorconfig Normal file

@@ -0,0 +1,7 @@
# top-most EditorConfig file
root = true
# Matches the exact files either package.json or .travis.yml
[{*.yml,*.yaml,config.yml,defaults.yml}]
indent_style = space
indent_size = 2

2
.gitignore vendored

@@ -15,3 +15,5 @@ output-pwnagotchi
build
dist
pwnagotchi.egg-info
*backup*.tgz
*backup*.gz

@@ -1,19 +1,91 @@
#!/usr/bin/python3
import logging
import argparse
import time
import yaml
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
def do_clear(display):
logging.info("clearing the display ...")
display.clear()
exit(0)
def do_manual_mode(agent):
logging.info("entering manual mode ...")
agent.mode = 'manual'
agent.last_session.parse(agent.view(), args.skip_session)
if not args.skip_session:
logging.info(
"the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
agent.last_session.duration_human,
agent.last_session.epochs,
agent.last_session.train_epochs,
agent.last_session.avg_reward,
agent.last_session.min_reward,
agent.last_session.max_reward))
while True:
display.on_manual_mode(agent.last_session)
time.sleep(5)
if grid.is_connected():
plugins.on('internet_available', agent)
def do_auto_mode(agent):
logging.info("entering auto mode ...")
agent.mode = 'auto'
agent.start()
while True:
try:
# recon on all channels
agent.recon()
# get nearby access points grouped by channel
channels = agent.get_access_points_by_channel()
# for each channel
for ch, aps in channels:
agent.set_channel(ch)
if not agent.is_stale() and agent.any_activity():
logging.info("%d access points on channel %d" % (len(aps), ch))
# for each ap on this channel
for ap in aps:
# send an association frame in order to get for a PMKID
agent.associate(ap)
# deauth all client stations in order to get a full handshake
for sta in ap['clients']:
agent.deauth(ap, sta)
# An interesting effect of this:
#
# From Pwnagotchi's perspective, the more new access points
# and / or client stations nearby, the longer one epoch of
# its relative time will take ... basically, in Pwnagotchi's universe,
# WiFi electromagnetic fields affect time like gravitational fields
# affect ours ... neat ^_^
agent.next_epoch()
if grid.is_connected():
plugins.on('internet_available', agent)
except Exception as e:
logging.exception("main loop exception")
if __name__ == '__main__':
import argparse
import time
import logging
import yaml
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
parser = argparse.ArgumentParser()
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.yml',
@@ -32,15 +104,22 @@ if __name__ == '__main__':
help="Enable debug logs.")
parser.add_argument('--version', dest="version", action="store_true", default=False,
help="Prints the version.")
help="Print the version.")
parser.add_argument('--print-config', dest="print_config", action="store_true", default=False,
help="Print the configuration.")
args = parser.parse_args()
if args.version:
print(pwnagotchi.version)
SystemExit(0)
exit(0)
config = utils.load_config(args)
if args.print_config:
print(yaml.dump(config, default_flow_style=False))
exit(0)
utils.setup_logging(args, config)
pwnagotchi.set_name(config['main']['name'])
@@ -48,79 +127,14 @@ if __name__ == '__main__':
plugins.load(config)
display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
keypair = KeyPair(view=display)
agent = Agent(view=display, config=config, keypair=keypair)
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent.fingerprint(), pwnagotchi.version))
logging.debug("effective configuration:\n\n%s\n\n" % yaml.dump(config, default_flow_style=False))
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s" % (plugin.__class__.__name__, plugin.__version__))
if args.do_clear:
logging.info("clearing the display ...")
display.clear()
do_clear(display)
exit(0)
elif args.do_manual:
logging.info("entering manual mode ...")
agent.mode = 'manual'
agent.last_session.parse(agent.view(), args.skip_session)
if not args.skip_session:
logging.info(
"the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
agent.last_session.duration_human,
agent.last_session.epochs,
agent.last_session.train_epochs,
agent.last_session.avg_reward,
agent.last_session.min_reward,
agent.last_session.max_reward))
while True:
display.on_manual_mode(agent.last_session)
time.sleep(5)
if grid.is_connected():
plugins.on('internet_available', agent)
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
if args.do_manual:
do_manual_mode(agent)
else:
logging.info("entering auto mode ...")
agent.mode = 'auto'
agent.start()
while True:
try:
# recon on all channels
agent.recon()
# get nearby access points grouped by channel
channels = agent.get_access_points_by_channel()
# for each channel
for ch, aps in channels:
agent.set_channel(ch)
if not agent.is_stale() and agent.any_activity():
logging.info("%d access points on channel %d" % (len(aps), ch))
# for each ap on this channel
for ap in aps:
# send an association frame in order to get for a PMKID
agent.associate(ap)
# deauth all client stations in order to get a full handshake
for sta in ap['clients']:
agent.deauth(ap, sta)
# An interesting effect of this:
#
# From Pwnagotchi's perspective, the more new access points
# and / or client stations nearby, the longer one epoch of
# its relative time will take ... basically, in Pwnagotchi's universe,
# WiFi electromagnetic fields affect time like gravitational fields
# affect ours ... neat ^_^
agent.next_epoch()
if grid.is_connected():
plugins.on('internet_available', agent)
except Exception as e:
logging.exception("main loop exception")
do_auto_mode(agent)

@@ -23,6 +23,7 @@
- bettercap.service
- pwngrid-peer.service
- epd-fuse.service
- fstrim.timer
disable:
- apt-daily.timer
- apt-daily.service

@@ -6,7 +6,7 @@ import re
import pwnagotchi.ui.view as view
import pwnagotchi
version = '1.2.1'
version = '1.3.0'
_name = None

@@ -32,10 +32,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._started_at = time.time()
self._filter = None if config['main']['filter'] is None else re.compile(config['main']['filter'])
self._current_channel = 0
self._tot_aps = 0
self._aps_on_channel = 0
self._supported_channels = utils.iface_channels(config['main']['iface'])
self._view = view
self._view.set_agent(self)
self._web_ui = Server(self, self._config['ui']['display'])
self._web_ui = Server(self, config['ui'])
self._access_points = []
self._last_pwnd = None
@@ -47,6 +49,10 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if not os.path.exists(config['bettercap']['handshakes']):
os.makedirs(config['bettercap']['handshakes'])
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), self.fingerprint(), pwnagotchi.version))
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s" % (plugin.__class__.__name__, plugin.__version__))
def config(self):
return self._config
@@ -176,7 +182,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
for ap in s['wifi']['aps']:
if ap['encryption'] == '' or ap['encryption'] == 'OPEN':
continue
elif ap['hostname'] not in whitelist:
elif ap['hostname'] not in whitelist \
and ap['mac'].lower() not in whitelist \
and ap['mac'][:8].lower() not in whitelist:
if self._filter_included(ap):
aps.append(ap)
except Exception as e:
@@ -185,6 +193,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
aps.sort(key=lambda ap: ap['channel'])
return self.set_access_points(aps)
def get_total_aps(self):
return self._tot_aps
def get_aps_on_channel(self):
return self._aps_on_channel
def get_current_channel(self):
return self._current_channel
def get_access_points_by_channel(self):
aps = self.get_access_points()
channels = self._config['personality']['channels']
@@ -221,16 +238,16 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
# self._view.set('epoch', '%04d' % self._epoch.epoch)
def _update_counters(self):
tot_aps = len(self._access_points)
self._tot_aps = len(self._access_points)
tot_stas = sum(len(ap['clients']) for ap in self._access_points)
if self._current_channel == 0:
self._view.set('aps', '%d' % tot_aps)
self._view.set('aps', '%d' % self._tot_aps)
self._view.set('sta', '%d' % tot_stas)
else:
aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
self._aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
stas_on_channel = sum(
[len(ap['clients']) for ap in self._access_points if ap['channel'] == self._current_channel])
self._view.set('aps', '%d (%d)' % (aps_on_channel, tot_aps))
self._view.set('aps', '%d (%d)' % (self._aps_on_channel, self._tot_aps))
self._view.set('sta', '%d (%d)' % (stas_on_channel, tot_stas))
def _update_handshakes(self, new_shakes=0):
@@ -324,10 +341,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
(ap, sta) = ap_and_station
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
'hostname'] != '<hidden>' else ap_mac
logging.warning("!!! captured new handshake on channel %d: %s (%s) -> %s [%s (%s)] !!!" % ( \
ap['channel'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor']))
logging.warning(
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!" % (
ap['channel'],
ap['rssi'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor']))
plugins.on('handshake', self, filename, ap, sta)
except Exception as e:
@@ -380,8 +399,8 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._view.on_assoc(ap)
try:
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients]..." % ( \
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients'])))
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm..." % ( \
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi']))
self.run('wifi.assoc %s' % ap['mac'])
self._epoch.track(assoc=True)
except Exception as e:
@@ -401,8 +420,8 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._view.on_deauth(sta)
try:
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d ..." % (
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel']))
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ..." % (
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi']))
self.run('wifi.deauth %s' % sta['mac'])
self._epoch.track(deauth=True)
except Exception as e:

@@ -4,31 +4,37 @@ import pwnagotchi.mesh.wifi as wifi
MAX_EPOCH_DURATION = 1024
histogram_size = wifi.NumChannels
shape = (1,
# aps per channel
histogram_size +
# clients per channel
histogram_size +
# peers per channel
histogram_size +
# duration
1 +
# inactive
1 +
# active
1 +
# missed
1 +
# hops
1 +
# deauths
1 +
# assocs
1 +
# handshakes
1)
def describe(extended=False):
if not extended:
histogram_size = wifi.NumChannels
else:
# see https://github.com/evilsocket/pwnagotchi/issues/583
histogram_size = wifi.NumChannelsExt
return histogram_size, (1,
# aps per channel
histogram_size +
# clients per channel
histogram_size +
# peers per channel
histogram_size +
# duration
1 +
# inactive
1 +
# active
1 +
# missed
1 +
# hops
1 +
# deauths
1 +
# assocs
1 +
# handshakes
1)
def featurize(state, step):

@@ -34,10 +34,14 @@ class Environment(gym.Env):
self._epoch_num = 0
self._last_render = None
channels = agent.supported_channels()
# see https://github.com/evilsocket/pwnagotchi/issues/583
self._supported_channels = agent.supported_channels()
self._extended_spectrum = any(ch > 140 for ch in self._supported_channels)
self._histogram_size, self._observation_shape = featurizer.describe(self._extended_spectrum)
Environment.params += [
Parameter('_channel_%d' % ch, min_value=0, max_value=1, meta=ch + 1) for ch in
range(featurizer.histogram_size) if ch + 1 in channels
range(self._histogram_size) if ch + 1 in self._supported_channels
]
self.last = {
@@ -50,7 +54,7 @@ class Environment(gym.Env):
}
self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.params if p.trainable])
self.observation_space = spaces.Box(low=0, high=1, shape=featurizer.shape, dtype=np.float32)
self.observation_space = spaces.Box(low=0, high=1, shape=self._observation_shape, dtype=np.float32)
self.reward_range = reward.range
@staticmethod
@@ -118,7 +122,7 @@ class Environment(gym.Env):
return self.last['state_v']
def _render_histogram(self, hist):
for ch in range(featurizer.histogram_size):
for ch in range(self._histogram_size):
if hist[ch]:
logging.info(" CH %d: %s" % (ch + 1, hist[ch]))

@@ -19,26 +19,10 @@ main:
report: false # don't report pwned networks by default!
exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
- YourHomeNetworkHere
auto-update:
enabled: true
install: true # if false, it will only warn that updates are available, if true it will install them
interval: 1 # every 1 hour
auto-backup:
enabled: false
interval: 1 # every day
max_tries: 0 # 0=infinity
files:
- /root/brain.nn
- /root/brain.json
- /root/.api-report.json
- /root/handshakes/
- /root/peers/
- /etc/pwnagotchi/
- /var/log/pwnagotchi.log
commands:
- 'tar czf /root/pwnagotchi-backup.tar.gz {files}'
net-pos:
enabled: false
api_key: 'test'
@@ -46,12 +30,8 @@ main:
enabled: false
speed: 19200
device: /dev/ttyUSB0
twitter:
webgpsmap:
enabled: false
consumer_key: aaa
consumer_secret: aaa
access_token_key: aaa
access_token_secret: aaa
onlinehashcrack:
enabled: false
email: ~
@@ -62,16 +42,8 @@ main:
wigle:
enabled: false
api_key: ~
screen_refresh:
enabled: false
refresh_interval: 50
quickdic:
enabled: false
wordlist_folder: /opt/wordlists/
AircrackOnly:
enabled: false
bt-tether:
enabled: false # if you want to use this, set ui.display.video.address to 0.0.0.0
enabled: false # if you want to use this, set ui.display.web.address to 0.0.0.0
devices:
android-phone:
enabled: false
@@ -97,8 +69,9 @@ main:
priority: 999 # routing priority
memtemp: # Display memory usage, cpu load and cpu temperature on screen
enabled: false
scale: celsius
orientation: horizontal # horizontal/vertical
pawgps:
paw-gps:
enabled: false
#The IP Address of your phone with Paw Server running, default (option is empty) is 192.168.44.1
ip: ''
@@ -106,24 +79,65 @@ main:
enabled: false
#The following is a list of the GPIO number for your button, and the command you want to run when it is pressed
gpios:
- 20: 'sudo touch /root/.pwnagotchi-auto && sudo systemctl restart pwnagotchi'
- 21: 'shutdown -h now'
#20: 'touch /root/.pwnagotchi-auto && systemctl restart pwnagotchi'
#21: 'shutdown -h now'
led:
enabled: true
# for /sys/class/leds/led0/brightness
led: 0
# time in milliseconds for each element of the patterns
delay: 200
# o=on space=off, comment the ones you don't want
patterns:
loaded: 'oo oo oo oo oo oo oo'
updating: 'oo oo oo oo oo oo oo'
# internet_available: 'oo oo oo oo oo oo oo'
unread_inbox: 'oo oo oo oo oo oo oo'
ready: 'oo oo oo oo oo oo oo'
ai_ready: 'oo oo oo oo oo oo oo'
ai_training_start: 'oo oo oo oo oo oo oo'
ai_best_reward: 'oo oo oo oo oo oo oo'
ai_worst_reward: 'oo oo oo oo oo oo oo'
bored: 'oo oo oo oo oo oo oo'
sad: 'oo oo oo oo oo oo oo'
excited: 'oo oo oo oo oo oo oo'
lonely: 'oo oo oo oo oo oo oo'
rebooting: 'oo oo oo oo oo oo oo'
wait: 'oo oo oo oo oo oo oo'
sleep: 'oo oo oo oo oo oo oo'
wifi_update: 'oo oo oo oo oo oo oo'
association: 'oo oo oo oo oo oo oo'
deauthentication: 'oo oo oo oo oo oo oo'
handshake: 'oo oo oo oo oo oo oo'
epoch: 'oo oo oo oo oo oo oo'
peer_detected: 'oo oo oo oo oo oo oo'
peer_lost: 'oo oo oo oo oo oo oo'
webcfg:
enabled: false
# monitor interface to use
iface: mon0
# command to run to bring the mon interface up in case it's not up already
mon_start_cmd: /usr/bin/monstart
mon_stop_cmd: /usr/bin/monstop
mon_max_blind_epochs: 50
# log file
log: /var/log/pwnagotchi.log
# if true, will not restart the wifi module
no_restart: false
# access points to ignore
# access points to ignore. Could be the ssid, bssid or the vendor part of bssid.
whitelist:
- EXAMPLE_NETWORK
- ANOTHER_EXAMPLE_NETWORK
- fo:od:ba:be:fo:od # BSSID
- fo:od:ba # Vendor BSSID
# if not null, filter access points by this regular expression
filter: null
# logging
log:
# file to log to
path: /var/log/pwnagotchi.log
rotation:
enabled: true
# specify a maximum size to rotate ( format is 10/10B, 10K, 10M 10G )
size: '10M'
ai:
# if false, only the default 'personality' will be used
@@ -227,25 +241,28 @@ ui:
# preserve your display over time, you should set this value to 0.0 so that the display will be refreshed only
# if any of the important data fields changed (the uptime and blinking cursor won't trigger a refresh).
fps: 0.0
# web ui
web:
enabled: true
address: '0.0.0.0'
username: changeme # !!! CHANGE THIS !!!
password: changeme # !!! CHANGE THIS !!!
origin: null
port: 8080
# command to be executed when a new png frame is available
# for instance, to use with framebuffer based displays:
# on_frame: 'fbi --noverbose -a -d /dev/fb1 -T 1 /root/pwnagotchi.png > /dev/null 2>&1'
on_frame: ''
# hardware display
display:
enabled: true
rotation: 180
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, lcdhat, waveshare154inch, waveshare27inch, dfrobot/df
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, lcdhat, waveshare154inch, waveshare27inch, waveshare29inch, dfrobot/df
type: 'waveshare_2'
# Possible options red/yellow/black (black used for monocromatic displays)
# Waveshare tri-color 2.13in display can be over-driven with color set as 'fastAndFurious'
# THIS IS POTENTIALLY DANGEROUS. DO NOT USE UNLESS YOU UNDERSTAND THAT IT COULD KILL YOUR DISPLAY
color: 'black'
video:
enabled: true
address: '0.0.0.0'
origin: null
port: 8080
# command to be executed when a new png frame is available
# for instance, to use with framebuffer based displays:
# on_frame: 'fbi --noverbose -a -d /dev/fb1 -T 1 /root/pwnagotchi.png > /dev/null 2>&1'
on_frame: ''
# bettercap rest api configuration
bettercap:

@@ -12,9 +12,13 @@ API_ADDRESS = "http://127.0.0.1:8666/api/v1"
def is_connected():
try:
socket.create_connection(("www.google.com", 80))
return True
except OSError:
# check DNS
host = socket.gethostbyname('api.pwnagotchi.ai')
if host:
# check connectivity itself
socket.create_connection((host, 443), timeout=30)
return True
except:
pass
return False
@@ -22,9 +26,11 @@ def is_connected():
def call(path, obj=None):
url = '%s%s' % (API_ADDRESS, path)
if obj is None:
r = requests.get(url, headers=None)
r = requests.get(url, headers=None, timeout=(30.0, 60.0))
elif isinstance(obj, dict):
r = requests.post(url, headers=None, json=obj, timeout=(30.0, 60.0))
else:
r = requests.post(url, headers=None, json=obj)
r = requests.post(url, headers=None, data=obj, timeout=(30.0, 60.0))
if r.status_code != 200:
raise Exception("(status %d) %s" % (r.status_code, r.text))
@@ -39,6 +45,14 @@ def set_advertisement_data(data):
return call("/mesh/data", obj=data)
def get_advertisement_data():
return call("/mesh/data")
def memory():
return call("/mesh/memory")
def peers():
return call("/mesh/peers")
@@ -95,3 +109,15 @@ def report_ap(essid, bssid):
def inbox(page=1, with_pager=False):
obj = call("/inbox?p=%d" % page)
return obj["messages"] if not with_pager else obj
def inbox_message(id):
return call("/inbox/%d" % int(id))
def mark_message(id, mark):
return call("/inbox/%d/%s" % (int(id), str(mark)))
def send_message(to, message):
return call("/unit/%s/inbox" % to, message.encode('utf-8'))

Binary file not shown.

@@ -0,0 +1,248 @@
# pwnagotchi norwegian voice data
# Copyright (C) 2019
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR untech <edvbot@gmail.com>, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Edvard Botten <edvbot@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: norwegian\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "ZzzzZZzzzzZzzz"
msgstr ""
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Hei, jeg er Pwnagotchi! Starter ..."
msgid "New day, new hunt, new pwns!"
msgstr "En ny dag, ny jakt, og nye pwns!"
msgid "Hack the Planet!"
msgstr "Hack planeten!"
msgid "AI ready."
msgstr "AI klart."
msgid "The neural network is ready."
msgstr "Det nevrale nettverket er klart."
msgid "Generating keys, do not turn off ..."
msgstr "Generer nøkkler, ikke skru meg av ..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hei, kanalen {channel} er åpen! AP-en din takker."
msgid "Reading last session logs ..."
msgstr "Leser forrige sesjonen's logs ..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Har lest {lines_so_far} linjer hittil ..."
msgid "I'm bored ..."
msgstr "Kjeder meg ..."
msgid "Let's go for a walk!"
msgstr "La oss stikke på tur!"
msgid "This is the best day of my life!"
msgstr "Dette er den beste dagen i mitt liv!"
msgid "Shitty day :/"
msgstr "Jævlig dag :/"
msgid "I'm extremely bored ..."
msgstr "Kjeder livet av meg ..."
msgid "I'm very sad ..."
msgstr "Jeg er veldig trist ..."
msgid "I'm sad"
msgstr "Jeg er trist ..."
msgid "Leave me alone ..."
msgstr "La meg være alene ..."
msgid "I'm mad at you!"
msgstr "Jeg er sint på deg!"
msgid "I'm living the life!"
msgstr "Lever livet, lett!"
msgid "I pwn therefore I am."
msgstr "Jeg pwner derfor er jeg."
msgid "So many networks!!!"
msgstr "Så mange nettverk!!!"
msgid "I'm having so much fun!"
msgstr "Jeg har det så gøy!"
msgid "My crime is that of curiosity ..."
msgstr "Nysgjerrighet er min eneste forbrytelse ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Hallo {name}! Hyggelig å treffe deg!"
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name}! Skjer'a?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Heisann {name} driver du med da?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "{name} er i nærheten!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ... Ha det {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} er borte ..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Oi da ... {name} forsvant."
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} bommet!"
msgid "Missed!"
msgstr "Bommet!"
msgid "Good friends are a blessing!"
msgstr "Gode venner er livet verdt!"
msgid "I love my friends!"
msgstr "Jeg digger vennene mine!"
msgid "Nobody wants to play with me ..."
msgstr "Ingen vil leke med meg ..."
msgid "I feel so alone ..."
msgstr "Jeg er så ensom ..."
msgid "Where's everybody?!"
msgstr "Hvor er alle sammen?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Sover i {secs}s ..."
msgid "Zzzzz"
msgstr ""
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr ""
msgid "Good night."
msgstr "God natt."
msgid "Zzz"
msgstr ""
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Venter i {secs}s ..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Ser meg rundt ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Hei {what} la oss være venner!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Tilkobler til {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr ""
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Bestemte meg att {mac} ikke lenger trenger WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "Kobler av {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Kickbanner {mac}"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Fett, vi fikk {num} nye håndtrykk!"
#, python-brace-format
msgid "You have {count} new message{plural}!"
msgstr "Du har {count} melding{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Oi, noe gikk helt skakk ... Rebooter ..."
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Kicket {num} stasjoner\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Møtte {num} nye venner\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Skaffet {num} håndtrykk\n"
msgid "Met 1 peer"
msgstr "Møtte 1 annen"
#, python-brace-format
msgid "Met {num} peers"
msgstr "Møtte {num} andre"
#, python-brace-format
msgid ""
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr "Jeg har pwnet for {duration} og kicket {dauthed} klienter! Jeg har også "
"møtt {associated} nye venner og spiste {handshakes} håndtrykk! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "timer"
msgid "minutes"
msgstr "minutter"
msgid "seconds"
msgstr "sekunder"
msgid "hour"
msgstr "time"
msgid "minute"
msgstr "minutt"
msgid "second"
msgstr "sekund"

Binary file not shown.

@@ -0,0 +1,227 @@
# Slovak language
# Copyright (C) 2019
# This file is distributed under the same license as the pwnagotchi package.
# mil1200 <mil.kyselica@gmail.com>, 2019.
#
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-8 17:55+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Milan Kyselica <mil.kyselica@gmail.com>\n"
"Language-Team: SK\n"
"Language: sk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "ZzzzZZzzzzZzzz"
msgstr "ZzzzZZzzzzZzzz"
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Ahoj, ja som Pwnagotchi! Začíname ..."
msgid "New day, new hunt, new pwns!"
msgstr "Nový deň, nový lov, nové pwns!"
msgid "Hack the Planet!"
msgstr "Hacknime Planétu!"
msgid "AI ready."
msgstr "AI pripravené."
msgid "The neural network is ready."
msgstr "Neurónová sieť je pripravená."
msgid "Generating keys, do not turn off ..."
msgstr "Generujú sa kľúče, nevypínaj ..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hej, kanál {channel} je voľný! Váš AP vám poďakuje."
msgid "I'm bored ..."
msgstr "Nudím sa ..."
msgid "Let's go for a walk!"
msgstr "Poďme na prechádzku!"
msgid "This is the best day of my life!"
msgstr "Toto je najlepší deň môjho života!"
msgid "Shitty day :/"
msgstr "Na hovno deň :/"
msgid "I'm extremely bored ..."
msgstr "Veľmi sa nudím ..."
msgid "I'm very sad ..."
msgstr "Som veľmi smutný ..."
msgid "I'm sad"
msgstr "Som smutný"
msgid "I'm living the life!"
msgstr "Žijem život!"
msgid "I pwn therefore I am."
msgstr "I pwn therefore I am."
msgid "So many networks!!!"
msgstr "Toľko sietí !!!"
msgid "I'm having so much fun!"
msgstr "Zabávam sa!"
msgid "My crime is that of curiosity ..."
msgstr "Môj zločin je zvedavosť ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Dobrý deň, {name}! Rád som ťa spoznal."
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Jednotka {name} je blízko!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ... zbohom {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} je preč ..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Hups ... {name} je preč."
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} nechytené!"
msgid "Missed!"
msgstr "Vedľa!"
msgid "Good friends are a blessing!"
msgstr "Dobrí priatelia sú požehnaním!"
msgid "I love my friends!"
msgstr "Milujem svojich priateľov!"
msgid "Nobody wants to play with me ..."
msgstr "Nikto sa so mnou nechce hrať ..."
msgid "I feel so alone ..."
msgstr "Cítim sa tak sám ..."
msgid "Where's everybody?!"
msgstr "Kde sú všetci ?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Zdriemnem si na {secs}s ..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s)"
msgid "Good night."
msgstr "Dobrú noc."
msgid "Zzz"
msgstr "Zzz"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Čaká sa {secs}s ..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Rozhliadam sa okolo ({secs} s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Ahoj {what} buďme priatelia!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Spájam sa s {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Yo {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Rozhodol som sa že {mac} nepotrebuje Wi-Fi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "Deautentifikujem {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Kickujem {mac}!"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Super, máme {num} nový handshake{plural}!"
#, python-brace-format
msgid "You have {count} new message{plural}!"
msgstr "Máte {count} novú správu{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Ops, niečo sa pokazilo ... Reštartujem sa ..."
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Kicknutá/ých {num} stanica/íc\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Získaní {num} noví kamaráti\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Získali sme {num} handshake/-y/ov rúk\n"
msgid "Met 1 peer"
msgstr "Sretli sme 1 rovesníka"
#, python-brace-format
msgid "Met {num} peers"
msgstr "Stretli sme {num} rovesníkov"
#, python-brace-format
msgid ""
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr "Pwnoval som {duration} a kickol som {deauthed} klienta/ov! Tiež som"
"stretol {associated} nového/ých kamaráta/ov a zjedol {handshakes} handshake/y!"
" #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "hodiny"
msgid "minutes"
msgstr "minúty"
msgid "seconds"
msgstr "sekundy"
msgid "hour"
msgstr "hodina"
msgid "minute"
msgstr "minúta"
msgid "second"
msgstr "sekunda"

@@ -0,0 +1,248 @@
# Interfaz en español para pwnagotchi
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Angel Hernandez Segura, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Angel Hernandez Segura <ahsec.7@gmail.com>\n"
"Language-Team: Español <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "ZzzzZZzzzzZzzz"
msgstr "ZzzzZZzzzzZzzz"
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Hola, Soy Pwnagotchi! Iniciando..."
msgid "New day, new hunt, new pwns!"
msgstr "Un nuevo dia, nuevos objetivos, nuevos pwns"
msgid "Hack the Planet!"
msgstr "Hack the Planet!"
msgid "AI ready."
msgstr "IA lista"
msgid "The neural network is ready."
msgstr "La red neuronal esta lista"
msgid "Generating keys, do not turn off ..."
msgstr "Generando llaves, no apagar"
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, canal {channel} esta libre! Tu AP te lo agredecera"
msgid "Reading last session logs ..."
msgstr "Leyendo logs de la ultima sesion"
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "He leido {lines_so_far} lineas de los logs hasta ahora "
msgid "I'm bored ..."
msgstr "Estoy aburrido"
msgid "Let's go for a walk!"
msgstr "Vamos a caminar!"
msgid "This is the best day of my life!"
msgstr "Este es el mejor dia de mi vida"
msgid "Shitty day :/"
msgstr "Dia de mierda :/"
msgid "I'm extremely bored ..."
msgstr "Estoy extremadamente aburrido ..."
msgid "I'm very sad ..."
msgstr "Estoy mut triste"
msgid "I'm sad"
msgstr "Estoy triste"
msgid "Leave me alone ..."
msgstr "Dejame solo ..."
msgid "I'm mad at you!"
msgstr "Estoy enojado contigo!"
msgid "I'm living the life!"
msgstr "Estoy disfrutando la vida!"
msgid "I pwn therefore I am."
msgstr "Yo pwn, por lo tanto existo"
msgid "So many networks!!!"
msgstr "Tantas redes!!!"
msgid "I'm having so much fun!"
msgstr "Me estoy divirtiendo mucho!"
msgid "My crime is that of curiosity ..."
msgstr "Mi crimen es la curiosidad ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Hola {name}! Mucho gusto."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name}! Que hay?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {name} como te va?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Unit {name} esta cerca!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ... adios {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} se fue ..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Whoops ... {name} se fue"
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} se ha perdido!"
msgid "Missed!"
msgstr "Perdido!"
msgid "Good friends are a blessing!"
msgstr "Los buenos amigos son una bendicion"
msgid "I love my friends!"
msgstr "Amo a mis amigos!"
msgid "Nobody wants to play with me ..."
msgstr "Nadie quiere jugar conmigo ..."
msgid "I feel so alone ..."
msgstr "Me siento muy solo ..."
msgid "Where's everybody?!"
msgstr "Donde estan todos?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Tomando una siesta por {secs}s ..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s) "
msgid "Good night."
msgstr "Buenas noches."
msgid "Zzz"
msgstr "Zzz"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Esperando por {secs}s ..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Mirando alrededor ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Hey {what} vamos a ser amigos!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Asociandose a {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Yo {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Acabo de decidir que {mac} no necesita WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "De-autenticando {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Vetando {mac}!"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Bien, obtuvimos {num} nuevos handshake{plural}!"
#, python-brace-format
msgid "You have {count} new message{plural}!"
msgstr "Tienes {count} nuevos mensajes{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Oops, algo salio mal ... Reiniciando ..."
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Bloquee {num} staciones\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Hice {num} nuevos amigos\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Obtuve {num} handshakes\n"
msgid "Met 1 peer"
msgstr "Conoci a 1 unidad"
#, python-brace-format
msgid "Met {num} peers"
msgstr "conoci {num} unidades"
#, python-brace-format
msgid ""
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr "He estado hackeando por {duration} y de-autenticando {deauthed} "
"clientes! Tambien conoci {associated} nuevos amigos y comi {handshakes} "
"handshakes! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "horas"
msgid "minutes"
msgstr "minutos"
msgid "seconds"
msgstr "segundos"
msgid "hour"
msgstr "hora"
msgid "minute"
msgstr "minuto"
msgid "second"
msgstr "segundo"

@@ -26,7 +26,7 @@ class LastSession(object):
def __init__(self, config):
self.config = config
self.voice = Voice(lang=config['main']['lang'])
self.path = config['main']['log']
self.path = config['main']['log']['path']
self.last_session = []
self.last_session_id = ''
self.last_saved_session_id = ''

@@ -1,4 +1,5 @@
NumChannels = 140
NumChannelsExt = 165 # see https://github.com/evilsocket/pwnagotchi/issues/583
def freq_to_channel(freq):

@@ -1,5 +1,6 @@
import os
import glob
import _thread
import importlib, importlib.util
import logging
@@ -31,7 +32,7 @@ def one(plugin_name, event_name, *args, **kwargs):
callback = getattr(plugin, cb_name, None)
if callback is not None and callable(callback):
try:
callback(*args, **kwargs)
_thread.start_new_thread(callback, (*args, *kwargs))
except Exception as e:
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
logging.error(e, exc_info=True)

@@ -1,57 +0,0 @@
import pwnagotchi.plugins as plugins
import logging
import subprocess
import string
import os
'''
Aircrack-ng needed, to install:
> apt-get install aircrack-ng
'''
class AircrackOnly(plugins.Plugin):
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
__version__ = '1.0.1'
__license__ = 'GPL3'
__description__ = 'confirm pcap contains handshake/PMKID or delete it'
def __init__(self):
super().__init__(self)
self.text_to_set = ""
def on_loaded(self):
logging.info("aircrackonly plugin loaded")
def on_handshake(self, agent, filename, access_point, client_station):
display = agent._view
todelete = 0
handshakeFound = 0
result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "1 handshake" | awk \'{print $2}\''),
shell=True, stdout=subprocess.PIPE)
result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace})
if result:
handshakeFound = 1
logging.info("[AircrackOnly] contains handshake")
if handshakeFound == 0:
result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "PMKID" | awk \'{print $2}\''),
shell=True, stdout=subprocess.PIPE)
result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace})
if result:
logging.info("[AircrackOnly] contains PMKID")
else:
todelete = 1
if todelete == 1:
os.remove(filename)
self.text_to_set = "Removed an uncrackable pcap"
display.update(force=True)
def on_ui_update(self, ui):
if self.text_to_set:
ui.set('face', "(>.<)")
ui.set('status', self.text_to_set)
self.text_to_set = ""

@@ -1,65 +0,0 @@
import pwnagotchi.plugins as plugins
from pwnagotchi.utils import StatusFile
import logging
import os
import subprocess
class AutoBackup(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'This plugin backups files when internet is available.'
def __init__(self):
self.ready = False
self.tries = 0
self.status = StatusFile('/root/.auto-backup')
def on_loaded(self):
for opt in ['files', 'interval', 'commands', 'max_tries']:
if opt not in self.options or (opt in self.options and self.options[opt] is None):
logging.error(f"AUTO-BACKUP: Option {opt} is not set.")
return
self.ready = True
logging.info("AUTO-BACKUP: Successfully loaded.")
def on_internet_available(self, agent):
if not self.ready:
return
if self.options['max_tries'] and self.tries >= self.options['max_tries']:
return
if self.status.newer_then_days(self.options['interval']):
return
# Only backup existing files to prevent errors
existing_files = list(filter(lambda f: os.path.exists(f), self.options['files']))
files_to_backup = " ".join(existing_files)
try:
display = agent.view()
logging.info("AUTO-BACKUP: Backing up ...")
display.set('status', 'Backing up ...')
display.update()
for cmd in self.options['commands']:
logging.info(f"AUTO-BACKUP: Running {cmd.format(files=files_to_backup)}")
process = subprocess.Popen(cmd.format(files=files_to_backup), shell=True, stdin=None,
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
process.wait()
if process.returncode > 0:
raise OSError(f"Command failed (rc: {process.returncode})")
logging.info("AUTO-BACKUP: backup done")
display.set('status', 'Backup done!')
display.update()
self.status.update()
except OSError as os_e:
self.tries += 1
logging.info(f"AUTO-BACKUP: Error: {os_e}")
display.set('status', 'Backup failed!')
display.update()

@@ -198,6 +198,7 @@ class AutoUpdate(plugins.Plugin):
if num_updates > 0:
if self.options['install']:
for update in to_install:
plugins.on('updating')
if install(display, update):
num_installed += 1
else:

@@ -1,14 +1,15 @@
import os
import time
import re
import logging
import os
import subprocess
import time
import dbus
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.utils import StatusFile
import pwnagotchi.plugins as plugins
class BTError(Exception):
@@ -382,7 +383,7 @@ class IfaceWrapper:
class Device:
def __init__(self, name, share_internet, mac, ip, netmask, interval, priority=10, scantime=15, search_order=0, max_tries=0, **kwargs):
def __init__(self, name, share_internet, mac, ip, netmask, interval, gateway=None, priority=10, scantime=15, search_order=0, max_tries=0, **kwargs):
self.name = name
self.status = StatusFile(f'/root/.bt-tether-{name}')
self.status.update()
@@ -394,6 +395,7 @@ class Device:
self.share_internet = share_internet
self.ip = ip
self.netmask = netmask
self.gateway = gateway
self.interval = interval
self.mac = mac
self.scantime = scantime
@@ -461,7 +463,7 @@ class BTTether(plugins.Plugin):
logging.error("BT-TETHER: Can't start bluetooth.service")
return
logging.info("BT-TETHER: Sussessfully loaded ...")
logging.info("BT-TETHER: Successfully loaded ...")
self.ready = True
def on_ui_setup(self, ui):
@@ -543,7 +545,10 @@ class BTTether(plugins.Plugin):
continue
addr = f"{device.ip}/{device.netmask}"
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
if device.gateway:
gateway = device.gateway
else:
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
wrapped_interface = IfaceWrapper(interface)
logging.debug('BT-TETHER: Add ip to %s', interface)

@@ -16,27 +16,16 @@ class Example(plugins.Plugin):
logging.debug("example plugin created")
# called when http://<host>:<port>/plugins/<plugin>/ is called
# must return a response
def on_webhook(self, path, args, req_method):
# must return a html page
# IMPORTANT: If you use "POST"s, add a csrf-token (via csrf_token() and render_template_string)
def on_webhook(self, path, request):
pass
# called when the plugin is loaded
def on_loaded(self):
logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
# called when <host>:<port>/plugins/<pluginname> is opened
def on_webhook(self, response, path):
res = "<html><body><a>Hook triggered</a></body></html>"
response.send_response(200)
response.send_header('Content-type', 'text/html')
response.end_headers()
try:
response.wfile.write(bytes(res, "utf-8"))
except Exception as ex:
logging.error(ex)
# called in manual mode when there's internet connectivity
# called hen there's internet connectivity
def on_internet_available(self, agent):
pass
@@ -106,7 +95,7 @@ class Example(plugins.Plugin):
pass
# called when the status is set to excited
def on_excited(aself, gent):
def on_excited(self, agent):
pass
# called when the status is set to lonely

@@ -31,10 +31,8 @@ class GPIOButtons(plugins.Plugin):
# set gpio numbering
GPIO.setmode(GPIO.BCM)
for i in gpios:
gpio = list(i)[0]
command = i[gpio]
for gpio, command in gpios.items():
self.ports[gpio] = command
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=250)
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)
logging.info("Added command: %s to GPIO #%d", command, gpio)

@@ -1,42 +1,116 @@
import logging
import json
import logging
import os
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
class GPS(plugins.Plugin):
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'Save GPS coordinates whenever an handshake is captured.'
__author__ = "evilsocket@gmail.com"
__version__ = "1.0.0"
__license__ = "GPL3"
__description__ = "Save GPS coordinates whenever an handshake is captured."
def __init__(self):
self.running = False
self.coordinates = None
def on_loaded(self):
logging.info("gps plugin loaded for %s" % self.options['device'])
logging.info(f"gps plugin loaded for {self.options['device']}")
def on_ready(self, agent):
if os.path.exists(self.options['device']):
logging.info("enabling gps bettercap's module for %s" % self.options['device'])
if os.path.exists(self.options["device"]):
logging.info(
f"enabling bettercap's gps module for {self.options['device']}"
)
try:
agent.run('gps off')
except:
agent.run("gps off")
except Exception:
pass
agent.run('set gps.device %s' % self.options['device'])
agent.run('set gps.speed %d' % self.options['speed'])
agent.run('gps on')
running = True
agent.run(f"set gps.device {self.options['device']}")
agent.run(f"set gps.baudrate {self.options['speed']}")
agent.run("gps on")
self.running = True
else:
logging.warning("no GPS detected")
def on_handshake(self, agent, filename, access_point, client_station):
if self.running:
info = agent.session()
gps = info['gps']
gps_filename = filename.replace('.pcap', '.gps.json')
self.coordinates = info["gps"]
gps_filename = filename.replace(".pcap", ".gps.json")
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
with open(gps_filename, 'w+t') as fp:
json.dump(gps, fp)
logging.info(f"saving GPS to {gps_filename} ({self.coordinates})")
with open(gps_filename, "w+t") as fp:
json.dump(self.coordinates, fp)
def on_ui_setup(self, ui):
# add coordinates for other displays
if ui.is_waveshare_v2():
lat_pos = (127, 75)
lon_pos = (122, 84)
alt_pos = (127, 94)
elif ui.is_inky():
# guessed values, add tested ones if you can
lat_pos = (112, 30)
lon_pos = (112, 49)
alt_pos = (87, 63)
else:
# guessed values, add tested ones if you can
lat_pos = (127, 51)
lon_pos = (127, 56)
alt_pos = (102, 71)
label_spacing = 0
ui.add_element(
"latitude",
LabeledValue(
color=BLACK,
label="lat:",
value="-",
position=lat_pos,
label_font=fonts.Small,
text_font=fonts.Small,
label_spacing=label_spacing,
),
)
ui.add_element(
"longitude",
LabeledValue(
color=BLACK,
label="long:",
value="-",
position=lon_pos,
label_font=fonts.Small,
text_font=fonts.Small,
label_spacing=label_spacing,
),
)
ui.add_element(
"altitude",
LabeledValue(
color=BLACK,
label="alt:",
value="-",
position=alt_pos,
label_font=fonts.Small,
text_font=fonts.Small,
label_spacing=label_spacing,
),
)
def on_ui_update(self, ui):
if self.coordinates and all([
# avoid 0.000... measurements
self.coordinates["Latitude"], self.coordinates["Longitude"]
]):
# last char is sometimes not completely drawn ¯\_(ツ)_/¯
# using an ending-whitespace as workaround on each line
ui.set("latitude", f"{self.coordinates['Latitude']:.4f} ")
ui.set("longitude", f" {self.coordinates['Longitude']:.4f} ")
ui.set("altitude", f" {self.coordinates['Altitude']:.1f}m ")

@@ -77,6 +77,7 @@ class Grid(plugins.Plugin):
self.unread_messages = len([m for m in messages if m['seen_at'] is None])
if self.unread_messages:
plugins.on('unread_inbox', self.unread_messages)
logging.debug("[grid] unread:%d total:%d" % (self.unread_messages, self.total_messages))
agent.view().on_unread_messages(self.unread_messages, self.total_messages)

@@ -0,0 +1,159 @@
from threading import Event
import _thread
import logging
import time
import pwnagotchi.plugins as plugins
class Led(plugins.Plugin):
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'This plugin blinks the PWR led with different patterns depending on the event.'
def __init__(self):
self._is_busy = False
self._event = Event()
self._event_name = None
self._led_file = "/sys/class/leds/led0/brightness"
self._delay = 200
# called when the plugin is loaded
def on_loaded(self):
self._led_file = "/sys/class/leds/led%d/brightness" % self.options['led']
self._delay = int(self.options['delay'])
logging.info("[led] plugin loaded for %s" % self._led_file)
self._on_event('loaded')
_thread.start_new_thread(self._worker, ())
def _on_event(self, event):
if not self._is_busy:
self._event_name = event
self._event.set()
logging.debug("[led] event '%s' set", event)
else:
logging.debug("[led] skipping event '%s' because the worker is busy", event)
def _led(self, on):
with open(self._led_file, 'wt') as fp:
fp.write(str(on))
def _blink(self, pattern):
logging.debug("[led] using pattern '%s' ..." % pattern)
for c in pattern:
if c == ' ':
self._led(0)
else:
self._led(1)
time.sleep(self._delay / 1000.0)
# reset
self._led(1)
def _worker(self):
while True:
self._event.wait()
self._event.clear()
self._is_busy = True
try:
if self._event_name in self.options['patterns']:
pattern = self.options['patterns'][self._event_name]
self._blink(pattern)
else:
logging.debug("[led] no pattern defined for %s" % self._event_name)
except Exception as e:
logging.exception("[led] error while blinking")
finally:
self._is_busy = False
# called when the unit is updating its software
def on_updating(self):
self._on_event('updating')
# called when there's one or more unread pwnmail messages
def on_unread_inbox(self, num_unread):
self._on_event('unread_inbox')
# called when there's internet connectivity
def on_internet_available(self, agent):
self._on_event('internet_available')
# called when everything is ready and the main loop is about to start
def on_ready(self, agent):
self._on_event('ready')
# called when the AI finished loading
def on_ai_ready(self, agent):
self._on_event('ai_ready')
# called when the AI starts training for a given number of epochs
def on_ai_training_start(self, agent, epochs):
self._on_event('ai_training_start')
# called when the AI got the best reward so far
def on_ai_best_reward(self, agent, reward):
self._on_event('ai_best_reward')
# called when the AI got the worst reward so far
def on_ai_worst_reward(self, agent, reward):
self._on_event('ai_worst_reward')
# called when the status is set to bored
def on_bored(self, agent):
self._on_event('bored')
# called when the status is set to sad
def on_sad(self, agent):
self._on_event('sad')
# called when the status is set to excited
def on_excited(self, agent):
self._on_event('excited')
# called when the status is set to lonely
def on_lonely(self, agent):
self._on_event('lonely')
# called when the agent is rebooting the board
def on_rebooting(self, agent):
self._on_event('rebooting')
# called when the agent is waiting for t seconds
def on_wait(self, agent, t):
self._on_event('wait')
# called when the agent is sleeping for t seconds
def on_sleep(self, agent, t):
self._on_event('sleep')
# called when the agent refreshed its access points list
def on_wifi_update(self, agent, access_points):
self._on_event('wifi_update')
# called when the agent is sending an association frame
def on_association(self, agent, access_point):
self._on_event('association')
# called when the agent is deauthenticating a client station from an AP
def on_deauthentication(self, agent, access_point, client_station):
self._on_event('deauthentication')
# called when a new handshake is captured, access_point and client_station are json objects
# if the agent could match the BSSIDs to the current list, otherwise they are just the strings of the BSSIDs
def on_handshake(self, agent, filename, access_point, client_station):
self._on_event('handshake')
# called when an epoch is over (where an epoch is a single loop of the main algorithm)
def on_epoch(self, agent, epoch, epoch_data):
self._on_event('epoch')
# called when a new peer is detected
def on_peer_detected(self, agent, peer):
self._on_event('peer_detected')
# called when a known peer is lost
def on_peer_lost(self, agent, peer):
self._on_event('peer_lost')

@@ -44,24 +44,39 @@ class MemTemp(plugins.Plugin):
if ui.is_waveshare_v2():
h_pos = (180, 80)
v_pos = (180, 61)
elif ui.is_inky():
h_pos = (140, 68)
v_pos = (165, 54)
else:
h_pos = (155, 76)
v_pos = (180, 61)
if self.options['orientation'] == "horizontal":
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -',
position=h_pos,
label_font=fonts.Small, text_font=fonts.Small))
elif self.options['orientation'] == "vertical":
if self.options['orientation'] == "vertical":
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value=' mem:-\n cpu:-\ntemp:-',
position=v_pos,
label_font=fonts.Small, text_font=fonts.Small))
else:
# default to horizontal
ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - - -',
position=h_pos,
label_font=fonts.Small, text_font=fonts.Small))
def on_ui_update(self, ui):
if self.options['orientation'] == "horizontal":
ui.set('memtemp',
" mem cpu temp\n %s%% %s%% %sc" % (self.mem_usage(), self.cpu_load(), pwnagotchi.temperature()))
if self.options['scale'] == "fahrenheit":
temp = (pwnagotchi.temperature() * 9 / 5) + 32
symbol = "f"
elif self.options['scale'] == "kelvin":
temp = pwnagotchi.temperature() + 273.15
symbol = "k"
else:
# default to celsius
temp = pwnagotchi.temperature()
symbol = "c"
elif self.options['orientation'] == "vertical":
if self.options['orientation'] == "vertical":
ui.set('memtemp',
" mem:%s%%\n cpu:%s%%\ntemp:%sc" % (self.mem_usage(), self.cpu_load(), pwnagotchi.temperature()))
" mem:%s%%\n cpu:%s%%\ntemp:%s%s" % (self.mem_usage(), self.cpu_load(), temp, symbol))
else:
# default to horizontal
ui.set('memtemp',
" mem cpu temp\n %s%% %s%% %s%s" % (self.mem_usage(), self.cpu_load(), temp, symbol))

@@ -73,15 +73,15 @@ class NetPos(plugins.Plugin):
try:
geo_data = self._get_geo_data(np_file) # returns json obj
except requests.exceptions.RequestException as req_e:
logging.error("NET-POS: %s", req_e)
logging.error("NET-POS: %s - RequestException: %s", np_file, req_e)
self.skip += np_file
continue
except json.JSONDecodeError as js_e:
logging.error("NET-POS: %s", js_e)
logging.error("NET-POS: %s - JSONDecodeError: %s", np_file, js_e)
self.skip += np_file
continue
except OSError as os_e:
logging.error("NET-POS: %s", os_e)
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
self.skip += np_file
continue

@@ -3,7 +3,10 @@ import requests
import pwnagotchi.plugins as plugins
'''
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemic:
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
NEW BETTER GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
Old guide here, (not recommended if you plan on using it with the webgpsmap plugin)
https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
'''
@@ -22,7 +25,9 @@ class PawGPS(plugins.Plugin):
def on_handshake(self, agent, filename, access_point, client_station):
if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None):
ip = "192.168.44.1"
ip = "192.168.44.1:8080"
else:
ip = self.options['ip']
gps = requests.get('http://' + ip + '/gps.xhtml')
gps_filename = filename.replace('.pcap', '.gps.json')

@@ -1,51 +0,0 @@
import logging
import subprocess
import string
import re
import pwnagotchi.plugins as plugins
'''
Aircrack-ng needed, to install:
> apt-get install aircrack-ng
Upload wordlist files in .txt format to folder in config file (Default: /opt/wordlists/)
Cracked handshakes stored in handshake folder as [essid].pcap.cracked
'''
class QuickDic(plugins.Plugin):
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'Run a quick dictionary scan against captured handshakes'
def __init__(self):
self.text_to_set = ""
def on_loaded(self):
logging.info("Quick dictionary check plugin loaded")
def on_handshake(self, agent, filename, access_point, client_station):
display = agent.view()
result = subprocess.run(('/usr/bin/aircrack-ng ' + filename + ' | grep "1 handshake" | awk \'{print $2}\''),
shell=True, stdout=subprocess.PIPE)
result = result.stdout.decode('utf-8').translate({ord(c): None for c in string.whitespace})
if not result:
logging.info("[quickdic] No handshake")
else:
logging.info("[quickdic] Handshake confirmed")
result2 = subprocess.run(('aircrack-ng -w `echo ' + self.options[
'wordlist_folder'] + '*.txt | sed \'s/\ /,/g\'` -l ' + filename + '.cracked -q -b ' + result + ' ' + filename + ' | grep KEY'),
shell=True, stdout=subprocess.PIPE)
result2 = result2.stdout.decode('utf-8').strip()
logging.info("[quickdic] " + result2)
if result2 != "KEY NOT FOUND":
key = re.search('\[(.*)\]', result2)
pwd = str(key.group(1))
self.text_to_set = "Cracked password: " + pwd
display.update(force=True)
def on_ui_update(self, ui):
if self.text_to_set:
ui.set('face', "(·ω·)")
ui.set('status', self.text_to_set)
self.text_to_set = ""

@@ -1,23 +0,0 @@
import logging
import pwnagotchi.plugins as plugins
class ScreenRefresh(plugins.Plugin):
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'Refresh he e-ink display after X amount of updates'
def __init__(self):
self.update_count = 0;
def on_loaded(self):
logging.info("Screen refresh plugin loaded")
def on_ui_update(self, ui):
self.update_count += 1
if self.update_count == self.options['refresh_interval']:
ui.init_display()
ui.set('status', "Screen cleaned")
logging.info("Screen refreshing")
self.update_count = 0

@@ -1,49 +0,0 @@
import logging
from pwnagotchi.voice import Voice
import pwnagotchi.plugins as plugins
class Twitter(plugins.Plugin):
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'This plugin creates tweets about the recent activity of pwnagotchi'
def on_loaded(self):
logging.info("twitter plugin loaded.")
# called in manual mode when there's internet connectivity
def on_internet_available(self, agent):
config = agent.config()
display = agent.view()
last_session = agent.last_session
if last_session.is_new() and last_session.handshakes > 0:
try:
import tweepy
except ImportError:
logging.error("Couldn't import tweepy")
return
logging.info("detected a new session and internet connectivity!")
picture = '/root/pwnagotchi.png'
display.on_manual_mode(last_session)
display.update(force=True)
display.image().save(picture, 'png')
display.set('status', 'Tweeting...')
display.update(force=True)
try:
auth = tweepy.OAuthHandler(self.options['consumer_key'], self.options['consumer_secret'])
auth.set_access_token(self.options['access_token_key'], self.options['access_token_secret'])
api = tweepy.API(auth)
tweet = Voice(lang=config['main']['lang']).on_last_session_tweet(last_session)
api.update_with_media(filename=picture, status=tweet)
last_session.save_session_id()
logging.info("tweeted: %s" % tweet)
except Exception as e:
logging.exception("error while tweeting")

@@ -55,8 +55,8 @@ class UPSLite(plugins.Plugin):
self.ups = UPS()
def on_ui_setup(self, ui):
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
def on_ui_update(self, ui):
ui.set('ups', "%4.2fV/%2i%%" % (self.ups.voltage(), self.ups.capacity()))
ui.set('ups', "%2i%%" % self.ups.capacity())

@@ -0,0 +1,511 @@
import logging
import json
import yaml
import _thread
import pwnagotchi.plugins as plugins
from pwnagotchi import restart
from flask import abort
from flask import render_template_string
INDEX = """
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=0" />
<title>
webcfg
</title>
<style>
#divTop {
position: -webkit-sticky;
position: sticky;
top: 0px;
width: 100%;
font-size: 16px;
padding: 5px;
border: 1px solid #ddd;
margin-bottom: 5px;
}
#searchText {
width: 100%;
}
table {
table-layout: auto;
width: 100%;
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
th, td {
padding: 15px;
text-align: left;
}
table tr:nth-child(even) {
background-color: #eee;
}
table tr:nth-child(odd) {
background-color: #fff;
}
table th {
background-color: black;
color: white;
}
.remove {
background-color: #f44336;
color: white;
border: 2px solid #f44336;
padding: 4px 8px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
}
.remove:hover {
background-color: white;
color: black;
}
#btnSave {
position: -webkit-sticky;
position: sticky;
bottom: 0px;
width: 100%;
background-color: #0061b0;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
float: right;
}
#divTop {
display: table;
width: 100%;
}
#divTop > * {
display: table-cell;
}
#divTop > span {
width: 1%;
}
#divTop > input {
width: 100%;
}
@media screen and (max-width:700px) {
table, tr, td {
padding:0;
border:1px solid black;
}
table {
border:none;
}
tr:first-child, thead, th {
display:none;
border:none;
}
tr {
float: left;
width: 100%;
margin-bottom: 2em;
}
table tr:nth-child(odd) {
background-color: #eee;
}
td {
float: left;
width: 100%;
padding:1em;
}
td::before {
content:attr(data-label);
word-wrap: break-word;
background: #eee;
border-right:2px solid black;
width: 20%;
float:left;
padding:1em;
font-weight: bold;
margin:-1em 1em -1em -1em;
}
.del_btn_wrapper {
content:attr(data-label);
word-wrap: break-word;
background: #eee;
border-right:2px solid black;
width: 20%;
float:left;
padding:1em;
font-weight: bold;
margin:-1em 1em -1em -1em;
}
}
</style>
</head>
<body>
<div id="divTop">
<input type="text" id="searchText" onkeyup="filterTable()" placeholder="Search for options ..." title="Type an option name">
<span><select id="selAddType"><option value="text">Text</option><option value="number">Number</option></select></span>
<span><button id="btnAdd" type="button" onclick="addOption()">+</button></span>
</div>
<div id="content"></div>
<button id="btnSave" type="button" onclick="saveConfig()">Save</button>
</body>
<script type="text/javascript">
function addOption() {
var input, table, tr, td, divDelBtn, btnDel, selType, selTypeVal;
input = document.getElementById("searchText");
inputVal = input.value;
selType = document.getElementById("selAddType");
selTypeVal = selType.options[selType.selectedIndex].value;
table = document.getElementById("tableOptions");
if (table) {
tr = table.insertRow();
// del button
divDelBtn = document.createElement("div");
divDelBtn.className = "del_btn_wrapper";
td = document.createElement("td");
td.setAttribute("data-label", "");
btnDel = document.createElement("Button");
btnDel.innerHTML = "X";
btnDel.onclick = function(){ delRow(this);};
btnDel.className = "remove";
divDelBtn.appendChild(btnDel);
td.appendChild(divDelBtn);
tr.appendChild(td);
// option
td = document.createElement("td");
td.setAttribute("data-label", "Option");
td.innerHTML = inputVal;
tr.appendChild(td);
// value
td = document.createElement("td");
td.setAttribute("data-label", "Value");
input = document.createElement("input");
input.type = selTypeVal;
input.value = "";
td.appendChild(input);
tr.appendChild(td);
input.value = "";
}
}
function saveConfig(){
// get table
var table = document.getElementById("tableOptions");
if (table) {
var json = tableToJson(table);
sendJSON("webcfg/save-config", json, function(response) {
if (response) {
if (response.status == "200") {
alert("Config got updated");
} else {
alert("Error while updating the config (err-code: " + response.status + ")");
}
}
});
}
}
function filterTable(){
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("searchText");
filter = input.value.toUpperCase();
table = document.getElementById("tableOptions");
if (table) {
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td")[1];
if (td) {
txtValue = td.textContent || td.innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
}else{
tr[i].style.display = "none";
}
}
}
}
}
function sendJSON(url, data, callback) {
var xobj = new XMLHttpRequest();
var csrf = "{{ csrf_token() }}";
xobj.open('POST', url);
xobj.setRequestHeader("Content-Type", "application/json");
xobj.setRequestHeader('x-csrf-token', csrf);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4) {
callback(xobj);
}
};
xobj.send(JSON.stringify(data));
}
function loadJSON(url, callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', url, true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(JSON.parse(xobj.responseText));
}
};
xobj.send(null);
}
// https://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects
function unFlattenJson(data) {
"use strict";
if (Object(data) !== data || Array.isArray(data))
return data;
var result = {}, cur, prop, idx, last, temp, inarray;
for(var p in data) {
cur = result, prop = "", last = 0, inarray = false;
do {
idx = p.indexOf(".", last);
temp = p.substring(last, idx !== -1 ? idx : undefined);
inarray = temp.startsWith('#') && !isNaN(parseInt(temp.substring(1)))
cur = cur[prop] || (cur[prop] = (inarray ? [] : {}));
if (inarray){
prop = temp.substring(1);
}else{
prop = temp;
}
last = idx + 1;
} while(idx >= 0);
cur[prop] = data[p];
}
return result[""];
}
function flattenJson(data) {
var result = {};
function recurse (cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
for(var i=0, l=cur.length; i<l; i++)
recurse(cur[i], prop ? prop+".#"+i : ""+i);
if (l == 0)
result[prop] = [];
} else {
var isEmpty = true;
for (var p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop+"."+p : p);
}
if (isEmpty)
result[prop] = {};
}
}
recurse(data, "");
return result;
}
function delRow(btn) {
var tr = btn.parentNode.parentNode.parentNode;
tr.parentNode.removeChild(tr);
}
function jsonToTable(json) {
var table = document.createElement("table");
table.id = "tableOptions";
// create header
var tr = table.insertRow();
var thDel = document.createElement("th");
thDel.innerHTML = "";
var thOpt = document.createElement("th");
thOpt.innerHTML = "Option";
var thVal = document.createElement("th");
thVal.innerHTML = "Value";
tr.appendChild(thDel);
tr.appendChild(thOpt);
tr.appendChild(thVal);
var td, divDelBtn, btnDel;
// iterate over keys
Object.keys(json).forEach(function(key) {
tr = table.insertRow();
// del button
divDelBtn = document.createElement("div");
divDelBtn.className = "del_btn_wrapper";
td = document.createElement("td");
td.setAttribute("data-label", "");
btnDel = document.createElement("Button");
btnDel.innerHTML = "X";
btnDel.onclick = function(){ delRow(this);};
btnDel.className = "remove";
divDelBtn.appendChild(btnDel);
td.appendChild(divDelBtn);
tr.appendChild(td);
// option
td = document.createElement("td");
td.setAttribute("data-label", "Option");
td.innerHTML = key;
tr.appendChild(td);
// value
td = document.createElement("td");
td.setAttribute("data-label", "Value");
if(typeof(json[key])==='boolean'){
input = document.createElement("select");
input.setAttribute("id", "boolSelect");
tvalue = document.createElement("option");
tvalue.setAttribute("value", "true");
ttext = document.createTextNode("True")
tvalue.appendChild(ttext);
fvalue = document.createElement("option");
fvalue.setAttribute("value", "false");
ftext = document.createTextNode("False");
fvalue.appendChild(ftext);
input.appendChild(tvalue);
input.appendChild(fvalue);
input.value = json[key];
document.body.appendChild(input);
td.appendChild(input);
tr.appendChild(td);
} else {
input = document.createElement("input");
if(Array.isArray(json[key])) {
input.type = 'text';
input.value = '[]';
}else{
input.type = typeof(json[key]);
input.value = json[key];
}
td.appendChild(input);
tr.appendChild(td);
}
});
return table;
}
function tableToJson(table) {
var rows = table.getElementsByTagName("tr");
var i, td, key, value;
var json = {};
for (i = 0; i < rows.length; i++) {
td = rows[i].getElementsByTagName("td");
if (td.length == 3) {
// td[0] = del button
key = td[1].textContent || td[1].innerText;
var input = td[2].getElementsByTagName("input");
var select = td[2].getElementsByTagName("select");
if (input && input != undefined && input.length > 0 ) {
if (input[0].type == "text") {
if (input[0].value.startsWith("[") && input[0].value.endsWith("]")) {
json[key] = JSON.parse(input[0].value);
}else{
json[key] = input[0].value;
}
}else if (input[0].type == "number") {
json[key] = Number(input[0].value);
}
} else if(select && select != undefined && select.length > 0) {
var myValue = select[0].options[select[0].selectedIndex].value;
json[key] = myValue === 'true';
}
}
}
return unFlattenJson(json);
}
loadJSON("webcfg/get-config", function(response) {
var flat_json = flattenJson(response);
var table = jsonToTable(flat_json);
var divContent = document.getElementById("content");
divContent.innerHTML = "";
divContent.appendChild(table);
});
</script>
</html>
"""
def serializer(obj):
if isinstance(obj, set):
return list(obj)
raise TypeError
class WebConfig(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'This plugin allows the user to make runtime changes.'
def __init__(self):
self.ready = False
def on_ready(self, agent):
self.config = agent.config()
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
self.ready = True
def on_internet_available(self, agent):
self.config = agent.config()
self.mode = "MANU" if agent.mode == "manual" else "AUTO"
self.ready = True
def on_loaded(self):
"""
Gets called when the plugin gets loaded
"""
logging.info("webcfg: Plugin loaded.")
def on_webhook(self, path, request):
"""
Serves the current configuration
"""
if not self.ready:
return "Plugin not ready"
if request.method == "GET":
if path == "/" or not path:
return render_template_string(INDEX)
elif path == "get-config":
# send configuration
return json.dumps(self.config, default=serializer)
else:
abort(404)
elif request.method == "POST":
if path == "save-config":
try:
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
yaml.safe_dump(request.get_json(), config_file, encoding='utf-8',
allow_unicode=True, default_flow_style=False)
_thread.start_new_thread(restart, (self.mode,))
return "success"
except yaml.YAMLError as yaml_ex:
return "config error"
abort(404)

@@ -0,0 +1,218 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/xml; charset=utf-8" />
<title>GPS MAP</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"/>
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" />
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" />
<script type='text/javascript' src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js'></script>
<style type="text/css">
/* for map */
html, body, #mapdiv { height: 100%; width: 100%; margin:0; background-color:#000;}
.pwnAPPin path {
fill: #ce7575;
}
.pwnAPPinOpen path {
fill: #76ce75;
}
/* animated ap marker */
.pwnAPPin .ring_outer, .pwnAPPinOpen .ring_outer {
animation: opacityPulse 2s cubic-bezier(1, 0.14, 1, 1);
animation-iteration-count: infinite;
opacity: .5;
}
.pwnAPPin .ring_inner, .pwnAPPinOpen .ring_inner {
animation: opacityPulse 2s cubic-bezier(0.4, 0.74, 0.56, 0.82);
animation-iteration-count: infinite;
opacity: .8;
}
@keyframes opacityPulse {
0% {
opacity: 0.1;
}
50% {
opacity: 1.0;
}
100% {
opacity: 0.1;
}
}
@keyframes bounceInDown {
from, 60%, 75%, 90%, to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, 5px, 0);
}
75% {
transform: translate3d(0, -3px, 0);
}
90% {
transform: translate3d(0, 5px, 0);
}
to {
transform: none;
}
}
.bounceInDown {
animation-name: bounceInDown;
animation-duration: 2s;
animation-fill-mode: both;
}
/* animated radar */
.radar {
animation: pulsate 1s ease-out;
-webkit-animation: pulsate 1s ease-out;
-webkit-animation-iteration-count: infinite;
/* opacity: 0.0 */
}
#loading {
top: 50%;
left: 50%;
position: fixed;
background-color: rgba(255, 255, 255, 0.9);
border: 0.5vw #ff0000 solid;
border-radius: 2vw;
padding: 5vw;
transform: translateX(-50%) translateY(-50%);
text-align:center;
display: none;
}
#loading .face { font-size:8vw; }
#loading .text {position:absolute;bottom:0;text-align:center; font-size: 1vw;color:#a0a0a0;}
</style>
</head>
<body>
<div id="mapdiv"></div>
<div id="loading"><div class="face"><nobr>(⌐■&nbsp;<span id="loading_ap_img"></span>&nbsp;■)</nobr></div><div class="text" id="loading_infotext">loading positions...</div></div>
<script type="text/javascript">
function loadJSON(url, callback) {
document.getElementById("loading").style.display = "flex";
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', url, true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
};
xobj.send(null);
}
function escapeHtml(unsafe) {
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function formatMacAddress(macAddress) {
if (macAddress !== null) {
macAddress = macAddress.toUpperCase();
if (macAddress.length >= 3 && macAddress.length <= 16) {
macAddress = macAddress.replace(/\W/ig, '');
macAddress = macAddress.replace(/(.{2})/g, "$1:");
macAddress = macAddress.replace(/:+$/,'');
}
}
return macAddress;
}
// select your map theme from https://leaflet-extras.github.io/leaflet-providers/preview/
// use 2 layers with alpha for a nice dark style
var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});
var CartoDB_DarkMatter = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
opacity:0.8,
maxZoom: 19
});
var mymap = L.map('mapdiv');
Esri_WorldImagery.addTo(mymap);
CartoDB_DarkMatter.addTo(mymap);
var svg = '<svg class="pwnAPPin" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#931F1F" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#931F1F" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#931F1F" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#931F1F" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
var svgOpen = '<svg class="pwnAPPinOpen" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#1f9321" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#1f9321" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#1f9321" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#1f9321" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
document.getElementById('loading_ap_img').innerHTML = svg;
var myIcon = L.divIcon({
className: "leaflet-data-marker",
html: svg.replace('#','%23'),
iconAnchor : [40, 30],
iconSize : [80, 60],
popupAnchor : [0, -30],
});
var myIconOpen = L.divIcon({
className: "leaflet-data-marker",
html: svgOpen.replace('#','%23'),
iconAnchor : [40, 30],
iconSize : [80, 60],
popupAnchor : [0, -30],
});
var accuracys = [];
var markers = [];
var marker_pos = [];
var markerClusters = L.markerClusterGroup();
loadJSON("/plugins/webgpsmap/all", function(response) {
var positions = JSON.parse(response);
count = 0;
Object.keys(positions).forEach(function(key) {
count++;
if(positions[key].lng){
new_marker_pos = [positions[key].lat, positions[key].lng];
if (positions[key].acc) {
radius = Math.round(Math.min(positions[key].acc, 500));
markerColor = 'red';
markerColorCode = '#f03';
fillOpacity = 0.002;
if (positions[key].pass) {
markerColor = 'green';
markerColorCode = '#1aff00';
fillOpacity = 0.1;
}
accuracys.push(
L.circle(new_marker_pos, {
color: markerColor,
fillColor: markerColorCode,
fillOpacity: fillOpacity,
weight: 1,
opacity: 0.1,
radius: Math.min(positions[key].acc, 500),
}).setStyle({'className': 'radar'}).addTo(mymap)
);
}
if (positions[key].pass) {
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
} else {
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
}
passInfo = '';
if (positions[key].pass) {
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
}
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
markers.push(newMarker);
marker_pos.push(new_marker_pos);
markerClusters.addLayer( newMarker );
}
});
if (count > 0) {
mymap.addLayer( markerClusters );
var bounds = new L.LatLngBounds(marker_pos);
mymap.fitBounds(bounds);
document.getElementById("loading").style.display = "none";
} else {
document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
}
});
</script>
</body></html>

@@ -0,0 +1,369 @@
import pwnagotchi.plugins as plugins
import logging
import os
import json
import re
import datetime
from flask import Response
from functools import lru_cache
'''
2do:
- make the cache handling multiple clients
- cleanup the javascript in a class and handle "/newest" additions
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
https://gis.stackexchange.com/questions/312737/filtering-interactive-leaflet-map-with-dropdown-menu
https://blogs.kent.ac.uk/websolutions/2015/01/29/filtering-map-markers-with-leaflet-js-a-brief-technical-overview/
http://www.digital-geography.com/filter-leaflet-maps-slider/
http://bl.ocks.org/zross/47760925fcb1643b4225
-
'''
class Webgpsmap(plugins.Plugin):
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
__version__ = '1.2.2'
__name__ = 'webgpsmap'
__license__ = 'GPL3'
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
__help__ = """
- install: copy "webgpsmap.py" and "webgpsmap.html" to your configured "custom_plugins" directory
- add webgpsmap.yml to your config
- connect your PC/Smartphone/* with USB, BT or other to your pwnagotchi and browse to http://pwnagotchi.local:8080/plugins/webgpsmap/
(change pwnagotchi.local to your pwnagotchis IP, if needed)
"""
ALREADY_SENT = list()
SKIP = list()
def __init__(self):
self.ready = False
def on_ready(self, agent):
self.config = agent.config()
self.ready = True
def on_loaded(self):
"""
Plugin got loaded
"""
logging.info("webgpsmap plugin loaded")
def on_webhook(self, path, request):
"""
Returns ewquested data
"""
# defaults:
response_header_contenttype = None
response_mimetype = "application/xhtml+xml"
if not self.ready:
try:
response_data = bytes('''<html>
<head>
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>Not ready yet</body>
</html>''', "utf-8")
response_status = 500
response_mimetype = "application/xhtml+xml"
response_header_contenttype = 'text/html'
except Exception as ex:
logging.error(ex)
return
else:
if request.method == "GET":
if path == '/' or not path:
# returns the html template
self.ALREADY_SENT = list()
try:
response_data = bytes(self.get_html(), "utf-8")
except Exception as ex:
logging.error(ex)
return
response_status = 200
response_mimetype = "application/xhtml+xml"
response_header_contenttype = 'text/html'
elif path.startswith('all'):
# returns all positions
try:
self.ALREADY_SENT = list()
response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes'])), "utf-8")
response_status = 200
response_mimetype = "application/json"
response_header_contenttype = 'application/json'
except Exception as ex:
logging.error(ex)
return
# elif path.startswith('/newest'):
# # returns all positions newer then timestamp
# response_data = bytes(json.dumps(self.load_gps_from_dir(self.config['bettercap']['handshakes']), newest_only=True), "utf-8")
# response_status = 200
# response_mimetype = "application/json"
# response_header_contenttype = 'application/json'
else:
# unknown GET path
response_data = bytes('''<html>
<head>
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>4😋4</body>
</html>''', "utf-8")
response_status = 404
else:
# unknown request.method
response_data = bytes('''<html>
<head>
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>4😋4</body>
</html>''', "utf-8")
response_status = 404
try:
r = Response(response=response_data, status=response_status, mimetype=response_mimetype)
if response_header_contenttype is not None:
r.headers["Content-Type"] = response_header_contenttype
return r
except Exception as ex:
logging.error(ex)
return
# cache 1024 items
@lru_cache(maxsize=1024, typed=False)
def _get_pos_from_file(self, path):
return PositionFile(path)
def load_gps_from_dir(self, gpsdir, newest_only=False):
"""
Parses the gps-data from disk
"""
handshake_dir = gpsdir
gps_data = dict()
logging.info("webgpsmap: scanning %s", handshake_dir)
all_files = os.listdir(handshake_dir)
#print(all_files)
all_pcap_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.pcap')
]
all_geo_or_gps_files = []
for filename_pcap in all_pcap_files:
filename_base = filename_pcap[:-5] # remove ".pcap"
logging.debug("webgpsmap: found: " + filename_base)
filename_position = None
check_for = os.path.basename(filename_base) + ".gps.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
check_for = os.path.basename(filename_base) + ".geo.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
if filename_position is not None:
# logging.debug("webgpsmap: -- found: %s %d" % (check_for, len(all_geo_or_gps_files)) )
all_geo_or_gps_files.append(filename_position)
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skiped networks? No!
if newest_only:
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
logging.info("webgpsmap: Found %d .(geo|gps).json files from %d handshakes. Fetching positions ...",
len(all_geo_or_gps_files), len(all_pcap_files))
for pos_file in all_geo_or_gps_files:
try:
pos = self._get_pos_from_file(pos_file)
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO:
continue
ssid, mac = pos.ssid(), pos.mac()
ssid = "unknown" if not ssid else ssid
# invalid mac is strange and should abort; ssid is ok
if not mac:
raise ValueError("Mac can't be parsed from filename")
gps_data[ssid+"_"+mac] = {
'ssid': ssid,
'mac': mac,
'type': 'gps' if pos.type() == PositionFile.GPS else 'geo',
'lng': pos.lng(),
'lat': pos.lat(),
'acc': pos.accuracy(),
'ts_first': pos.timestamp_first(),
'ts_last': pos.timestamp_last(),
}
check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
if check_for in all_files:
gps_data[ssid + "_" + mac]["pass"] = pos.password()
self.ALREADY_SENT += pos_file
except json.JSONDecodeError as js_e:
self.SKIP += pos_file
logging.error(js_e)
continue
except ValueError as v_e:
self.SKIP += pos_file
logging.error(v_e)
continue
except OSError as os_e:
self.SKIP += pos_file
logging.error(os_e)
continue
logging.info("webgpsmap loaded %d positions", len(gps_data))
return gps_data
def get_html(self):
"""
Returns the html page
"""
try:
template_file = os.path.dirname(os.path.realpath(__file__))+"/"+"webgpsmap.html"
html_data = open(template_file, "r").read()
except Exception as ex:
logging.error("error loading template file: %s", template_file)
logging.error(ex)
return html_data
class PositionFile:
"""
Wraps gps / net-pos files
"""
GPS = 0
GEO = 1
def __init__(self, path):
self._file = path
self._filename = os.path.basename(path)
try:
with open(path, 'r') as json_file:
self._json = json.load(json_file)
except json.JSONDecodeError as js_e:
raise js_e
def mac(self):
"""
Returns the mac from filename
"""
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo)\.json', self._filename)
if parsed_mac:
mac = parsed_mac.groups()[0]
return mac
return None
def ssid(self):
"""
Returns the ssid from filename
"""
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo)\.json', self._filename)
if parsed_ssid:
return parsed_ssid.groups()[0]
return None
def json(self):
"""
returns the parsed json
"""
return self._json
def timestamp_first(self):
"""
returns the timestamp of AP first seen
"""
# use file timestamp creation time of the pcap file
return int("%.0f" % os.path.getctime(self._file))
def timestamp_last(self):
"""
returns the timestamp of AP last seen
"""
return_ts = None
if 'ts' in self._json:
return_ts = self._json['ts']
elif 'Updated' in self._json:
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
date_iso_formated = self._json['Updated']
# fill milliseconds to 6 numbers
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
part2 = part2.ljust(6, '0')
date_iso_formated = part1 + "." + part2 + "+" + part3
dateObj = datetime.datetime.fromisoformat(date_iso_formated)
return_ts = int("%.0f" % dateObj.timestamp())
else:
# use file timestamp last modification of the pcap file
return_ts = int("%.0f" % os.path.getmtime(self._file))
return return_ts
def password(self):
"""
returns the password from file.pcap.cracked od None
"""
return_pass = None
password_file_path = self._file[:-9] + ".pcap.cracked"
if os.path.isfile(password_file_path):
try:
password_file = open(password_file_path, 'r')
return_pass = password_file.read()
password_file.close()
except OSError as err:
print("OS error: {0}".format(err))
except:
print("Unexpected error:", sys.exc_info()[0])
raise
return return_pass
def type(self):
"""
returns the type of the file
"""
if self._file.endswith('.gps.json'):
return PositionFile.GPS
if self._file.endswith('.geo.json'):
return PositionFile.GEO
return None
def lat(self):
try:
if self.type() == PositionFile.GPS:
lat = self._json['Latitude']
if self.type() == PositionFile.GEO:
lat = self._json['location']['lat']
if lat != 0:
return lat
raise ValueError("Lat is 0")
except KeyError:
pass
return None
def lng(self):
try:
if self.type() == PositionFile.GPS:
lng = self._json['Longitude']
if self.type() == PositionFile.GEO:
lng = self._json['location']['lng']
if lng != 0:
return lng
raise ValueError("Lng is 0")
except KeyError:
pass
return None
def accuracy(self):
if self.type() == PositionFile.GPS:
return 50.0
if self.type() == PositionFile.GEO:
try:
return self._json['accuracy']
except KeyError:
pass
return None

@@ -58,12 +58,13 @@ class Text(Widget):
class LabeledValue(Widget):
def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0):
def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0, label_spacing=5):
super().__init__(position, color)
self.label = label
self.value = value
self.label_font = label_font
self.text_font = text_font
self.label_spacing = label_spacing
def draw(self, canvas, drawer):
if self.label is None:
@@ -71,4 +72,4 @@ class LabeledValue(Widget):
else:
pos = self.xy
drawer.text(pos, self.label, font=self.label_font, fill=self.color)
drawer.text((pos[0] + 5 + 5 * len(self.label), pos[1]), self.value, font=self.text_font, fill=self.color)
drawer.text((pos[0] + self.label_spacing + 5 * len(self.label), pos[1]), self.value, font=self.text_font, fill=self.color)

@@ -25,9 +25,6 @@ class Display(View):
)
self._render_thread_instance.start()
def set_ready(self):
self._webui.start()
def is_inky(self):
return self._implementation.name == 'inky'
@@ -61,6 +58,9 @@ class Display(View):
def is_waveshare213d(self):
return self._implementation.name == 'waveshare213d'
def is_spotpear24inch(self):
return self._implementation.name == 'spotpear24inch'
def is_waveshare_any(self):
return self.is_waveshare_v1() or self.is_waveshare_v2()
@@ -91,8 +91,8 @@ class Display(View):
def _on_view_rendered(self, img):
try:
if self._config['ui']['display']['video']['on_frame'] != '':
os.system(self._config['ui']['display']['video']['on_frame'])
if self._config['ui']['web']['on_frame'] != '':
os.system(self._config['ui']['web']['on_frame'])
except Exception as e:
logging.error("%s" % e)

@@ -9,7 +9,7 @@ from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
def display_for(config):
# config has been normalized already in utils.load_config
@@ -44,4 +44,7 @@ def display_for(config):
return Waveshare154inch(config)
elif config['ui']['display']['type'] == 'waveshare213d':
return Waveshare213d(config)
return Waveshare213d(config)
elif config['ui']['display']['type'] == 'spotpear24inch':
return Spotpear24inch(config)

@@ -0,0 +1,144 @@
FBIOGET_VSCREENINFO=0x4600
FBIOPUT_VSCREENINFO=0x4601
FBIOGET_FSCREENINFO=0x4602
FBIOGETCMAP=0x4604
FBIOPUTCMAP=0x4605
FBIOPAN_DISPLAY=0x4606
FBIOGET_CON2FBMAP=0x460F
FBIOPUT_CON2FBMAP=0x4610
FBIOBLANK=0x4611
FBIO_ALLOC=0x4613
FBIO_FREE=0x4614
FBIOGET_GLYPH=0x4615
FBIOGET_HWCINFO=0x4616
FBIOPUT_MODEINFO=0x4617
FBIOGET_DISPINFO=0x4618
from mmap import mmap
from fcntl import ioctl
import struct
mm = None
bpp, w, h = 0, 0, 0 # framebuffer bpp and size
bytepp = 0
vx, vy, vw, vh = 0, 0, 0, 0 #virtual window offset and size
vi, fi = None, None
_fb_cmap = 'IIPPPP' # start, len, r, g, b, a
RGB = False
_verbose = False
msize_kb = 0
def report_fb(i=0, layer=0):
with open('/dev/fb'+str(i), 'r+b')as f:
vi = ioctl(f, FBIOGET_VSCREENINFO, bytes(160))
vi = list(struct.unpack('I'*40, vi))
ffm = 'c'*16+'L'+'I'*4+'H'*3+'ILIIHHH'
fic = struct.calcsize(ffm)
fi = struct.unpack(ffm, ioctl(f, FBIOGET_FSCREENINFO, bytes(fic)))
def ready_fb(_bpp=None, i=0, layer=0, _win=None):
global mm, bpp, w, h, vi, fi, RGB, msize_kb, vx, vy, vw, vh, bytepp
if mm and bpp == _bpp: return mm, w, h, bpp
with open('/dev/fb'+str(i), 'r+b')as f:
vi = ioctl(f, FBIOGET_VSCREENINFO, bytes(160))
vi = list(struct.unpack('I'*40, vi))
bpp = vi[6]
bytepp = bpp//8
if _bpp:
vi[6] = _bpp # 24 bit = BGR 888 mode
try:
vi = ioctl(f, FBIOPUT_VSCREENINFO, struct.pack('I'*40, *vi)) # fb_var_screeninfo
vi = struct.unpack('I'*40,vi)
bpp = vi[6]
bytepp = bpp//8
except:
pass
if vi[8] == 0 : RGB = True
ffm = 'c'*16+'L'+'I'*4+'H'*3+'ILIIHHH'
fic = struct.calcsize(ffm)
fi = struct.unpack(ffm, ioctl(f, FBIOGET_FSCREENINFO, bytes(fic)))
msize = fi[17] # = w*h*bpp//8
ll, start = fi[-7:-5]
w, h = ll//bytepp, vi[1] # when screen is vertical, width becomes wrong. ll//3 is more accurate at such time.
if _win and len(_win)==4: # virtual window settings
vx, vy, vw, vh = _win
if vw == 'w': vw = w
if vh == 'h': vh = h
vx, vy, vw, vh = map(int, (vx, vy, vw, vh))
if vx>=w: vx = 0
if vy>=h: vy = 0
if vx>w: vw = w - vx
else: vw -= vx
if vy>h: vh = h - vy
else: vh -= vy
else:
vx, vy, vw, vh = 0,0,w,h
msize_kb = vw*vh*bytepp//1024 # more accurate FB memory size in kb
mm = mmap(f.fileno(), msize, offset=start)
return mm, w, h, bpp#ll//(bpp//8), h
def fill_scr(r,g,b):
if bpp == 32:
seed = struct.pack('BBBB', b, g, r, 255)
elif bpp == 24:
seed = struct.pack('BBB', b, g, r)
elif bpp == 16:
seed = struct.pack('H', r>>3<<11 | g>>2<<5 | b>>3)
mm.seek(0)
show_img(seed * vw * vh)
def black_scr():
fill_scr(0,0,0)
def white_scr():
fill_scr(255,255,255)
def mmseekto(x,y):
mm.seek((x + y*w) * bytepp)
def dot(x, y, r, g, b):
mmseekto(x,y)
mm.write(struct.pack('BBB',*((r,g,b) if RGB else (b,g,r))))
def get_pixel(x,y):
mmseekto(x,y)
return mm.read(bytepp)
def _888_to_565(bt):
b = b''
for i in range(0, len(bt),3):
b += int.to_bytes(bt[i]>>3<<11|bt[i+1]>>2<<5|bt[i+2]>>3, 2, 'little')
return b
def numpy_888_565(bt):
import numpy as np
arr = np.fromstring(bt, dtype=np.uint32)
return (((0xF80000 & arr)>>8)|((0xFC00 & arr)>>5)|((0xF8 & arr)>>3)).astype(np.uint16).tostring()
def show_img(img):
if not type(img) is bytes:
if not RGB:
if bpp == 24: # for RPI
img = img.tobytes('raw', 'BGR')
else:
img = img.convert('RGBA').tobytes('raw', 'BGRA')
if bpp == 16:
img = numpy_888_565(img)
else:
if bpp == 24:
img = img.tobytes()
else:
img = img.convert('RGBA').tobytes()
if bpp == 16:
img = numpy_888_565(img)
from io import BytesIO
b = BytesIO(img)
s = vw*bytepp
for y in range(vh): # virtual window drawing
mmseekto(vx,vy+y)
mm.write(b.read(s))

@@ -0,0 +1,52 @@
import logging
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.hw.base import DisplayImpl
import os,time
class Spotpear24inch(DisplayImpl):
def __init__(self, config):
super(Spotpear24inch, self).__init__(config, 'spotpear24inch')
self._display = None
def layout(self):
fonts.setup(12, 10, 12, 70)
self._layout['width'] = 320
self._layout['height'] = 240
self._layout['face'] = (35, 50)
self._layout['name'] = (5, 20)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (40, 0)
self._layout['uptime'] = (240, 0)
self._layout['line1'] = [0, 14, 320, 14]
self._layout['line2'] = [0, 220, 320, 220]
self._layout['friend_face'] = (0, 130)
self._layout['friend_name'] = (40, 135)
self._layout['shakes'] = (0, 220)
self._layout['mode'] = (280, 220)
self._layout['status'] = {
'pos': (80, 160),
'font': fonts.Medium,
'max': 20
}
return self._layout
def refresh(self):
time.sleep(0.1)
def initialize(self):
from pwnagotchi.ui.hw.libs.fb import fb
self._display = fb
logging.info("initializing spotpear 24inch lcd display")
self._display.ready_fb(i=1)
self._display.black_scr()
def render(self, canvas):
self._display.show_img(canvas.rotate(180))
self.refresh()
def clear(self):
self._display.black_scr()
self.refresh()

@@ -119,12 +119,14 @@ class View(object):
def _refresh_handler(self):
delay = 1.0 / self._config['ui']['fps']
# logging.info("view refresh handler started with period of %.2fs" % delay)
while True:
name = self._state.get('name')
self.set('name', name.rstrip('').strip() if '' in name else (name + ''))
self.update()
try:
name = self._state.get('name')
self.set('name', name.rstrip('').strip() if '' in name else (name + ''))
self.update()
except Exception as e:
logging.warning("non fatal error while updating view: %s" % e)
time.sleep(delay)
def set(self, key, value):
@@ -360,14 +362,15 @@ class View(object):
if self._frozen:
return
changes = self._state.changes(ignore=self._ignore_changes)
state = self._state
changes = state.changes(ignore=self._ignore_changes)
if force or len(changes):
self._canvas = Image.new('1', (self._width, self._height), WHITE)
drawer = ImageDraw.Draw(self._canvas)
plugins.on('ui_update', self)
for key, lv in self._state.items():
for key, lv in state.items():
lv.draw(self._canvas, drawer)
web.update_frame(self._canvas)

@@ -1,56 +1,194 @@
import logging
import os
import base64
import _thread
import secrets
import json
from functools import wraps
# https://stackoverflow.com/questions/14888799/disable-console-messages-in-flask-server
logging.getLogger('werkzeug').setLevel(logging.ERROR)
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.ui.web as web
from pwnagotchi import plugins
from flask import send_file
from flask import Response
from flask import request
from flask import jsonify
from flask import abort
from flask import redirect
from flask import render_template, render_template_string
class Handler:
def __init__(self, agent, app):
def __init__(self, config, agent, app):
self._config = config
self._agent = agent
self._app = app
self._app.add_url_rule('/', 'index', self.index)
self._app.add_url_rule('/ui', 'ui', self.ui)
self._app.add_url_rule('/shutdown', 'shutdown', self.shutdown, methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.restart, methods=['POST'])
self._app.add_url_rule('/', 'index', self.with_auth(self.index))
self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
# inbox
self._app.add_url_rule('/inbox', 'inbox', self.with_auth(self.inbox))
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.with_auth(self.inbox_profile))
self._app.add_url_rule('/inbox/peers', 'inbox_peers', self.with_auth(self.inbox_peers))
self._app.add_url_rule('/inbox/<id>', 'show_message', self.with_auth(self.show_message))
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.with_auth(self.mark_message))
self._app.add_url_rule('/inbox/new', 'new_message', self.with_auth(self.new_message))
self._app.add_url_rule('/inbox/send', 'send_message', self.with_auth(self.send_message), methods=['POST'])
# plugins
self._app.add_url_rule('/plugins', 'plugins', self.plugins, strict_slashes=False,
plugins_with_auth = self.with_auth(self.plugins)
self._app.add_url_rule('/plugins', 'plugins', plugins_with_auth, strict_slashes=False,
defaults={'name': None, 'subpath': None})
self._app.add_url_rule('/plugins/<name>', 'plugins', self.plugins, strict_slashes=False,
self._app.add_url_rule('/plugins/<name>', 'plugins', plugins_with_auth, strict_slashes=False,
methods=['GET', 'POST'], defaults={'subpath': None})
self._app.add_url_rule('/plugins/<name>/<path:subpath>', 'plugins', self.plugins, methods=['GET', 'POST'])
self._app.add_url_rule('/plugins/<name>/<path:subpath>', 'plugins', plugins_with_auth, methods=['GET', 'POST'])
def _check_creds(self, u, p):
# trying to be timing attack safe
return secrets.compare_digest(u, self._config['username']) and \
secrets.compare_digest(p, self._config['password'])
def with_auth(self, f):
@wraps(f)
def wrapper(*args, **kwargs):
auth = request.authorization
if not auth or not auth.username or not auth.password or not self._check_creds(auth.username,
auth.password):
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'})
return f(*args, **kwargs)
return wrapper
def index(self):
return render_template('index.html', title=pwnagotchi.name(),
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU')
return render_template('index.html',
title=pwnagotchi.name(),
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU',
fingerprint=self._agent.fingerprint())
def inbox(self):
page = request.args.get("p", default=1, type=int)
inbox = {
"pages": 1,
"records": 0,
"messages": []
}
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
inbox = grid.inbox(page, with_pager=True)
except Exception as e:
logging.exception('error while reading pwnmail inbox')
error = str(e)
return render_template('inbox.html',
name=pwnagotchi.name(),
page=page,
error=error,
inbox=inbox)
def inbox_profile(self):
data = {}
error = None
try:
data = grid.get_advertisement_data()
except Exception as e:
logging.exception('error while reading pwngrid data')
error = str(e)
return render_template('profile.html',
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=json.dumps(data, indent=2),
error=error)
def inbox_peers(self):
peers = {}
error = None
try:
peers = grid.memory()
except Exception as e:
logging.exception('error while reading pwngrid peers')
error = str(e)
return render_template('peers.html',
name=pwnagotchi.name(),
peers=peers,
error=error)
def show_message(self, id):
message = {}
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
message = grid.inbox_message(id)
if message['data']:
message['data'] = base64.b64decode(message['data']).decode("utf-8")
except Exception as e:
logging.exception('error while reading pwnmail message %d' % int(id))
error = str(e)
return render_template('message.html',
name=pwnagotchi.name(),
error=error,
message=message)
def new_message(self):
to = request.args.get("to", default="")
return render_template('new_message.html', to=to)
def send_message(self):
to = request.form["to"]
message = request.form["message"]
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
grid.send_message(to, message)
except Exception as e:
error = str(e)
return jsonify({"error": error})
def mark_message(self, id, mark):
if not grid.is_connected():
abort(200)
logging.info("marking message %d as %s" % (int(id), mark))
grid.mark_message(id, mark)
return redirect("/inbox")
def plugins(self, name, subpath):
if name is None:
# show plugins overview
abort(404)
else:
# call plugin on_webhook
arguments = request.args
req_method = request.method
# need to return something here
if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'):
return render_template_string(
plugins.loaded[name].on_webhook(subpath, args=arguments, req_method=req_method))
abort(500)
try:
return plugins.loaded[name].on_webhook(subpath, request)
except Exception:
abort(500)
else:
abort(404)
# serve a message and shuts down the unit
def shutdown(self):

@@ -13,16 +13,16 @@ from flask_wtf.csrf import CSRFProtect
from pwnagotchi.ui.web.handler import Handler
class Server:
def __init__(self, agent, config):
self._enabled = config['video']['enabled']
self._port = config['video']['port']
self._address = config['video']['address']
self._config = config['web']
self._enabled = self._config['enabled']
self._port = self._config['port']
self._address = self._config['address']
self._origin = None
self._agent = agent
if 'origin' in config['video']:
self._origin = config['video']['origin']
if 'origin' in self._config:
self._origin = self._config['origin']
if self._enabled:
_thread.start_new_thread(self._http_serve, ())
@@ -35,13 +35,14 @@ class Server:
static_url_path='',
static_folder=os.path.join(web_path, 'static'),
template_folder=os.path.join(web_path, 'templates'))
app.secret_key = secrets.token_urlsafe(256)
if self._origin:
CORS(app, resources={r"*": {"origins": self._origin}})
CSRFProtect(app)
Handler(self._agent, app)
Handler(self._config, self._agent, app)
logging.info("web ui available at http://%s:%d/" % (self._address, self._port))

@@ -1,42 +1,34 @@
.block {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
display: block;
cursor: pointer;
text-align: center;
}
img#ui {
width:100%;
}
.full {
position: absolute;
top:0;
left:0;
width:100%;
.ui-image {
width: 100%;
}
.pixelated {
image-rendering:optimizeSpeed; /* Legal fallback */
image-rendering:-moz-crisp-edges; /* Firefox */
image-rendering:-o-crisp-edges; /* Opera */
image-rendering:-webkit-optimize-contrast; /* Safari */
image-rendering:optimize-contrast; /* CSS3 Proposed */
image-rendering:crisp-edges; /* CSS4 Proposed */
image-rendering:pixelated; /* CSS4 Proposed */
-ms-interpolation-mode:nearest-neighbor; /* IE8+ */
image-rendering: optimizeSpeed; /* Legal fallback */
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: -o-crisp-edges; /* Opera */
image-rendering: -webkit-optimize-contrast; /* Safari */
image-rendering: optimize-contrast; /* CSS3 Proposed */
image-rendering: crisp-edges; /* CSS4 Proposed */
image-rendering: pixelated; /* CSS4 Proposed */
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */
}
form.action {
display:inline;
.image-wrapper {
flex: 1;
position: relative;
}
div.status {
position: absolute;
top:0;
left:0;
width:100%;
top: 0;
left: 0;
width: 100%;
}
a.read {
color: #777 !important;
}
p.messagebody {
padding: 1em;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

(image error) Size: 6.1 KiB

Binary file not shown.

After

(image error) Size: 219 B

Binary file not shown.

After

(image error) Size: 227 B

Binary file not shown.

After

(image error) Size: 244 B

Binary file not shown.

After

(image error) Size: 243 B

Binary file not shown.

After

(image error) Size: 146 B

Binary file not shown.

After

(image error) Size: 167 B

Binary file not shown.

After

(image error) Size: 173 B

Binary file not shown.

After

(image error) Size: 159 B

Binary file not shown.

After

(image error) Size: 171 B

Binary file not shown.

After

(image error) Size: 149 B

Binary file not shown.

After

(image error) Size: 149 B

Binary file not shown.

After

(image error) Size: 156 B

Binary file not shown.

After

(image error) Size: 147 B

Binary file not shown.

After

(image error) Size: 152 B

Binary file not shown.

After

(image error) Size: 147 B

Binary file not shown.

After

(image error) Size: 163 B

Binary file not shown.

After

(image error) Size: 169 B

Binary file not shown.

After

(image error) Size: 163 B

Binary file not shown.

After

(image error) Size: 165 B

Binary file not shown.

After

(image error) Size: 151 B

Binary file not shown.

After

(image error) Size: 307 B

Binary file not shown.

After

(image error) Size: 314 B

Binary file not shown.

After

(image error) Size: 233 B

Binary file not shown.

After

(image error) Size: 240 B

Binary file not shown.

After

(image error) Size: 132 B

Binary file not shown.

After

(image error) Size: 135 B

Binary file not shown.

After

(image error) Size: 147 B

Binary file not shown.

After

(image error) Size: 152 B

Binary file not shown.

After

(image error) Size: 146 B

Binary file not shown.

After

(image error) Size: 143 B

Binary file not shown.

After

(image error) Size: 250 B

Binary file not shown.

After

(image error) Size: 251 B

Binary file not shown.

After

(image error) Size: 207 B

Binary file not shown.

After

(image error) Size: 213 B

Binary file not shown.

After

(image error) Size: 174 B

Binary file not shown.

After

(image error) Size: 177 B

Binary file not shown.

After

(image error) Size: 184 B

Binary file not shown.

After

(image error) Size: 194 B

Binary file not shown.

After

(image error) Size: 196 B

Binary file not shown.

After

(image error) Size: 204 B

Binary file not shown.

After

(image error) Size: 169 B

Binary file not shown.

After

(image error) Size: 172 B

Binary file not shown.

After

(image error) Size: 310 B

Binary file not shown.

After

(image error) Size: 316 B

Binary file not shown.

After

(image error) Size: 212 B

Binary file not shown.

After

(image error) Size: 210 B

Binary file not shown.

After

(image error) Size: 165 B

Binary file not shown.

After

(image error) Size: 160 B

Binary file not shown.

After

(image error) Size: 171 B

Binary file not shown.

After

(image error) Size: 185 B

Some files were not shown because too many files have changed in this diff Show More