254 Commits

Author SHA1 Message Date
Simone Margaritelli
a7164ea742 releasing v1.4.2 2019-12-19 17:36:09 +01:00
evilsocket
cae2a18016 Merge pull request #735 from dadav/fix/add_unload
add unload method
2019-12-17 16:48:23 +01:00
dadav
9d63eba232 add unload method 2019-12-16 18:50:40 +01:00
evilsocket
f141e15ba3 Merge pull request #727 from ArnCo/master
Set correct position for memtemp plugin so that it does not overlap with regular messages for waveshare27inch.
2019-12-16 12:00:46 +02:00
evilsocket
e68165ce06 Merge pull request #732 from xenDE/patch-5
webgpsmap: fix parsing new timezone format "Z" in gps data
2019-12-16 12:00:17 +02:00
evilsocket
3758806919 Merge pull request #725 from dadav/fix/remove_sysd_process_limits
remove process limits
2019-12-16 11:59:59 +02:00
evilsocket
1f91c6f09e Merge pull request #724 from dadav/feature/ensure_write
add ensure_write
2019-12-16 11:59:40 +02:00
evilsocket
3e8b6eafbd Merge pull request #722 from dadav/fix/no_comment
no comment
2019-12-16 11:59:12 +02:00
xenDE
44138ba463 webgpsmap: fix parsing new timezone format "Z" in gps data
fix timezone "Z": "2019-11-28T04:44:46.79231Z" >> "2019-11-28T04:44:46.79231+00:00"
issue: https://github.com/evilsocket/pwnagotchi/issues/708
2019-12-15 23:37:48 +01:00
ACO
4b71fea404 Set correct position for memtemp plugin so that it does not overlap with regular messages for waveshare27inch.
Signed-off-by: ArnCo <arnaud@cordier.work>
2019-12-14 11:49:21 +01:00
dadav
6babad0d02 remove process limits 2019-12-14 09:00:54 +01:00
dadav
e8513240ea add ensure_write 2019-12-13 19:42:04 +01:00
dadav
00101ccd07 no comment 2019-12-13 19:29:44 +01:00
Simone Margaritelli
e0a66f5c99 misc: small fix or general refactoring i did not bother commenting 2019-12-10 21:22:44 +02:00
Simone Margaritelli
81061cea24 fix: fixed locked callback call on plugins 2019-12-10 21:17:46 +02:00
evilsocket
93bb633010 Merge pull request #713 from alanyee/patch-1
Replace string formatting with logging laziness in __init__.py
2019-12-10 21:06:57 +02:00
evilsocket
dbb64e0fab Merge pull request #711 from dadav/fix/bugs
Unknown variable; Logic error
2019-12-10 19:40:57 +02:00
Alan Yee
fa8751017d Update __init__.py 2019-12-09 15:36:26 -08:00
dadav
9d56c97aa5 Unknown variable; Logic error 2019-12-09 20:12:34 +01:00
evilsocket
1523dfc1ef Merge pull request #701 from alanyee/patch-1
Update automata.py
2019-12-09 18:10:15 +02:00
evilsocket
a02960b56d Merge pull request #705 from hmax42/master
UI adjustments for waveshare1
2019-12-09 18:09:19 +02:00
evilsocket
b6f59f99d4 Merge pull request #707 from AliceGrey/master
Added text overflow checking for over 999 associations
2019-12-09 18:08:33 +02:00
Simone Margaritelli
09a00adab9 fix: added a plugin::callback level mutex to avoid calling a callback while a previous call is still running 2019-12-09 17:46:44 +02:00
AliceGrey
7fa30c2868 Added text overflow checking for over 999 associations 2019-12-08 17:16:05 -08:00
hmax42
774d9c693c Update defaults.yml 2019-12-08 16:26:14 +01:00
hmax42
87b6cf7d40 Merge branch 'master' of https://github.com/hmax42/pwnagotchi 2019-12-08 16:25:23 +01:00
hmax42
88928eec82 remove buttonshim 2019-12-08 16:25:07 +01:00
hmax42
60f7849838 Merge branch 'master' into pr/3 2019-12-08 08:46:24 +01:00
hmax42
3cf041617c Update memtemp.py 2019-12-08 08:44:20 +01:00
evilsocket
ee6c06f306 Merge pull request #700 from dadav/fix/auto-update-lock
Add Lock
2019-12-07 16:28:57 +02:00
dadav
2e22a17610 Add Lock 2019-12-07 15:27:41 +01:00
Simone Margaritelli
1615fc8817 releasing v1.4.1 2019-12-07 15:45:15 +02:00
Simone Margaritelli
714cb00610 misc: small fix or general refactoring i did not bother commenting 2019-12-07 15:44:03 +02:00
Simone Margaritelli
a0a790635a fix: fuck me for trusting people's PR without checking 10000 times 2019-12-07 15:43:10 +02:00
Simone Margaritelli
9d17be959d releasing v1.4.0 2019-12-07 15:08:17 +02:00
evilsocket
4b651cd17b Merge pull request #694 from Evg33/fr_waveshare144lcd
waveshare 1.44inch lcd hat
2019-12-07 14:11:25 +02:00
evilsocket
2f70512076 Merge pull request #693 from Nels885/improve-plugins-web-page
improvement of the plugins web page
2019-12-07 14:10:37 +02:00
evilsocket
b79c59c639 Merge pull request #698 from dadav/feature/usr1_handler
Add signal handler
2019-12-07 14:09:36 +02:00
evilsocket
56f7b67699 Merge pull request #699 from dadav/fix/add-lock-to-ohc
Add lock to onlinehashcrack plugin
2019-12-07 14:09:05 +02:00
dadav
1a8472268e Add signal handler 2019-12-07 09:36:56 +01:00
dadav
4fb7205281 Add lock 2019-12-07 09:30:05 +01:00
Alan Yee
f8ffab426b Update automata.py
Replace string formatting with logging laziness in automata.py
2019-12-06 12:20:54 -08:00
Evg33
b4daf19401 layout redesign 2019-12-06 20:11:37 +03:00
evilsocket
e04e053cee Merge pull request #690 from alanyee/patch-1
Replace string formatting with logging laziness in agent.py
2019-12-06 15:04:45 +02:00
evilsocket
4f153e3899 Merge pull request #689 from dadav/fix/bt-tether-lock
Add lock to bt-tether
2019-12-06 15:01:29 +02:00
Evg33
04720ecc42 memtemp plugin for waveshare.com/1.44inch-lcd-hat 2019-12-06 03:24:44 +03:00
Evg33
a12e2aafa5 gps plugin for waveshare.com/1.44inch-lcd-hat 2019-12-06 03:24:24 +03:00
Evg33
1721f67ec3 fr waveshare.com/1.44inch-lcd-hat 2019-12-06 03:24:01 +03:00
Nels885
7693e42aa1 Change the name of the CSS class 'element' to 'plugins-box' and centering the text 2019-12-05 11:49:34 +01:00
Nels885
2ae48a2ef2 Changing the display style of the plugins page 2019-12-05 11:15:29 +01:00
Alan Yee
663bca41cd Update agent.py
Leverage logging laziness rather than string formatting
2019-12-03 11:34:40 -08:00
dadav
ede01e50cd Add lock 2019-12-03 18:15:35 +01:00
evilsocket
19973574e7 Merge pull request #677 from dadav/fix/plugins_setup_ui
Fix/plugins setup ui
2019-12-03 11:44:57 +01:00
evilsocket
a019b4c778 Merge pull request #678 from dadav/fix/net-pos-bug
Add lock; make less verbose
2019-12-03 11:44:12 +01:00
evilsocket
526c5bed87 Merge pull request #680 from Nels885/update-french-translation
Update french translation
2019-12-03 11:42:47 +01:00
evilsocket
4d6136633a Merge pull request #682 from jakubmilkowski/master
fix: Prevent duplicate entries for uploaded pcaps
2019-12-03 11:42:22 +01:00
evilsocket
6d90b75d10 Merge pull request #684 from dadav/feature/session_stats_plugin
Add session-stats plugin
2019-12-03 11:41:20 +01:00
evilsocket
03695be807 Merge pull request #687 from alanyee/patch-1
Use list comprehension in setup.py
2019-12-03 11:40:34 +01:00
Alan Yee
6a97476732 Update setup.py
Use list comprehension
2019-12-02 13:32:36 -08:00
dadav
b5e620684b this is apparently needed 2019-12-02 19:22:36 +01:00
dadav
95557ab37d Axes information will be lost otherwise 2019-12-02 19:20:47 +01:00
dadav
988d093e36 Add session-stats plugin 2019-12-01 21:49:27 +01:00
hmax42
f563d71477 Update gps.py 2019-12-01 14:27:13 +01:00
hmax42
7b219fd139 Update memtemp.py 2019-12-01 14:27:10 +01:00
hmax42
42ed698583 Update memtemp.py 2019-12-01 08:06:14 +01:00
hmax42
6df7bcd885 add ws1 2019-12-01 07:56:09 +01:00
hmax42
1c299832ae add ws1 2019-12-01 07:56:03 +01:00
hmax42
11476433ca Merge pull request #2 from evilsocket/master
update
2019-12-01 07:49:23 +01:00
jakubmilkowski
6e57e131b3 fix: Prevent duplicate entries for uploaded pcaps
For the same reasons like described here https://github.com/evilsocket/pwnagotchi/issues/657 duplicated entries could be created in /root/.ohc_uploads

Signed-off-by: jakubmilkowski <jakub.milkowsky@gmail.com>
2019-11-30 22:44:03 +01:00
dadav
548b42ef20 Lock ui on change 2019-11-30 14:20:37 +01:00
dadav
99614c8cd4 Call on_ready 2019-11-30 14:01:35 +01:00
VOIRIN Lionel
e19ea999e2 Correction of some French translations 2019-11-30 12:51:53 +01:00
VOIRIN Lionel
2207a1eacf Updating the French translation 2019-11-30 10:54:38 +01:00
dadav
9509dd0aa5 Add lock; make less verbose 2019-11-30 10:05:43 +01:00
dadav
608904daf8 Call unload with ui arg 2019-11-30 09:43:39 +01:00
dadav
f973997cdb Call on_ui_setup when plugin reloads 2019-11-30 09:35:19 +01:00
evilsocket
9b594f7fb2 Merge pull request #669 from cdiemel/master
added on_unfiltered_wifi_list
2019-11-29 15:45:43 +01:00
evilsocket
b8eed4f52a Merge pull request #671 from Evg33/reboot
feature/plugin/web/reboot
2019-11-29 15:45:26 +01:00
evilsocket
ad510429fe Merge pull request #672 from dadav/feature/plugins_urls
Add urls to plugins
2019-11-29 15:44:58 +01:00
dadav
f5a94fde96 Add url to plugin 2019-11-28 21:55:20 +01:00
Evg33
855bda9104 feature/plugin/web/reboot 2019-11-28 23:02:22 +03:00
Casey Diemel
e72fd08fb4 added on_unfiltered_wifi_list
Signed-off-by: Casey Diemel <diemelcw@gmail.com>
2019-11-28 14:39:15 -05:00
evilsocket
e44ebac43f Merge pull request #627 from soebbing/improve-german-translations
Improve German translations slightly
2019-11-28 11:29:30 +01:00
evilsocket
b5ddb716e2 Merge pull request #660 from mbgroot/master
Updating the Russian translation
2019-11-28 11:28:54 +01:00
evilsocket
82e7e09fa1 Merge pull request #664 from dadav/feature/wpa-sec-download
Add wpa-sec password download
2019-11-28 11:28:36 +01:00
evilsocket
8b40e94ca8 Merge pull request #665 from dadav/feature/plugins_web_page
Add plugins page
2019-11-28 11:28:15 +01:00
dadav
cc5c46906f Add plugins page 2019-11-27 21:22:40 +01:00
dadav
7cb52ba33a Add wpa-sec password download 2019-11-27 18:51:37 +01:00
Evgeny Zelenin
07f8e7bd4a Update voice.po 2019-11-27 22:01:43 +05:00
Evgeny Zelenin
48dc751d13 Update voice.po 2019-11-27 12:09:24 +05:00
Evgeny Zelenin
3c154ffe0c Update voice.po 2019-11-27 02:58:16 +05:00
Evgeny Zelenin
167f559d73 Update voice.po 2019-11-27 02:54:48 +05:00
evilsocket
19775b7d27 Merge pull request #654 from dadav/fix/webcfg_check_for_error
Fix/webcfg check for error
2019-11-26 12:32:10 +01:00
evilsocket
48e3a372cc Merge pull request #658 from sayak-brm/master
[BUGFIX] Prevent duplicate entries for reported networks
2019-11-26 12:04:00 +01:00
Sayak Brahmachari
d2c44797e5 Prevent duplicate entries for reported networks
Due to duplicate entries in `/root/.api-report.json`, [this code](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/plugins/default/grid.py#L90) incorrectly reports the number of pwned networks, resulting in incorrect stats on the [pwnagotchi.ai website](https://pwnagotchi.ai/).
2019-11-26 14:05:10 +05:30
dadav
a03443986b Parse to str 2019-11-25 20:08:20 +01:00
dadav
a7ea499fac Should fail before write 2019-11-25 19:47:23 +01:00
evilsocket
722a91655a Merge pull request #649 from xenDE/patch-4
webgpsmap: add filter for: SSID, MAC, isCracked, Password
2019-11-25 12:03:49 +01:00
xenDE
93e06d7f59 add filter for: SSID, MAC, isCracked, Password 2019-11-24 18:49:32 +01:00
evilsocket
7de5121033 Merge pull request #645 from xenDE/patch-3
fix gpio_buttons plugin: gpio needs to be a number
2019-11-23 11:30:28 +01:00
xenDE
83f741bbb0 fix: gpio needs to be a number
fixes gpio id as string in config
https://github.com/evilsocket/pwnagotchi/issues/643
2019-11-23 02:05:01 +01:00
evilsocket
a779fb9b0b Merge pull request #640 from xenDE/patch-2
cleanup, fixes and add handling of .paw-gps.json
2019-11-21 17:03:08 +01:00
xenDE
c4a007e72a cleanup, fixes and add handling of .paw-gps.json
cleanup logging, increase cache item count from 1024 to 2048, add handling of corrupt long/lat position null, added handling of .paw-gps.json files - tested with master
2019-11-20 15:45:16 +01:00
evilsocket
1a71615fa8 Merge pull request #635 from Arttumiro/patch-3
Update to using .paw-gps.json files
2019-11-20 10:11:16 +01:00
evilsocket
7a9f84f495 Merge pull request #638 from xslendix/master
Added romanian language
2019-11-20 10:11:06 +01:00
evilsocket
6e3f5a1181 Merge pull request #632 from daniel156161/master
fix backup.sh (find with type f for no zero byte files into archive)
2019-11-20 10:10:33 +01:00
root
d045ed5afa Added romanian language 2019-11-19 22:58:49 +00:00
Arttumiro
0ee0aaff37 Update to using .paw-gps.json files
Remove old link due to it not being too good, update file extension to .paw-gps.json for better support on problems with webgpsmap.
2019-11-19 15:38:11 +02:00
evilsocket
0fb81a11c4 Merge pull request #629 from xenDE/patch-1
fix gps iso-datetime parsing
2019-11-19 11:30:15 +01:00
daniel156161
cfc0ad1b48 fix backup.sh (find with type f for no zero byte files into archive) 2019-11-19 05:03:21 +01:00
xenDE
3351c251ef fix gps timestamp parsing
problem on timestamp parsing if microseconds are more then 6 numbers.

will fix bug reported in this pr: https://github.com/evilsocket/pwnagotchi/pull/619

tested with testdata from https://github.com/xenDE/pwnagotchi-plugin-webgpsmap/tree/master/handshakes.gps-map-test

before:

[2019-11-19 00:37:51,946] [INFO] webgpsmap: scanning /root/handshakes.gps-map-test
[2019-11-19 00:37:52,022] [INFO] webgpsmap: Found 4 .(geo|gps).json files from 5 handshakes. Fetching positions ...
[2019-11-19 00:37:52,144] [ERROR] Lng is 0
[2019-11-19 00:37:52,241] [ERROR] Invalid isoformat string: '2019-11-14T12:30:41.097414739+01:00'
[2019-11-19 00:37:52,280] [ERROR] Lng is 0
[2019-11-19 00:37:52,329] [INFO] webgpsmap loaded 2 positions

after:

[2019-11-19 00:48:04,652] [INFO] webgpsmap: scanning /root/handshakes.gps-map-test
[2019-11-19 00:48:04,693] [INFO] webgpsmap: Found 5 .(geo|gps).json files from 6 handshakes. Fetching positions ...
[2019-11-19 00:48:04,760] [ERROR] Lng is 0
[2019-11-19 00:48:04,822] [ERROR] Lng is 0
[2019-11-19 00:48:04,850] [INFO] webgpsmap loaded 3 positions
2019-11-19 00:56:59 +01:00
Hendrik Söbbing
d9d399429c Improve German translations slightly
Signed-off-by: Hendrik Söbbing <h.soebbing@shopware.com>
2019-11-18 18:31:11 +01:00
hmax42
b1ad247e11 Merge pull request #1 from evilsocket/master
pull
2019-11-18 17:19:52 +01:00
evilsocket
b5a148f287 Merge pull request #625 from neutralinsomniac/invert_led_blink
fix: apparently for the led, 0 is ON and 1 is OFF
2019-11-18 15:56:50 +01:00
Jeremy O'Brien
7138f6469b fix: apparently for the led, 0 is ON and 1 is OFF
Signed-off-by: Jeremy O'Brien <neutral@fastmail.com>
2019-11-18 08:57:10 -05:00
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 #623 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 #617 from andrewbeard/master
Fix for issue #616
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 #615 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 #606 from Arttumiro/master
Fixed Paw-Gps config, added a - mark
2019-11-15 12:06:06 +01:00
evilsocket
8ed2950eb5 Merge pull request #607 from Arttumiro/patch-1
Fixes to paw-gps.py
2019-11-15 12:05:58 +01:00
evilsocket
6f8133b2b8 Merge pull request #608 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 #612 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 #613 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 #609 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 #601 from benleb/add-gateway-option
add gateway option to bt-tether
2019-11-14 11:04:30 +01:00
evilsocket
39ccd141eb Merge pull request #602 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 #605 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 #597) 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 #599 from ahsec/add_spanish_support
Adding support for Spanish language
2019-11-13 11:39:06 +01:00
evilsocket
7261073073 Merge pull request #600 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 #583) 2019-11-13 01:32:05 +01:00
evilsocket
6bd09c7f43 Merge pull request #595 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 #592) 2019-11-13 00:11:50 +01:00
evilsocket
369d7a65a8 Merge pull request #578 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 #594 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 #589 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 #581 from dwi/patch-1
Small UPS Lite typo fix
2019-11-12 11:39:59 +01:00
evilsocket
4b9ebc2512 Merge pull request #586 from benleb/patch-2
shift word
2019-11-12 11:39:16 +01:00
evilsocket
41a3fad43e Merge pull request #587 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
hmax42
30b1874a0a New defaults for buttonshim 2019-11-11 19:08:12 +01:00
hmax42
b903f636d2 Blinking works now freely 2019-11-11 19:04:56 +01:00
dwi
52b40f049e Small UPS Lite typo fix
Fixing dfd534ac41 that fixes #521
2019-11-11 17:43:20 +01:00
evilsocket
3df35ef03b Merge pull request #580 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 #577 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 #521) 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 #575 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 #562 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 #574 from budd3993/fix-memtemp-inky
fixed memtemp location for inky display
2019-11-11 10:51:22 +01:00
evilsocket
307b3890f1 Merge pull request #567 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
hmax42
92c1b6b005 blinking with static colors 2019-11-10 13:44:35 +01:00
Simone Margaritelli
a2ac679499 new: pwnfile link in the web ui (closes #557) 2019-11-10 13:24:56 +01:00
evilsocket
d7e1c59709 Merge pull request #569 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 #566 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 #564) 2019-11-10 13:10:16 +01:00
hmax42
eddfdb3ebc Plugin for the Pimoroni Button Shim
this plugin enabled support for the 5 buttons of the shim. the rgb led is not supported yet. an example for config.yml lloks like this

      buttonshim:
        enabled: true
        buttons:
          - "date >> /home/pi/buttonA.txt"
          - "date >> /home/pi/buttonB.txt"
          - "date >> /home/pi/buttonC.txt"
          - "date >> /home/pi/buttonD.txt"
          - "date >> /home/pi/buttonE.txt"
2019-11-10 08:54:04 +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 #561 from mil1200/master
Added Slovak language
2019-11-09 10:30:49 +01:00
evilsocket
1f17d3cbbe Merge pull request #564 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 #559 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 #522) 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 #551 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 #527) 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 #554 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 #545 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 #542) 2019-11-07 10:59:40 +01:00
evilsocket
e23e1affae Merge pull request #538 from neutralinsomniac/normalize_waveshare29inch_config
normalize the waveshare29inch config string
2019-11-07 10:39:43 +01:00
evilsocket
8d8333b586 Merge pull request #546 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 #535 from deveth0/deveth0-523-webui
#523: 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 #531 from deveth0/deveth0-netpos-logging
Additional Logging for net-pos plugin
2019-11-06 11:24:14 +01:00
evilsocket
8a2d6eac9d Merge pull request #537 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 #523: Add some style 2019-11-06 00:03:29 +01:00
amuthmann
f952bcd298 Fixes #523: 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
Simone Margaritelli
9e92201d82 releasing v1.2.1 2019-11-05 19:11:37 +01:00
Simone Margaritelli
4e592df6d8 fix: fixed a bug which prevented rebooting 2019-11-05 19:07:16 +01:00
339 changed files with 73697 additions and 964 deletions

7
.editorconfig Normal file
View 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
View File

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

View File

@@ -1,19 +1,93 @@
#!/usr/bin/python3
import logging
import argparse
import time
import yaml
import signal
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
from pwnagotchi import restart
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 +106,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 +129,20 @@ 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 = Agent(view=display, config=config, keypair=KeyPair(view=display))
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))
def usr1_handler(*unused):
logging.info('Received USR1 singal. Restart process ...')
restart("MANU" if args.do_manual else "AUTO")
while True:
display.on_manual_mode(agent.last_session)
time.sleep(5)
if grid.is_connected():
plugins.on('internet_available', agent)
signal.signal(signal.SIGUSR1, usr1_handler)
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)

View File

@@ -10,6 +10,8 @@ PermissionsStartOnly=true
ExecStart=/usr/bin/pwnagotchi-launcher
Restart=always
RestartSec=30
TasksMax=infinity
LimitNPROC=infinity
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target

View File

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

View File

@@ -1,12 +1,12 @@
import subprocess
import os
import logging
import time
import re
import pwnagotchi.ui.view as view
import pwnagotchi
version = '1.2.0'
version = '1.4.2'
_name = None
@@ -27,17 +27,17 @@ def set_name(new_name):
if new_name != current:
global _name
logging.info("setting unit hostname '%s' -> '%s'" % (current, new_name))
logging.info("setting unit hostname '%s' -> '%s'", current, new_name)
with open('/etc/hostname', 'wt') as fp:
fp.write(new_name)
with open('/etc/hosts', 'rt') as fp:
prev = fp.read()
logging.debug("old hosts:\n%s\n" % prev)
logging.debug("old hosts:\n%s\n", prev)
with open('/etc/hosts', 'wt') as fp:
patched = prev.replace(current, new_name, -1)
logging.debug("new hosts:\n%s\n" % patched)
logging.debug("new hosts:\n%s\n", patched)
fp.write(patched)
os.system("hostname '%s'" % new_name)
@@ -109,7 +109,7 @@ def shutdown():
def restart(mode):
logging.warning("restarting in %s mode ..." % mode)
logging.warning("restarting in %s mode ...", mode)
if mode == 'AUTO':
os.system("touch /root/.pwnagotchi-auto")
@@ -123,12 +123,12 @@ def restart(mode):
def reboot(mode=None):
if mode is not None:
mode = mode.upper()
logging.warning("rebooting in %s mode ..." % mode)
logging.warning("rebooting in %s mode ...", mode)
else:
logging.warning("rebooting ...")
if view.ROOT:
view.ROOT.on_reboot()
view.ROOT.on_rebooting()
# give it some time to refresh the ui
time.sleep(10)

View File

@@ -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
@@ -57,7 +63,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
return self._supported_channels
def setup_events(self):
logging.info("connecting to %s ..." % self.url)
logging.info("connecting to %s ...", self.url)
for tag in self._config['bettercap']['silence']:
try:
@@ -84,7 +90,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
s = self.session()
for iface in s['interfaces']:
if iface['name'] == mon_iface:
logging.info("found monitor interface: %s" % iface['name'])
logging.info("found monitor interface: %s", iface['name'])
has_mon = True
break
@@ -93,11 +99,11 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
logging.info("starting monitor interface ...")
self.run('!%s' % mon_start_cmd)
else:
logging.info("waiting for monitor interface %s ..." % mon_iface)
logging.info("waiting for monitor interface %s ...", mon_iface)
time.sleep(1)
logging.info("supported channels: %s" % self._supported_channels)
logging.info("handshakes will be collected inside %s" % self._config['bettercap']['handshakes'])
logging.info("supported channels: %s", self._supported_channels)
logging.info("handshakes will be collected inside %s", self._config['bettercap']['handshakes'])
self._reset_wifi_settings()
@@ -145,10 +151,10 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if not channels:
self._current_channel = 0
logging.debug("RECON %ds" % recon_time)
logging.debug("RECON %ds", recon_time)
self.run('wifi.recon.channel clear')
else:
logging.debug("RECON %ds ON CHANNELS %s" % (recon_time, ','.join(map(str, channels))))
logging.debug("RECON %ds ON CHANNELS %s", recon_time, ','.join(map(str, channels)))
try:
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
except Exception as e:
@@ -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']
@@ -195,7 +212,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
ch = ap['channel']
# if we're sticking to a channel, skip anything
# which is not on that channel
if channels != [] and ch not in channels:
if channels and ch not in channels:
continue
if ch not in grouped:
@@ -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):
@@ -257,7 +274,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
pwnagotchi.reboot()
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
with open(RECOVERY_DATA_FILE, 'w') as fp:
data = {
'started_at': self._started_at,
@@ -272,7 +289,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
try:
with open(RECOVERY_DATA_FILE, 'rt') as fp:
data = json.load(fp)
logging.info("found recovery data: %s" % data)
logging.info("found recovery data: %s", data)
self._started_at = data['started_at']
self._epoch.epoch = data['epoch']
self._handshakes = data['handshakes']
@@ -280,7 +297,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._last_pwnd = data['last_pwnd']
if delete:
logging.info("deleting %s" % RECOVERY_DATA_FILE)
logging.info("deleting %s", RECOVERY_DATA_FILE)
os.unlink(RECOVERY_DATA_FILE)
except:
if not no_exceptions:
@@ -317,21 +334,23 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
new_shakes += 1
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
if ap_and_station is None:
logging.warning("!!! captured new handshake: %s !!!" % key)
logging.warning("!!! captured new handshake: %s !!!", key)
self._last_pwnd = ap_mac
plugins.on('handshake', self, filename, ap_mac, sta_mac)
else:
(ap, sta) = ap_and_station
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
'hostname'] != '<hidden>' else ap_mac
logging.warning("!!! captured new handshake on channel %d: %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:
logging.error("error: %s" % e)
logging.error("error: %s", e)
finally:
self._update_handshakes(new_shakes)
@@ -373,15 +392,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def associate(self, ap, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping assoc(%s)" % ap['mac'])
logging.debug("recon is stale, skipping assoc(%s)", ap['mac'])
return
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
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:
@@ -394,15 +413,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def deauth(self, ap, sta, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping deauth(%s)" % sta['mac'])
logging.debug("recon is stale, skipping deauth(%s)", sta['mac'])
return
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
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:
@@ -415,7 +434,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def set_channel(self, channel, verbose=True):
if self.is_stale():
logging.debug("recon is stale, skipping set_channel(%d)" % channel)
logging.debug("recon is stale, skipping set_channel(%d)", channel)
return
# if in the previous loop no client stations has been deauthenticated
@@ -431,12 +450,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if channel != self._current_channel:
if self._current_channel != 0 and wait > 0:
if verbose:
logging.info("waiting for %ds on channel %d ..." % (wait, self._current_channel))
logging.info("waiting for %ds on channel %d ...", wait, self._current_channel)
else:
logging.debug("waiting for %ds on channel %d ..." % (wait, self._current_channel))
logging.debug("waiting for %ds on channel %d ...", wait, self._current_channel)
self.wait_for(wait)
if verbose and self._epoch.any_activity:
logging.info("CHANNEL %d" % channel)
logging.info("CHANNEL %d", channel)
try:
self.run('wifi.recon.channel %d' % channel)
self._current_channel = channel
@@ -446,4 +465,4 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
plugins.on('channel_hop', self, channel)
except Exception as e:
logging.error("error: %s" % e)
logging.error("error: %s", e)

View File

@@ -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):

View File

@@ -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]))

View File

@@ -12,19 +12,18 @@ class Automata(object):
self._epoch = Epoch(config)
def _on_miss(self, who):
logging.info("it looks like %s is not in range anymore :/" % who)
logging.info("it looks like %s is not in range anymore :/", who)
self._epoch.track(miss=True)
self._view.on_miss(who)
def _on_error(self, who, e):
error = "%s" % e
# when we're trying to associate or deauth something that is not in range anymore
# (if we are moving), we get the following error from bettercap:
# error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list.
if 'is an unknown BSSID' in error:
if 'is an unknown BSSID' in str(e):
self._on_miss(who)
else:
logging.error("%s" % e)
logging.error(e)
def set_starting(self):
self._view.on_starting()
@@ -58,7 +57,7 @@ class Automata(object):
def set_bored(self):
factor = self._epoch.inactive_for / self._config['personality']['bored_num_epochs']
if not self._has_support_network_for(factor):
logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for)
logging.warning("%d epochs with no activity -> bored", self._epoch.inactive_for)
self._view.on_bored()
plugins.on('bored', self)
else:
@@ -68,7 +67,7 @@ class Automata(object):
def set_sad(self):
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
if not self._has_support_network_for(factor):
logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for)
logging.warning("%d epochs with no activity -> sad", self._epoch.inactive_for)
self._view.on_sad()
plugins.on('sad', self)
else:
@@ -77,7 +76,7 @@ class Automata(object):
def set_angry(self, factor):
if not self._has_support_network_for(factor):
logging.warning("%d epochs with no activity -> angry" % self._epoch.inactive_for)
logging.warning("%d epochs with no activity -> angry", self._epoch.inactive_for)
self._view.on_angry()
plugins.on('angry', self)
else:
@@ -85,7 +84,7 @@ class Automata(object):
self.set_grateful()
def set_excited(self):
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
logging.warning("%d epochs with activity -> excited", self._epoch.active_for)
self._view.on_excited()
plugins.on('excited', self)
@@ -118,7 +117,7 @@ class Automata(object):
if factor >= 2.0:
self.set_angry(factor)
else:
logging.warning("agent missed %d interactions -> lonely" % did_miss)
logging.warning("agent missed %d interactions -> lonely", did_miss)
self.set_lonely()
# after X times being bored, the status is set to sad or angry
elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
@@ -139,6 +138,6 @@ class Automata(object):
plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for)
logging.critical("%d epochs without visible access points -> rebooting ...", self._epoch.blind_for)
self._reboot()
self._epoch.blind_for = 0

View File

@@ -1,5 +1,4 @@
# WARNING WARNING WARNING WARNING
#
#
# This file is recreated with default settings on every pwnagotchi restart,
# use /etc/pwnagotchi/config.yml to configure this unit.
#
@@ -19,26 +18,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 +29,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: ~
@@ -59,19 +38,12 @@ main:
enabled: false
api_key: ~
api_url: "https://wpa-sec.stanev.org"
download_results: false
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,67 @@ 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
session-stats:
enabled: true
# monitor interface to use
iface: mon0
# command to run to bring the mon interface up in case it's not up already
mon_start_cmd: /usr/bin/monstart
mon_stop_cmd: /usr/bin/monstop
mon_max_blind_epochs: 50
# 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 +243,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, waveshare144lcd/ws144inch
type: 'waveshare_2'
# Possible options red/yellow/black (black used for monocromatic displays)
# Waveshare tri-color 2.13in display can be over-driven with color set as 'fastAndFurious'
# THIS IS POTENTIALLY DANGEROUS. DO NOT USE UNLESS YOU UNDERSTAND THAT IT COULD KILL YOUR DISPLAY
color: 'black'
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:

View File

@@ -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'))

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
"POT-Creation-Date: 2019-11-14 21:15+0100\n"
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
@@ -20,7 +20,7 @@ msgid "ZzzzZZzzzzZzzz"
msgstr ""
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Hi, ich bin ein Pwnagotchi! Starte ..."
msgstr "Hi, ich bin ein Pwnagotchi! Starte..."
msgid "New day, new hunt, new pwns!"
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
@@ -35,23 +35,30 @@ msgid "The neural network is ready."
msgstr "Das neurale Netz ist bereit."
msgid "Generating keys, do not turn off ..."
msgstr "Generiere Keys, nicht ausschalten ..."
msgstr "Generiere Keys, nicht ausschalten..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, Channel {channel} ist frei! Dein AP wir des dir danken."
msgstr "Hey, Channel {channel} ist frei! Dein AP wird es Dir danken."
msgid "Reading last session logs ..."
msgstr "Lese die Logs der letzten Session..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Bisher {lines_so_far} Zeilen im Log gelesen..."
msgid "I'm bored ..."
msgstr "Mir ist langweilig..."
msgid "Let's go for a walk!"
msgstr "Lass uns laufen gehen!"
msgstr "Lass uns spazieren gehen!"
msgid "This is the best day of my life!"
msgstr "Das ist der beste Tag meines Lebens."
msgstr "Das ist der beste Tag meines Lebens!"
msgid "Shitty day :/"
msgstr "Scheis Tag :/"
msgstr "Scheißtag :/"
msgid "I'm extremely bored ..."
msgstr "Mir ist sau langweilig..."
@@ -62,6 +69,13 @@ msgstr "Ich bin sehr traurig..."
msgid "I'm sad"
msgstr "Ich bin traurig"
#, fuzzy
msgid "Leave me alone ..."
msgstr "Lass mich in Ruhe..."
msgid "I'm mad at you!"
msgstr "Ich bin sauer auf Dich!"
msgid "I'm living the life!"
msgstr "Ich lebe das Leben!"
@@ -75,27 +89,35 @@ msgid "I'm having so much fun!"
msgstr "Ich habe sooo viel Spaß!"
msgid "My crime is that of curiosity ..."
msgstr "Mein Verbrechen ist das der Neugier ..."
msgstr "Mein Verbrechen ist das der Neugier..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Hallo {name}, nett Dich kennenzulernen."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Jo {name}! Was geht!?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {name}, wie geht's?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Gerät {name} ist in der nähe!!"
msgstr "Gerät {name} ist in der Nähe!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ...tschüß {name}"
msgstr "Uhm... tschüß {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} ist weg ..."
msgstr "{name} ist weg..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Whoops ...{name} ist weg."
msgstr "Whoops... {name} ist weg."
#, python-brace-format
msgid "{name} missed!"
@@ -111,17 +133,17 @@ msgid "I love my friends!"
msgstr "Ich liebe meine Freunde!"
msgid "Nobody wants to play with me ..."
msgstr "Niemand will mit mir spielen ..."
msgstr "Niemand will mit mir spielen..."
msgid "I feel so alone ..."
msgstr "Ich fühl michso alleine ..."
msgstr "Ich fühl' mich so allein..."
msgid "Where's everybody?!"
msgstr "Wo sind denn alle?"
msgstr "Wo sind denn alle?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Schlafe für {secs}s"
msgstr "Schlafe für {secs}s..."
msgid "Zzzzz"
msgstr ""
@@ -138,7 +160,7 @@ msgstr ""
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Warte für {secs}s ..."
msgstr "Warte für {secs}s..."
#, python-brace-format
msgid "Looking around ({secs}s)"
@@ -158,7 +180,7 @@ msgstr "Jo {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Ich denke, dass {mac} kein WiFi brauch!"
msgstr "Ich denke, dass {mac} kein WiFi braucht!"
#, python-brace-format
msgid "Deauthenticating {mac}"
@@ -177,7 +199,7 @@ msgid "You have {count} new message{plural}!"
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
msgstr "Ops, da ist was schief gelaufen... Starte neu..."
#, python-brace-format
msgid "Kicked {num} stations\n"

View File

@@ -3,12 +3,11 @@
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR <7271496+quantumsheep@users.noreply.github.com>, 2019.
#
#,
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-23 18:37+0200\n"
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
"PO-Revision-Date: 2019-10-03 10:34+0200\n"
"Last-Translator: quantumsheep <7271496+quantumsheep@users.noreply.github."
"com>\n"
@@ -43,6 +42,13 @@ msgstr "Génération des clés, ne pas éteindre..."
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, le canal {channel} est libre! Ton point d'accès va te remercier."
msgid "Reading last session logs ..."
msgstr "Lecture des logs de la dernière session ..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Jusqu'ici, {lines_so_far} lignes lues dans le log ..."
msgid "I'm bored ..."
msgstr "Je m'ennuie..."
@@ -64,6 +70,13 @@ msgstr "Je suis très triste..."
msgid "I'm sad"
msgstr "Je suis triste"
#, fuzzy
msgid "Leave me alone ..."
msgstr "Je me sens si seul..."
msgid "I'm mad at you!"
msgstr "Je t'en veux !"
msgid "I'm living the life!"
msgstr "Je vis la vie !"
@@ -83,6 +96,14 @@ msgstr "Mon crime, c'est la curiosité..."
msgid "Hello {name}! Nice to meet you."
msgstr "Bonjour {name} ! Ravi de te rencontrer."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name} ! Quoi de neuf ?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {name} comment vas-tu ?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "L'unité {name} est proche !"
@@ -106,6 +127,12 @@ msgstr "{name} raté !"
msgid "Missed!"
msgstr "Raté !"
msgid "Good friends are a blessing!"
msgstr "Les bons amis sont une bénédiction !"
msgid "I love my friends!"
msgstr "J'aime mes amis !"
msgid "Nobody wants to play with me ..."
msgstr "Personne ne veut jouer avec moi..."
@@ -120,11 +147,11 @@ msgid "Napping for {secs}s ..."
msgstr "Fais la sieste pendant {secs}s..."
msgid "Zzzzz"
msgstr ""
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr ""
msgstr "ZzzZzzz ({secs}s)"
msgid "Good night."
msgstr "Bonne nuit."
@@ -181,18 +208,18 @@ msgstr "{num} stations kick\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Fait {num} nouveaux amis\n"
msgstr "A {num} nouveaux amis\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Récupéré {num} handshakes\n"
msgstr "A {num} handshakes\n"
msgid "Met 1 peer"
msgstr "1 peer rencontré"
msgstr "1 pair rencontré"
#, python-brace-format
msgid "Met {num} peers"
msgstr "{num} peers recontrés"
msgstr "{num} pairs recontrés"
#, python-brace-format
msgid ""

Binary file not shown.

View File

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

Binary file not shown.

View File

@@ -0,0 +1,249 @@
# Pwnagotchi translation.
# Copyright (C) 2019
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR <radu.ungureanu@techie.com>, 2019.
#
#,
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"PO-Revision-Date: 2019-11-20 00:18+594\n"
"Last-Translator: Ungureanu Radu-Andrei <radu.ungureanu@techie.com>\n"
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github."
"com>\n"
"Language: ro\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "ZzzzZZzzzzZzzz"
msgstr ""
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Buna, sunt Pwnagotchi! Pornesc..."
msgid "New day, new hunt, new pwns!"
msgstr "O noua zi, o noua vanatoare, noi pwn-uri!"
msgid "Hack the Planet!"
msgstr "Pirateaza planeta!"
msgid "AI ready."
msgstr "AI-ul e gata."
msgid "The neural network is ready."
msgstr "Rețeaua neuronală este gata."
msgid "Generating keys, do not turn off ..."
msgstr "Se generează chei, nu închide..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, canalul {channel} este liber! AP-ul tău îti va mulțumi."
msgid "Reading last session logs ..."
msgstr "Se citesc log-urile din sesiunea anterioara..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Am citit {lines_so_far} linii din log pana acum..."
msgid "I'm bored ..."
msgstr "Sunt plictisit..."
msgid "Let's go for a walk!"
msgstr "Hai să ne plimbăm!"
msgid "This is the best day of my life!"
msgstr "Asta este cea mai buna zi din viața mea!"
msgid "Shitty day :/"
msgstr "O zi proasta :/"
msgid "I'm extremely bored ..."
msgstr "Sunt extrem de plictisit..."
msgid "I'm very sad ..."
msgstr "Sunt foarte trist..."
msgid "I'm sad"
msgstr "Sunt trist"
msgid "Leave me alone ..."
msgstr "Lasă-mă in pace..."
msgid "I'm mad at you!"
msgstr "Sunt supărat pe tine!"
msgid "I'm living the life!"
msgstr "Trăiesc viața!"
msgid "I pwn therefore I am."
msgstr "Eu pwn-ez, deci aici sunt."
msgid "So many networks!!!"
msgstr "Atât de multe rețele!"
msgid "I'm having so much fun!"
msgstr "Mă distrez așa de mult!"
msgid "My crime is that of curiosity ..."
msgstr "Crima mea este una de curiozitate..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Bună {name}! Mă bucur să te cunosc."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name}! Cmf?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {nume} ce mai faci?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Unitatea {name} este aproape!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm... Pa {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} a dispărut."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Oops... {name} a dispărut."
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} ratat!"
msgid "Missed!"
msgstr "Ratat!"
msgid "Good friends are a blessing!"
msgstr "Prietenii buni sunt o binecuvântare!"
msgid "I love my friends!"
msgstr "Îmi iubesc prietenii!"
msgid "Nobody wants to play with me ..."
msgstr "Nimeni nu vrea sa se joace cu mine..."
msgid "I feel so alone ..."
msgstr "Mă simt așa de singuratic..."
msgid "Where's everybody?!"
msgstr "Unde-i toată lumea?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Dorm pentru {secs}s..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s)"
msgid "Good night."
msgstr "Noapte bună."
msgid "Zzz"
msgstr "Zzz"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Aștept pentru {secs}s..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Mă uit împrejur ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Hey {what} hai să fim prieteni!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Mă asociez cu {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Yo {what}"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Am decis că lui {mac} nu-i trebuie WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "Îl deautentific pe {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Îi dau kickban lui {mac}"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Șmecher, avem {num} de handshake-uri noi!"
#, python-brace-format
msgid "You have {count} new message{plural}!"
msgstr "Ai {count} mesaj(e) nou/noi!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Am dat afară {num} de stații\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Am făcut {num} prieteni noi \n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Am primit {num} de handshake-uri\n"
msgid "Met 1 peer"
msgstr "Am întalnit un peer"
#, python-brace-format
msgid "Met {num} peers"
msgstr "Am întalnit {num} de peer-uri"
#, python-brace-format
msgid ""
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr "Eu am făcut pwning pentru {duration} și am dat afara {deauthed} clienți! "
"De asemenea, am întalnit {associated} prieteni noi și am mancat {handshakes} de "
"handshake-uri! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "ore"
msgid "minutes"
msgstr "minute"
msgid "seconds"
msgstr "secunde"
msgid "hour"
msgstr "oră"
msgid "minute"
msgstr "minut"
msgid "second"
msgstr "secundă"

View File

@@ -1,45 +1,51 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Pwnagotchi Russian translation.
# Copyright (C) 2019
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
#
# Second author <https://github.com/mbgroot>, 2019
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-03 16:47+0200\n"
"PO-Revision-Date: 2019-10-05 18:50+0300\n"
"Language-Team: \n"
"Project-Id-Version: 0.0.2"
"Report-Msgid-Bugs-To: m-b-g@yandex.ru"
"POT-Creation-Date: 2019-11-27 16:47+0200"
"PO-Revision-Date: 2019-11-27 18:50+0300"
"Last-Translator: Evgeny Zelenin <m-b-g@yandex.ru>"
"Language-Team: ru"
"Language: ru"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.4\n"
"Last-Translator: Elliot Manson\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"Language: ru_RU\n"
msgid "ZzzzZZzzzzZzzz"
msgstr "ZzzzZZzzzzZzzz"
msgstr "Хрррр..."
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Привет, я Pwnagotchi! Поехали …"
msgstr "Привет, я Pwnagotchi! Стартуем!"
msgid "New day, new hunt, new pwns!"
msgstr "Новый день, новая охота, новые взломы!"
msgid "Hack the Planet!"
msgstr "Хак зе планет!"
msgstr "Взломай эту Планету!"
msgid "AI ready."
msgstr "AI готов."
msgstr "A.I. готов."
msgid "The neural network is ready."
msgstr "Нейронная сеть готова."
msgid "Generating keys, do not turn off ..."
msgstr "Генерация ключей, не выключайте..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
msgid "Reading last session logs ..."
msgstr "Чтение логов последнего сеанса..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Чтение {lines_so_far} строк журнала..."
msgid "I'm bored ..."
msgstr "Мне скучно …"
@@ -61,9 +67,14 @@ msgstr "Мне очень грустно …"
msgid "I'm sad"
msgstr "Мне грустно"
msgid "Leave me alone ..."
msgstr "Оставь меня в покое..."
msgid "I'm mad at you!"
msgstr "Я зол на тебя!"
msgid "I'm living the life!"
msgstr "Угараю по полной!"
msgstr "Живу полной жизнью!"
msgid "I pwn therefore I am."
msgstr "Я взламываю, поэтому я существую."
@@ -75,15 +86,22 @@ msgid "I'm having so much fun!"
msgstr "Мне так весело!"
msgid "My crime is that of curiosity ..."
msgstr "Моe преступление - это любопытство …"
msgstr "Моё преступление - это любопытство…"
#, python-brace-format
msgid "Hello {name}! Nice to meet you. {name}"
msgstr "Привет, {name}! Приятно познакомиться. {name}"
msgstr "Привет, {name}! Рад встрече с тобой!"
#, python-brace-format
msgid "Unit {name} is nearby! {name}"
msgstr "Цель {name} близко! {name}"
msgstr "Цель {name} близко!"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Хэй {nume}! Как дела?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Цель {name} рядом!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
@@ -91,11 +109,11 @@ msgstr "Хм … до свидания {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} исчезла …"
msgstr "{name} ушла…"
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Упс … {name} исчезла."
msgstr "Упс… {name} исчезла."
#, python-brace-format
msgid "{name} missed!"
@@ -103,12 +121,17 @@ msgstr "{name} упустил!"
msgid "Missed!"
msgstr "Промахнулся!"
msgid "Good friends are a blessing!"
msgstr "Хорошие друзья - это благословение!"
msgid "I love my friends!"
msgstr "Я люблю своих друзей!"
msgid "Nobody wants to play with me ..."
msgstr "Никто не хочет со мной играть ..."
msgid "I feel so alone ..."
msgstr "Мне так одиноко …"
msgstr "Я так одинок…"
msgid "Where's everybody?!"
msgstr "Где все?!"
@@ -118,11 +141,16 @@ msgid "Napping for {secs}s ..."
msgstr "Дремлет {secs}с …"
msgid "Zzzzz"
msgstr "Zzzzz"
msgstr "Хррр..."
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}c)"
msgstr "Хррррр.. ({secs}c)"
msgid "Good night."
msgstr "Доброй ночи."
msgid "Zzz"
msgstr "Хрррр"
#, python-brace-format
msgid "Waiting for {secs}s ..."
@@ -130,7 +158,7 @@ msgstr "Ждем {secs}c …"
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Оглядываюсь вокруг ({secs}с)"
msgstr "Осматриваюсь вокруг ({secs}с)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
@@ -173,7 +201,7 @@ msgstr "Заимел {num} новых друзей\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Получил {num} рукопожатие\n"
msgstr "Получил {num} рукопожатий\n"
msgid "Met 1 peer"
msgstr "Встретился один знакомый"

Binary file not shown.

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -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 = ''

View File

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

View File

@@ -1,37 +1,81 @@
import os
import glob
import _thread
import threading
import importlib, importlib.util
import logging
from pwnagotchi.ui import view
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
loaded = {}
database = {}
locks = {}
class Plugin:
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
global loaded
global loaded, locks
plugin_name = cls.__module__.split('.')[0]
plugin_instance = cls()
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
loaded[plugin_name] = plugin_instance
for attr_name in plugin_instance.__dir__():
if attr_name.startswith('on_'):
cb = getattr(plugin_instance, attr_name, None)
if cb is not None and callable(cb):
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
def toggle_plugin(name, enable=True):
"""
Load or unload a plugin
returns True if changed, otherwise False
"""
global loaded, database
if not enable and name in loaded:
if getattr(loaded[name], 'on_unload', None):
loaded[name].on_unload(view.ROOT)
del loaded[name]
return True
if enable and name in database and name not in loaded:
load_from_file(database[name])
one(name, 'loaded')
one(name, 'ui_setup', view.ROOT)
one(name, 'ready', view.ROOT._agent)
return True
return False
def on(event_name, *args, **kwargs):
for plugin_name, plugin in loaded.items():
one(plugin_name, event_name, *args, **kwargs)
def locked_cb(lock_name, cb, *args, **kwargs):
global locks
with locks[lock_name]:
cb(*args, *kwargs)
def one(plugin_name, event_name, *args, **kwargs):
global loaded
if plugin_name in loaded:
plugin = loaded[plugin_name]
cb_name = 'on_%s' % event_name
callback = getattr(plugin, cb_name, None)
if callback is not None and callable(callback):
try:
callback(*args, **kwargs)
lock_name = "%s::%s" % (plugin_name, cb_name)
locked_cb_args = (lock_name, callback, *args, *kwargs)
_thread.start_new_thread(locked_cb, locked_cb_args)
except Exception as e:
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
logging.error(e, exc_info=True)
@@ -47,10 +91,11 @@ def load_from_file(filename):
def load_from_path(path, enabled=()):
global loaded
global loaded, database
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
for filename in glob.glob(os.path.join(path, "*.py")):
plugin_name = os.path.basename(filename.replace(".py", ""))
database[plugin_name] = filename
if plugin_name in enabled:
try:
load_from_file(filename)

View File

@@ -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 = ""

View File

@@ -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()

View File

@@ -7,6 +7,7 @@ import platform
import shutil
import glob
import pkg_resources
from threading import Lock
import pwnagotchi
import pwnagotchi.plugins as plugins
@@ -150,6 +151,7 @@ class AutoUpdate(plugins.Plugin):
def __init__(self):
self.ready = False
self.status = StatusFile('/root/.auto-update')
self.lock = Lock()
def on_loaded(self):
if 'interval' not in self.options or ('interval' in self.options and self.options['interval'] is None):
@@ -159,59 +161,61 @@ class AutoUpdate(plugins.Plugin):
logging.info("[update] plugin loaded.")
def on_internet_available(self, agent):
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
with self.lock:
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
if not self.ready:
return
if not self.ready:
return
if self.status.newer_then_hours(self.options['interval']):
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
return
if self.status.newer_then_hours(self.options['interval']):
logging.debug("[update] last check happened less than %d hours ago" % self.options['interval'])
return
logging.info("[update] checking for updates ...")
logging.info("[update] checking for updates ...")
display = agent.view()
prev_status = display.get('status')
display = agent.view()
prev_status = display.get('status')
try:
display.update(force=True, new_data={'status': 'Checking for updates ...'})
try:
display.update(force=True, new_data={'status': 'Checking for updates ...'})
to_install = []
to_check = [
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
]
to_install = []
to_check = [
('bettercap/bettercap', parse_version('bettercap -version'), True, 'bettercap'),
('evilsocket/pwngrid', parse_version('pwngrid -version'), True, 'pwngrid-peer'),
('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
]
for repo, local_version, is_native, svc_name in to_check:
info = check(local_version, repo, is_native)
if info['url'] is not None:
logging.warning(
"update for %s available (local version is '%s'): %s" % (
repo, info['current'], info['url']))
info['service'] = svc_name
to_install.append(info)
for repo, local_version, is_native, svc_name in to_check:
info = check(local_version, repo, is_native)
if info['url'] is not None:
logging.warning(
"update for %s available (local version is '%s'): %s" % (
repo, info['current'], info['url']))
info['service'] = svc_name
to_install.append(info)
num_updates = len(to_install)
num_installed = 0
num_updates = len(to_install)
num_installed = 0
if num_updates > 0:
if self.options['install']:
for update in to_install:
if install(display, update):
num_installed += 1
else:
prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
if num_updates > 0:
if self.options['install']:
for update in to_install:
plugins.on('updating')
if install(display, update):
num_installed += 1
else:
prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
logging.info("[update] done")
logging.info("[update] done")
self.status.update()
self.status.update()
if num_installed > 0:
display.update(force=True, new_data={'status': 'Rebooting ...'})
pwnagotchi.reboot()
if num_installed > 0:
display.update(force=True, new_data={'status': 'Rebooting ...'})
pwnagotchi.reboot()
except Exception as e:
logging.error("[update] %s" % e)
except Exception as e:
logging.error("[update] %s" % e)
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})

View File

@@ -1,14 +1,16 @@
import os
import time
import re
import logging
import os
import subprocess
import time
from threading import Lock
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 +384,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 +396,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
@@ -424,6 +427,7 @@ class BTTether(plugins.Plugin):
self.ready = False
self.options = dict()
self.devices = dict()
self.lock = Lock()
def on_loaded(self):
# new config
@@ -461,110 +465,119 @@ 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_unload(self, ui):
with ui._lock:
ui.remove_element('bluetooth')
def on_ui_setup(self, ui):
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
with ui._lock:
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
def on_ui_update(self, ui):
if not self.ready:
return
devices_to_try = list()
connected_priorities = list()
any_device_connected = False # if this is true, last status on screen should be C
with self.lock:
devices_to_try = list()
connected_priorities = list()
any_device_connected = False # if this is true, last status on screen should be C
for _, device in self.devices.items():
if device.connected():
connected_priorities.append(device.priority)
any_device_connected = True
continue
for _, device in self.devices.items():
if device.connected():
connected_priorities.append(device.priority)
any_device_connected = True
continue
if not device.max_tries or (device.max_tries > device.tries):
if not device.status.newer_then_minutes(device.interval):
devices_to_try.append(device)
device.status.update()
device.tries += 1
if not device.max_tries or (device.max_tries > device.tries):
if not device.status.newer_then_minutes(device.interval):
devices_to_try.append(device)
device.status.update()
device.tries += 1
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
for device in sorted_devices:
bt = BTNap(device.mac)
for device in sorted_devices:
bt = BTNap(device.mac)
try:
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
dev_remote = bt.wait_for_device(timeout=device.scantime)
if dev_remote is None:
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
try:
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
dev_remote = bt.wait_for_device(timeout=device.scantime)
if dev_remote is None:
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
ui.set('bluetooth', 'NF')
continue
except Exception as bt_ex:
logging.error(bt_ex)
ui.set('bluetooth', 'NF')
continue
except Exception as bt_ex:
logging.error(bt_ex)
ui.set('bluetooth', 'NF')
continue
paired = bt.is_paired()
if not paired:
if BTNap.pair(dev_remote):
logging.debug('BT-TETHER: Paired with %s.', device.name)
paired = bt.is_paired()
if not paired:
if BTNap.pair(dev_remote):
logging.debug('BT-TETHER: Paired with %s.', device.name)
else:
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
ui.set('bluetooth', 'PE')
continue
else:
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
ui.set('bluetooth', 'PE')
continue
else:
logging.debug('BT-TETHER: Already paired.')
logging.debug('BT-TETHER: Already paired.')
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
device.network, success = BTNap.nap(dev_remote)
interface = None
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
device.network, success = BTNap.nap(dev_remote)
interface = None
if success:
try:
interface = device.interface()
except Exception:
if success:
try:
interface = device.interface()
except Exception:
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
continue
if interface is None:
ui.set('bluetooth', 'BE')
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
continue
logging.debug('BT-TETHER: Created interface (%s)', interface)
ui.set('bluetooth', 'C')
any_device_connected = True
device.tries = 0 # reset tries
else:
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
ui.set('bluetooth', 'NF')
continue
if interface is None:
ui.set('bluetooth', 'BE')
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
addr = f"{device.ip}/{device.netmask}"
if device.gateway:
gateway = device.gateway
else:
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
wrapped_interface = IfaceWrapper(interface)
logging.debug('BT-TETHER: Add ip to %s', interface)
if not wrapped_interface.set_addr(addr):
ui.set('bluetooth', 'AE')
logging.debug("BT-TETHER: Could not add ip to %s", interface)
continue
logging.debug('BT-TETHER: Created interface (%s)', interface)
if device.share_internet:
if not connected_priorities or device.priority > max(connected_priorities):
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
IfaceWrapper.set_route(gateway, interface)
connected_priorities.append(device.priority)
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
with open('/etc/resolv.conf', 'r+') as resolv:
nameserver = resolv.read()
if 'nameserver 9.9.9.9' not in nameserver:
logging.debug('BT-TETHER: Added nameserver')
resolv.seek(0)
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
if any_device_connected:
ui.set('bluetooth', 'C')
any_device_connected = True
device.tries = 0 # reset tries
else:
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
ui.set('bluetooth', 'NF')
continue
addr = f"{device.ip}/{device.netmask}"
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
wrapped_interface = IfaceWrapper(interface)
logging.debug('BT-TETHER: Add ip to %s', interface)
if not wrapped_interface.set_addr(addr):
ui.set('bluetooth', 'AE')
logging.debug("BT-TETHER: Could not add ip to %s", interface)
continue
if device.share_internet:
if not connected_priorities or device.priority > max(connected_priorities):
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
IfaceWrapper.set_route(gateway, interface)
connected_priorities.append(device.priority)
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
with open('/etc/resolv.conf', 'r+') as resolv:
nameserver = resolv.read()
if 'nameserver 9.9.9.9' not in nameserver:
logging.debug('BT-TETHER: Added nameserver')
resolv.seek(0)
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
if any_device_connected:
ui.set('bluetooth', 'C')

View File

@@ -16,27 +16,20 @@ 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()
# called before the plugin is unloaded
def on_unload(self, ui):
pass
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 +99,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
@@ -129,6 +122,11 @@ class Example(plugins.Plugin):
def on_wifi_update(self, agent, access_points):
pass
# called when the agent refreshed an unfiltered access point list
# this list contains all access points that were detected BEFORE filtering
def on_unfiltered_ap_list(self, agent, access_points):
pass
# called when the agent is sending an association frame
def on_association(self, agent, access_point):
pass

View File

@@ -31,10 +31,9 @@ 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():
gpio = int(gpio)
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)

View File

@@ -1,42 +1,132 @@
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_waveshare_v1():
lat_pos = (130, 70)
lon_pos = (125, 80)
alt_pos = (130, 90)
elif ui.is_inky():
# guessed values, add tested ones if you can
lat_pos = (112, 30)
lon_pos = (112, 49)
alt_pos = (87, 63)
elif ui.is_waveshare144lcd():
# guessed values, add tested ones if you can
lat_pos = (67, 73)
lon_pos = (62, 83)
alt_pos = (67, 93)
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_unload(self, ui):
with ui._lock:
ui.remove_element('latitude')
ui.remove_element('longitude')
ui.remove_element('altitude')
def on_ui_update(self, ui):
if self.coordinates and all([
# avoid 0.000... measurements
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 ")

View File

@@ -67,7 +67,8 @@ class Grid(plugins.Plugin):
logging.info("grid plugin loaded.")
def set_reported(self, reported, net_id):
reported.append(net_id)
if net_id not in reported:
reported.append(net_id)
self.report.update(data={'reported': reported})
def check_inbox(self, agent):
@@ -77,6 +78,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)

View File

@@ -0,0 +1,159 @@
from threading import Event
import _thread
import logging
import time
import pwnagotchi.plugins as plugins
class Led(plugins.Plugin):
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'This plugin blinks the PWR led with different patterns depending on the event.'
def __init__(self):
self._is_busy = False
self._event = Event()
self._event_name = None
self._led_file = "/sys/class/leds/led0/brightness"
self._delay = 200
# called when the plugin is loaded
def on_loaded(self):
self._led_file = "/sys/class/leds/led%d/brightness" % self.options['led']
self._delay = int(self.options['delay'])
logging.info("[led] plugin loaded for %s" % self._led_file)
self._on_event('loaded')
_thread.start_new_thread(self._worker, ())
def _on_event(self, event):
if not self._is_busy:
self._event_name = event
self._event.set()
logging.debug("[led] event '%s' set", event)
else:
logging.debug("[led] skipping event '%s' because the worker is busy", event)
def _led(self, on):
with open(self._led_file, 'wt') as fp:
fp.write(str(on))
def _blink(self, pattern):
logging.debug("[led] using pattern '%s' ..." % pattern)
for c in pattern:
if c == ' ':
self._led(1)
else:
self._led(0)
time.sleep(self._delay / 1000.0)
# reset
self._led(0)
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')

View File

@@ -44,24 +44,52 @@ class MemTemp(plugins.Plugin):
if ui.is_waveshare_v2():
h_pos = (180, 80)
v_pos = (180, 61)
elif ui.is_waveshare_v1():
h_pos = (170, 80)
v_pos = (170, 61)
elif ui.is_waveshare144lcd():
h_pos = (53, 77)
v_pos = (78, 67)
elif ui.is_inky():
h_pos = (140, 68)
v_pos = (165, 54)
elif ui.is_waveshare27inch():
h_pos = (192, 138)
v_pos = (216, 122)
else:
h_pos = (155, 76)
v_pos = (180, 61)
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_unload(self, ui):
with ui._lock:
ui.remove_element('memtemp')
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))

View File

@@ -1,6 +1,7 @@
import logging
import json
import os
import threading
import requests
import time
import pwnagotchi.plugins as plugins
@@ -11,7 +12,7 @@ MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
class NetPos(plugins.Plugin):
__author__ = 'zenzen san'
__version__ = '2.0.1'
__version__ = '2.0.2'
__license__ = 'GPL3'
__description__ = """Saves a json file with the access points with more signal
whenever a handshake is captured.
@@ -22,6 +23,7 @@ class NetPos(plugins.Plugin):
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
self.skip = list()
self.ready = False
self.lock = threading.Lock()
def on_loaded(self):
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
@@ -45,60 +47,64 @@ class NetPos(plugins.Plugin):
saved_file.write(x + "\n")
def on_internet_available(self, agent):
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
with self.lock:
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
all_files = os.listdir(handshake_dir)
all_np_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.net-pos.json')]
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
all_files = os.listdir(handshake_dir)
all_np_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.net-pos.json')]
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
if new_np_files:
logging.info("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
display.update(force=True)
for idx, np_file in enumerate(new_np_files):
if new_np_files:
logging.debug("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
display.update(force=True)
for idx, np_file in enumerate(new_np_files):
geo_file = np_file.replace('.net-pos.json', '.geo.json')
if os.path.exists(geo_file):
# got already the position
reported.append(np_file)
self.report.update(data={'reported': reported})
continue
try:
geo_data = self._get_geo_data(np_file) # returns json obj
except requests.exceptions.RequestException as req_e:
logging.error("NET-POS: %s - RequestException: %s", np_file, req_e)
self.skip += np_file
continue
except json.JSONDecodeError as js_e:
logging.error("NET-POS: %s - JSONDecodeError: %s, removing it...", np_file, js_e)
os.remove(np_file)
continue
except OSError as os_e:
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
self.skip += np_file
continue
with open(geo_file, 'w+t') as sf:
json.dump(geo_data, sf)
geo_file = np_file.replace('.net-pos.json', '.geo.json')
if os.path.exists(geo_file):
# got already the position
reported.append(np_file)
self.report.update(data={'reported': reported})
continue
try:
geo_data = self._get_geo_data(np_file) # returns json obj
except requests.exceptions.RequestException as req_e:
logging.error("NET-POS: %s", req_e)
self.skip += np_file
continue
except json.JSONDecodeError as js_e:
logging.error("NET-POS: %s", js_e)
self.skip += np_file
continue
except OSError as os_e:
logging.error("NET-POS: %s", os_e)
self.skip += np_file
continue
with open(geo_file, 'w+t') as sf:
json.dump(geo_data, sf)
reported.append(np_file)
self.report.update(data={'reported': reported})
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
display.update(force=True)
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
display.update(force=True)
def on_handshake(self, agent, filename, access_point, client_station):
netpos = self._get_netpos(agent)
if not netpos['wifiAccessPoints']:
return
netpos["ts"] = int("%.0f" % time.time())
netpos_filename = filename.replace('.pcap', '.net-pos.json')
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
logging.debug("NET-POS: Saving net-location to %s", netpos_filename)
try:
with open(netpos_filename, 'w+t') as net_pos_file:
@@ -106,6 +112,7 @@ class NetPos(plugins.Plugin):
except OSError as os_e:
logging.error("NET-POS: %s", os_e)
def _get_netpos(self, agent):
aps = agent.get_access_points()
netpos = dict()

View File

@@ -2,20 +2,27 @@ import os
import logging
import re
import requests
from threading import Lock
from pwnagotchi.utils import StatusFile
import pwnagotchi.plugins as plugins
from json.decoder import JSONDecodeError
class OnlineHashCrack(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '2.0.0'
__version__ = '2.0.1'
__license__ = 'GPL3'
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
def __init__(self):
self.ready = False
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
try:
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
except JSONDecodeError as json_err:
os.remove('/root/.ohc_uploads')
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
self.skip = list()
self.lock = Lock()
def on_loaded(self):
"""
@@ -68,38 +75,40 @@ class OnlineHashCrack(plugins.Plugin):
"""
Called in manual mode when there's internet connectivity
"""
if self.ready:
display = agent.view()
config = agent.config()
reported = self.report.data_field_or('reported', default=list())
with self.lock:
if self.ready:
display = agent.view()
config = agent.config()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
# pull out whitelisted APs
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
# pull out whitelisted APs
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
if handshake_new:
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
if handshake_new:
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
for idx, handshake in enumerate(handshake_new):
display.set('status',
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_ohc(handshake)
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info(f"OHC: Successfully uploaded {handshake}")
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("OHC: %s", req_e)
continue
except OSError as os_e:
self.skip.append(handshake)
logging.error("OHC: %s", os_e)
continue
for idx, handshake in enumerate(handshake_new):
display.set('status',
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_ohc(handshake)
if handshake not in reported:
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info(f"OHC: Successfully uploaded {handshake}")
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("OHC: %s", req_e)
continue
except OSError as os_e:
self.skip.append(handshake)
logging.error("OHC: %s", os_e)
continue

View File

@@ -3,8 +3,8 @@ 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:
https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
'''
@@ -22,10 +22,12 @@ 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')
gps_filename = filename.replace('.pcap', '.paw-gps.json')
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
with open(gps_filename, 'w+t') as f:

View File

@@ -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 = ""

View File

@@ -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

View File

@@ -0,0 +1,195 @@
import os
import logging
import threading
from datetime import datetime
from pwnagotchi import plugins
from flask import render_template_string
from flask import jsonify
TEMPLATE = """
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
Session stats
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/css/jquery.jqplot.min.css"/>
<link rel="stylesheet" href="/css/jquery.jqplot.css"/>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="/js/jquery.jqplot.min.js"></script>
<script type="text/javascript" src="/js/jquery.jqplot.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.mobile.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.json2.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.dateAxisRenderer.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.highlighter.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.cursor.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.enhancedLegendRenderer.js"></script>
{% endblock %}
{% block script %}
$(document).ready(function(){
var ajaxDataRenderer = function(url, plot, options) {
var ret = null;
$.ajax({
async: false,
url: url,
dataType:"json",
success: function(data) {
ret = data;
}
});
return ret;
};
function loadData(url, elm, title) {
var data = ajaxDataRenderer(url);
var plot_os = $.jqplot(elm, data.values,{
title: title,
stackSeries: true,
seriesDefaults: {
showMarker: false,
fill: true,
fillAndStroke: true
},
legend: {
show: true,
renderer: $.jqplot.EnhancedLegendRenderer,
placement: 'outsideGrid',
labels: data.labels,
location: 's',
rendererOptions: {
numberRows: '2',
},
rowSpacing: '0px'
},
axes:{
xaxis:{
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
min: 0,
tickOptions:{formatString:'%.2f'}
}
},
highlighter: {
show: true,
sizeAdjust: 7.5
},
cursor:{
show: true,
tooltipLocation:'sw'
}
}).replot({
axes:{
xaxis:{
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
min: 0,
tickOptions:{formatString:'%.2f'}
}
}
});
}
function loadAll() {
loadData('/plugins/session-stats/os', 'chart_os', 'OS')
loadData('/plugins/session-stats/temp', 'chart_temp', 'Temp')
loadData('/plugins/session-stats/nums', 'chart_nums', 'Wifi')
loadData('/plugins/session-stats/duration', 'chart_duration', 'Sleeping')
loadData('/plugins/session-stats/epoch', 'chart_epoch', 'Epochs')
}
loadAll();
setInterval(loadAll, 60000);
});
{% endblock %}
{% block content %}
<div id="chart_os" style="height:400px;width:100%; "></div>
<div id="chart_temp" style="height:400px;width:100%; "></div>
<div id="chart_nums" style="height:400px;width:100%; "></div>
<div id="chart_duration" style="height:400px;width:100%; "></div>
<div id="chart_epoch" style="height:400px;width:100%; "></div>
{% endblock %}
"""
class SessionStats(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '0.1.0'
__license__ = 'GPL3'
__description__ = 'This plugin displays stats of the current session.'
def __init__(self):
self.ready = False
self.lock = threading.Lock()
self.options = dict()
self.stats = dict()
def on_loaded(self):
"""
Gets called when the plugin gets loaded
"""
logging.info("Session-stats plugin loaded.")
self.ready = True
def on_unloaded(self, ui):
pass
def on_epoch(self, agent, epoch, epoch_data):
"""
Save the epoch_data to self.stats
"""
with self.lock:
self.stats[datetime.now().strftime("%H:%M:%S")] = epoch_data
@staticmethod
def extract_key_values(data, subkeys):
result = dict()
result['values'] = list()
result['labels'] = subkeys
for plot_key in subkeys:
v = [ [ts,d[plot_key]] for ts, d in data.items()]
result['values'].append(v)
return result
def on_webhook(self, path, request):
if not path or path == "/":
return render_template_string(TEMPLATE)
if path == "os":
extract_keys = ['cpu_load','mem_usage',]
elif path == "temp":
extract_keys = ['temperature']
elif path == "nums":
extract_keys = [
'missed_interactions',
'num_hops',
'num_peers',
'tot_bond',
'avg_bond',
'num_deauths',
'num_associations',
'num_handshakes',
]
elif path == "duration":
extract_keys = [
'duration_secs',
'slept_for_secs',
]
elif path == "epoch":
extract_keys = [
'blind_for_epochs',
'inactive_for_epochs',
'active_for_epochs',
]
with self.lock:
return jsonify(SessionStats.extract_key_values(self.stats, extract_keys))

View File

@@ -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")

View File

@@ -55,8 +55,12 @@ 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_unload(self, ui):
with ui._lock:
ui.remove_element('ups')
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())

View File

@@ -0,0 +1,512 @@
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:
parsed_yaml = yaml.safe_load(str(request.get_json()))
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
yaml.safe_dump(parsed_yaml, config_file, encoding='utf-8',
allow_unicode=True, default_flow_style=False)
_thread.start_new_thread(restart, (self.mode,))
return "success"
except yaml.YAMLError as yaml_ex:
return "config error"
abort(404)

View File

@@ -0,0 +1,274 @@
<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 { 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: 2vw;color:#a0a0a0;}
#filterbox { position: fixed;top:0px;left:0px;z-index:999999;margin-left:55px;width:100%;height:20px;border-bottom:2px solid #303030;display: grid;grid-template-columns: 1fr 0.1fr;grid-template-rows: 1fr;grid-template-areas: ". .";}
#search { grid-area: 1 / 1 / 2 / 2;height:30px;padding:3px;background-color:#000;color:#e0e0e0;border:none;}
#matchcount { grid-area: 1 / 2 / 2 / 3;height:30px;margin-right:55px;padding-right:5px;background-color:#000;color:#a0a0a0;font-weight:bold;}
#mapdiv { width:100%; height: 100%; }
</style>
</head>
<body>
<div id="mapdiv"></div>
<div id="filterbox">
<input type="text" id="search" placeholder="filter: #cracked #notcracked AA:BB:CC aabbcc AndroidAP ..."/>
<div id="matchcount">0&nbsp;APs</div>
</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');
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 positionsLoaded = false;
var positions = [];
var accuracys = [];
var markers = [];
var marker_pos = [];
var markerClusters = L.markerClusterGroup();
function drawPositions() {
count = 0;
//mymap.removeLayer(markerClusters);
mymap.eachLayer(function (layer) {
mymap.removeLayer(layer);
});
Esri_WorldImagery.addTo(mymap);
CartoDB_DarkMatter.addTo(mymap);
markerClusters = L.markerClusterGroup();
accuracys = [];
markers = [];
marker_pos = [];
filterText = document.getElementById("search").value;
//console.log(filterText);
Object.keys(positions).forEach(function(key) {
if(positions[key].lng){
filterPattern =
positions[key].ssid + ' ' +
formatMacAddress(positions[key].mac) + ' ' +
positions[key].mac
;
if (positions[key].pass) {
filterPattern += positions[key].pass + ' #cracked';
} else {
filterPattern += ' #notcracked';
}
filterPattern = filterPattern.toLowerCase();
//console.log(filterPattern);
var matched = true;
if (filterText) {
filterText.split(" ").forEach(function (item) {
if (!filterPattern.includes(item.toLowerCase())) {
matched = false;
}
});
}
if (matched) {
count++;
new_marker_pos = [positions[key].lat, positions[key].lng];
if (positions[key].acc) {
radius = Math.round(Math.min(positions[key].acc, 500));
markerColor = 'red';
markerColorCode = '#f03';
fillOpacity = 0.002;
if (positions[key].pass) {
markerColor = 'green';
markerColorCode = '#1aff00';
fillOpacity = 0.1;
}
accuracys.push(
L.circle(new_marker_pos, {
color: markerColor,
fillColor: markerColorCode,
fillOpacity: fillOpacity,
weight: 1,
opacity: 0.1,
radius: Math.min(positions[key].acc, 500),
}).setStyle({'className': 'radar'}).addTo(mymap)
);
}
passInfo = '';
if (positions[key].pass) {
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
} else {
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
}
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
markers.push(newMarker);
marker_pos.push(new_marker_pos);
markerClusters.addLayer( newMarker );
}
}
});
document.getElementById("matchcount").innerHTML = count + "&nbsp;APs";
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 :(";
}
}
// draw map on Enter in FilterInputField
const node = document.getElementById("search").addEventListener("keyup", function(event) {
if (event.key === "Enter") {
if (positionsLoaded) {
drawPositions();
}
}
});
// load positions
loadJSON("/plugins/webgpsmap/all", function(response) {
positions = JSON.parse(response);
positionsLoaded = true;
drawPositions();
});
</script>
</body></html>

View File

@@ -0,0 +1,408 @@
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+test the cache handling multiple clients
- cleanup the javascript in a class and handle "/newest" additions
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
https://gis.stackexchange.com/questions/312737/filtering-interactive-leaflet-map-with-dropdown-menu
https://blogs.kent.ac.uk/websolutions/2015/01/29/filtering-map-markers-with-leaflet-js-a-brief-technical-overview/
http://www.digital-geography.com/filter-leaflet-maps-slider/
http://bl.ocks.org/zross/47760925fcb1643b4225
-
'''
class Webgpsmap(plugins.Plugin):
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
__version__ = '1.3.0'
__name__ = 'webgpsmap'
__license__ = 'GPL3'
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
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 error:
logging.error(f"[webgpsmap] error: {error}")
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 error:
logging.error(f"[webgpsmap] error: {error}")
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 error:
logging.error(f"[webgpsmap] error: {error}")
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 for bad boys</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 error:
logging.error(f"[webgpsmap] error: {error}")
return
# cache 2048 items
@lru_cache(maxsize=2048, 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(f"[webgpsmap] scanning {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(f"[webgpsmap] found: {filename_base}")
filename_position = None
logging.debug("[webgpsmap] search for .gps.json")
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))
logging.debug("[webgpsmap] search for .geo.json")
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))
logging.debug("[webgpsmap] search for .paw-gps.json")
check_for = os.path.basename(filename_base) + ".paw-gps.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
logging.debug(f"[webgpsmap] end search for position data files and use {filename_position}")
if filename_position is not None:
all_geo_or_gps_files.append(filename_position)
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skipped networks? No!
if newest_only:
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
logging.info(f"[webgpsmap] Found {len(all_geo_or_gps_files)} position-data files from {len(all_pcap_files)} handshakes. Fetching positions ...")
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 and not pos.type() == PositionFile.PAWGPS:
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")
pos_type = 'unknown'
if pos.type() == PositionFile.GPS:
pos_type = 'gps'
elif pos.type() == PositionFile.GEO:
pos_type = 'geo'
elif pos.type() == PositionFile.PAWGPS:
pos_type = 'paw'
gps_data[ssid+"_"+mac] = {
'ssid': ssid,
'mac': mac,
'type': pos_type,
'lng': pos.lng(),
'lat': pos.lat(),
'acc': pos.accuracy(),
'ts_first': pos.timestamp_first(),
'ts_last': pos.timestamp_last(),
}
# get ap password if exist
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 error:
self.SKIP += pos_file
logging.error(f"[webgpsmap] JSONDecodeError in: {error}")
continue
except ValueError as error:
self.SKIP += pos_file
logging.error(f"[webgpsmap] ValueError: {error}")
continue
except OSError as error:
self.SKIP += pos_file
logging.error(f"[webgpsmap] OSError: {error}")
continue
logging.info(f"[webgpsmap] loaded {len(gps_data)} positions")
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 error:
logging.error(f"[webgpsmap] error loading template file {template_file} - error: {error}")
return html_data
class PositionFile:
"""
Wraps gps / net-pos files
"""
GPS = 1
GEO = 2
PAWGPS = 3
def __init__(self, path):
self._file = path
self._filename = os.path.basename(path)
try:
logging.debug(f"[webgpsmap] loading {path}")
with open(path, 'r') as json_file:
self._json = json.load(json_file)
logging.debug(f"[webgpsmap] loaded {path}")
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|paw-gps)\.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|paw-gps)\.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']
#fix timezone "Z": "2019-11-28T04:44:46.79231Z" >> "2019-11-28T04:44:46.79231+00:00"
if date_iso_formated.endswith("Z"):
date_iso_formated = date_iso_formated[:-1] + "+00:00"
# bad microseconds fix: fill/cut microseconds to 6 numbers
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
part2 = part2.ljust(6, '0')[:6]
# timezone fix: 0200 >>> 02:00
if len(part3) == 4:
part3 = part3[1:2].rjust(2, '0') + ':' + part3[3:4].rjust(2, '0')
date_iso_formated = part1 + "." + part2 + "+" + part3
dateObj = datetime.datetime.fromisoformat(date_iso_formated)
return_ts = int("%.0f" % dateObj.timestamp())
else:
# use file timestamp last modification of the json file
return_ts = int("%.0f" % os.path.getmtime(self._file))
return return_ts
def password(self):
"""
returns the password from file.pcap.cracked or None
"""
return_pass = None
# 2do: make better filename split/remove extension because this one has problems with "." in path
base_filename, ext1, ext2 = re.split('\.', self._file)
password_file_path = base_filename + ".pcap.cracked"
if os.path.isfile(password_file_path):
try:
password_file = open(password_file_path, 'r')
return_pass = password_file.read()
password_file.close()
except OSError as error:
logging.error(f"[webgpsmap] OS error: {format(error)}")
except:
logging.error(f"[webgpsmap] 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
if self._file.endswith('.paw-gps.json'):
return PositionFile.PAWGPS
return None
def lat(self):
try:
lat = None
# try to get value from known formats
if 'Latitude' in self._json:
lat = self._json['Latitude']
if 'lat' in self._json:
lat = self._json['lat'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
if 'location' in self._json:
if 'lat' in self._json['location']:
lat = self._json['location']['lat']
# check value
if lat is None:
raise ValueError(f"Lat is None in {self._filename}")
if lat == 0:
raise ValueError(f"Lat is 0 in {self._filename}")
return lat
except KeyError:
pass
return None
def lng(self):
try:
lng = None
# try to get value from known formats
if 'Longitude' in self._json:
lng = self._json['Longitude']
if 'long' in self._json:
lng = self._json['long'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
if 'location' in self._json:
if 'lng' in self._json['location']:
lng = self._json['location']['lng']
# check value
if lng is None:
raise ValueError(f"Lng is None in {self._filename}")
if lng == 0:
raise ValueError(f"Lng is 0 in {self._filename}")
return lng
except KeyError:
pass
return None
def accuracy(self):
if self.type() == PositionFile.GPS:
return 50.0 # a default
if self.type() == PositionFile.PAWGPS:
return 50.0 # a default
if self.type() == PositionFile.GEO:
try:
return self._json['accuracy']
except KeyError:
pass
return None

View File

@@ -1,19 +1,27 @@
import os
import logging
import requests
from datetime import datetime
from threading import Lock
from pwnagotchi.utils import StatusFile
import pwnagotchi.plugins as plugins
from pwnagotchi import plugins
from json.decoder import JSONDecodeError
class WpaSec(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '2.0.1'
__version__ = '2.1.0'
__license__ = 'GPL3'
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
def __init__(self):
self.ready = False
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
self.lock = Lock()
try:
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
except JSONDecodeError as json_err:
os.remove("/root/.wpa_sec_uploads")
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
self.options = dict()
self.skip = list()
@@ -35,6 +43,29 @@ class WpaSec(plugins.Plugin):
except requests.exceptions.RequestException as req_e:
raise req_e
def _download_from_wpasec(self, output, timeout=30):
"""
Downloads the results from wpasec and safes them to output
Output-Format: bssid, station_mac, ssid, password
"""
api_url = self.options['api_url']
if not api_url.endswith('/'):
api_url = f"{api_url}/"
api_url = f"{api_url}?api&dl=1"
cookie = {'key': self.options['api_key']}
try:
result = requests.get(api_url, cookies=cookie, timeout=timeout)
with open(output, 'wb') as output_file:
output_file.write(result.content)
except requests.exceptions.RequestException as req_e:
raise req_e
except OSError as os_e:
raise os_e
def on_loaded(self):
"""
Gets called when the plugin gets loaded
@@ -53,32 +84,48 @@ class WpaSec(plugins.Plugin):
"""
Called in manual mode when there's internet connectivity
"""
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
with self.lock:
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
if handshake_new:
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
if handshake_new:
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
for idx, handshake in enumerate(handshake_new):
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_wpasec(handshake)
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("WPA_SEC: %s", req_e)
continue
except OSError as os_e:
logging.error("WPA_SEC: %s", os_e)
continue
if 'download_results' in self.options and self.options['download_results']:
cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
if os.path.exists(cracked_file):
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
return
for idx, handshake in enumerate(handshake_new):
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_wpasec(handshake)
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile'))
logging.info("WPA_SEC: Downloaded cracked passwords.")
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("WPA_SEC: %s", req_e)
continue
logging.debug("WPA_SEC: %s", req_e)
except OSError as os_e:
logging.error("WPA_SEC: %s", os_e)
continue
logging.debug("WPA_SEC: %s", os_e)

View File

@@ -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)

View File

@@ -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'
@@ -55,12 +52,18 @@ class Display(View):
def is_dfrobot(self):
return self._implementation.name == 'dfrobot'
def is_waveshare144lcd(self):
return self._implementation.name == 'waveshare144lcd'
def is_waveshare154inch(self):
return self._implementation.name == 'waveshare154inch'
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 +94,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)

View File

@@ -7,9 +7,10 @@ from pwnagotchi.ui.hw.waveshare1 import WaveshareV1
from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd
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
@@ -40,8 +41,14 @@ def display_for(config):
elif config['ui']['display']['type'] == 'waveshare29inch':
return Waveshare29inch(config)
elif config['ui']['display']['type'] == 'waveshare144lcd':
return Waveshare144lcd(config)
elif config['ui']['display']['type'] == 'waveshare154inch':
return Waveshare154inch(config)
elif config['ui']['display']['type'] == 'waveshare213d':
return Waveshare213d(config)
return Waveshare213d(config)
elif config['ui']['display']['type'] == 'spotpear24inch':
return Spotpear24inch(config)

View File

View File

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

View File

@@ -0,0 +1,319 @@
# -*- coding:UTF-8 -*-
##
# | file : LCD_1IN44.py
# | version : V2.0
# | date : 2018-07-16
# | function : On the ST7735S chip driver and clear screen, drawing lines, drawing, writing
# and other functions to achieve
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import RPi.GPIO as GPIO
import time
import numpy as np
from . import config
LCD_1IN44 = 1
LCD_1IN8 = 0
if LCD_1IN44 == 1:
LCD_WIDTH = 128 #LCD width
LCD_HEIGHT = 128 #LCD height
LCD_X = 2
LCD_Y = 1
if LCD_1IN8 == 1:
LCD_WIDTH = 160
LCD_HEIGHT = 128
LCD_X = 1
LCD_Y = 2
LCD_X_MAXPIXEL = 132 #LCD width maximum memory
LCD_Y_MAXPIXEL = 162 #LCD height maximum memory
#scanning method
L2R_U2D = 1
L2R_D2U = 2
R2L_U2D = 3
R2L_D2U = 4
U2D_L2R = 5
U2D_R2L = 6
D2U_L2R = 7
D2U_R2L = 8
SCAN_DIR_DFT = U2D_R2L
class LCD:
def __init__(self):
GPIO.setmode(GPIO.BCM)
self.width = LCD_WIDTH
self.height = LCD_HEIGHT
self.LCD_Scan_Dir = SCAN_DIR_DFT
self.LCD_X_Adjust = LCD_X
self.LCD_Y_Adjust = LCD_Y
""" Hardware reset """
def LCD_Reset(self):
GPIO.output(config.LCD_RST_PIN, GPIO.HIGH)
config.Driver_Delay_ms(100)
GPIO.output(config.LCD_RST_PIN, GPIO.LOW)
config.Driver_Delay_ms(100)
GPIO.output(config.LCD_RST_PIN, GPIO.HIGH)
config.Driver_Delay_ms(100)
""" Write register address and data """
def LCD_WriteReg(self, Reg):
GPIO.setmode(GPIO.BCM)
GPIO.setup(config.LCD_DC_PIN, GPIO.OUT)
GPIO.output(config.LCD_DC_PIN, GPIO.LOW)
config.SPI_Write_Byte([Reg])
def LCD_WriteData_8bit(self, Data):
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
config.SPI_Write_Byte([Data])
def LCD_WriteData_NLen16Bit(self, Data, DataLen):
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
for i in range(0, DataLen):
config.SPI_Write_Byte([Data >> 8])
config.SPI_Write_Byte([Data & 0xff])
""" Common register initialization """
def LCD_InitReg(self):
#ST7735R Frame Rate
self.LCD_WriteReg(0xB1)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
self.LCD_WriteReg(0xB2)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
self.LCD_WriteReg(0xB3)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
#Column inversion
self.LCD_WriteReg(0xB4)
self.LCD_WriteData_8bit(0x07)
#ST7735R Power Sequence
self.LCD_WriteReg(0xC0)
self.LCD_WriteData_8bit(0xA2)
self.LCD_WriteData_8bit(0x02)
self.LCD_WriteData_8bit(0x84)
self.LCD_WriteReg(0xC1)
self.LCD_WriteData_8bit(0xC5)
self.LCD_WriteReg(0xC2)
self.LCD_WriteData_8bit(0x0A)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteReg(0xC3)
self.LCD_WriteData_8bit(0x8A)
self.LCD_WriteData_8bit(0x2A)
self.LCD_WriteReg(0xC4)
self.LCD_WriteData_8bit(0x8A)
self.LCD_WriteData_8bit(0xEE)
self.LCD_WriteReg(0xC5)#VCOM
self.LCD_WriteData_8bit(0x0E)
#ST7735R Gamma Sequence
self.LCD_WriteReg(0xe0)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x1a)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x18)
self.LCD_WriteData_8bit(0x2f)
self.LCD_WriteData_8bit(0x28)
self.LCD_WriteData_8bit(0x20)
self.LCD_WriteData_8bit(0x22)
self.LCD_WriteData_8bit(0x1f)
self.LCD_WriteData_8bit(0x1b)
self.LCD_WriteData_8bit(0x23)
self.LCD_WriteData_8bit(0x37)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(0x07)
self.LCD_WriteData_8bit(0x02)
self.LCD_WriteData_8bit(0x10)
self.LCD_WriteReg(0xe1)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x1b)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x17)
self.LCD_WriteData_8bit(0x33)
self.LCD_WriteData_8bit(0x2c)
self.LCD_WriteData_8bit(0x29)
self.LCD_WriteData_8bit(0x2e)
self.LCD_WriteData_8bit(0x30)
self.LCD_WriteData_8bit(0x30)
self.LCD_WriteData_8bit(0x39)
self.LCD_WriteData_8bit(0x3f)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(0x07)
self.LCD_WriteData_8bit(0x03)
self.LCD_WriteData_8bit(0x10)
#Enable test command
self.LCD_WriteReg(0xF0)
self.LCD_WriteData_8bit(0x01)
#Disable ram power save mode
self.LCD_WriteReg(0xF6)
self.LCD_WriteData_8bit(0x00)
#65k mode
self.LCD_WriteReg(0x3A)
self.LCD_WriteData_8bit(0x05)
#********************************************************************************
#function: Set the display scan and color transfer modes
#parameter:
# Scan_dir : Scan direction
# Colorchose : RGB or GBR color format
#********************************************************************************
def LCD_SetGramScanWay(self, Scan_dir):
#Get the screen scan direction
self.LCD_Scan_Dir = Scan_dir
#Get GRAM and LCD width and height
if (Scan_dir == L2R_U2D) or (Scan_dir == L2R_D2U) or (Scan_dir == R2L_U2D) or (Scan_dir == R2L_D2U) :
self.width = LCD_HEIGHT
self.height = LCD_WIDTH
if Scan_dir == L2R_U2D:
MemoryAccessReg_Data = 0X00 | 0x00
elif Scan_dir == L2R_D2U:
MemoryAccessReg_Data = 0X00 | 0x80
elif Scan_dir == R2L_U2D:
MemoryAccessReg_Data = 0x40 | 0x00
else: #R2L_D2U:
MemoryAccessReg_Data = 0x40 | 0x80
else:
self.width = LCD_WIDTH
self.height = LCD_HEIGHT
if Scan_dir == U2D_L2R:
MemoryAccessReg_Data = 0X00 | 0x00 | 0x20
elif Scan_dir == U2D_R2L:
MemoryAccessReg_Data = 0X00 | 0x40 | 0x20
elif Scan_dir == D2U_L2R:
MemoryAccessReg_Data = 0x80 | 0x00 | 0x20
else: #R2L_D2U
MemoryAccessReg_Data = 0x40 | 0x80 | 0x20
#please set (MemoryAccessReg_Data & 0x10) != 1
if (MemoryAccessReg_Data & 0x10) != 1:
self.LCD_X_Adjust = LCD_Y
self.LCD_Y_Adjust = LCD_X
else:
self.LCD_X_Adjust = LCD_X
self.LCD_Y_Adjust = LCD_Y
# Set the read / write scan direction of the frame memory
self.LCD_WriteReg(0x36) #MX, MY, RGB mode
if LCD_1IN44 == 1:
self.LCD_WriteData_8bit( MemoryAccessReg_Data | 0x08) #0x08 set RGB
else:
self.LCD_WriteData_8bit( MemoryAccessReg_Data & 0xf7) #RGB color filter panel
#/********************************************************************************
#function:
# initialization
#********************************************************************************/
def LCD_Init(self, Lcd_ScanDir):
if (config.GPIO_Init() != 0):
return -1
#Turn on the backlight
#GPIO.setup(config.LCD_BL_PIN, GPIO.OUT)
GPIO.output(config.LCD_BL_PIN,GPIO.HIGH)
#Hardware reset
self.LCD_Reset()
#Set the initialization register
self.LCD_InitReg()
#Set the display scan and color transfer modes
self.LCD_SetGramScanWay(Lcd_ScanDir)
config.Driver_Delay_ms(200)
#sleep out
self.LCD_WriteReg(0x11)
config.Driver_Delay_ms(120)
#Turn on the LCD display
self.LCD_WriteReg(0x29)
#/********************************************************************************
#function: Sets the start position and size of the display area
#parameter:
# Xstart : X direction Start coordinates
# Ystart : Y direction Start coordinates
# Xend : X direction end coordinates
# Yend : Y direction end coordinates
#********************************************************************************/
def LCD_SetWindows(self, Xstart, Ystart, Xend, Yend):
#set the X coordinates
self.LCD_WriteReg(0x2A)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit((Xstart & 0xff) + self.LCD_X_Adjust)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(((Xend - 1) & 0xff) + self.LCD_X_Adjust)
#set the Y coordinates
self.LCD_WriteReg (0x2B)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit((Ystart & 0xff) + self.LCD_Y_Adjust)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(((Yend - 1) & 0xff )+ self.LCD_Y_Adjust)
self.LCD_WriteReg(0x2C)
def LCD_Clear(self):
#hello
_buffer = [0xff]*(self.width * self.height * 2)
self.LCD_SetWindows(0, 0, self.width, self.height)
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
for i in range(0,len(_buffer),4096):
config.SPI_Write_Byte(_buffer[i:i+4096])
def LCD_ShowImage(self,Image,Xstart,Ystart):
if (Image == None):
return
imwidth, imheight = Image.size
if imwidth != self.width or imheight != self.height:
raise ValueError('Image must be same dimensions as display \
({0}x{1}).' .format(self.width, self.height))
img = np.asarray(Image)
pix = np.zeros((self.width,self.height,2), dtype = np.uint8)
pix[...,[0]] = np.add(np.bitwise_and(img[...,[0]],0xF8),np.right_shift(img[...,[1]],5))
pix[...,[1]] = np.add(np.bitwise_and(np.left_shift(img[...,[1]],3),0xE0),np.right_shift(img[...,[2]],3))
pix = pix.flatten().tolist()
self.LCD_SetWindows(0, 0, self.width , self.height)
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
for i in range(0,len(pix),4096):
config.SPI_Write_Byte(pix[i:i+4096])

View File

@@ -0,0 +1,73 @@
# /*****************************************************************************
# * | File : config.py
# * | Author :
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2019-12-06
# * | Info :
# ******************************************************************************/
Device_SPI = 1
Device_I2C = 0
Device = Device_SPI
#spi = spidev.SpiDev(0, 0)
##
# @filename : DEV_Config.py
# @brief : LCD hardware interface implements (GPIO, SPI)
# @author : Yehui from Waveshare
#
# Copyright (C) Waveshare July 10 2017
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import spidev
import RPi.GPIO as GPIO
import time
# Pin definition
LCD_RST_PIN = 27
LCD_DC_PIN = 25
LCD_CS_PIN = 8
LCD_BL_PIN = 24
# SPI device, bus = 0, device = 0
SPI = spidev.SpiDev(0, 0)
def epd_digital_write(pin, value):
GPIO.output(pin, value)
def Driver_Delay_ms(xms):
time.sleep(xms / 1000.0)
def SPI_Write_Byte(data):
SPI.writebytes(data)
def GPIO_Init():
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(LCD_RST_PIN, GPIO.OUT)
GPIO.setup(LCD_DC_PIN, GPIO.OUT)
GPIO.setup(LCD_CS_PIN, GPIO.OUT)
GPIO.setup(LCD_BL_PIN, GPIO.OUT)
SPI.max_speed_hz = 9000000
SPI.mode = 0b00
return 0;
### END OF FILE ###

View File

@@ -0,0 +1,32 @@
# Waveshare 1.44inch LCD HAT
# https://www.waveshare.com/1.44inch-lcd-hat.htm
# https://www.waveshare.com/wiki/1.44inch_LCD_HAT
# Driver: ST7735S
# Interface: SPI
# Display color: RGB, 65K color
# Resolution: 128x128
# Backlight: LED
# Operating voltage: 3.3V
from . import config
from . import LCD_1in44
from PIL import ImageOps
class EPD(object):
def __init__(self):
self.width = 128
self.height = 128
self.LCD = LCD_1in44.LCD()
self.LCD.Lcd_ScanDir = LCD_1in44.SCAN_DIR_DFT #SCAN_DIR_DFT = D2U_L2R
self.LCD.LCD_Init(self.LCD.Lcd_ScanDir)
def init(self):
pass
def clear(self):
#self.LCD.LCD_Clear()
pass
def display(self, image):
rgb_im = ImageOps.colorize(image.convert("L"), black ="green", white ="black")
self.LCD.LCD_ShowImage(rgb_im, 0, 0)

View File

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

View File

@@ -0,0 +1,46 @@
import logging
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.hw.base import DisplayImpl
class Waveshare144lcd(DisplayImpl):
def __init__(self, config):
super(Waveshare144lcd, self).__init__(config, 'waveshare144lcd')
self._display = None
def layout(self):
fonts.setup(10, 8, 10, 18)
self._layout['width'] = 128
self._layout['height'] = 128
self._layout['face'] = (0, 43)
self._layout['name'] = (0, 14)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (0, 71)
self._layout['uptime'] = (0, 25)
self._layout['line1'] = [0, 12, 127, 12]
self._layout['line2'] = [0, 116, 127, 116]
self._layout['friend_face'] = (12, 88)
self._layout['friend_name'] = (1, 103)
self._layout['shakes'] = (26, 117)
self._layout['mode'] = (0, 117)
self._layout['status'] = {
'pos': (65, 26),
'font': fonts.Small,
'max': 12
}
return self._layout
def initialize(self):
logging.info("initializing waveshare 1.44 inch lcd display")
from pwnagotchi.ui.hw.libs.waveshare.lcdhat144.epd import EPD
self._display = EPD()
self._display.init()
self._display.clear()
def render(self, canvas):
self._display.display(canvas)
def clear(self):
pass
#self._display.clear()

View File

@@ -6,7 +6,7 @@ from pwnagotchi.ui.hw.base import DisplayImpl
class Waveshare27inch(DisplayImpl):
def __init__(self, config):
super(Waveshare27inch, self).__init__(config, 'waveshare_2_7inch')
super(Waveshare27inch, self).__init__(config, 'waveshare27inch')
self._display = None
def layout(self):

View File

@@ -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)

View File

@@ -1,56 +1,198 @@
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('/reboot', 'reboot', self.with_auth(self.reboot), 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)
return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database)
if name == 'toggle' and request.method == 'POST':
checked = True if 'enabled' in request.form else False
return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed'
if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'):
try:
return plugins.loaded[name].on_webhook(subpath, request)
except Exception:
abort(500)
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)
abort(404)
# serve a message and shuts down the unit
def shutdown(self):
@@ -60,6 +202,14 @@ class Handler:
finally:
_thread.start_new_thread(pwnagotchi.shutdown, ())
# serve a message and reboot the unit
def reboot(self):
try:
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
message='Rebooting ...')
finally:
_thread.start_new_thread(pwnagotchi.reboot, ())
# serve a message and restart the unit in the other mode
def restart(self):
mode = request.form['mode']

View File

@@ -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))

View File

@@ -0,0 +1,259 @@
/*rules for the plot target div. These will be cascaded down to all plot elements according to css rules*/
.jqplot-target {
position: relative;
color: #666666;
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 1em;
/* height: 300px;
width: 400px;*/
}
/*rules applied to all axes*/
.jqplot-axis {
font-size: 0.75em;
}
.jqplot-xaxis {
margin-top: 10px;
}
.jqplot-x2axis {
margin-bottom: 10px;
}
.jqplot-yaxis {
margin-right: 10px;
}
.jqplot-y2axis, .jqplot-y3axis, .jqplot-y4axis, .jqplot-y5axis, .jqplot-y6axis, .jqplot-y7axis, .jqplot-y8axis, .jqplot-y9axis, .jqplot-yMidAxis {
margin-left: 10px;
margin-right: 10px;
}
/*rules applied to all axis tick divs*/
.jqplot-axis-tick, .jqplot-xaxis-tick, .jqplot-yaxis-tick, .jqplot-x2axis-tick, .jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick, .jqplot-yMidAxis-tick {
position: absolute;
white-space: pre;
}
.jqplot-xaxis-tick {
top: 0px;
/* initial position untill tick is drawn in proper place */
left: 15px;
/* padding-top: 10px;*/
vertical-align: top;
}
.jqplot-x2axis-tick {
bottom: 0px;
/* initial position untill tick is drawn in proper place */
left: 15px;
/* padding-bottom: 10px;*/
vertical-align: bottom;
}
.jqplot-yaxis-tick {
right: 0px;
/* initial position untill tick is drawn in proper place */
top: 15px;
/* padding-right: 10px;*/
text-align: right;
}
.jqplot-yaxis-tick.jqplot-breakTick {
right: -20px;
margin-right: 0px;
padding:1px 5px 1px 5px;
/*background-color: white;*/
z-index: 2;
font-size: 1.5em;
}
.jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick {
left: 0px;
/* initial position untill tick is drawn in proper place */
top: 15px;
/* padding-left: 10px;*/
/* padding-right: 15px;*/
text-align: left;
}
.jqplot-yMidAxis-tick {
text-align: center;
white-space: nowrap;
}
.jqplot-xaxis-label {
margin-top: 10px;
font-size: 11pt;
position: absolute;
}
.jqplot-x2axis-label {
margin-bottom: 10px;
font-size: 11pt;
position: absolute;
}
.jqplot-yaxis-label {
margin-right: 10px;
/* text-align: center;*/
font-size: 11pt;
position: absolute;
}
.jqplot-yMidAxis-label {
font-size: 11pt;
position: absolute;
}
.jqplot-y2axis-label, .jqplot-y3axis-label, .jqplot-y4axis-label, .jqplot-y5axis-label, .jqplot-y6axis-label, .jqplot-y7axis-label, .jqplot-y8axis-label, .jqplot-y9axis-label {
/* text-align: center;*/
font-size: 11pt;
margin-left: 10px;
position: absolute;
}
.jqplot-meterGauge-tick {
font-size: 0.75em;
color: #999999;
}
.jqplot-meterGauge-label {
font-size: 1em;
color: #999999;
}
table.jqplot-table-legend {
margin-top: 12px;
margin-bottom: 12px;
margin-left: 12px;
margin-right: 12px;
}
table.jqplot-table-legend, table.jqplot-cursor-legend {
background-color: rgba(255,255,255,0.6);
border: 1px solid #cccccc;
position: absolute;
font-size: 0.75em;
}
td.jqplot-table-legend {
vertical-align:middle;
}
/*
These rules could be used instead of assigning
element styles and relying on js object properties.
*/
/*
td.jqplot-table-legend-swatch {
padding-top: 0.5em;
text-align: center;
}
tr.jqplot-table-legend:first td.jqplot-table-legend-swatch {
padding-top: 0px;
}
*/
td.jqplot-seriesToggle:hover, td.jqplot-seriesToggle:active {
cursor: pointer;
}
.jqplot-table-legend .jqplot-series-hidden {
text-decoration: line-through;
}
div.jqplot-table-legend-swatch-outline {
border: 1px solid #cccccc;
padding:1px;
}
div.jqplot-table-legend-swatch {
width:0px;
height:0px;
border-top-width: 5px;
border-bottom-width: 5px;
border-left-width: 6px;
border-right-width: 6px;
border-top-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-right-style: solid;
}
.jqplot-title {
top: 0px;
left: 0px;
padding-bottom: 0.5em;
font-size: 1.2em;
}
table.jqplot-cursor-tooltip {
border: 1px solid #cccccc;
font-size: 0.75em;
}
.jqplot-cursor-tooltip {
border: 1px solid #cccccc;
font-size: 0.75em;
white-space: nowrap;
background: rgba(208,208,208,0.5);
padding: 1px;
}
.jqplot-highlighter-tooltip, .jqplot-canvasOverlay-tooltip {
border: 1px solid #cccccc;
font-size: 0.75em;
white-space: nowrap;
background: rgba(208,208,208,0.5);
padding: 1px;
}
.jqplot-point-label {
font-size: 0.75em;
z-index: 2;
}
td.jqplot-cursor-legend-swatch {
vertical-align: middle;
text-align: center;
}
div.jqplot-cursor-legend-swatch {
width: 1.2em;
height: 0.7em;
}
.jqplot-error {
/* Styles added to the plot target container when there is an error go here.*/
text-align: center;
}
.jqplot-error-message {
/* Styling of the custom error message div goes here.*/
position: relative;
top: 46%;
display: inline-block;
}
div.jqplot-bubble-label {
font-size: 0.8em;
/* background: rgba(90%, 90%, 90%, 0.15);*/
padding-left: 2px;
padding-right: 2px;
color: rgb(20%, 20%, 20%);
}
div.jqplot-bubble-label.jqplot-bubble-label-highlight {
background: rgba(90%, 90%, 90%, 0.7);
}
div.jqplot-noData-container {
text-align: center;
background-color: rgba(96%, 96%, 96%, 0.3);
}

View File

@@ -0,0 +1 @@
.jqplot-xaxis,.jqplot-xaxis-label{margin-top:10px}.jqplot-x2axis,.jqplot-x2axis-label{margin-bottom:10px}.jqplot-target{position:relative;color:#666;font-family:"Trebuchet MS",Arial,Helvetica,sans-serif;font-size:1em}.jqplot-axis{font-size:.75em}.jqplot-yaxis{margin-right:10px}.jqplot-y2axis,.jqplot-y3axis,.jqplot-y4axis,.jqplot-y5axis,.jqplot-y6axis,.jqplot-y7axis,.jqplot-y8axis,.jqplot-y9axis,.jqplot-yMidAxis{margin-left:10px;margin-right:10px}.jqplot-axis-tick,.jqplot-x2axis-tick,.jqplot-xaxis-tick,.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick,.jqplot-yMidAxis-tick,.jqplot-yaxis-tick{position:absolute;white-space:pre}.jqplot-xaxis-tick{top:0;left:15px;vertical-align:top}.jqplot-x2axis-tick{bottom:0;left:15px;vertical-align:bottom}.jqplot-yaxis-tick{right:0;top:15px;text-align:right}.jqplot-yaxis-tick.jqplot-breakTick{right:-20px;margin-right:0;padding:1px 5px;z-index:2;font-size:1.5em}.jqplot-x2axis-label,.jqplot-xaxis-label,.jqplot-yMidAxis-label,.jqplot-yaxis-label{font-size:11pt;position:absolute}.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{left:0;top:15px;text-align:left}.jqplot-yMidAxis-tick{text-align:center;white-space:nowrap}.jqplot-yaxis-label{margin-right:10px}.jqplot-y2axis-label,.jqplot-y3axis-label,.jqplot-y4axis-label,.jqplot-y5axis-label,.jqplot-y6axis-label,.jqplot-y7axis-label,.jqplot-y8axis-label,.jqplot-y9axis-label{font-size:11pt;margin-left:10px;position:absolute}.jqplot-meterGauge-tick{font-size:.75em;color:#999}.jqplot-meterGauge-label{font-size:1em;color:#999}table.jqplot-table-legend{margin:12px}table.jqplot-cursor-legend,table.jqplot-table-legend{background-color:rgba(255,255,255,.6);border:1px solid #ccc;position:absolute;font-size:.75em}td.jqplot-table-legend{vertical-align:middle}td.jqplot-seriesToggle:active,td.jqplot-seriesToggle:hover{cursor:pointer}.jqplot-table-legend .jqplot-series-hidden{text-decoration:line-through}div.jqplot-table-legend-swatch-outline{border:1px solid #ccc;padding:1px}div.jqplot-table-legend-swatch{width:0;height:0;border-width:5px 6px;border-style:solid}.jqplot-title{top:0;left:0;padding-bottom:.5em;font-size:1.2em}table.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em}.jqplot-canvasOverlay-tooltip,.jqplot-cursor-tooltip,.jqplot-highlighter-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,.5);padding:1px}.jqplot-point-label{font-size:.75em;z-index:2}td.jqplot-cursor-legend-swatch{vertical-align:middle;text-align:center}div.jqplot-cursor-legend-swatch{width:1.2em;height:.7em}.jqplot-error{text-align:center}.jqplot-error-message{position:relative;top:46%;display:inline-block}div.jqplot-bubble-label{font-size:.8em;padding-left:2px;padding-right:2px;color:rgb(20%,20%,20%)}div.jqplot-bubble-label.jqplot-bubble-label-highlight{background:rgba(90%,90%,90%,.7)}div.jqplot-noData-container{text-align:center;background-color:rgba(96%,96%,96%,.3)}

View File

@@ -1,42 +1,82 @@
.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;
}
li.navitem {
width: 16.66% !important;
clear: none !important;
}
/* Custom indentations are needed because the length of custom labels differs from
the length of the standard labels */
.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on {
text-indent: -5.9em;
}
.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off {
text-indent: 0.5em;
}
/* Custom widths are needed because the length of custom labels differs from
the length of the standard labels */
.custom-size-flipswitch.ui-flipswitch {
width: 8.875em;
}
.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active {
padding-left: 7em;
width: 1.875em;
}
@media (min-width: 28em) {
/*Repeated from rule .ui-flipswitch above*/
.ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch {
width: 1.875em;
}
}
#container {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.plugins-box {
margin: 0.5rem;
padding: 0.2rem;
border-style: groove;
border-radius: 0.5rem;
background-color: lightgrey;
text-align: center;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

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