Compare commits
889 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cedcc17391 | ||
|
6478b3827d | ||
|
463f4b50e4 | ||
|
793cde7147 | ||
|
518b9e665d | ||
|
91ea7bdb9b | ||
|
3ce88f1d80 | ||
|
6d45d01baf | ||
|
1f2dd73976 | ||
|
5d8d86204a | ||
|
7017e39c6d | ||
|
1360c734ff | ||
|
7c90050b17 | ||
|
9ca8aacdf6 | ||
|
d39c849daf | ||
|
58bbae89c2 | ||
|
0dedd0974f | ||
|
5bac678771 | ||
|
3b9aacdd16 | ||
|
305f837486 | ||
|
54ffbbcb0b | ||
|
60167fb8fe | ||
|
9a1565813c | ||
|
03c014f414 | ||
|
d10bf6bf1d | ||
|
9a22321799 | ||
|
76b71f5c3f | ||
|
4aa05bb834 | ||
|
71c4458858 | ||
|
8260b41bab | ||
|
86530d4b97 | ||
|
c7d9a757f6 | ||
|
b6a0ae9d3a | ||
|
34f52b0d3a | ||
|
c68cefe80d | ||
|
052c99b858 | ||
|
ccea7cbeef | ||
|
a16a5f7bcb | ||
|
a5df77d737 | ||
|
489bce0c01 | ||
|
da4319f81b | ||
|
0e1a1f4c79 | ||
|
b3bdb34e3f | ||
|
c791c86bee | ||
|
61e5872229 | ||
|
23616095ba | ||
|
3c8e7fbea4 | ||
|
4a5d2d36cc | ||
|
52d432e5b6 | ||
|
93bdf2e3a1 | ||
|
fe97315b0f | ||
|
7e80a7b9ca | ||
|
64385f43ed | ||
|
6a4d7a895e | ||
|
5606ad7281 | ||
|
23f09bc4b6 | ||
|
238b90d988 | ||
|
138316d55a | ||
|
67bbcfef9b | ||
|
1a0e0c46d2 | ||
|
4cd0f46ad7 | ||
|
155ea54d08 | ||
|
461e53ed79 | ||
|
6d40388002 | ||
|
37b25a142f | ||
|
665ad938b4 | ||
|
301a3d99cf | ||
|
c4e0acad17 | ||
|
9339ecb2fd | ||
|
a28c9a1176 | ||
|
c5d6f6d362 | ||
|
ff843f0367 | ||
|
717cb02743 | ||
|
8be643b2e0 | ||
|
814392daa5 | ||
|
4cc1c2ac1f | ||
|
fae6a0942b | ||
|
53ab63cf8a | ||
|
0764304be9 | ||
|
cdc0e0fa3e | ||
|
5ccd65e46e | ||
|
afc3636939 | ||
|
f154b97ab9 | ||
|
66acecb387 | ||
|
a31d0a5e19 | ||
|
026b9fc513 | ||
|
1cdc1641fc | ||
|
71de5925ee | ||
|
4733e90e77 | ||
|
7cf0a2ef4b | ||
|
97e03843bd | ||
|
8b078383c2 | ||
|
3e5bece3cf | ||
|
7645be6f3e | ||
|
779da95f78 | ||
|
51e13aa1ad | ||
|
e489678cf5 | ||
|
52015014b4 | ||
|
f691f737ab | ||
|
2617a6edea | ||
|
6001b7630b | ||
|
78fba1f74b | ||
|
6075296884 | ||
|
9457622713 | ||
|
7ef1c1f2f0 | ||
|
8e0488e16f | ||
|
5934ac4a55 | ||
|
15bae093fb | ||
|
0c76cd7ea7 | ||
|
5834b27ed8 | ||
|
a8ed9bcc1c | ||
|
0e88c3aa6a | ||
|
b1d61d95e6 | ||
|
215af0fc88 | ||
|
c09b72ff7e | ||
|
2f1b35b3fa | ||
|
d435ef2ba9 | ||
|
4164e7c067 | ||
|
bb7737762c | ||
|
c300e73726 | ||
|
0587c4b09a | ||
|
63dc672b11 | ||
|
0dac137df0 | ||
|
3db9ccb47e | ||
|
f375e4905f | ||
|
8d17cf0bd2 | ||
|
d981b26842 | ||
|
a7164ea742 | ||
|
cae2a18016 | ||
|
9d63eba232 | ||
|
f141e15ba3 | ||
|
e68165ce06 | ||
|
3758806919 | ||
|
1f91c6f09e | ||
|
3e8b6eafbd | ||
|
44138ba463 | ||
|
4b71fea404 | ||
|
6babad0d02 | ||
|
e8513240ea | ||
|
00101ccd07 | ||
|
a0bc911c0e | ||
|
6d71bcd965 | ||
|
91447a2a31 | ||
|
e06480e474 | ||
|
819146f83a | ||
|
cdd4c13336 | ||
|
704d7ceaa1 | ||
|
a4daf4af61 | ||
|
eddcf32b62 | ||
|
6117235c52 | ||
|
e0a66f5c99 | ||
|
81061cea24 | ||
|
93bb633010 | ||
|
dbb64e0fab | ||
|
fa8751017d | ||
|
9d56c97aa5 | ||
|
10f7161240 | ||
|
9b02548176 | ||
|
f5f47c4f88 | ||
|
1523dfc1ef | ||
|
a02960b56d | ||
|
b6f59f99d4 | ||
|
09a00adab9 | ||
|
7fa30c2868 | ||
|
774d9c693c | ||
|
87b6cf7d40 | ||
|
88928eec82 | ||
|
60f7849838 | ||
|
3cf041617c | ||
|
ee6c06f306 | ||
|
2e22a17610 | ||
|
1615fc8817 | ||
|
714cb00610 | ||
|
a0a790635a | ||
|
9d17be959d | ||
|
4b651cd17b | ||
|
2f70512076 | ||
|
b79c59c639 | ||
|
56f7b67699 | ||
|
1a8472268e | ||
|
4fb7205281 | ||
|
f8ffab426b | ||
|
b4daf19401 | ||
|
e04e053cee | ||
|
4f153e3899 | ||
|
04720ecc42 | ||
|
a12e2aafa5 | ||
|
1721f67ec3 | ||
|
7693e42aa1 | ||
|
2ae48a2ef2 | ||
|
663bca41cd | ||
|
ede01e50cd | ||
|
19973574e7 | ||
|
a019b4c778 | ||
|
526c5bed87 | ||
|
4d6136633a | ||
|
6d90b75d10 | ||
|
03695be807 | ||
|
6a97476732 | ||
|
b5e620684b | ||
|
95557ab37d | ||
|
988d093e36 | ||
|
f563d71477 | ||
|
7b219fd139 | ||
|
42ed698583 | ||
|
6df7bcd885 | ||
|
1c299832ae | ||
|
11476433ca | ||
|
6e57e131b3 | ||
|
548b42ef20 | ||
|
99614c8cd4 | ||
|
e19ea999e2 | ||
|
2207a1eacf | ||
|
9509dd0aa5 | ||
|
608904daf8 | ||
|
f973997cdb | ||
|
9b594f7fb2 | ||
|
b8eed4f52a | ||
|
ad510429fe | ||
|
f5a94fde96 | ||
|
855bda9104 | ||
|
e72fd08fb4 | ||
|
e44ebac43f | ||
|
b5ddb716e2 | ||
|
82e7e09fa1 | ||
|
8b40e94ca8 | ||
|
cc5c46906f | ||
|
7cb52ba33a | ||
|
07f8e7bd4a | ||
|
48dc751d13 | ||
|
3c154ffe0c | ||
|
167f559d73 | ||
|
19775b7d27 | ||
|
48e3a372cc | ||
|
d2c44797e5 | ||
|
a03443986b | ||
|
a7ea499fac | ||
|
722a91655a | ||
|
93e06d7f59 | ||
|
7de5121033 | ||
|
83f741bbb0 | ||
|
a779fb9b0b | ||
|
c4a007e72a | ||
|
1a71615fa8 | ||
|
7a9f84f495 | ||
|
6e3f5a1181 | ||
|
d045ed5afa | ||
|
0ee0aaff37 | ||
|
0fb81a11c4 | ||
|
cfc0ad1b48 | ||
|
3351c251ef | ||
|
d9d399429c | ||
|
b1ad247e11 | ||
|
b5a148f287 | ||
|
7138f6469b | ||
|
785d678e30 | ||
|
9c8784e533 | ||
|
fd288b4acd | ||
|
b704614254 | ||
|
2dc36651df | ||
|
d8d6d52eda | ||
|
43c5ab7ecf | ||
|
de62424dbc | ||
|
8c51936c13 | ||
|
87e46610f9 | ||
|
a96dead519 | ||
|
8ed2950eb5 | ||
|
6f8133b2b8 | ||
|
16afa87112 | ||
|
ed22343877 | ||
|
c70f2c30e9 | ||
|
5111490c70 | ||
|
59ae35372e | ||
|
5f593a4231 | ||
|
56cc872daa | ||
|
6e1490da78 | ||
|
69597103b5 | ||
|
96ca5dd8e3 | ||
|
3efa3a935a | ||
|
39ccd141eb | ||
|
1a30b52a90 | ||
|
8965ad9272 | ||
|
5dae0ce982 | ||
|
92266a783a | ||
|
9e656d4ea6 | ||
|
1d255b577d | ||
|
1ff14c05a9 | ||
|
ab63ecccd7 | ||
|
a7e37115d9 | ||
|
b1d8aa3ba1 | ||
|
6e26463278 | ||
|
7261073073 | ||
|
a02c1d6d92 | ||
|
40caf3f51a | ||
|
21f1273bd8 | ||
|
a8c07ba997 | ||
|
6bd09c7f43 | ||
|
1830a19b37 | ||
|
9dcc647656 | ||
|
8fcfd4cafd | ||
|
369d7a65a8 | ||
|
d7ad8ee0d7 | ||
|
a5f9b9b2ee | ||
|
b266671864 | ||
|
c47b8f2d11 | ||
|
8129fb7dd2 | ||
|
c16cabc852 | ||
|
9a7de86057 | ||
|
440f2a470a | ||
|
81a89d43e0 | ||
|
91b409053b | ||
|
df01a03a4b | ||
|
e2be21004d | ||
|
0f3d9db01d | ||
|
19abc17816 | ||
|
4b9ebc2512 | ||
|
41a3fad43e | ||
|
f4b886cf7b | ||
|
0eb8e1829e | ||
|
30b1874a0a | ||
|
b903f636d2 | ||
|
52b40f049e | ||
|
3df35ef03b | ||
|
e87bcc4744 | ||
|
dfd534ac41 | ||
|
0b2c156d29 | ||
|
14064c3b5b | ||
|
313fd66634 | ||
|
c939af4248 | ||
|
e934181606 | ||
|
2505cbf14c | ||
|
b2c6de72cd | ||
|
307b3890f1 | ||
|
b9a909de2b | ||
|
b180f16aa6 | ||
|
2d517e3de5 | ||
|
a1746da7f1 | ||
|
1a1a70d6e8 | ||
|
229e2671e8 | ||
|
92c1b6b005 | ||
|
a2ac679499 | ||
|
d7e1c59709 | ||
|
a5ed09cd08 | ||
|
8c83f8129c | ||
|
eddfdb3ebc | ||
|
7ca1168fed | ||
|
d41e5c1152 | ||
|
25eee18e7b | ||
|
38144a7abb | ||
|
1f17d3cbbe | ||
|
1da59b50b4 | ||
|
1130c72098 | ||
|
6b99deb7bd | ||
|
c3ed3509e9 | ||
|
e3a2e8c811 | ||
|
89046bf0c5 | ||
|
4cc61322de | ||
|
94521f2174 | ||
|
b50acd364c | ||
|
9bc7fcccb3 | ||
|
bd61196c3c | ||
|
186042aa20 | ||
|
78bf801273 | ||
|
89450ec1bd | ||
|
09f80cc842 | ||
|
fcb5c87ef0 | ||
|
bf0e480266 | ||
|
fce57ad8eb | ||
|
425fe7e55a | ||
|
5760864495 | ||
|
3d9c559cdb | ||
|
97a019fe25 | ||
|
8d5834232b | ||
|
ebc161e82f | ||
|
9485e53484 | ||
|
0d66f93ef3 | ||
|
ad80fab554 | ||
|
b7380018f1 | ||
|
2ea8e7fe6b | ||
|
e23e1affae | ||
|
8d8333b586 | ||
|
5712f5cd51 | ||
|
9cc15403c3 | ||
|
15fa7039e8 | ||
|
f83c820b38 | ||
|
323c9a74cc | ||
|
8a2d6eac9d | ||
|
61d8e28aad | ||
|
337ebd6f9f | ||
|
399dbf2b41 | ||
|
f952bcd298 | ||
|
9dc7c92c86 | ||
|
9e92201d82 | ||
|
4e592df6d8 | ||
|
403ee242a6 | ||
|
106d72c4a2 | ||
|
5ddc2d7080 | ||
|
52f1111a5b | ||
|
346773f790 | ||
|
9264b837c8 | ||
|
81032fe5e3 | ||
|
2aa73d1a7e | ||
|
b796384345 | ||
|
a6ca99c693 | ||
|
60d9fd46ae | ||
|
de71d18a72 | ||
|
aba5b938bc | ||
|
0830e0c74b | ||
|
cf8a4da9e7 | ||
|
80e2cdcd8d | ||
|
a5cfb9aa8b | ||
|
62a0cc6276 | ||
|
f8523eb382 | ||
|
58b0b0fea0 | ||
|
bdf585afe5 | ||
|
b7d1c82788 | ||
|
74838d6b96 | ||
|
7fc46ddcf6 | ||
|
4503e71bfb | ||
|
11fb95d299 | ||
|
0aaeeb8011 | ||
|
3e1f3d5eec | ||
|
86a3443b8d | ||
|
806efa1fc2 | ||
|
537519dea6 | ||
|
0c1d98f2ab | ||
|
4852b3f59e | ||
|
364af70ad5 | ||
|
e336fca0de | ||
|
00e7c04980 | ||
|
2549433e34 | ||
|
9f9fca02e5 | ||
|
6945e260bd | ||
|
c21986488d | ||
|
b52ceae2ee | ||
|
19fc25d508 | ||
|
ba22b7d5d7 | ||
|
59019efad0 | ||
|
20aa0d1909 | ||
|
c56c6bb8f5 | ||
|
1e426f7411 | ||
|
aeb6002e10 | ||
|
dc2362c371 | ||
|
d6c0ec0dfd | ||
|
04e551600d | ||
|
62983dfea5 | ||
|
b2c812d05d | ||
|
7ba9b35d06 | ||
|
b4b14ba9fd | ||
|
1ba3a69651 | ||
|
0aef199131 | ||
|
ace61836e5 | ||
|
d04f124add | ||
|
bc1db7ceea | ||
|
fd506b1533 | ||
|
6c44a687b1 | ||
|
a2bb66ad57 | ||
|
61af8b4762 | ||
|
9b58fed862 | ||
|
53ae8ea1cf | ||
|
5b66d687c4 | ||
|
4b74de48bf | ||
|
22e76f956c | ||
|
4418492637 | ||
|
31a89cbe4b | ||
|
d91f49d596 | ||
|
66dc03ec05 | ||
|
2f948306eb | ||
|
ae330dc0b5 | ||
|
e06f2a32e8 | ||
|
e184176ae4 | ||
|
1827ee564c | ||
|
bfdaffa14b | ||
|
53f99f4c28 | ||
|
3efa96b292 | ||
|
8118a10a6a | ||
|
bd63f71a1d | ||
|
31d401e03b | ||
|
3c32bbb582 | ||
|
03067da005 | ||
|
64aad56fba | ||
|
4240d05872 | ||
|
f03e07f0cf | ||
|
13064879e0 | ||
|
96c617e152 | ||
|
783ac61594 | ||
|
279d885ec6 | ||
|
a8705c1d3c | ||
|
90386c7a64 | ||
|
205480bc38 | ||
|
d2726c1a14 | ||
|
1c9a25d22a | ||
|
e9494992fc | ||
|
c7931450c3 | ||
|
cc7299153c | ||
|
a27f09871f | ||
|
3714899e95 | ||
|
be414e57b3 | ||
|
1600d8cbd1 | ||
|
965416483d | ||
|
78a036ed1a | ||
|
667c64832f | ||
|
9d19fb8e7a | ||
|
cb09648ba1 | ||
|
03b85ac66b | ||
|
5255e5fd13 | ||
|
d5a8cda278 | ||
|
bcdbf41bb8 | ||
|
3c86b58696 | ||
|
18a41f3e29 | ||
|
09f33112b4 | ||
|
432d654cc5 | ||
|
3b1d90baef | ||
|
0ad6e887ac | ||
|
d42ab1574b | ||
|
7189f3c461 | ||
|
32e3343596 | ||
|
b76144909a | ||
|
7676b55b87 | ||
|
92773a2b37 | ||
|
d3c6194e0f | ||
|
7fd31e14f9 | ||
|
c0434b7dde | ||
|
4d5bfc2adf | ||
|
4814e10940 | ||
|
dfcc2f6bf2 | ||
|
a897e00c98 | ||
|
caec837050 | ||
|
786564ebb8 | ||
|
bfe0ea7623 | ||
|
15b815d8ad | ||
|
f74979b10c | ||
|
5119bf4326 | ||
|
74778a0acf | ||
|
65a3553521 | ||
|
f4fa259781 | ||
|
f86d5afb5c | ||
|
321595f93c | ||
|
d1d7107923 | ||
|
787a498c49 | ||
|
9c5006fefd | ||
|
18957c6ad3 | ||
|
5e1486be93 | ||
|
8cb3e1c8d5 | ||
|
723e1892e4 | ||
|
f1bc2d945b | ||
|
16e310e377 | ||
|
b66c86b31a | ||
|
b93bcd07d4 | ||
|
5ae7695229 | ||
|
8a687b723b | ||
|
0b495ebd13 | ||
|
9888e1fb39 | ||
|
5a16819feb | ||
|
102a061814 | ||
|
ecfc5f9063 | ||
|
1c2ff20ab4 | ||
|
a2c3df5c99 | ||
|
28c28b8b0c | ||
|
3bc887631b | ||
|
13b73a64c8 | ||
|
479f9cf79d | ||
|
8a87bbeb90 | ||
|
d3d4df2500 | ||
|
69b3fabba1 | ||
|
d5007385f3 | ||
|
d2af951b34 | ||
|
a44c325877 | ||
|
1522470fa5 | ||
|
3463740fd9 | ||
|
d6228b133b | ||
|
441bd905a5 | ||
|
db7fc74b4a | ||
|
88f7384f52 | ||
|
d984ea8a76 | ||
|
4b606d9a6b | ||
|
ea51ab7014 | ||
|
68c11ada8d | ||
|
546c7fe397 | ||
|
5790f494df | ||
|
063cc73689 | ||
|
5643f9ae70 | ||
|
99f6758aae | ||
|
0a33f9c0af | ||
|
d231541403 | ||
|
f1eb3316c6 | ||
|
f3a96b7981 | ||
|
beb2b83f36 | ||
|
24ae443ee9 | ||
|
3f785ee06a | ||
|
cf146a54ee | ||
|
31c1d742e0 | ||
|
c0252c9830 | ||
|
4aa29f1b79 | ||
|
5dc780a88f | ||
|
39a6ae1be5 | ||
|
047f0d5d63 | ||
|
715e696537 | ||
|
22afb563e3 | ||
|
5f4ee26f99 | ||
|
1959bc08ae | ||
|
2c9f54567f | ||
|
63694d57e5 | ||
|
c6c2e0e7ce | ||
|
cb6365b9f2 | ||
|
8b00e0ae10 | ||
|
7954bb5fcf | ||
|
06d8cc63fb | ||
|
c4ae3c15bd | ||
|
3604f483aa | ||
|
d51d3f61ac | ||
|
6bb8fede0c | ||
|
c35d202ffd | ||
|
ffa432587a | ||
|
6f013bb1ef | ||
|
f8f6608968 | ||
|
0c176ca308 | ||
|
ec430a5cba | ||
|
97733cbf43 | ||
|
4445ef6432 | ||
|
a25395a945 | ||
|
0f171c35ce | ||
|
61b957ac77 | ||
|
30e3898f3c | ||
|
094dde0e8c | ||
|
dc5a626bd5 | ||
|
608aad4820 | ||
|
30f9c16778 | ||
|
97cccf5a1d | ||
|
ce0c248cf4 | ||
|
d5ac988498 | ||
|
6d70a24aae | ||
|
032e183ff7 | ||
|
f00c861844 | ||
|
4addefd57d | ||
|
2239406272 | ||
|
0df5e54f91 | ||
|
3e0833698a | ||
|
ab9ddb4513 | ||
|
2beef33251 | ||
|
2b583d7458 | ||
|
da7e21bbdd | ||
|
d9ddae9a41 | ||
|
d306877c7a | ||
|
7d34e631f5 | ||
|
f39c154b23 | ||
|
04b2ee5b44 | ||
|
9a72a8868b | ||
|
d636c2b1f2 | ||
|
84616827ad | ||
|
d16180189e | ||
|
885fddfce8 | ||
|
68d7686a03 | ||
|
d20f6c8a52 | ||
|
5bf9f25a46 | ||
|
b85e4174dc | ||
|
0de2e3d150 | ||
|
412fca5290 | ||
|
3ab2088c79 | ||
|
eda8a18788 | ||
|
ea14cb370e | ||
|
6f2228f439 | ||
|
1d53dc7c4e | ||
|
202cd1f32e | ||
|
ab871f9d2d | ||
|
f2ceec0f8f | ||
|
f734c9cd68 | ||
|
36c3ea5bbc | ||
|
062de3b618 | ||
|
77ada644ed | ||
|
277906a673 | ||
|
a78a4b0b3e | ||
|
3ad426916f | ||
|
23ef17d4c7 | ||
|
7779ebc983 | ||
|
ddc264bbb9 | ||
|
f0092ff154 | ||
|
ed0df18f68 | ||
|
4fa7e9f077 | ||
|
9ca2424df1 | ||
|
8c22b1d6f1 | ||
|
538b547560 | ||
|
414a6b4c7a | ||
|
cdf11df270 | ||
|
ef52cbcc25 | ||
|
b0dab7b589 | ||
|
ace1244d10 | ||
|
84fa293a11 | ||
|
139c9df88c | ||
|
7a721be7dc | ||
|
56079dfd9d | ||
|
90998be24c | ||
|
5cb721f490 | ||
|
c954bf8ffa | ||
|
41ea0e0747 | ||
|
e943cfad70 | ||
|
f576fb609b | ||
|
1e015fe6f6 | ||
|
d526915cce | ||
|
0ac940f636 | ||
|
da6f3608f3 | ||
|
bdfd021820 | ||
|
4a2091e688 | ||
|
16832cd414 | ||
|
020be7185c | ||
|
c8ff69c068 | ||
|
79d252254f | ||
|
64e677f5df | ||
|
bceb3c4c4f | ||
|
df246b255a | ||
|
d6d810497e | ||
|
9df1dbe077 | ||
|
3c97dbf8dc | ||
|
376a74d7ac | ||
|
855b493040 | ||
|
375486f9d0 | ||
|
0263777e4b | ||
|
132666935a | ||
|
74a75f03b8 | ||
|
399e78e675 | ||
|
649091766f | ||
|
999b130224 | ||
|
2a450e64ef | ||
|
6152374024 | ||
|
f9cbef9697 | ||
|
682b373c66 | ||
|
8d0d3df2b0 | ||
|
6d5054387b | ||
|
4018071bba | ||
|
63568f1725 | ||
|
c72cb5b962 | ||
|
0cdb8c3221 | ||
|
ddeefa037d | ||
|
a4e072cf33 | ||
|
677b335403 | ||
|
f762c3ac0d | ||
|
152676f651 | ||
|
0ce1fa9d12 | ||
|
2e8f2aa1b8 | ||
|
f7a23b32c1 | ||
|
4653c5d95d | ||
|
c947d5c43b | ||
|
26bb5d6183 | ||
|
916be1f63e | ||
|
4a4b973f60 | ||
|
d49cefe1e4 | ||
|
c6b3e11e04 | ||
|
e8fa682302 | ||
|
fbd12bf87b | ||
|
40c01db1d1 | ||
|
ca2becd9ce | ||
|
ff6bf5c198 | ||
|
cd5d783c52 | ||
|
99e0a31ea8 | ||
|
5f6cc378f1 | ||
|
d45e8c7ba0 | ||
|
539df810ed | ||
|
bd7c64b2af | ||
|
892fda775d | ||
|
f9c0efc24a | ||
|
400d0e7290 | ||
|
fb2c65ef0a | ||
|
6d88cb17f3 | ||
|
c10f25140c | ||
|
d2966098b0 | ||
|
a3a4854427 | ||
|
e5d623c114 | ||
|
68801adcaf | ||
|
58a085188f | ||
|
f52687642e | ||
|
2d7b6b54fe | ||
|
b3aa5bc2c1 | ||
|
10cba6872a | ||
|
b213f9f214 | ||
|
fa72a53129 | ||
|
49f7c652c7 | ||
|
9c78e16c1b | ||
|
22ebc09c9d | ||
|
0ea67cb97f | ||
|
2324423cab | ||
|
be2cd8293f | ||
|
86cbe01081 | ||
|
eea4feee71 | ||
|
cff487008c | ||
|
6cecca67a4 | ||
|
fb4d46d71a | ||
|
e220a869e0 | ||
|
f4a59549fa | ||
|
a058d2e00d | ||
|
8c73e32853 | ||
|
eeee4bdd3c | ||
|
1ddf020ba8 | ||
|
e992834b37 | ||
|
55bac8a8b9 | ||
|
61a6b77a52 | ||
|
0aa4f95235 | ||
|
95b871aade | ||
|
7b05f10c6f | ||
|
299bd9a5ca | ||
|
10688ca7b0 | ||
|
6b1bca7cb2 | ||
|
79688305fd | ||
|
13b1fb6d14 | ||
|
8a1aad1a99 | ||
|
aeb6536959 | ||
|
970b6922b7 | ||
|
18dd71b989 | ||
|
256ccab05c | ||
|
0aa80d2307 | ||
|
5987f93009 | ||
|
ebeb22081b | ||
|
f97b106858 | ||
|
c449c77ef9 | ||
|
a05ea2f48a | ||
|
beb2fedf02 | ||
|
1936c309f0 | ||
|
ee3fb285be | ||
|
aa60a369a9 | ||
|
0e9f9c0f2e | ||
|
6645c80db3 | ||
|
13d68c7c24 | ||
|
df33d20cb2 | ||
|
f3eb208c6a | ||
|
ae5ca2a05e | ||
|
a9b9c6677e | ||
|
f85d80d3fd | ||
|
4be54cf3ee | ||
|
c8953d4654 | ||
|
26ff5c95f2 | ||
|
2f0f0edab0 | ||
|
b81c80cf99 | ||
|
baf20a9ac8 | ||
|
280ca22261 | ||
|
277dbd5a16 | ||
|
5b29f65042 | ||
|
9f66d7ab96 | ||
|
9625cf1122 | ||
|
6b42e48dff | ||
|
d814de75ab | ||
|
35b442f941 | ||
|
0f2ad47c17 | ||
|
748dbea13e | ||
|
b66f1b66e5 | ||
|
1642663c84 | ||
|
54a8fd81a5 | ||
|
2fe7ac0a71 | ||
|
4b563398f4 | ||
|
84be7c0d34 | ||
|
82bf9b9853 | ||
|
d9d38e7a1e | ||
|
5b78a13e95 | ||
|
a8a0f842a3 | ||
|
f8a28d375b | ||
|
cfa8a02abc | ||
|
e32be6ff27 | ||
|
ab74395602 | ||
|
b7a806c8ad | ||
|
e146a87b44 | ||
|
dfb4bcaf21 | ||
|
9aca3a3a5b | ||
|
d15f8c18b5 | ||
|
87a3fb5f0c | ||
|
8b366ca736 | ||
|
2bbcc36f2a | ||
|
308746a7de | ||
|
d648f7cdf5 | ||
|
5a53670133 | ||
|
e15d0f3323 | ||
|
1ce361a839 | ||
|
e9899dda94 | ||
|
2430b4a134 | ||
|
b539a76124 | ||
|
ba7c0ee4e6 | ||
|
f0c5ad4b74 | ||
|
8d2cbee8df | ||
|
6793312691 | ||
|
5989d2571c | ||
|
ad2fbdb9dd | ||
|
1215fda459 | ||
|
1808392a1d | ||
|
77efeafd65 | ||
|
ac5ee1ba7b | ||
|
ef31366078 | ||
|
5d28557608 | ||
|
3a14d1d87f | ||
|
1691896531 | ||
|
e08b633c88 | ||
|
77c16c38f4 | ||
|
3773f96901 |
@@ -3,7 +3,6 @@ maintainers:
|
||||
- caquino
|
||||
- dadav
|
||||
- justin-p
|
||||
- hexwaxwing
|
||||
|
||||
features:
|
||||
- comments
|
||||
|
31
.editorconfig
Normal file
31
.editorconfig
Normal file
@@ -0,0 +1,31 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = ignore
|
||||
|
||||
[*.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
[*.{md,txt}]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ output-pwnagotchi
|
||||
build
|
||||
dist
|
||||
pwnagotchi.egg-info
|
||||
*backup*.tgz
|
||||
*backup*.gz
|
||||
.vscode
|
||||
|
@@ -20,7 +20,7 @@ deploy:
|
||||
repo: evilsocket/pwnagotchi
|
||||
branches:
|
||||
only:
|
||||
- "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]+?$/"
|
||||
- "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*$/"
|
||||
cache:
|
||||
apt: true
|
||||
before_script:
|
||||
|
@@ -6,3 +6,4 @@ include LICENSE
|
||||
recursive-include bin *
|
||||
recursive-include pwnagotchi *.py
|
||||
recursive-include pwnagotchi *.yml
|
||||
recursive-include pwnagotchi *.*
|
||||
|
5
Makefile
5
Makefile
@@ -1,10 +1,11 @@
|
||||
PACKER_VERSION=1.5.2
|
||||
PWN_HOSTNAME=pwnagotchi
|
||||
PWN_VERSION=master
|
||||
|
||||
all: install image clean
|
||||
all: clean install image
|
||||
|
||||
install:
|
||||
curl https://releases.hashicorp.com/packer/1.3.5/packer_1.3.5_linux_amd64.zip -o /tmp/packer.zip
|
||||
curl https://releases.hashicorp.com/packer/$(PACKER_VERSION)/packer_$(PACKER_VERSION)_linux_amd64.zip -o /tmp/packer.zip
|
||||
unzip /tmp/packer.zip -d /tmp
|
||||
sudo mv /tmp/packer /usr/bin/packer
|
||||
git clone https://github.com/solo-io/packer-builder-arm-image /tmp/packer-builder-arm-image
|
||||
|
32
README.md
32
README.md
@@ -1,40 +1,29 @@
|
||||
# Pwnagotchi
|
||||
|
||||
<p align="center">
|
||||
<p align="center">
|
||||
<a href="https://github.com/evilsocket/pwnagotchi/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/evilsocket/pwnagotchi.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/evilsocket/pwnagotchi/blob/master/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-GPL3-brightgreen.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/evilsocket/pwnagotchi/graphs/contributors"><img alt="Contributors" src="https://img.shields.io/github/contributors/evilsocket/pwnagotchi"/></a>
|
||||
<a href="https://travis-ci.org/evilsocket/pwnagotchi"><img alt="Travis" src="https://img.shields.io/travis/evilsocket/pwnagotchi/master.svg?style=flat-square"></a>
|
||||
<a href="https://pwnagotchi.herokuapp.com/"><img alt="Slack" src="https://pwnagotchi.herokuapp.com/badge.svg"></a>
|
||||
<a href="https://invite.pwnagotchi.ai/"><img alt="Slack" src="https://invite.pwnagotchi.ai/badge.svg"></a>
|
||||
<a href="https://community.pwnagotchi.ai/"><img alt="Forum" src="https://img.shields.io/discourse/posts?server=https%3A%2F%2Fcommunity.pwnagotchi.ai%2F&style=flat-square"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=pwnagotchi"><img src="https://img.shields.io/twitter/follow/pwnagotchi?style=social&logo=twitter" alt="follow on Twitter"></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
[Pwnagotchi](https://twitter.com/pwnagotchi) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment in order to maximize the crackable WPA key material it captures (either passively, or by performing deauthentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/),
|
||||
[Pwnagotchi](https://pwnagotchi.ai/) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment to maximize the crackable WPA key material it captures (either passively, or by performing authentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/),
|
||||
full and half WPA handshakes.
|
||||
|
||||

|
||||

|
||||
|
||||
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/defaults.yml#L73) over time to **get better at pwning WiFi things** in the environments you expose it to.
|
||||
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning-based "AI" *(yawn)*, Pwnagotchi tunes [its parameters](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/defaults.yml#L73) over time to **get better at pwning WiFi things to** in the environments you expose it to.
|
||||
|
||||
More specifically, Pwnagotchi is using an [LSTM with MLP feature extractor](https://stable-baselines.readthedocs.io/en/master/modules/policies.html#stable_baselines.common.policies.MlpLstmPolicy) as its policy network for the [A2C agent](https://stable-baselines.readthedocs.io/en/master/modules/a2c.html). If you're unfamiliar with A2C, here is [a very good introductory explanation](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) (in comic form!) of the basic principles behind how Pwnagotchi learns. (You can read more about how Pwnagotchi learns in the [Usage](https://www.pwnagotchi.ai/usage/#training-the-ai) doc.)
|
||||
|
||||
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://www.pwnagotchi.ai/usage/#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your Pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
|
||||
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://www.pwnagotchi.ai/usage/#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but ** listen to your Pwnagotchi when it tells you it's boring!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
|
||||
|
||||
Multiple units within close physical proximity can "talk" to each other, advertising their own presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
|
||||
|
||||
## Why does Pwnagotchi exist?
|
||||
|
||||
For hackers to learn reinforcement learning, WiFi networking, and have an excuse to get out for more walks. Also? **It's cute as f---**.
|
||||
|
||||
**In case you're curious about the name:** *Pwnagotchi* is a portmanteau of *pwn* (which we shouldn't have to explain if you are interested in this project :kissing_heart:) and *-gotchi*. It is a nostalgic reference made in homage to a very popular children's toy from the 1990s called the [Tamagotchi](https://en.wikipedia.org/wiki/Tamagotchi). The Tamagotchi (たまごっち, derived from *tamago* (たまご) "egg" + *uotchi* (ウオッチ) "watch") is a cultural touchstone for many Millennial hackers as a formative electronic toy from our collective childhoods. Were you lucky enough to possess a Tamagotchi as a kid? Well, with your Pwnagotchi, you too can enjoy the nostalgic delight of being strangely emotionally attached to a handheld automata *yet again!* Except, this time around...you get to #HackThePlanet. >:D
|
||||
Multiple units within close physical proximity can "talk" to each other, advertising their presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
|
||||
|
||||
## Documentation
|
||||
---
|
||||
:warning: **THE FOLLOWING DOCUMENTATION IS BEING PREPARED FOR THE v1.0 RELEASE OF PWNAGOTCHI. Since this effort is an active (and unstable) work-in-progress, the docs displayed here are in various stages of [in]completion. There will be dead links and placeholders throughout as we are still building things out in preparation for the v1.0 release.** :warning:
|
||||
|
||||
**IMPORTANT NOTE:** If you'd like to alphatest Pwnagotchi and are trying to get yours up and running while the project is still very unstable, please understand that the documentation here may not reflect what is currently implemented. If you have questions, ask the community of alphatesters in the [official Pwnagotchi Slack](https://pwnagotchi.herokuapp.com). The Pwnagotchi dev team is entirely focused on the v1.0 release and will NOT be providing support for alphatesters trying to get their Pwnagotchis working in the meantime. All technical support during this period of development is being provided by your fellow alphatesters in the Slack (thanks, everybody! :heart:).
|
||||
|
||||
https://www.pwnagotchi.ai
|
||||
|
||||
@@ -42,10 +31,11 @@ https://www.pwnagotchi.ai
|
||||
|
||||
| Official Links
|
||||
---------|-------
|
||||
Slack | [pwnagotchi.slack.com](https://pwnagotchi.herokuapp.com)
|
||||
Twitter | [@pwnagotchi](https://twitter.com/pwnagotchi)
|
||||
Subreddit | [r/pwnagotchi](https://www.reddit.com/r/pwnagotchi/)
|
||||
Website | [pwnagotchi.ai](https://pwnagotchi.ai/)
|
||||
Forum | [community.pwnagotchi.ai](https://community.pwnagotchi.ai/)
|
||||
Slack | [pwnagotchi.slack.com](https://invite.pwnagotchi.ai/)
|
||||
Subreddit | [r/pwnagotchi](https://www.reddit.com/r/pwnagotchi/)
|
||||
Twitter | [@pwnagotchi](https://twitter.com/pwnagotchi)
|
||||
|
||||
## License
|
||||
|
||||
|
235
bin/pwnagotchi
235
bin/pwnagotchi
@@ -1,56 +1,32 @@
|
||||
#!/usr/bin/python3
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import time
|
||||
import os
|
||||
import logging
|
||||
import logging
|
||||
import argparse
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
import toml
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi
|
||||
from pwnagotchi import utils
|
||||
from pwnagotchi.plugins import cmd as plugins_cmd
|
||||
from pwnagotchi import log
|
||||
from pwnagotchi import restart
|
||||
from pwnagotchi import fs
|
||||
from pwnagotchi.utils import DottedTomlEncoder
|
||||
|
||||
from pwnagotchi.identity import KeyPair
|
||||
from pwnagotchi.agent import Agent
|
||||
from pwnagotchi.ui.display import Display
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
def do_clear(display):
|
||||
logging.info("clearing the display ...")
|
||||
display.clear()
|
||||
sys.exit(0)
|
||||
|
||||
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.yml',
|
||||
help='Main configuration file.')
|
||||
parser.add_argument('-U', '--user-config', action='store', dest='user_config', default='/etc/pwnagotchi/config.yml',
|
||||
help='If this file exists, configuration will be merged and this will override default values.')
|
||||
|
||||
parser.add_argument('--manual', dest="do_manual", action="store_true", default=False, help="Manual mode.")
|
||||
parser.add_argument('--clear', dest="do_clear", action="store_true", default=False,
|
||||
help="Clear the ePaper display and exit.")
|
||||
|
||||
parser.add_argument('--debug', dest="debug", action="store_true", default=False,
|
||||
help="Enable debug logs.")
|
||||
|
||||
args = parser.parse_args()
|
||||
config = utils.load_config(args)
|
||||
utils.setup_logging(args, config)
|
||||
|
||||
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._keypair.fingerprint, pwnagotchi.version))
|
||||
|
||||
for _, plugin in plugins.loaded.items():
|
||||
logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__))
|
||||
|
||||
if args.do_clear:
|
||||
logging.info("clearing the display ...")
|
||||
display.clear()
|
||||
|
||||
elif args.do_manual:
|
||||
logging.info("entering manual mode ...")
|
||||
|
||||
agent.last_session.parse()
|
||||
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,
|
||||
@@ -60,50 +36,131 @@ if __name__ == '__main__':
|
||||
agent.last_session.min_reward,
|
||||
agent.last_session.max_reward))
|
||||
|
||||
while True:
|
||||
display.on_manual_mode(agent.last_session)
|
||||
time.sleep(1)
|
||||
while True:
|
||||
display.on_manual_mode(agent.last_session)
|
||||
time.sleep(5)
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
if Agent.is_connected():
|
||||
|
||||
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 (%s)", e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = plugins_cmd.add_parsers(parser)
|
||||
|
||||
parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.toml',
|
||||
help='Main configuration file.')
|
||||
parser.add_argument('-U', '--user-config', action='store', dest='user_config', default='/etc/pwnagotchi/config.toml',
|
||||
help='If this file exists, configuration will be merged and this will override default values.')
|
||||
|
||||
parser.add_argument('--manual', dest="do_manual", action="store_true", default=False, help="Manual mode.")
|
||||
parser.add_argument('--skip-session', dest="skip_session", action="store_true", default=False,
|
||||
help="Skip last session parsing in manual mode.")
|
||||
|
||||
parser.add_argument('--clear', dest="do_clear", action="store_true", default=False,
|
||||
help="Clear the ePaper display and exit.")
|
||||
|
||||
parser.add_argument('--debug', dest="debug", action="store_true", default=False,
|
||||
help="Enable debug logs.")
|
||||
|
||||
parser.add_argument('--version', dest="version", action="store_true", default=False,
|
||||
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 plugins_cmd.used_plugin_cmd(args):
|
||||
config = utils.load_config(args)
|
||||
log.setup_logging(args, config)
|
||||
rc = plugins_cmd.handle_cmd(args, config)
|
||||
sys.exit(rc)
|
||||
|
||||
if args.version:
|
||||
print(pwnagotchi.__version__)
|
||||
sys.exit(0)
|
||||
|
||||
config = utils.load_config(args)
|
||||
|
||||
if args.print_config:
|
||||
print(toml.dumps(config, encoder=DottedTomlEncoder()))
|
||||
sys.exit(0)
|
||||
|
||||
from pwnagotchi.identity import KeyPair
|
||||
from pwnagotchi.agent import Agent
|
||||
from pwnagotchi.ui import fonts
|
||||
from pwnagotchi.ui.display import Display
|
||||
from pwnagotchi import grid
|
||||
from pwnagotchi import plugins
|
||||
|
||||
pwnagotchi.config = config
|
||||
fs.setup_mounts(config)
|
||||
log.setup_logging(args, config)
|
||||
fonts.init(config)
|
||||
|
||||
pwnagotchi.set_name(config['main']['name'])
|
||||
|
||||
plugins.load(config)
|
||||
|
||||
display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
|
||||
|
||||
if args.do_clear:
|
||||
do_clear(display)
|
||||
sys.exit(0)
|
||||
|
||||
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
|
||||
|
||||
def usr1_handler(*unused):
|
||||
logging.info('Received USR1 singal. Restart process ...')
|
||||
restart("MANU" if args.do_manual else "AUTO")
|
||||
|
||||
signal.signal(signal.SIGUSR1, usr1_handler)
|
||||
|
||||
if args.do_manual:
|
||||
do_manual_mode(agent)
|
||||
else:
|
||||
logging.info("entering auto mode ...")
|
||||
|
||||
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 Agent.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("main loop exception")
|
||||
do_auto_mode(agent)
|
||||
|
2
builder/data/etc/network/interfaces.d/eth0-cfg
Normal file
2
builder/data/etc/network/interfaces.d/eth0-cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
allow-hotplug eth0
|
||||
iface eth0 inet dhcp
|
2
builder/data/etc/network/interfaces.d/lo-cfg
Normal file
2
builder/data/etc/network/interfaces.d/lo-cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
auto lo
|
||||
iface lo inet loopback
|
7
builder/data/etc/network/interfaces.d/usb0-cfg
Normal file
7
builder/data/etc/network/interfaces.d/usb0-cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
allow-hotplug usb0
|
||||
iface usb0 inet static
|
||||
address 10.0.0.2
|
||||
netmask 255.255.255.0
|
||||
network 10.0.0.0
|
||||
broadcast 10.0.0.255
|
||||
gateway 10.0.0.1
|
2
builder/data/etc/network/interfaces.d/wlan0-cfg
Normal file
2
builder/data/etc/network/interfaces.d/wlan0-cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
allow-hotplug wlan0
|
||||
iface wlan0 inet static
|
14
builder/data/etc/systemd/system/bettercap.service
Normal file
14
builder/data/etc/systemd/system/bettercap.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=bettercap api.rest service.
|
||||
Documentation=https://bettercap.org
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/bettercap-launcher
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
20
builder/data/etc/systemd/system/pwnagotchi.service
Normal file
20
builder/data/etc/systemd/system/pwnagotchi.service
Normal file
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=pwnagotchi Deep Reinforcement Learning instrumenting bettercap for WiFI pwning.
|
||||
Documentation=https://pwnagotchi.ai
|
||||
Wants=network.target
|
||||
After=pwngrid-peer.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/tmp
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwnagotchi-launcher
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
TasksMax=infinity
|
||||
LimitNPROC=infinity
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
15
builder/data/etc/systemd/system/pwngrid-peer.service
Normal file
15
builder/data/etc/systemd/system/pwngrid-peer.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=pwngrid peer service.
|
||||
Documentation=https://pwnagotchi.ai
|
||||
Wants=network.target
|
||||
After=bettercap.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -client-token /root/.api-enrollment.json -wait -log /var/log/pwngrid-peer.log -iface mon0
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
19
builder/data/usr/bin/bettercap-launcher
Executable file
19
builder/data/usr/bin/bettercap-launcher
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
source /usr/bin/pwnlib
|
||||
|
||||
# we need to decrypt something
|
||||
if is_crypted_mode; then
|
||||
while ! is_decrypted; do
|
||||
echo "Waiting for decryption..."
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
# start mon0
|
||||
start_monitor_interface
|
||||
|
||||
if is_auto_mode_no_delete; then
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-auto -iface mon0
|
||||
else
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-manual -iface mon0
|
||||
fi
|
71
builder/data/usr/bin/decryption-webserver
Executable file
71
builder/data/usr/bin/decryption-webserver
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
|
||||
HTML_FORM = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Decryption</title>
|
||||
<style>
|
||||
body { text-align: center; padding: 150px; }
|
||||
h1 { font-size: 50px; }
|
||||
body { font: 20px Helvetica, sans-serif; color: #333; }
|
||||
article { display: block; text-align: center; width: 650px; margin: 0 auto;}
|
||||
input {
|
||||
padding: 12px 20px;
|
||||
margin: 8px 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
input[type=password] {
|
||||
width: 75%;
|
||||
font-size: 24px;
|
||||
}
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
width: 75%;
|
||||
}
|
||||
input[type=submit]:hover {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<h1>Decryption</h1>
|
||||
<p>Some of your files are encrypted.</p>
|
||||
<p>Please provide the decryption password.</p>
|
||||
<div>
|
||||
<form action="/set-password" method="POST">
|
||||
<input type="password" id="password" name="password" value=""><br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(HTML_FORM.encode())
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
body = self.rfile.read(content_length)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
password = body.decode('UTF-8').split('=')[1]
|
||||
|
||||
with open('/tmp/.pwnagotchi-secret', 'wt') as pwfile:
|
||||
pwfile.write(password)
|
||||
|
||||
|
||||
httpd = HTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
|
||||
httpd.serve_forever()
|
2
builder/data/usr/bin/hdmioff
Executable file
2
builder/data/usr/bin/hdmioff
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
sudo /opt/vc/bin/tvservice -o
|
2
builder/data/usr/bin/hdmion
Executable file
2
builder/data/usr/bin/hdmion
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
sudo /opt/vc/bin/tvservice -p
|
3
builder/data/usr/bin/monstart
Executable file
3
builder/data/usr/bin/monstart
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
source /usr/bin/pwnlib
|
||||
start_monitor_interface
|
3
builder/data/usr/bin/monstop
Executable file
3
builder/data/usr/bin/monstop
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
source /usr/bin/pwnlib
|
||||
stop_monitor_interface
|
19
builder/data/usr/bin/pwnagotchi-launcher
Executable file
19
builder/data/usr/bin/pwnagotchi-launcher
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
source /usr/bin/pwnlib
|
||||
|
||||
# we need to decrypt something
|
||||
if is_crypted_mode; then
|
||||
while ! is_decrypted; do
|
||||
echo "Waiting for decryption..."
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
# blink 10 times to signal ready state
|
||||
blink_led 10 &
|
||||
|
||||
if is_auto_mode; then
|
||||
/usr/local/bin/pwnagotchi
|
||||
else
|
||||
/usr/local/bin/pwnagotchi --manual
|
||||
fi
|
163
builder/data/usr/bin/pwnlib
Executable file
163
builder/data/usr/bin/pwnlib
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# well ... it blinks the led
|
||||
blink_led() {
|
||||
for i in $(seq 1 "$1"); do
|
||||
echo 0 >/sys/class/leds/led0/brightness
|
||||
sleep 0.3
|
||||
echo 1 >/sys/class/leds/led0/brightness
|
||||
sleep 0.3
|
||||
done
|
||||
echo 0 >/sys/class/leds/led0/brightness
|
||||
sleep 0.3
|
||||
}
|
||||
|
||||
# starts mon0
|
||||
start_monitor_interface() {
|
||||
iw phy phy0 interface add mon0 type monitor && ifconfig mon0 up
|
||||
}
|
||||
|
||||
# stops mon0
|
||||
stop_monitor_interface() {
|
||||
ifconfig mon0 down && iw dev mon0 del
|
||||
}
|
||||
|
||||
# returns 0 if the specificed network interface is up
|
||||
is_interface_up() {
|
||||
if grep -qi 'up' /sys/class/net/$1/operstate; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# returns 0 if conditions for AUTO mode are met
|
||||
is_auto_mode() {
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-manual ]; then
|
||||
# remove the override file if found
|
||||
rm -rf /root/.pwnagotchi-manual
|
||||
return 1
|
||||
fi
|
||||
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-auto ]; then
|
||||
# remove the override file if found
|
||||
rm -rf /root/.pwnagotchi-auto
|
||||
return 0
|
||||
fi
|
||||
|
||||
# if usb0 is up, we're in MANU
|
||||
if is_interface_up usb0; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# if eth0 is up (for other boards), we're in MANU
|
||||
if is_interface_up eth0; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# no override, but none of the interfaces is up -> AUTO
|
||||
return 0
|
||||
}
|
||||
|
||||
# returns 0 if conditions for AUTO mode are met
|
||||
is_auto_mode_no_delete() {
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-manual ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# check override file first
|
||||
if [ -f /root/.pwnagotchi-auto ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# if usb0 is up, we're in MANU
|
||||
if is_interface_up usb0; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# if eth0 is up (for other boards), we're in MANU
|
||||
if is_interface_up eth0; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# no override, but none of the interfaces is up -> AUTO
|
||||
return 0
|
||||
}
|
||||
|
||||
# check if we need to decrypt something
|
||||
is_crypted_mode() {
|
||||
if [ -f /root/.pwnagotchi-crypted ]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# decryption loop
|
||||
is_decrypted() {
|
||||
while read -r mapping container mount; do
|
||||
# mapping = name the device or file will be mapped to
|
||||
# container = the luks encrypted device or file
|
||||
# mount = the mountpoint
|
||||
|
||||
# fail if not mounted
|
||||
if ! mountpoint -q "$mount" >/dev/null 2>&1; then
|
||||
if [ -f /tmp/.pwnagotchi-secret ]; then
|
||||
</tmp/.pwnagotchi-secret read -r SECRET
|
||||
if ! test -b /dev/disk/by-id/dm-uuid-*"$(cryptsetup luksUUID "$container" | tr -d -)"*; then
|
||||
if echo -n "$SECRET" | cryptsetup luksOpen -d- "$container" "$mapping" >/dev/null 2>&1; then
|
||||
echo "Container decrypted!"
|
||||
|
||||
fi
|
||||
fi
|
||||
|
||||
if mount /dev/mapper/"$mapping" "$mount" >/dev/null 2>&1; then
|
||||
echo "Mounted /dev/mapper/$mapping to $mount"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! ip -4 addr show wlan0 | grep inet >/dev/null 2>&1; then
|
||||
>/dev/null 2>&1 ip addr add 192.168.0.10/24 dev wlan0
|
||||
fi
|
||||
|
||||
if ! pgrep -f decryption-webserver >/dev/null 2>&1; then
|
||||
>/dev/null 2>&1 decryption-webserver &
|
||||
fi
|
||||
|
||||
if ! pgrep wpa_supplicant >/dev/null 2>&1; then
|
||||
>/tmp/wpa_supplicant.conf cat <<EOF
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
ap_scan=2
|
||||
|
||||
network={
|
||||
ssid="DECRYPT-ME"
|
||||
mode=2
|
||||
key_mgmt=WPA-PSK
|
||||
psk="pwnagotchi"
|
||||
frequency=2437
|
||||
}
|
||||
EOF
|
||||
>/dev/null 2>&1 wpa_supplicant -D nl80211 -i wlan0 -c /tmp/wpa_supplicant.conf &
|
||||
fi
|
||||
|
||||
if ! pgrep dnsmasq >/dev/null 2>&1; then
|
||||
>/dev/null 2>&1 dnsmasq -k -p 53 -h -O "6,192.168.0.10" -A "/#/192.168.0.10" -i wlan0 -K -F 192.168.0.50,192.168.0.60,255.255.255.0,24h &
|
||||
fi
|
||||
|
||||
return 1
|
||||
fi
|
||||
done </root/.pwnagotchi-crypted
|
||||
|
||||
# overwrite password
|
||||
>/tmp/.pwnagotchi-secret python3 -c 'print("A"*4096)'
|
||||
sync # flush
|
||||
|
||||
pkill wpa_supplicant
|
||||
pkill dnsmasq
|
||||
kill "$(pgrep -f "decryption-webserver")"
|
||||
|
||||
return 0
|
||||
}
|
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"builders": [{
|
||||
"name": "pwnagotchi",
|
||||
"type": "arm-image",
|
||||
"iso_url" : "https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-07-12/2019-07-10-raspbian-buster-lite.zip",
|
||||
"iso_checksum_type":"sha256",
|
||||
"iso_checksum":"9e5cf24ce483bb96e7736ea75ca422e3560e7b455eee63dd28f66fa1825db70e",
|
||||
"last_partition_extra_size" : 3221225472
|
||||
}],
|
||||
"builders": [
|
||||
{
|
||||
"name": "pwnagotchi",
|
||||
"type": "arm-image",
|
||||
"iso_url": "https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-07-12/2019-07-10-raspbian-buster-lite.zip",
|
||||
"iso_checksum_type": "sha256",
|
||||
"iso_checksum": "9e5cf24ce483bb96e7736ea75ca422e3560e7b455eee63dd28f66fa1825db70e",
|
||||
"last_partition_extra_size": 3221225472
|
||||
}
|
||||
],
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
@@ -18,8 +20,85 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"type":"ansible-local",
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/pwnlib",
|
||||
"destination": "/usr/bin/pwnlib"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/bettercap-launcher",
|
||||
"destination": "/usr/bin/bettercap-launcher"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/pwnagotchi-launcher",
|
||||
"destination": "/usr/bin/pwnagotchi-launcher"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/monstop",
|
||||
"destination": "/usr/bin/monstop"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/monstart",
|
||||
"destination": "/usr/bin/monstart"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/hdmion",
|
||||
"destination": "/usr/bin/hdmion"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/usr/bin/hdmioff",
|
||||
"destination": "/usr/bin/hdmioff"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/network/interfaces.d/lo-cfg",
|
||||
"destination": "/etc/network/interfaces.d/lo-cfg"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/network/interfaces.d/wlan0-cfg",
|
||||
"destination": "/etc/network/interfaces.d/wlan0-cfg"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/network/interfaces.d/usb0-cfg",
|
||||
"destination": "/etc/network/interfaces.d/usb0-cfg"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/network/interfaces.d/eth0-cfg",
|
||||
"destination": "/etc/network/interfaces.d/eth0-cfg"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/systemd/system/pwngrid-peer.service",
|
||||
"destination": "/etc/systemd/system/pwngrid-peer.service"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/systemd/system/pwnagotchi.service",
|
||||
"destination": "/etc/systemd/system/pwnagotchi.service"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "data/etc/systemd/system/bettercap.service",
|
||||
"destination": "/etc/systemd/system/bettercap.service"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": [
|
||||
"chmod +x /usr/bin/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ansible-local",
|
||||
"playbook_file": "pwnagotchi.yml",
|
||||
"extra_arguments": [ "--extra-vars \"ansible_python_interpreter=/usr/bin/python3\"" ],
|
||||
"command": "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 PWN_VERSION={{user `pwn_version`}} PWN_HOSTNAME={{user `pwn_hostname`}} ansible-playbook"
|
||||
},
|
||||
{
|
||||
|
@@ -9,10 +9,13 @@
|
||||
system:
|
||||
boot_options:
|
||||
- "dtoverlay=dwc2"
|
||||
- "dtparam=spi=on"
|
||||
- "dtoverlay=spi1-3cs"
|
||||
- "dtoverlay=i2c_arm=on"
|
||||
- "dtoverlay=i2c1=on"
|
||||
- "dtparam=spi=on"
|
||||
- "dtparam=i2c_arm=on"
|
||||
- "dtparam=i2c1=on"
|
||||
- "gpu_mem=16"
|
||||
modules:
|
||||
- "i2c-dev"
|
||||
services:
|
||||
enable:
|
||||
- dphys-swapfile.service
|
||||
@@ -20,6 +23,7 @@
|
||||
- bettercap.service
|
||||
- pwngrid-peer.service
|
||||
- epd-fuse.service
|
||||
- fstrim.timer
|
||||
disable:
|
||||
- apt-daily.timer
|
||||
- apt-daily.service
|
||||
@@ -31,10 +35,10 @@
|
||||
- ifup@wlan0.service
|
||||
packages:
|
||||
bettercap:
|
||||
url: "https://github.com/bettercap/bettercap/releases/download/v2.25/bettercap_linux_armv6l_2.25.zip"
|
||||
url: "https://github.com/bettercap/bettercap/releases/download/v2.27.1/bettercap_linux_armhf_v2.27.1.zip"
|
||||
ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip"
|
||||
pwngrid:
|
||||
url: "https://github.com/evilsocket/pwngrid/releases/download/v1.6.3/pwngrid_linux_armv6l_v1.6.3.zip"
|
||||
url: "https://github.com/evilsocket/pwngrid/releases/download/v1.10.1/pwngrid_linux_armhf_v1.10.1.zip"
|
||||
apt:
|
||||
hold:
|
||||
- firmware-atheros
|
||||
@@ -43,12 +47,13 @@
|
||||
- firmware-misc-nonfree
|
||||
- firmware-realtek
|
||||
remove:
|
||||
- rasberrypi-net-mods
|
||||
- raspberrypi-net-mods
|
||||
- dhcpcd5
|
||||
- triggerhappy
|
||||
- wpa_supplicant
|
||||
- nfs-common
|
||||
install:
|
||||
- rsync
|
||||
- vim
|
||||
- screen
|
||||
- golang
|
||||
@@ -95,26 +100,31 @@
|
||||
- libfuse-dev
|
||||
- bc
|
||||
- fonts-freefont-ttf
|
||||
- fbi
|
||||
- fonts-ipaexfont-gothic
|
||||
- cryptsetup
|
||||
- dnsmasq
|
||||
|
||||
tasks:
|
||||
|
||||
- name: selected hostname
|
||||
debug:
|
||||
msg: "{{ pwnagotchi.hostname }}"
|
||||
|
||||
- name: build version
|
||||
debug:
|
||||
msg: "{{ pwnagotchi.version }}"
|
||||
|
||||
- name: change hostname
|
||||
hostname:
|
||||
name: "{{pwnagotchi.hostname}}"
|
||||
when: lookup('file', '/etc/hostname') == "raspberrypi"
|
||||
register: hostname
|
||||
|
||||
- name: add hostname to /etc/hosts
|
||||
lineinfile:
|
||||
dest: /etc/hosts
|
||||
regexp: '^127\.0\.0\.1[ \t]+localhost'
|
||||
line: '127.0.0.1 localhost {{pwnagotchi.hostname}} {{pwnagotchi.hostname}}.local'
|
||||
regexp: '^127\.0\.1\.1[ \t]+raspberrypi'
|
||||
line: "127.0.1.1\t{{pwnagotchi.hostname}}"
|
||||
state: present
|
||||
when: hostname.changed
|
||||
|
||||
- name: disable sap plugin for bluetooth.service
|
||||
lineinfile:
|
||||
dest: /lib/systemd/system/bluetooth.service
|
||||
regexp: '^ExecStart=/usr/lib/bluetooth/bluetoothd$'
|
||||
line: 'ExecStart=/usr/lib/bluetooth/bluetoothd --noplugin=sap'
|
||||
state: present
|
||||
|
||||
- name: Add re4son-kernel repo key
|
||||
@@ -161,6 +171,7 @@
|
||||
git:
|
||||
repo: https://github.com/repaper/gratis.git
|
||||
dest: /usr/local/src/gratis
|
||||
register: gratisgit
|
||||
|
||||
- name: build papirus service
|
||||
make:
|
||||
@@ -169,6 +180,7 @@
|
||||
params:
|
||||
EPD_IO: epd_io.h
|
||||
PANEL_VERSION: 'V231_G2'
|
||||
when: gratisgit.changed
|
||||
|
||||
- name: install papirus service
|
||||
make:
|
||||
@@ -177,6 +189,7 @@
|
||||
params:
|
||||
EPD_IO: epd_io.h
|
||||
PANEL_VERSION: 'V231_G2'
|
||||
when: gratisgit.changed
|
||||
|
||||
- name: configure papirus display size
|
||||
lineinfile:
|
||||
@@ -184,6 +197,16 @@
|
||||
regexp: "#EPD_SIZE=2.0"
|
||||
line: "EPD_SIZE=2.0"
|
||||
|
||||
- name: collect python pip package list
|
||||
command: "pip3 list"
|
||||
register: pip_output
|
||||
|
||||
- name: set python pip package facts
|
||||
set_fact:
|
||||
pip_packages: >
|
||||
{{ pip_packages | default({}) | combine( { item.split()[0]: item.split()[1] } ) }}
|
||||
with_items: "{{ pip_output.stdout_lines }}"
|
||||
|
||||
- name: acquire python3 pip target
|
||||
command: "python3 -c 'import sys;print(sys.path.pop())'"
|
||||
register: pip_target
|
||||
@@ -192,26 +215,49 @@
|
||||
git:
|
||||
repo: https://github.com/evilsocket/pwnagotchi.git
|
||||
dest: /usr/local/src/pwnagotchi
|
||||
register: pwnagotchigit
|
||||
|
||||
- name: create /usr/local/share/pwnagotchi/ folder
|
||||
file:
|
||||
path: /usr/local/share/pwnagotchi/
|
||||
state: directory
|
||||
|
||||
- name: clone pwnagotchi plugins repository
|
||||
git:
|
||||
repo: https://github.com/evilsocket/pwnagotchi-plugins-contrib.git
|
||||
dest: /usr/local/share/pwnagotchi/availaible-plugins
|
||||
|
||||
- name: fetch pwnagotchi version
|
||||
set_fact:
|
||||
pwnagotchi_version: "{{ lookup('file', '/usr/local/src/pwnagotchi/pwnagotchi/_version.py') | regex_replace('.*__version__.*=.*''([0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*)''.*', '\\1') }}"
|
||||
|
||||
- name: pwnagotchi version found
|
||||
debug:
|
||||
msg: "{{ pwnagotchi_version }}"
|
||||
|
||||
- name: build pwnagotchi wheel
|
||||
command: "python3 setup.py sdist bdist_wheel"
|
||||
args:
|
||||
chdir: /usr/local/src/pwnagotchi
|
||||
when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi_version)
|
||||
|
||||
- name: install opencv-python
|
||||
pip:
|
||||
name: "https://www.piwheels.hostedpi.com/simple/opencv-python/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl"
|
||||
extra_args: "--no-deps --no-cache-dir --platform=linux_armv6l --only-binary=:all: --target={{ pip_target.stdout }}"
|
||||
when: (pip_packages['opencv-python'] is undefined) or (pip_packages['opencv-python'] != '3.4.3.18')
|
||||
|
||||
- name: install tensorflow
|
||||
pip:
|
||||
name: "https://www.piwheels.hostedpi.com/simple/tensorflow/tensorflow-1.13.1-cp37-none-linux_armv6l.whl"
|
||||
extra_args: "--no-deps --no-cache-dir --platform=linux_armv6l --only-binary=:all: --target={{ pip_target.stdout }}"
|
||||
when: (pip_packages['tensorflow'] is undefined) or (pip_packages['tensorflow'] != '1.13.1')
|
||||
|
||||
- name: install pwnagotchi wheel and dependencies
|
||||
pip:
|
||||
name: "{{ lookup('fileglob', '/usr/local/src/pwnagotchi/dist/pwnagotchi*.whl') }}"
|
||||
extra_args: "--no-cache-dir"
|
||||
when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi_version)
|
||||
|
||||
- name: download and install pwngrid
|
||||
unarchive:
|
||||
@@ -234,11 +280,13 @@
|
||||
git:
|
||||
repo: https://github.com/bettercap/caplets.git
|
||||
dest: /tmp/caplets
|
||||
register: capletsgit
|
||||
|
||||
- name: install bettercap caplets
|
||||
make:
|
||||
chdir: /tmp/caplets
|
||||
target: install
|
||||
when: capletsgit.changed
|
||||
|
||||
- name: download and install bettercap ui
|
||||
unarchive:
|
||||
@@ -247,121 +295,12 @@
|
||||
remote_src: yes
|
||||
mode: 0755
|
||||
|
||||
- name: create cpuusage script
|
||||
copy:
|
||||
dest: /usr/bin/cpuusage
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
while true
|
||||
do
|
||||
top -b -n1 | awk '/Cpu\(s\)/ { printf("%d %", $2 + $4 + 0.5) }'
|
||||
sleep 3
|
||||
done
|
||||
|
||||
- name: create memusage script
|
||||
copy:
|
||||
dest: /usr/bin/memusage
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
free -m | awk '/Mem/ { printf( "%d %", $3 / $2 * 100 + 0.5 ) }'
|
||||
|
||||
- name: create bootblink script
|
||||
copy:
|
||||
dest: /usr/bin/bootblink
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
for i in $(seq 1 "$1");
|
||||
do
|
||||
echo 0 >/sys/class/leds/led0/brightness
|
||||
sleep 0.3
|
||||
echo 1 >/sys/class/leds/led0/brightness
|
||||
sleep 0.3
|
||||
done
|
||||
echo 0 >/sys/class/leds/led0/brightness
|
||||
sleep 0.3
|
||||
|
||||
- name: create pwnagotchi-launcher script
|
||||
copy:
|
||||
dest: /usr/bin/pwnagotchi-launcher
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
# blink 10 times to signal ready state
|
||||
/usr/bin/bootblink 10 &
|
||||
# start a detached screen session with bettercap
|
||||
if ifconfig | grep usb0 | grep RUNNING; then
|
||||
# if override file exists, go into auto mode
|
||||
if [ -f /root/.pwnagotchi-auto ]; then
|
||||
rm /root/.pwnagotchi-auto
|
||||
/usr/local/bin/pwnagotchi
|
||||
else
|
||||
/usr/local/bin/pwnagotchi --manual
|
||||
fi
|
||||
else
|
||||
/usr/local/bin/pwnagotchi
|
||||
fi
|
||||
|
||||
- name: create bettercap-launcher script
|
||||
copy:
|
||||
dest: /usr/bin/bettercap-launcher
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
# blink 10 times to signal ready state
|
||||
/usr/bin/bootblink 10 &
|
||||
if ifconfig | grep usb0 | grep RUNNING; then
|
||||
# if override file exists, go into auto mode
|
||||
if [ -f /root/.pwnagotchi-auto ]; then
|
||||
rm /root/.pwnagotchi-auto
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-auto
|
||||
else
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-manual
|
||||
fi
|
||||
else
|
||||
/usr/bin/bettercap -no-colors -caplet pwnagotchi-auto
|
||||
fi
|
||||
|
||||
- name: create monstart script
|
||||
copy:
|
||||
dest: /usr/bin/monstart
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
iw phy phy0 interface add mon0 type monitor && ifconfig mon0 up
|
||||
|
||||
- name: create monstop script
|
||||
copy:
|
||||
dest: /usr/bin/monstop
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
ifconfig mon0 down && iw dev mon0 del
|
||||
|
||||
- name: create hdmion script
|
||||
copy:
|
||||
dest: /usr/bin/hdmion
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
sudo /opt/vc/bin/tvservice -p
|
||||
|
||||
- name: create hdmioff script
|
||||
copy:
|
||||
dest: /usr/bin/hdmioff
|
||||
mode: 0755
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
sudo /opt/vc/bin/tvservice -o
|
||||
|
||||
- name: add HDMI powersave to rc.local
|
||||
blockinfile:
|
||||
path: /etc/rc.local
|
||||
insertbefore: "exit 0"
|
||||
block: |
|
||||
if ! /opt/vc/bin/tvservice -s | grep HDMI; then
|
||||
if ! /opt/vc/bin/tvservice -s | egrep 'HDMI|DVI'; then
|
||||
/opt/vc/bin/tvservice -o
|
||||
fi
|
||||
|
||||
@@ -379,7 +318,7 @@
|
||||
copy:
|
||||
dest: /etc/pwnagotchi/config.yml
|
||||
content: |
|
||||
# Add your configuration overrides on this file any configuration changes done to defaults.yml will be lost!
|
||||
# Add your configuration overrides on this file any configuration changes done to default.yml will be lost!
|
||||
# Example:
|
||||
#
|
||||
# ui:
|
||||
@@ -389,39 +328,6 @@
|
||||
#
|
||||
when: not user_config.stat.exists
|
||||
|
||||
- name: configure lo interface
|
||||
copy:
|
||||
dest: /etc/network/interfaces.d/lo-cfg
|
||||
content: |
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
- name: configure wlan interface
|
||||
copy:
|
||||
dest: /etc/network/interfaces.d/wlan0-cfg
|
||||
content: |
|
||||
allow-hotplug wlan0
|
||||
iface wlan0 inet static
|
||||
|
||||
- name: configure usb interface
|
||||
copy:
|
||||
dest: /etc/network/interfaces.d/usb0-cfg
|
||||
content: |
|
||||
allow-hotplug usb0
|
||||
iface usb0 inet static
|
||||
address 10.0.0.2
|
||||
netmask 255.255.255.0
|
||||
network 10.0.0.0
|
||||
broadcast 10.0.0.255
|
||||
gateway 10.0.0.1
|
||||
|
||||
- name: configure eth0 interface (pi2/3/4)
|
||||
copy:
|
||||
dest: /etc/network/interfaces.d/eth0-cfg
|
||||
content: |
|
||||
allow-hotplug eth0
|
||||
iface eth0 inet dhcp
|
||||
|
||||
- name: enable ssh on boot
|
||||
file:
|
||||
path: /boot/ssh
|
||||
@@ -434,6 +340,13 @@
|
||||
line: '{{ item }}'
|
||||
with_items: "{{system.boot_options}}"
|
||||
|
||||
- name: adjust /etc/modules
|
||||
lineinfile:
|
||||
dest: /etc/modules
|
||||
insertafter: EOF
|
||||
line: '{{ item }}'
|
||||
with_items: "{{system.modules}}"
|
||||
|
||||
- name: change root partition
|
||||
replace:
|
||||
dest: /boot/cmdline.txt
|
||||
@@ -454,14 +367,14 @@
|
||||
copy:
|
||||
dest: /etc/motd
|
||||
content: |
|
||||
(◕‿‿◕) {{pwnagotchi.hostname}} (pwnagotchi-{{pwnagotchi.version}})
|
||||
(◕‿‿◕) {{pwnagotchi.hostname}}
|
||||
|
||||
Hi! I'm a pwnagotchi, please take good care of me!
|
||||
Here are some basic things you need to know to raise me properly!
|
||||
|
||||
If you want to change my configuration, use /etc/pwnagotchi/config.yml
|
||||
|
||||
All the configuration options can be found on /etc/pwnagotchi/defaults.yml,
|
||||
All the configuration options can be found on /etc/pwnagotchi/default.yml,
|
||||
but don't change this file because I will recreate it every time I'm restarted!
|
||||
|
||||
I'm managed by systemd. Here are some basic commands.
|
||||
@@ -480,6 +393,7 @@
|
||||
touch /root/.pwnagotchi-auto && systemctl restart pwnagotchi
|
||||
|
||||
You learn more about me at https://pwnagotchi.ai/
|
||||
when: hostname.changed
|
||||
|
||||
- name: clean apt cache
|
||||
apt:
|
||||
@@ -489,74 +403,6 @@
|
||||
apt:
|
||||
autoremove: yes
|
||||
|
||||
- name: add pwngrid-peer service to systemd
|
||||
copy:
|
||||
dest: /etc/systemd/system/pwngrid-peer.service
|
||||
content: |
|
||||
[Unit]
|
||||
Description=pwngrid peer service.
|
||||
Documentation=https://pwnagotchi.ai
|
||||
Wants=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -wait -log /var/log/pwngrid-peer.log
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
notify:
|
||||
- reload systemd services
|
||||
|
||||
- name: add bettercap service to systemd
|
||||
copy:
|
||||
dest: /etc/systemd/system/bettercap.service
|
||||
content: |
|
||||
[Unit]
|
||||
Description=bettercap api.rest service.
|
||||
Documentation=https://bettercap.org
|
||||
Wants=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PermissionsStartOnly=true
|
||||
ExecStartPre=/usr/bin/monstart
|
||||
ExecStart=/usr/bin/bettercap-launcher
|
||||
ExecStopPost=/usr/bin/monstop
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
notify:
|
||||
- reload systemd services
|
||||
|
||||
- name: add pwnagotchi service to systemd
|
||||
copy:
|
||||
dest: /etc/systemd/system/pwnagotchi.service
|
||||
content: |
|
||||
[Unit]
|
||||
Description=pwnagotchi Deep Reinforcement Learning instrumenting bettercap for WiFI pwning.
|
||||
Documentation=https://pwnagotchi.ai
|
||||
Wants=network.target
|
||||
After=bettercap.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwnagotchi-launcher
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
notify:
|
||||
- reload systemd services
|
||||
|
||||
- name: enable services
|
||||
systemd:
|
||||
name: "{{ item }}"
|
||||
|
@@ -1,12 +1,47 @@
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import pwnagotchi.ui.view as view
|
||||
import re
|
||||
|
||||
version = '1.0.0RC3'
|
||||
|
||||
|
||||
from pwnagotchi._version import __version__
|
||||
|
||||
_name = None
|
||||
config = None
|
||||
|
||||
|
||||
def set_name(new_name):
|
||||
if new_name is None:
|
||||
return
|
||||
|
||||
new_name = new_name.strip()
|
||||
if new_name == '':
|
||||
return
|
||||
|
||||
if not re.match(r'^[a-zA-Z0-9\-]{2,25}$', new_name):
|
||||
logging.warning("name '%s' is invalid: min length is 2, max length 25, only a-zA-Z0-9- allowed", new_name)
|
||||
return
|
||||
|
||||
current = name()
|
||||
if new_name != current:
|
||||
global _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)
|
||||
|
||||
with open('/etc/hosts', 'wt') as fp:
|
||||
patched = prev.replace(current, new_name, -1)
|
||||
logging.debug("new hosts:\n%s\n", patched)
|
||||
fp.write(patched)
|
||||
|
||||
os.system("hostname '%s'" % new_name)
|
||||
pwnagotchi.reboot()
|
||||
|
||||
|
||||
def name():
|
||||
@@ -17,32 +52,50 @@ def name():
|
||||
return _name
|
||||
|
||||
|
||||
def uptime():
|
||||
with open('/proc/uptime') as fp:
|
||||
return int(fp.read().split('.')[0])
|
||||
|
||||
|
||||
def mem_usage():
|
||||
out = subprocess.getoutput("free -m")
|
||||
for line in out.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("Mem:"):
|
||||
parts = list(map(int, line.split()[1:]))
|
||||
tot = parts[0]
|
||||
used = parts[1]
|
||||
free = parts[2]
|
||||
return used / tot
|
||||
with open('/proc/meminfo') as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith("MemTotal:"):
|
||||
kb_mem_total = int(line.split()[1])
|
||||
if line.startswith("MemFree:"):
|
||||
kb_mem_free = int(line.split()[1])
|
||||
if line.startswith("Buffers:"):
|
||||
kb_main_buffers = int(line.split()[1])
|
||||
if line.startswith("Cached:"):
|
||||
kb_main_cached = int(line.split()[1])
|
||||
kb_mem_used = kb_mem_total - kb_mem_free - kb_main_cached - kb_main_buffers
|
||||
return round(kb_mem_used / kb_mem_total, 1)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _cpu_stat():
|
||||
"""
|
||||
Returns the splitted first line of the /proc/stat file
|
||||
"""
|
||||
with open('/proc/stat', 'rt') as fp:
|
||||
return list(map(int,fp.readline().split()[1:]))
|
||||
|
||||
|
||||
def cpu_load():
|
||||
with open('/proc/stat', 'rt') as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith('cpu '):
|
||||
parts = list(map(int, line.split()[1:]))
|
||||
user_n = parts[0]
|
||||
sys_n = parts[2]
|
||||
idle_n = parts[3]
|
||||
tot = user_n + sys_n + idle_n
|
||||
return (user_n + sys_n) / tot
|
||||
return 0
|
||||
"""
|
||||
Returns the current cpuload
|
||||
"""
|
||||
parts0 = _cpu_stat()
|
||||
time.sleep(0.1)
|
||||
parts1 = _cpu_stat()
|
||||
parts_diff = [p1 - p0 for (p0, p1) in zip(parts0, parts1)]
|
||||
user, nice, sys, idle, iowait, irq, softirq, steal, _guest, _guest_nice = parts_diff
|
||||
idle_sum = idle + iowait
|
||||
non_idle_sum = user + nice + sys + irq + softirq + steal
|
||||
total = idle_sum + non_idle_sum
|
||||
return non_idle_sum / total
|
||||
|
||||
|
||||
def temperature(celsius=True):
|
||||
@@ -53,10 +106,52 @@ def temperature(celsius=True):
|
||||
|
||||
|
||||
def shutdown():
|
||||
logging.warning("syncing...")
|
||||
|
||||
from pwnagotchi import fs
|
||||
for m in fs.mounts:
|
||||
m.sync()
|
||||
|
||||
logging.warning("shutting down ...")
|
||||
|
||||
from pwnagotchi.ui import view
|
||||
if view.ROOT:
|
||||
view.ROOT.on_shutdown()
|
||||
# give it some time to refresh the ui
|
||||
time.sleep(5)
|
||||
time.sleep(10)
|
||||
os.system("sync")
|
||||
os.system("halt")
|
||||
|
||||
|
||||
def restart(mode):
|
||||
logging.warning("restarting in %s mode ...", mode)
|
||||
|
||||
if mode == 'AUTO':
|
||||
os.system("touch /root/.pwnagotchi-auto")
|
||||
else:
|
||||
os.system("touch /root/.pwnagotchi-manual")
|
||||
|
||||
os.system("service bettercap restart")
|
||||
os.system("service pwnagotchi restart")
|
||||
|
||||
|
||||
def reboot(mode=None):
|
||||
if mode is not None:
|
||||
mode = mode.upper()
|
||||
logging.warning("rebooting in %s mode ...", mode)
|
||||
else:
|
||||
logging.warning("rebooting ...")
|
||||
|
||||
from pwnagotchi.ui import view
|
||||
if view.ROOT:
|
||||
view.ROOT.on_rebooting()
|
||||
# give it some time to refresh the ui
|
||||
time.sleep(10)
|
||||
|
||||
if mode == 'AUTO':
|
||||
os.system("touch /root/.pwnagotchi-auto")
|
||||
elif mode == 'MANU':
|
||||
os.system("touch /root/.pwnagotchi-manual")
|
||||
|
||||
os.system("sync")
|
||||
os.system("shutdown -r now")
|
||||
|
1
pwnagotchi/_version.py
Normal file
1
pwnagotchi/_version.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = '1.5.1'
|
@@ -2,13 +2,15 @@ import time
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import asyncio
|
||||
import _thread
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.ui.web.server import Server
|
||||
from pwnagotchi.automata import Automata
|
||||
from pwnagotchi.log import LastSession
|
||||
from pwnagotchi.bettercap import Client
|
||||
from pwnagotchi.mesh.utils import AsyncAdvertiser
|
||||
@@ -17,38 +19,40 @@ from pwnagotchi.ai.train import AsyncTrainer
|
||||
RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
|
||||
|
||||
|
||||
class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
|
||||
def __init__(self, view, config, keypair):
|
||||
Client.__init__(self, config['bettercap']['hostname'],
|
||||
config['bettercap']['scheme'],
|
||||
config['bettercap']['port'],
|
||||
config['bettercap']['username'],
|
||||
config['bettercap']['password'])
|
||||
Automata.__init__(self, config, view)
|
||||
AsyncAdvertiser.__init__(self, config, view, keypair)
|
||||
AsyncTrainer.__init__(self, config)
|
||||
|
||||
self._started_at = time.time()
|
||||
self._filter = None if config['main']['filter'] is None else re.compile(config['main']['filter'])
|
||||
self._filter = None if not config['main']['filter'] else re.compile(config['main']['filter'])
|
||||
self._current_channel = 0
|
||||
self._tot_aps = 0
|
||||
self._aps_on_channel = 0
|
||||
self._supported_channels = utils.iface_channels(config['main']['iface'])
|
||||
self._view = view
|
||||
self._view.set_agent(self)
|
||||
self._web_ui = Server(self, config['ui'])
|
||||
|
||||
self._access_points = []
|
||||
self._last_pwnd = None
|
||||
self._history = {}
|
||||
self._handshakes = {}
|
||||
self.last_session = LastSession(self._config)
|
||||
self.mode = 'auto'
|
||||
|
||||
if not os.path.exists(config['bettercap']['handshakes']):
|
||||
os.makedirs(config['bettercap']['handshakes'])
|
||||
|
||||
@staticmethod
|
||||
def is_connected():
|
||||
try:
|
||||
socket.create_connection(("www.google.com", 80))
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
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
|
||||
@@ -59,43 +63,13 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
def supported_channels(self):
|
||||
return self._supported_channels
|
||||
|
||||
def set_starting(self):
|
||||
self._view.on_starting()
|
||||
|
||||
def set_ready(self):
|
||||
plugins.on('ready', self)
|
||||
|
||||
def set_free_channel(self, channel):
|
||||
self._view.on_free_channel(channel)
|
||||
plugins.on('free_channel', self, channel)
|
||||
|
||||
def set_bored(self):
|
||||
self._view.on_bored()
|
||||
plugins.on('bored', self)
|
||||
|
||||
def set_sad(self):
|
||||
self._view.on_sad()
|
||||
plugins.on('sad', self)
|
||||
|
||||
def set_excited(self):
|
||||
self._view.on_excited()
|
||||
plugins.on('excited', self)
|
||||
|
||||
def set_lonely(self):
|
||||
self._view.on_lonely()
|
||||
plugins.on('lonely', self)
|
||||
|
||||
def set_rebooting(self):
|
||||
self._view.on_rebooting()
|
||||
plugins.on('rebooting', self)
|
||||
|
||||
def setup_events(self):
|
||||
logging.info("connecting to %s ..." % self.url)
|
||||
logging.info("connecting to %s ...", self.url)
|
||||
|
||||
for tag in self._config['bettercap']['silence']:
|
||||
try:
|
||||
self.run('events.ignore %s' % tag, verbose_errors=False)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _reset_wifi_settings(self):
|
||||
@@ -117,7 +91,7 @@ class Agent(Client, 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
|
||||
|
||||
@@ -126,11 +100,11 @@ class Agent(Client, 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,21 +119,27 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
|
||||
self.start_advertising()
|
||||
|
||||
def _wait_bettercap(self):
|
||||
while True:
|
||||
try:
|
||||
_s = self.session()
|
||||
return
|
||||
except Exception:
|
||||
logging.info("waiting for bettercap API to be available ...")
|
||||
time.sleep(1)
|
||||
|
||||
def start(self):
|
||||
self.start_ai()
|
||||
self._wait_bettercap()
|
||||
self.setup_events()
|
||||
self.set_starting()
|
||||
self.start_monitor_mode()
|
||||
self.start_event_polling()
|
||||
self.start_session_fetcher()
|
||||
# print initial stats
|
||||
self.next_epoch()
|
||||
self.set_ready()
|
||||
|
||||
def wait_for(self, t, sleeping=True):
|
||||
plugins.on('sleep' if sleeping else 'wait', self, t)
|
||||
self._view.wait(t, sleeping)
|
||||
self._epoch.track(sleep=True, inc=t)
|
||||
|
||||
def recon(self):
|
||||
recon_time = self._config['personality']['recon_time']
|
||||
max_inactive = self._config['personality']['max_inactive_scale']
|
||||
@@ -173,14 +153,14 @@ class Agent(Client, 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:
|
||||
logging.exception("error")
|
||||
logging.exception("Error while setting wifi.recon.channels (%s)", e)
|
||||
|
||||
self.wait_for(recon_time, sleeping=False)
|
||||
|
||||
@@ -192,7 +172,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
def set_access_points(self, aps):
|
||||
self._access_points = aps
|
||||
plugins.on('wifi_update', self, aps)
|
||||
self._epoch.observe(aps, self._advertiser.peers() if self._advertiser is not None else ())
|
||||
self._epoch.observe(aps, list(self._peers.values()))
|
||||
return self._access_points
|
||||
|
||||
def get_access_points(self):
|
||||
@@ -200,16 +180,30 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
aps = []
|
||||
try:
|
||||
s = self.session()
|
||||
plugins.on("unfiltered_ap_list", self, s['wifi']['aps'])
|
||||
for ap in s['wifi']['aps']:
|
||||
if ap['hostname'] not in whitelist:
|
||||
if ap['encryption'] == '' or ap['encryption'] == 'OPEN':
|
||||
continue
|
||||
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:
|
||||
logging.exception("error")
|
||||
logging.exception("Error while getting acces points (%s)", e)
|
||||
|
||||
aps.sort(key=lambda ap: ap['channel'])
|
||||
return self.set_access_points(aps)
|
||||
|
||||
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']
|
||||
@@ -220,7 +214,7 @@ class Agent(Client, 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:
|
||||
@@ -241,21 +235,21 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
return None
|
||||
|
||||
def _update_uptime(self, s):
|
||||
secs = time.time() - self._started_at
|
||||
secs = pwnagotchi.uptime()
|
||||
self._view.set('uptime', utils.secs_to_hhmmss(secs))
|
||||
self._view.set('epoch', '%04d' % self._epoch.epoch)
|
||||
# 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):
|
||||
@@ -273,25 +267,16 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
if new_shakes > 0:
|
||||
self._view.on_handshakes(new_shakes)
|
||||
|
||||
def _update_advertisement(self, s):
|
||||
run_handshakes = len(self._handshakes)
|
||||
tot_handshakes = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
|
||||
started = s['started_at'].split('.')[0]
|
||||
started = datetime.strptime(started, '%Y-%m-%dT%H:%M:%S')
|
||||
started = time.mktime(started.timetuple())
|
||||
self._advertiser.update({ \
|
||||
'pwnd_run': run_handshakes,
|
||||
'pwnd_tot': tot_handshakes,
|
||||
'uptime': time.time() - started,
|
||||
'epoch': self._epoch.epoch})
|
||||
|
||||
def _update_peers(self):
|
||||
peer = self._advertiser.closest_peer()
|
||||
tot = self._advertiser.num_peers()
|
||||
self._view.set_closest_peer(peer, tot)
|
||||
self._view.set_closest_peer(self._closest_peer, len(self._peers))
|
||||
|
||||
def _reboot(self):
|
||||
self.set_rebooting()
|
||||
self._save_recovery_data()
|
||||
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,
|
||||
@@ -306,7 +291,7 @@ class Agent(Client, 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']
|
||||
@@ -314,64 +299,73 @@ class Agent(Client, 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:
|
||||
raise
|
||||
|
||||
def _event_poller(self):
|
||||
self._load_recovery_data()
|
||||
|
||||
self.run('events.clear')
|
||||
def start_session_fetcher(self):
|
||||
_thread.start_new_thread(self._fetch_stats, ())
|
||||
|
||||
logging.debug("event polling started ...")
|
||||
|
||||
def _fetch_stats(self):
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
new_shakes = 0
|
||||
s = self.session()
|
||||
self._update_uptime(s)
|
||||
|
||||
if self._advertiser is not None:
|
||||
self._update_advertisement(s)
|
||||
self._update_peers()
|
||||
|
||||
self._update_advertisement(s)
|
||||
self._update_peers()
|
||||
self._update_counters()
|
||||
self._update_handshakes(0)
|
||||
time.sleep(1)
|
||||
|
||||
async def _on_event(self, msg):
|
||||
found_handshake = False
|
||||
jmsg = json.loads(msg)
|
||||
|
||||
if jmsg['tag'] == 'wifi.client.handshake':
|
||||
filename = jmsg['data']['file']
|
||||
sta_mac = jmsg['data']['station']
|
||||
ap_mac = jmsg['data']['ap']
|
||||
key = "%s -> %s" % (sta_mac, ap_mac)
|
||||
if key not in self._handshakes:
|
||||
self._handshakes[key] = jmsg
|
||||
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
|
||||
if ap_and_station is None:
|
||||
logging.warning("!!! captured new handshake: %s !!!", key)
|
||||
self._last_pwnd = ap_mac
|
||||
plugins.on('handshake', self, filename, ap_mac, sta_mac)
|
||||
else:
|
||||
(ap, sta) = ap_and_station
|
||||
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
||||
'hostname'] != '<hidden>' else ap_mac
|
||||
logging.warning(
|
||||
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
|
||||
ap['channel'],
|
||||
ap['rssi'],
|
||||
sta['mac'], sta['vendor'],
|
||||
ap['hostname'], ap['mac'], ap['vendor'])
|
||||
plugins.on('handshake', self, filename, ap, sta)
|
||||
found_handshake = True
|
||||
self._update_handshakes(1 if found_handshake else 0)
|
||||
|
||||
def _event_poller(self, loop):
|
||||
self._load_recovery_data()
|
||||
self.run('events.clear')
|
||||
|
||||
while True:
|
||||
logging.debug("polling events ...")
|
||||
try:
|
||||
for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
|
||||
filename = h['data']['file']
|
||||
sta_mac = h['data']['station']
|
||||
ap_mac = h['data']['ap']
|
||||
key = "%s -> %s" % (sta_mac, ap_mac)
|
||||
|
||||
if key not in self._handshakes:
|
||||
self._handshakes[key] = h
|
||||
new_shakes += 1
|
||||
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
|
||||
if ap_and_station is None:
|
||||
logging.warning("!!! captured new handshake: %s !!!" % key)
|
||||
self._last_pwnd = ap_mac
|
||||
plugins.on('handshake', self, filename, ap_mac, sta_mac)
|
||||
else:
|
||||
(ap, sta) = ap_and_station
|
||||
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
||||
'hostname'] != '<hidden>' else ap_mac
|
||||
logging.warning("!!! captured new handshake on channel %d: %s (%s) -> %s [%s (%s)] !!!" % ( \
|
||||
ap['channel'],
|
||||
sta['mac'], sta['vendor'],
|
||||
ap['hostname'], ap['mac'], ap['vendor']))
|
||||
plugins.on('handshake', self, filename, ap, sta)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
|
||||
finally:
|
||||
self._update_handshakes(new_shakes)
|
||||
loop.create_task(self.start_websocket(self._on_event))
|
||||
loop.run_forever()
|
||||
except Exception as ex:
|
||||
logging.debug("Error while polling via websocket (%s)", ex)
|
||||
|
||||
def start_event_polling(self):
|
||||
_thread.start_new_thread(self._event_poller, ())
|
||||
# start a thread and pass in the mainloop
|
||||
_thread.start_new_thread(self._event_poller, (asyncio.new_event_loop(),))
|
||||
|
||||
|
||||
def is_module_running(self, module):
|
||||
s = self.session()
|
||||
@@ -405,32 +399,17 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
|
||||
return self._history[who] < self._config['personality']['max_interactions']
|
||||
|
||||
def _on_miss(self, 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:
|
||||
self._on_miss(who)
|
||||
else:
|
||||
logging.error("%s" % e)
|
||||
|
||||
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:
|
||||
@@ -443,15 +422,15 @@ class Agent(Client, 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:
|
||||
@@ -464,7 +443,7 @@ class Agent(Client, 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
|
||||
@@ -480,12 +459,12 @@ class Agent(Client, 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
|
||||
@@ -495,47 +474,4 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
plugins.on('channel_hop', self, channel)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("error: %s" % e)
|
||||
|
||||
def is_stale(self):
|
||||
return self._epoch.num_missed > self._config['personality']['max_misses_for_recon']
|
||||
|
||||
def any_activity(self):
|
||||
return self._epoch.any_activity
|
||||
|
||||
def _reboot(self):
|
||||
self.set_rebooting()
|
||||
self._save_recovery_data()
|
||||
logging.warning("rebooting the system ...")
|
||||
os.system("/usr/bin/sync")
|
||||
os.system("/usr/sbin/shutdown -r now")
|
||||
|
||||
def next_epoch(self):
|
||||
was_stale = self.is_stale()
|
||||
did_miss = self._epoch.num_missed
|
||||
|
||||
self._epoch.next()
|
||||
|
||||
# after X misses during an epoch, set the status to lonely
|
||||
if was_stale:
|
||||
logging.warning("agent missed %d interactions -> lonely" % did_miss)
|
||||
self.set_lonely()
|
||||
# after X times being bored, the status is set to sad
|
||||
elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
|
||||
logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for)
|
||||
self.set_sad()
|
||||
# after X times being inactive, the status is set to bored
|
||||
elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']:
|
||||
logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for)
|
||||
self.set_bored()
|
||||
# after X times being active, the status is set to happy / excited
|
||||
elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']:
|
||||
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
|
||||
self.set_excited()
|
||||
|
||||
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)
|
||||
self._reboot()
|
||||
self._epoch.blind_for = 0
|
||||
logging.error("Error while setting channel (%s)", e)
|
||||
|
@@ -1,13 +1,9 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
# https://stackoverflow.com/questions/40426502/is-there-a-way-to-suppress-the-messages-tensorflow-prints/40426709
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # or any {'0', '1', '2'}
|
||||
import warnings
|
||||
|
||||
# https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
|
||||
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def load(config, agent, epoch, from_disk=True):
|
||||
@@ -16,27 +12,51 @@ def load(config, agent, epoch, from_disk=True):
|
||||
logging.info("ai disabled")
|
||||
return False
|
||||
|
||||
logging.info("[ai] bootstrapping dependencies ...")
|
||||
try:
|
||||
begin = time.time()
|
||||
|
||||
from stable_baselines import A2C
|
||||
from stable_baselines.common.policies import MlpLstmPolicy
|
||||
from stable_baselines.common.vec_env import DummyVecEnv
|
||||
logging.info("[ai] bootstrapping dependencies ...")
|
||||
|
||||
import pwnagotchi.ai.gym as wrappers
|
||||
start = time.time()
|
||||
from stable_baselines import A2C
|
||||
logging.debug("[ai] A2C imported in %.2fs" % (time.time() - start))
|
||||
|
||||
env = wrappers.Environment(agent, epoch)
|
||||
env = DummyVecEnv([lambda: env])
|
||||
start = time.time()
|
||||
from stable_baselines.common.policies import MlpLstmPolicy
|
||||
logging.debug("[ai] MlpLstmPolicy imported in %.2fs" % (time.time() - start))
|
||||
|
||||
logging.info("[ai] bootstrapping model ...")
|
||||
start = time.time()
|
||||
from stable_baselines.common.vec_env import DummyVecEnv
|
||||
logging.debug("[ai] DummyVecEnv imported in %.2fs" % (time.time() - start))
|
||||
|
||||
a2c = A2C(MlpLstmPolicy, env, **config['params'])
|
||||
start = time.time()
|
||||
import pwnagotchi.ai.gym as wrappers
|
||||
logging.debug("[ai] gym wrapper imported in %.2fs" % (time.time() - start))
|
||||
|
||||
if from_disk and os.path.exists(config['path']):
|
||||
logging.info("[ai] loading %s ..." % config['path'])
|
||||
a2c.load(config['path'], env)
|
||||
else:
|
||||
logging.info("[ai] model created:")
|
||||
for key, value in config['params'].items():
|
||||
logging.info(" %s: %s" % (key, value))
|
||||
env = wrappers.Environment(agent, epoch)
|
||||
env = DummyVecEnv([lambda: env])
|
||||
|
||||
return a2c
|
||||
logging.info("[ai] creating model ...")
|
||||
|
||||
start = time.time()
|
||||
a2c = A2C(MlpLstmPolicy, env, **config['params'])
|
||||
logging.debug("[ai] A2C created in %.2fs" % (time.time() - start))
|
||||
|
||||
if from_disk and os.path.exists(config['path']):
|
||||
logging.info("[ai] loading %s ..." % config['path'])
|
||||
start = time.time()
|
||||
a2c.load(config['path'], env)
|
||||
logging.debug("[ai] A2C loaded in %.2fs" % (time.time() - start))
|
||||
else:
|
||||
logging.info("[ai] model created:")
|
||||
for key, value in config['params'].items():
|
||||
logging.info(" %s: %s" % (key, value))
|
||||
|
||||
logging.debug("[ai] total loading time is %.2fs" % (time.time() - begin))
|
||||
|
||||
return a2c
|
||||
except Exception as e:
|
||||
logging.exception("error while starting AI (%s)", e)
|
||||
|
||||
logging.warning("[ai] AI not loaded!")
|
||||
return False
|
||||
|
@@ -19,6 +19,10 @@ class Epoch(object):
|
||||
self.active_for = 0
|
||||
# number of epochs with no visible access points
|
||||
self.blind_for = 0
|
||||
# number of epochs in sad state
|
||||
self.sad_for = 0
|
||||
# number of epochs in bored state
|
||||
self.bored_for = 0
|
||||
# did deauth in this epoch in the current channel?
|
||||
self.did_deauth = False
|
||||
# number of deauths in this epoch
|
||||
@@ -37,6 +41,12 @@ class Epoch(object):
|
||||
self.num_hops = 0
|
||||
# number of seconds sleeping
|
||||
self.num_slept = 0
|
||||
# number of peers seen during this epoch
|
||||
self.num_peers = 0
|
||||
# cumulative bond factor
|
||||
self.tot_bond_factor = 0.0 # cum_bond_factor sounded really bad ...
|
||||
# average bond factor
|
||||
self.avg_bond_factor = 0.0
|
||||
# any activity at all during this epoch?
|
||||
self.any_activity = False
|
||||
# when the current epoch started
|
||||
@@ -74,10 +84,16 @@ class Epoch(object):
|
||||
else:
|
||||
self.blind_for = 0
|
||||
|
||||
bond_unit_scale = self.config['personality']['bond_encounters_factor']
|
||||
|
||||
self.num_peers = len(peers)
|
||||
num_peers = self.num_peers + 1e-10 # avoid division by 0
|
||||
|
||||
self.tot_bond_factor = sum((peer.encounters for peer in peers)) / bond_unit_scale
|
||||
self.avg_bond_factor = self.tot_bond_factor / num_peers
|
||||
|
||||
num_aps = len(aps) + 1e-10
|
||||
num_sta = sum(len(ap['clients']) for ap in aps) + 1e-10
|
||||
num_peers = len(peers) + 1e-10
|
||||
|
||||
aps_per_chan = [0.0] * wifi.NumChannels
|
||||
sta_per_chan = [0.0] * wifi.NumChannels
|
||||
peers_per_chan = [0.0] * wifi.NumChannels
|
||||
@@ -87,13 +103,13 @@ class Epoch(object):
|
||||
try:
|
||||
aps_per_chan[ch_idx] += 1.0
|
||||
sta_per_chan[ch_idx] += len(ap['clients'])
|
||||
except IndexError as e:
|
||||
except IndexError:
|
||||
logging.error("got data on channel %d, we can store %d channels" % (ap['channel'], wifi.NumChannels))
|
||||
|
||||
for peer in peers:
|
||||
try:
|
||||
peers_per_chan[peer.last_channel - 1] += 1.0
|
||||
except IndexError as e:
|
||||
except IndexError:
|
||||
logging.error(
|
||||
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
|
||||
|
||||
@@ -145,6 +161,20 @@ class Epoch(object):
|
||||
else:
|
||||
self.active_for += 1
|
||||
self.inactive_for = 0
|
||||
self.sad_for = 0
|
||||
self.bored_for = 0
|
||||
|
||||
if self.inactive_for >= self.config['personality']['sad_num_epochs']:
|
||||
# sad > bored; cant be sad and bored
|
||||
self.bored_for = 0
|
||||
self.sad_for += 1
|
||||
elif self.inactive_for >= self.config['personality']['bored_num_epochs']:
|
||||
# sad_treshhold > inactive > bored_treshhold; cant be sad and bored
|
||||
self.sad_for = 0
|
||||
self.bored_for += 1
|
||||
else:
|
||||
self.sad_for = 0
|
||||
self.bored_for = 0
|
||||
|
||||
now = time.time()
|
||||
cpu = pwnagotchi.cpu_load()
|
||||
@@ -160,8 +190,13 @@ class Epoch(object):
|
||||
'blind_for_epochs': self.blind_for,
|
||||
'inactive_for_epochs': self.inactive_for,
|
||||
'active_for_epochs': self.active_for,
|
||||
'sad_for_epochs': self.sad_for,
|
||||
'bored_for_epochs': self.bored_for,
|
||||
'missed_interactions': self.num_missed,
|
||||
'num_hops': self.num_hops,
|
||||
'num_peers': self.num_peers,
|
||||
'tot_bond': self.tot_bond_factor,
|
||||
'avg_bond': self.avg_bond_factor,
|
||||
'num_deauths': self.num_deauths,
|
||||
'num_associations': self.num_assocs,
|
||||
'num_handshakes': self.num_shakes,
|
||||
@@ -173,14 +208,20 @@ class Epoch(object):
|
||||
self._epoch_data['reward'] = self._reward(self.epoch + 1, self._epoch_data)
|
||||
self._epoch_data_ready.set()
|
||||
|
||||
logging.info("[epoch %d] duration=%s slept_for=%s blind=%d inactive=%d active=%d hops=%d missed=%d "
|
||||
"deauths=%d assocs=%d handshakes=%d cpu=%d%% mem=%d%% temperature=%dC reward=%s" % (
|
||||
logging.info("[epoch %d] duration=%s slept_for=%s blind=%d sad=%d bored=%d inactive=%d active=%d peers=%d tot_bond=%.2f "
|
||||
"avg_bond=%.2f hops=%d missed=%d deauths=%d assocs=%d handshakes=%d cpu=%d%% mem=%d%% "
|
||||
"temperature=%dC reward=%s" % (
|
||||
self.epoch,
|
||||
utils.secs_to_hhmmss(self.epoch_duration),
|
||||
utils.secs_to_hhmmss(self.num_slept),
|
||||
self.blind_for,
|
||||
self.sad_for,
|
||||
self.bored_for,
|
||||
self.inactive_for,
|
||||
self.active_for,
|
||||
self.num_peers,
|
||||
self.tot_bond_factor,
|
||||
self.avg_bond_factor,
|
||||
self.num_hops,
|
||||
self.num_missed,
|
||||
self.num_deauths,
|
||||
@@ -195,6 +236,9 @@ class Epoch(object):
|
||||
self.epoch_started = now
|
||||
self.did_deauth = False
|
||||
self.num_deauths = 0
|
||||
self.num_peers = 0
|
||||
self.tot_bond_factor = 0.0
|
||||
self.avg_bond_factor = 0.0
|
||||
self.did_associate = False
|
||||
self.num_assocs = 0
|
||||
self.num_missed = 0
|
||||
|
@@ -4,31 +4,37 @@ import pwnagotchi.mesh.wifi as wifi
|
||||
|
||||
MAX_EPOCH_DURATION = 1024
|
||||
|
||||
histogram_size = wifi.NumChannels
|
||||
|
||||
shape = (1,
|
||||
# aps per channel
|
||||
histogram_size +
|
||||
# clients per channel
|
||||
histogram_size +
|
||||
# peers per channel
|
||||
histogram_size +
|
||||
# duration
|
||||
1 +
|
||||
# inactive
|
||||
1 +
|
||||
# active
|
||||
1 +
|
||||
# missed
|
||||
1 +
|
||||
# hops
|
||||
1 +
|
||||
# deauths
|
||||
1 +
|
||||
# assocs
|
||||
1 +
|
||||
# handshakes
|
||||
1)
|
||||
def describe(extended=False):
|
||||
if not extended:
|
||||
histogram_size = wifi.NumChannels
|
||||
else:
|
||||
# see https://github.com/evilsocket/pwnagotchi/issues/583
|
||||
histogram_size = wifi.NumChannelsExt
|
||||
|
||||
return histogram_size, (1,
|
||||
# aps per channel
|
||||
histogram_size +
|
||||
# clients per channel
|
||||
histogram_size +
|
||||
# peers per channel
|
||||
histogram_size +
|
||||
# duration
|
||||
1 +
|
||||
# inactive
|
||||
1 +
|
||||
# active
|
||||
1 +
|
||||
# missed
|
||||
1 +
|
||||
# hops
|
||||
1 +
|
||||
# deauths
|
||||
1 +
|
||||
# assocs
|
||||
1 +
|
||||
# handshakes
|
||||
1)
|
||||
|
||||
|
||||
def featurize(state, step):
|
||||
|
@@ -34,10 +34,14 @@ class Environment(gym.Env):
|
||||
self._epoch_num = 0
|
||||
self._last_render = None
|
||||
|
||||
channels = agent.supported_channels()
|
||||
# see https://github.com/evilsocket/pwnagotchi/issues/583
|
||||
self._supported_channels = agent.supported_channels()
|
||||
self._extended_spectrum = any(ch > 140 for ch in self._supported_channels)
|
||||
self._histogram_size, self._observation_shape = featurizer.describe(self._extended_spectrum)
|
||||
|
||||
Environment.params += [
|
||||
Parameter('_channel_%d' % ch, min_value=0, max_value=1, meta=ch + 1) for ch in
|
||||
range(featurizer.histogram_size) if ch + 1 in channels
|
||||
range(self._histogram_size) if ch + 1 in self._supported_channels
|
||||
]
|
||||
|
||||
self.last = {
|
||||
@@ -50,7 +54,7 @@ class Environment(gym.Env):
|
||||
}
|
||||
|
||||
self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.params if p.trainable])
|
||||
self.observation_space = spaces.Box(low=0, high=1, shape=featurizer.shape, dtype=np.float32)
|
||||
self.observation_space = spaces.Box(low=0, high=1, shape=self._observation_shape, dtype=np.float32)
|
||||
self.reward_range = reward.range
|
||||
|
||||
@staticmethod
|
||||
@@ -118,7 +122,7 @@ class Environment(gym.Env):
|
||||
return self.last['state_v']
|
||||
|
||||
def _render_histogram(self, hist):
|
||||
for ch in range(featurizer.histogram_size):
|
||||
for ch in range(self._histogram_size):
|
||||
if hist[ch]:
|
||||
logging.info(" CH %d: %s" % (ch + 1, hist[ch]))
|
||||
|
||||
|
@@ -18,4 +18,10 @@ class RewardFunction(object):
|
||||
m = -.3 * (state['missed_interactions'] / tot_interactions)
|
||||
i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
|
||||
|
||||
return h + a + c + b + i + m
|
||||
# include emotions if state >= 5 epochs
|
||||
_sad = state['sad_for_epochs'] if state['sad_for_epochs'] >= 5 else 0
|
||||
_bored = state['bored_for_epochs'] if state['bored_for_epochs'] >= 5 else 0
|
||||
s = -.2 * (_sad / tot_epochs)
|
||||
l = -.1 * (_bored / tot_epochs)
|
||||
|
||||
return h + a + c + b + i + m + s + l
|
||||
|
@@ -8,7 +8,6 @@ import logging
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.ai as ai
|
||||
from pwnagotchi.ai.epoch import Epoch
|
||||
|
||||
|
||||
class Stats(object):
|
||||
@@ -88,7 +87,6 @@ class AsyncTrainer(object):
|
||||
def __init__(self, config):
|
||||
self._config = config
|
||||
self._model = None
|
||||
self._epoch = Epoch(config)
|
||||
self._is_training = False
|
||||
self._training_epochs = 0
|
||||
self._nn_path = self._config['ai']['path']
|
||||
@@ -178,7 +176,7 @@ class AsyncTrainer(object):
|
||||
self.set_training(True, epochs_per_episode)
|
||||
self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
|
||||
except Exception as e:
|
||||
logging.exception("[ai] error while training")
|
||||
logging.exception("[ai] error while training (%s)", e)
|
||||
finally:
|
||||
self.set_training(False)
|
||||
obs = self._model.env.reset()
|
||||
|
143
pwnagotchi/automata.py
Normal file
143
pwnagotchi/automata.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.ai.epoch import Epoch
|
||||
|
||||
|
||||
# basic mood system
|
||||
class Automata(object):
|
||||
def __init__(self, config, view):
|
||||
self._config = config
|
||||
self._view = view
|
||||
self._epoch = Epoch(config)
|
||||
|
||||
def _on_miss(self, 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):
|
||||
# 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 str(e):
|
||||
self._on_miss(who)
|
||||
else:
|
||||
logging.error(e)
|
||||
|
||||
def set_starting(self):
|
||||
self._view.on_starting()
|
||||
|
||||
def set_ready(self):
|
||||
plugins.on('ready', self)
|
||||
|
||||
def in_good_mood(self):
|
||||
return self._has_support_network_for(1.0)
|
||||
|
||||
def _has_support_network_for(self, factor):
|
||||
bond_factor = self._config['personality']['bond_encounters_factor']
|
||||
total_encounters = sum(peer.encounters for _, peer in self._peers.items())
|
||||
support_factor = total_encounters / bond_factor
|
||||
return support_factor >= factor
|
||||
|
||||
# triggered when it's a sad/bad day but you have good friends around ^_^
|
||||
def set_grateful(self):
|
||||
self._view.on_grateful()
|
||||
plugins.on('grateful', self)
|
||||
|
||||
def set_lonely(self):
|
||||
if not self._has_support_network_for(1.0):
|
||||
logging.info("unit is lonely")
|
||||
self._view.on_lonely()
|
||||
plugins.on('lonely', self)
|
||||
else:
|
||||
logging.info("unit is grateful instead of lonely")
|
||||
self.set_grateful()
|
||||
|
||||
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)
|
||||
self._view.on_bored()
|
||||
plugins.on('bored', self)
|
||||
else:
|
||||
logging.info("unit is grateful instead of bored")
|
||||
self.set_grateful()
|
||||
|
||||
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)
|
||||
self._view.on_sad()
|
||||
plugins.on('sad', self)
|
||||
else:
|
||||
logging.info("unit is grateful instead of sad")
|
||||
self.set_grateful()
|
||||
|
||||
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)
|
||||
self._view.on_angry()
|
||||
plugins.on('angry', self)
|
||||
else:
|
||||
logging.info("unit is grateful instead of angry")
|
||||
self.set_grateful()
|
||||
|
||||
def set_excited(self):
|
||||
logging.warning("%d epochs with activity -> excited", self._epoch.active_for)
|
||||
self._view.on_excited()
|
||||
plugins.on('excited', self)
|
||||
|
||||
def set_rebooting(self):
|
||||
self._view.on_rebooting()
|
||||
plugins.on('rebooting', self)
|
||||
|
||||
def wait_for(self, t, sleeping=True):
|
||||
plugins.on('sleep' if sleeping else 'wait', self, t)
|
||||
self._view.wait(t, sleeping)
|
||||
self._epoch.track(sleep=True, inc=t)
|
||||
|
||||
def is_stale(self):
|
||||
return self._epoch.num_missed > self._config['personality']['max_misses_for_recon']
|
||||
|
||||
def any_activity(self):
|
||||
return self._epoch.any_activity
|
||||
|
||||
def next_epoch(self):
|
||||
logging.debug("agent.next_epoch()")
|
||||
|
||||
was_stale = self.is_stale()
|
||||
did_miss = self._epoch.num_missed
|
||||
|
||||
self._epoch.next()
|
||||
|
||||
# after X misses during an epoch, set the status to lonely or angry
|
||||
if was_stale:
|
||||
factor = did_miss / self._config['personality']['max_misses_for_recon']
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
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.sad_for:
|
||||
factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs']
|
||||
if factor >= 2.0:
|
||||
self.set_angry(factor)
|
||||
else:
|
||||
self.set_sad()
|
||||
# after X times being inactive, the status is set to bored
|
||||
elif self._epoch.bored_for:
|
||||
self.set_bored()
|
||||
# after X times being active, the status is set to happy / excited
|
||||
elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']:
|
||||
self.set_excited()
|
||||
elif self._epoch.active_for >= 5 and self._has_support_network_for(5.0):
|
||||
self.set_grateful()
|
||||
|
||||
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)
|
||||
self._reboot()
|
||||
self._epoch.blind_for = 0
|
@@ -1,8 +1,25 @@
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import websockets
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
|
||||
def decode(r, verbose_errors=True):
|
||||
try:
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
if r.status_code == 200:
|
||||
logging.error("error while decoding json: error='%s' resp='%s'" % (e, r.text))
|
||||
else:
|
||||
err = "error %d: %s" % (r.status_code, r.text.strip())
|
||||
if verbose_errors:
|
||||
logging.info(err)
|
||||
raise Exception(err)
|
||||
return r.text
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, hostname='localhost', scheme='http', port=8081, username='user', password='pass'):
|
||||
self.hostname = hostname
|
||||
@@ -11,29 +28,26 @@ class Client(object):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.url = "%s://%s:%d/api" % (scheme, hostname, port)
|
||||
self.websocket = "ws://%s:%s@%s:%d/api" % (username, password, hostname, port)
|
||||
self.auth = HTTPBasicAuth(username, password)
|
||||
|
||||
def _decode(self, r, verbose_errors=True):
|
||||
try:
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
if r.status_code == 200:
|
||||
logging.error("error while decoding json: error='%s' resp='%s'" % (e, r.text))
|
||||
else:
|
||||
err = "error %d: %s" % (r.status_code, r.text.strip())
|
||||
if verbose_errors:
|
||||
logging.info(err)
|
||||
raise Exception(err)
|
||||
return r.text
|
||||
|
||||
def session(self):
|
||||
r = requests.get("%s/session" % self.url, auth=self.auth)
|
||||
return self._decode(r)
|
||||
return decode(r)
|
||||
|
||||
def events(self):
|
||||
r = requests.get("%s/events" % self.url, auth=self.auth)
|
||||
return self._decode(r)
|
||||
async def start_websocket(self, consumer):
|
||||
s = "%s/events" % self.websocket
|
||||
while True:
|
||||
try:
|
||||
async with websockets.connect(s, ping_interval=60, ping_timeout=90) as ws:
|
||||
async for msg in ws:
|
||||
try:
|
||||
await consumer(msg)
|
||||
except Exception as ex:
|
||||
logging.debug("Error while parsing event (%s)", ex)
|
||||
except websockets.exceptions.ConnectionClosedError:
|
||||
logging.debug("Lost websocket connection. Reconnecting...")
|
||||
|
||||
def run(self, command, verbose_errors=True):
|
||||
r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command})
|
||||
return self._decode(r, verbose_errors=verbose_errors)
|
||||
return decode(r, verbose_errors=verbose_errors)
|
||||
|
230
pwnagotchi/defaults.toml
Normal file
230
pwnagotchi/defaults.toml
Normal file
@@ -0,0 +1,230 @@
|
||||
main.name = ""
|
||||
main.lang = "en"
|
||||
main.confd = "/etc/pwnagotchi/conf.d/"
|
||||
main.custom_plugins = ""
|
||||
main.iface = "mon0"
|
||||
main.mon_start_cmd = "/usr/bin/monstart"
|
||||
main.mon_stop_cmd = "/usr/bin/monstop"
|
||||
main.mon_max_blind_epochs = 50
|
||||
main.no_restart = false
|
||||
main.whitelist = [
|
||||
"EXAMPLE_NETWORK",
|
||||
"ANOTHER_EXAMPLE_NETWORK",
|
||||
"fo:od:ba:be:fo:od",
|
||||
"fo:od:ba"
|
||||
]
|
||||
main.filter = ""
|
||||
|
||||
main.plugins.grid.enabled = true
|
||||
main.plugins.grid.report = false
|
||||
main.plugins.grid.exclude = [
|
||||
"YourHomeNetworkHere"
|
||||
]
|
||||
|
||||
main.plugins.auto-update.enabled = true
|
||||
main.plugins.auto-update.install = true
|
||||
main.plugins.auto-update.interval = 1
|
||||
|
||||
main.plugins.net-pos.enabled = false
|
||||
main.plugins.net-pos.api_key = "test"
|
||||
|
||||
main.plugins.gps.enabled = false
|
||||
main.plugins.gps.speed = 19200
|
||||
main.plugins.gps.device = "/dev/ttyUSB0"
|
||||
|
||||
main.plugins.webgpsmap.enabled = false
|
||||
|
||||
main.plugins.onlinehashcrack.enabled = false
|
||||
main.plugins.onlinehashcrack.email = ""
|
||||
main.plugins.onlinehashcrack.dashboard = ""
|
||||
main.plugins.onlinehashcrack.single_files = false
|
||||
main.plugins.onlinehashcrack.whitelist = []
|
||||
|
||||
main.plugins.wpa-sec.enabled = false
|
||||
main.plugins.wpa-sec.api_key = ""
|
||||
main.plugins.wpa-sec.api_url = "https://wpa-sec.stanev.org"
|
||||
main.plugins.wpa-sec.download_results = false
|
||||
main.plugins.wpa-sec.whitelist = []
|
||||
|
||||
main.plugins.wigle.enabled = false
|
||||
main.plugins.wigle.api_key = ""
|
||||
main.plugins.wigle.whitelist = []
|
||||
|
||||
main.plugins.bt-tether.enabled = false
|
||||
|
||||
main.plugins.bt-tether.devices.android-phone.enabled = false
|
||||
main.plugins.bt-tether.devices.android-phone.search_order = 1
|
||||
main.plugins.bt-tether.devices.android-phone.mac = ""
|
||||
main.plugins.bt-tether.devices.android-phone.ip = "192.168.44.44"
|
||||
main.plugins.bt-tether.devices.android-phone.netmask = 24
|
||||
main.plugins.bt-tether.devices.android-phone.interval = 1
|
||||
main.plugins.bt-tether.devices.android-phone.scantime = 10
|
||||
main.plugins.bt-tether.devices.android-phone.max_tries = 10
|
||||
main.plugins.bt-tether.devices.android-phone.share_internet = false
|
||||
main.plugins.bt-tether.devices.android-phone.priority = 1
|
||||
|
||||
main.plugins.bt-tether.devices.ios-phone.enabled = false
|
||||
main.plugins.bt-tether.devices.ios-phone.search_order = 2
|
||||
main.plugins.bt-tether.devices.ios-phone.mac = ""
|
||||
main.plugins.bt-tether.devices.ios-phone.ip = "172.20.10.6"
|
||||
main.plugins.bt-tether.devices.ios-phone.netmask = 24
|
||||
main.plugins.bt-tether.devices.ios-phone.interval = 5
|
||||
main.plugins.bt-tether.devices.ios-phone.scantime = 20
|
||||
main.plugins.bt-tether.devices.ios-phone.max_tries = 0
|
||||
main.plugins.bt-tether.devices.ios-phone.share_internet = false
|
||||
main.plugins.bt-tether.devices.ios-phone.priority = 999
|
||||
|
||||
main.plugins.memtemp.enabled = false
|
||||
main.plugins.memtemp.scale = "celsius"
|
||||
main.plugins.memtemp.orientation = "horizontal"
|
||||
|
||||
main.plugins.paw-gps.enabled = false
|
||||
main.plugins.paw-gps.ip = ""
|
||||
|
||||
main.plugins.gpio_buttons.enabled = false
|
||||
|
||||
main.plugins.led.enabled = true
|
||||
main.plugins.led.led = 0
|
||||
main.plugins.led.delay = 200
|
||||
main.plugins.led.patterns.loaded = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.updating = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.unread_inbox = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.ready = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.ai_ready = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.ai_training_start = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.ai_best_reward = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.ai_worst_reward = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.bored = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.sad = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.excited = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.lonely = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.rebooting = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.wait = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.sleep = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.wifi_update = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.association = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.deauthentication = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.handshake = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.epoch = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.peer_detected = "oo oo oo oo oo oo oo"
|
||||
main.plugins.led.patterns.peer_lost = "oo oo oo oo oo oo oo"
|
||||
|
||||
main.plugins.logtail.enabled = false
|
||||
|
||||
main.plugins.session-stats.enabled = true
|
||||
main.plugins.session-stats.save_directory = "/var/tmp/pwnagotchi/sessions/"
|
||||
|
||||
main.log.path = "/var/log/pwnagotchi.log"
|
||||
main.log.rotation.enabled = true
|
||||
main.log.rotation.size = "10M"
|
||||
|
||||
ai.enabled = true
|
||||
ai.path = "/root/brain.nn"
|
||||
ai.laziness = 0.1
|
||||
ai.epochs_per_episode = 50
|
||||
|
||||
ai.params.gamma = 0.99
|
||||
ai.params.n_steps = 1
|
||||
ai.params.vf_coef = 0.25
|
||||
ai.params.ent_coef = 0.01
|
||||
ai.params.max_grad_norm = 0.5
|
||||
ai.params.learning_rate = 0.001
|
||||
ai.params.alpha = 0.99
|
||||
ai.params.epsilon = 0.00001
|
||||
ai.params.verbose = 1
|
||||
ai.params.lr_schedule = "constant"
|
||||
|
||||
personality.advertise = true
|
||||
personality.deauth = true
|
||||
personality.associate = true
|
||||
personality.channels = []
|
||||
personality.min_rssi = -200
|
||||
personality.ap_ttl = 120
|
||||
personality.sta_ttl = 300
|
||||
personality.recon_time = 30
|
||||
personality.max_inactive_scale = 2
|
||||
personality.recon_inactive_multiplier = 2
|
||||
personality.hop_recon_time = 10
|
||||
personality.min_recon_time = 5
|
||||
personality.max_interactions = 3
|
||||
personality.max_misses_for_recon = 5
|
||||
personality.excited_num_epochs = 10
|
||||
personality.bored_num_epochs = 15
|
||||
personality.sad_num_epochs = 25
|
||||
personality.bond_encounters_factor = 20000
|
||||
|
||||
ui.fps = 0.0
|
||||
ui.font.name = "DejaVuSansMono" # for japanese: fonts-japanese-gothic
|
||||
ui.font.size_offset = 0 # will be added to the font size
|
||||
|
||||
ui.faces.look_r = "( ⚆_⚆)"
|
||||
ui.faces.look_l = "(☉_☉ )"
|
||||
ui.faces.look_r_happy = "( ◕‿◕)"
|
||||
ui.faces.look_l_happy = "(◕‿◕ )"
|
||||
ui.faces.sleep = "(⇀‿‿↼)"
|
||||
ui.faces.sleep2 = "(≖‿‿≖)"
|
||||
ui.faces.awake = "(◕‿‿◕)"
|
||||
ui.faces.bored = "(-__-)"
|
||||
ui.faces.intense = "(°▃▃°)"
|
||||
ui.faces.cool = "(⌐■_■)"
|
||||
ui.faces.happy = "(•‿‿•)"
|
||||
ui.faces.excited = "(ᵔ◡◡ᵔ)"
|
||||
ui.faces.grateful = "(^‿‿^)"
|
||||
ui.faces.motivated = "(☼‿‿☼)"
|
||||
ui.faces.demotivated = "(≖__≖)"
|
||||
ui.faces.smart = "(✜‿‿✜)"
|
||||
ui.faces.lonely = "(ب__ب)"
|
||||
ui.faces.sad = "(╥☁╥ )"
|
||||
ui.faces.angry = "(-_-')"
|
||||
ui.faces.friend = "(♥‿‿♥)"
|
||||
ui.faces.broken = "(☓‿‿☓)"
|
||||
ui.faces.debug = "(#__#)"
|
||||
|
||||
ui.web.enabled = true
|
||||
ui.web.address = "0.0.0.0"
|
||||
ui.web.username = "changeme"
|
||||
ui.web.password = "changeme"
|
||||
ui.web.origin = ""
|
||||
ui.web.port = 8080
|
||||
ui.web.on_frame = ""
|
||||
|
||||
ui.display.enabled = true
|
||||
ui.display.rotation = 180
|
||||
ui.display.type = "waveshare_2"
|
||||
ui.display.color = "black"
|
||||
|
||||
bettercap.scheme = "http"
|
||||
bettercap.hostname = "localhost"
|
||||
bettercap.port = 8081
|
||||
bettercap.username = "pwnagotchi"
|
||||
bettercap.password = "pwnagotchi"
|
||||
bettercap.handshakes = "/root/handshakes"
|
||||
bettercap.silence = [
|
||||
"ble.device.new",
|
||||
"ble.device.lost",
|
||||
"ble.device.disconnected",
|
||||
"ble.device.connected",
|
||||
"ble.device.service.discovered",
|
||||
"ble.device.characteristic.discovered",
|
||||
"wifi.client.new",
|
||||
"wifi.client.lost",
|
||||
"wifi.client.probe",
|
||||
"wifi.ap.new",
|
||||
"wifi.ap.lost",
|
||||
"mod.started"
|
||||
]
|
||||
|
||||
fs.memory.enabled = false
|
||||
fs.memory.mounts.log.enabled = false
|
||||
fs.memory.mounts.log.mount = "/var/log"
|
||||
fs.memory.mounts.log.size = "50M"
|
||||
fs.memory.mounts.log.sync = 60
|
||||
fs.memory.mounts.log.zram = true
|
||||
fs.memory.mounts.log.rsync = true
|
||||
|
||||
fs.memory.mounts.data.enabled = false
|
||||
fs.memory.mounts.data.mount = "/var/tmp/pwnagotchi"
|
||||
fs.memory.mounts.data.size = "10M"
|
||||
fs.memory.mounts.data.sync = 3600
|
||||
fs.memory.mounts.data.zram = false
|
||||
fs.memory.mounts.data.rsync = true
|
@@ -1,200 +0,0 @@
|
||||
# WARNING WARNING WARNING WARNING
|
||||
#
|
||||
# This file is recreated with default settings on every pwnagotchi restart,
|
||||
# use /etc/pwnagotchi/config.yml to configure this unit.
|
||||
#
|
||||
#
|
||||
# main algorithm configuration
|
||||
main:
|
||||
# currently implemented: en (default), de, el, fr, it, mk, nl, ru, se, pt-BR, es, pt
|
||||
lang: en
|
||||
# custom plugins path, if null only default plugins with be loaded
|
||||
custom_plugins:
|
||||
# which plugins to load and enable
|
||||
plugins:
|
||||
grid:
|
||||
enabled: true
|
||||
report: false # don't report pwned networks by default!
|
||||
exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
|
||||
- YourHomeNetworkHere
|
||||
auto-update:
|
||||
enabled: false
|
||||
system: false # set to true to also enable system updates via apt
|
||||
interval: 1 # every day
|
||||
auto-backup:
|
||||
enabled: false
|
||||
interval: 1 # every day
|
||||
files:
|
||||
- /root/brain.nn
|
||||
- /root/brain.json
|
||||
- /root/handshakes/
|
||||
- /etc/pwnagotchi/
|
||||
- /etc/hostname
|
||||
- /etc/hosts
|
||||
- /etc/motd
|
||||
- /var/log/pwnagotchi.log
|
||||
commands:
|
||||
- 'tar czf /tmp/backup.tar.gz {files}'
|
||||
- 'scp /tmp/backup.tar.gz pwnagotchi@10.0.0.1:/home/pwnagotchi/backups/backup-$(date +%s).tar.gz'
|
||||
net-pos:
|
||||
enabled: false
|
||||
api_key: 'test'
|
||||
gps:
|
||||
enabled: false
|
||||
speed: 19200
|
||||
device: /dev/ttyUSB0
|
||||
twitter:
|
||||
enabled: false
|
||||
consumer_key: aaa
|
||||
consumer_secret: aaa
|
||||
access_token_key: aaa
|
||||
access_token_secret: aaa
|
||||
onlinehashcrack:
|
||||
enabled: false
|
||||
email: ~
|
||||
wpa-sec:
|
||||
enabled: false
|
||||
api_key: ~
|
||||
wigle:
|
||||
enabled: false
|
||||
api_key: ~
|
||||
screen_refresh:
|
||||
enabled: false
|
||||
refresh_interval: 50
|
||||
quickdic:
|
||||
enabled: false
|
||||
wordlist_folder: /opt/wordlists/
|
||||
AircrackOnly:
|
||||
enabled: false
|
||||
# monitor interface to use
|
||||
iface: mon0
|
||||
# command to run to bring the mon interface up in case it's not up already
|
||||
mon_start_cmd: /usr/bin/monstart
|
||||
mon_stop_cmd: /usr/bin/monstop
|
||||
mon_max_blind_epochs: 50
|
||||
# log file
|
||||
log: /var/log/pwnagotchi.log
|
||||
# if true, will not restart the wifi module
|
||||
no_restart: false
|
||||
# access points to ignore
|
||||
whitelist:
|
||||
- EXAMPLE_NETWORK
|
||||
- ANOTHER_EXAMPLE_NETWORK
|
||||
# if not null, filter access points by this regular expression
|
||||
filter: null
|
||||
|
||||
ai:
|
||||
# if false, only the default 'personality' will be used
|
||||
enabled: true
|
||||
path: /root/brain.nn
|
||||
# 1.0 - laziness = probability of start training
|
||||
laziness: 0.1
|
||||
# how many epochs to train on
|
||||
epochs_per_episode: 50
|
||||
params:
|
||||
# discount factor
|
||||
gamma: 0.99
|
||||
# the number of steps to run for each environment per update
|
||||
n_steps: 1
|
||||
# value function coefficient for the loss calculation
|
||||
vf_coef: 0.25
|
||||
# entropy coefficient for the loss calculation
|
||||
ent_coef: 0.01
|
||||
# maximum value for the gradient clipping
|
||||
max_grad_norm: 0.5
|
||||
# the learning rate
|
||||
learning_rate: 0.0010
|
||||
# rmsprop decay parameter
|
||||
alpha: 0.99
|
||||
# rmsprop epsilon
|
||||
epsilon: 0.00001
|
||||
# the verbosity level: 0 none, 1 training information, 2 tensorflow debug
|
||||
verbose: 1
|
||||
# type of scheduler for the learning rate update ('linear', 'constant', 'double_linear_con', 'middle_drop' or 'double_middle_drop')
|
||||
lr_schedule: 'constant'
|
||||
# the log location for tensorboard (if None, no logging)
|
||||
tensorboard_log: null
|
||||
|
||||
personality:
|
||||
# advertise our presence
|
||||
advertise: true
|
||||
# perform a deauthentication attack to client stations in order to get full or half handshakes
|
||||
deauth: true
|
||||
# send association frames to APs in order to get the PMKID
|
||||
associate: true
|
||||
# list of channels to recon on, or empty for all channels
|
||||
channels: []
|
||||
# minimum WiFi signal strength in dBm
|
||||
min_rssi: -200
|
||||
# number of seconds for wifi.ap.ttl
|
||||
ap_ttl: 120
|
||||
# number of seconds for wifi.sta.ttl
|
||||
sta_ttl: 300
|
||||
# time in seconds to wait during channel recon
|
||||
recon_time: 30
|
||||
# number of inactive epochs after which recon_time gets multiplied by recon_inactive_multiplier
|
||||
max_inactive_scale: 2
|
||||
# if more than max_inactive_scale epochs are inactive, recon_time *= recon_inactive_multiplier
|
||||
recon_inactive_multiplier: 2
|
||||
# time in seconds to wait during channel hopping if activity has been performed
|
||||
hop_recon_time: 10
|
||||
# time in seconds to wait during channel hopping if no activity has been performed
|
||||
min_recon_time: 5
|
||||
# maximum amount of deauths/associations per BSSID per session
|
||||
max_interactions: 3
|
||||
# maximum amount of misses before considering the data stale and triggering a new recon
|
||||
max_misses_for_recon: 5
|
||||
# number of active epochs that triggers the excited state
|
||||
excited_num_epochs: 10
|
||||
# number of inactive epochs that triggers the bored state
|
||||
bored_num_epochs: 15
|
||||
# number of inactive epochs that triggers the sad state
|
||||
sad_num_epochs: 25
|
||||
|
||||
# ui configuration
|
||||
ui:
|
||||
# ePaper display can update every 3 secs anyway, set to 0 to only refresh for major data changes
|
||||
# IMPORTANT: The lifespan of an eINK display depends on the cumulative amount of refreshes. If you want to
|
||||
# preserve your display over time, you should set this value to 0.0 so that the display will be refreshed only
|
||||
# if any of the important data fields changed (the uptime and blinking cursor won't trigger a refresh).
|
||||
fps: 0.0
|
||||
display:
|
||||
enabled: true
|
||||
rotation: 180
|
||||
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2
|
||||
type: 'waveshare_2'
|
||||
# Possible options red/yellow/black (black used for monocromatic displays)
|
||||
color: 'black'
|
||||
video:
|
||||
enabled: true
|
||||
address: '10.0.0.2'
|
||||
port: 8080
|
||||
|
||||
|
||||
# bettercap rest api configuration
|
||||
bettercap:
|
||||
# api scheme://hostname:port username and password
|
||||
scheme: http
|
||||
hostname: localhost
|
||||
port: 8081
|
||||
username: pwnagotchi
|
||||
password: pwnagotchi
|
||||
# folder where bettercap stores the WPA handshakes, given that
|
||||
# wifi.handshakes.aggregate will be set to false and individual
|
||||
# pcap files will be created in order to minimize the chances
|
||||
# of a single pcap file to get corrupted
|
||||
handshakes: /root/handshakes
|
||||
# events to mute in bettercap's events stream
|
||||
silence:
|
||||
- ble.device.new
|
||||
- ble.device.lost
|
||||
- ble.device.disconnected
|
||||
- ble.device.connected
|
||||
- ble.device.service.discovered
|
||||
- ble.device.characteristic.discovered
|
||||
- wifi.client.new
|
||||
- wifi.client.lost
|
||||
- wifi.client.probe
|
||||
- wifi.ap.new
|
||||
- wifi.ap.lost
|
||||
- mod.started
|
190
pwnagotchi/fs/__init__.py
Normal file
190
pwnagotchi/fs/__init__.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import contextlib
|
||||
import shutil
|
||||
import _thread
|
||||
import logging
|
||||
|
||||
from time import sleep
|
||||
from distutils.dir_util import copy_tree
|
||||
|
||||
mounts = list()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ensure_write(filename, mode='w'):
|
||||
path = os.path.dirname(filename)
|
||||
fd, tmp = tempfile.mkstemp(dir=path)
|
||||
|
||||
with os.fdopen(fd, mode) as f:
|
||||
yield f
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
os.replace(tmp, filename)
|
||||
|
||||
|
||||
def size_of(path):
|
||||
"""
|
||||
Calculate the sum of all the files in path
|
||||
"""
|
||||
total = 0
|
||||
for root, _, files in os.walk(path):
|
||||
for f in files:
|
||||
total += os.path.getsize(os.path.join(root, f))
|
||||
return total
|
||||
|
||||
|
||||
def is_mountpoint(path):
|
||||
"""
|
||||
Checks if path is mountpoint
|
||||
"""
|
||||
return os.system(f"mountpoint -q {path}") == 0
|
||||
|
||||
|
||||
def setup_mounts(config):
|
||||
"""
|
||||
Sets up all the configured mountpoints
|
||||
"""
|
||||
global mounts
|
||||
fs_cfg = config['fs']['memory']
|
||||
if not fs_cfg['enabled']:
|
||||
return
|
||||
|
||||
for name, options in fs_cfg['mounts'].items():
|
||||
if not options['enabled']:
|
||||
continue
|
||||
logging.debug("[FS] Trying to setup mount %s (%s)", name, options['mount'])
|
||||
size,unit = re.match(r"(\d+)([a-zA-Z]+)", options['size']).groups()
|
||||
target = os.path.join('/run/pwnagotchi/disk/', os.path.basename(options['mount']))
|
||||
|
||||
is_mounted = is_mountpoint(target)
|
||||
logging.debug("[FS] %s is %s mounted", options['mount'],
|
||||
"already" if is_mounted else "not yet")
|
||||
|
||||
m = MemoryFS(
|
||||
options['mount'],
|
||||
target,
|
||||
size=options['size'],
|
||||
zram=options['zram'],
|
||||
zram_disk_size=f"{int(size)*2}{unit}",
|
||||
rsync=options['rsync'])
|
||||
|
||||
if not is_mounted:
|
||||
if not m.mount():
|
||||
logging.debug(f"Error while mounting {m.mountpoint}")
|
||||
continue
|
||||
|
||||
if not m.sync(to_ram=True):
|
||||
logging.debug(f"Error while syncing to {m.mountpoint}")
|
||||
m.umount()
|
||||
continue
|
||||
|
||||
interval = int(options['sync'])
|
||||
if interval:
|
||||
logging.debug("[FS] Starting thread to sync %s (interval: %d)",
|
||||
options['mount'], interval)
|
||||
_thread.start_new_thread(m.daemonize, (interval,))
|
||||
else:
|
||||
logging.debug("[FS] Not syncing %s, because interval is 0",
|
||||
options['mount'])
|
||||
|
||||
mounts.append(m)
|
||||
|
||||
|
||||
class MemoryFS:
|
||||
@staticmethod
|
||||
def zram_install():
|
||||
if not os.path.exists("/sys/class/zram-control"):
|
||||
logging.debug("[FS] Installing zram")
|
||||
return os.system("modprobe zram") == 0
|
||||
return True
|
||||
|
||||
|
||||
@staticmethod
|
||||
def zram_dev():
|
||||
logging.debug("[FS] Adding zram device")
|
||||
return open("/sys/class/zram-control/hot_add", "rt").read().strip("\n")
|
||||
|
||||
|
||||
def __init__(self, mount, disk, size="40M",
|
||||
zram=True, zram_alg="lz4", zram_disk_size="100M",
|
||||
zram_fs_type="ext4", rsync=True):
|
||||
self.mountpoint = mount
|
||||
self.disk = disk
|
||||
self.size = size
|
||||
self.zram = zram
|
||||
self.zram_alg = zram_alg
|
||||
self.zram_disk_size = zram_disk_size
|
||||
self.zram_fs_type = zram_fs_type
|
||||
self.zdev = None
|
||||
self.rsync = True
|
||||
self._setup()
|
||||
|
||||
|
||||
def _setup(self):
|
||||
if self.zram and MemoryFS.zram_install():
|
||||
# setup zram
|
||||
self.zdev = MemoryFS.zram_dev()
|
||||
open(f"/sys/block/zram{self.zdev}/comp_algorithm", "wt").write(self.zram_alg)
|
||||
open(f"/sys/block/zram{self.zdev}/disksize", "wt").write(self.zram_disk_size)
|
||||
open(f"/sys/block/zram{self.zdev}/mem_limit", "wt").write(self.size)
|
||||
logging.debug("[FS] Creating fs (type: %s)", self.zram_fs_type)
|
||||
os.system(f"mke2fs -t {self.zram_fs_type} /dev/zram{self.zdev} >/dev/null 2>&1")
|
||||
|
||||
# ensure mountpoints exist
|
||||
if not os.path.exists(self.disk):
|
||||
logging.debug("[FS] Creating %s", self.disk)
|
||||
os.makedirs(self.disk)
|
||||
|
||||
if not os.path.exists(self.mountpoint):
|
||||
logging.debug("[FS] Creating %s", self.mountpoint)
|
||||
os.makedirs(self.mountpoint)
|
||||
|
||||
|
||||
def daemonize(self, interval=60):
|
||||
logging.debug("[FS] Daemonized...")
|
||||
while True:
|
||||
self.sync()
|
||||
sleep(interval)
|
||||
|
||||
|
||||
def sync(self, to_ram=False):
|
||||
source, dest = (self.disk, self.mountpoint) if to_ram else (self.mountpoint, self.disk)
|
||||
needed, actually_free = size_of(source), shutil.disk_usage(dest)[2]
|
||||
if actually_free >= needed:
|
||||
logging.debug("[FS] Syning %s -> %s", source,dest)
|
||||
if self.rsync:
|
||||
os.system(f"rsync -aXv --inplace --no-whole-file --delete-after {source}/ {dest}/ >/dev/null 2>&1")
|
||||
else:
|
||||
copy_tree(source, dest, preserve_symlinks=True)
|
||||
os.system("sync")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def mount(self):
|
||||
if os.system(f"mount --bind {self.mountpoint} {self.disk}"):
|
||||
return False
|
||||
|
||||
if os.system(f"mount --make-private {self.disk}"):
|
||||
return False
|
||||
|
||||
if self.zram and self.zdev is not None:
|
||||
if os.system(f"mount -t {self.zram_fs_type} -o nosuid,noexec,nodev,user=pwnagotchi /dev/zram{self.zdev} {self.mountpoint}/"):
|
||||
return False
|
||||
else:
|
||||
if os.system(f"mount -t tmpfs -o nosuid,noexec,nodev,mode=0755,size={self.size} pwnagotchi {self.mountpoint}/"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def umount(self):
|
||||
if os.system(f"umount -l {self.mountpoint}"):
|
||||
return False
|
||||
|
||||
if os.system(f"umount -l {self.disk}"):
|
||||
return False
|
||||
return True
|
123
pwnagotchi/grid.py
Normal file
123
pwnagotchi/grid.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import subprocess
|
||||
import socket
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pwnagotchi
|
||||
|
||||
# pwngrid-peer is running on port 8666
|
||||
API_ADDRESS = "http://127.0.0.1:8666/api/v1"
|
||||
|
||||
|
||||
def is_connected():
|
||||
try:
|
||||
# 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
|
||||
|
||||
|
||||
def call(path, obj=None):
|
||||
url = '%s%s' % (API_ADDRESS, path)
|
||||
if obj is 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, data=obj, timeout=(30.0, 60.0))
|
||||
|
||||
if r.status_code != 200:
|
||||
raise Exception("(status %d) %s" % (r.status_code, r.text))
|
||||
return r.json()
|
||||
|
||||
|
||||
def advertise(enabled=True):
|
||||
return call("/mesh/%s" % 'true' if enabled else 'false')
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def closest_peer():
|
||||
all = peers()
|
||||
return all[0] if len(all) else None
|
||||
|
||||
|
||||
def update_data(last_session):
|
||||
brain = {}
|
||||
try:
|
||||
with open('/root/brain.json') as fp:
|
||||
brain = json.load(fp)
|
||||
except:
|
||||
pass
|
||||
|
||||
data = {
|
||||
'session': {
|
||||
'duration': last_session.duration,
|
||||
'epochs': last_session.epochs,
|
||||
'train_epochs': last_session.train_epochs,
|
||||
'avg_reward': last_session.avg_reward,
|
||||
'min_reward': last_session.min_reward,
|
||||
'max_reward': last_session.max_reward,
|
||||
'deauthed': last_session.deauthed,
|
||||
'associated': last_session.associated,
|
||||
'handshakes': last_session.handshakes,
|
||||
'peers': last_session.peers,
|
||||
},
|
||||
'uname': subprocess.getoutput("uname -a"),
|
||||
'brain': brain,
|
||||
'version': pwnagotchi.__version__
|
||||
}
|
||||
|
||||
logging.debug("updating grid data: %s" % data)
|
||||
|
||||
call("/data", data)
|
||||
|
||||
|
||||
def report_ap(essid, bssid):
|
||||
try:
|
||||
call("/report/ap", {
|
||||
'essid': essid,
|
||||
'bssid': bssid,
|
||||
})
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception("error while reporting ap %s(%s)" % (essid, bssid))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
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'))
|
@@ -16,6 +16,7 @@ class KeyPair(object):
|
||||
self.priv_key = None
|
||||
self.pub_path = "%s.pub" % self.priv_path
|
||||
self.pub_key = None
|
||||
self.fingerprint_path = os.path.join(path, "fingerprint")
|
||||
self._view = view
|
||||
|
||||
if not os.path.exists(self.path):
|
||||
@@ -26,7 +27,7 @@ class KeyPair(object):
|
||||
if not os.path.exists(self.priv_path) or not os.path.exists(self.pub_path):
|
||||
self._view.on_keys_generation()
|
||||
logging.info("generating %s ..." % self.priv_path)
|
||||
os.system("/usr/bin/ssh-keygen -t rsa -m PEM -b 4096 -N '' -f '%s'" % self.priv_path)
|
||||
os.system("pwngrid -generate -keys '%s'" % self.path)
|
||||
|
||||
# load keys: they might be corrupted if the unit has been turned off during the generation, in this case
|
||||
# the exception will remove the files and go back at the beginning of this loop.
|
||||
@@ -46,6 +47,9 @@ class KeyPair(object):
|
||||
self.pub_key_pem_b64 = base64.b64encode(pem_ascii).decode("ascii")
|
||||
self.fingerprint = hashlib.sha256(pem_ascii).hexdigest()
|
||||
|
||||
with open(self.fingerprint_path, 'w+t') as fp:
|
||||
fp.write(self.fingerprint)
|
||||
|
||||
# no exception, keys loaded correctly.
|
||||
self._view.on_starting()
|
||||
return
|
||||
|
BIN
pwnagotchi/locale/bg/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/bg/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
226
pwnagotchi/locale/bg/LC_MESSAGES/voice.po
Normal file
226
pwnagotchi/locale/bg/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,226 @@
|
||||
# pwnagotchi voice data.
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <https://github.com/georgikoemdzhiev>, 2019.
|
||||
#
|
||||
#,
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"PO-Revision-Date: 2019-10-23 20:56+0200\n"
|
||||
"Last-Translator: Georgi Koemdzhiev <https://github.com/georgikoemdzhiev>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: bulgarian\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 "Здравей, аз съм Pwnagotchi! Стартиране ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Нов ден, нов лов, нови pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Хакни планетата!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI готов."
|
||||
|
||||
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} е свободен! твоя AP ще каже благодаря."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Скучно ми е ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Хайда да се поразходим!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Това е най-добрият ден в живота ми!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Тъп ден :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Супер много ми е скучно ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Много съм тъжен ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Тъжен съм"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Живота ми е фантастичен!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Аз живея за да pwn-вам."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Толкова много мрежи!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Толкова много се забавлявам!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Моето престъпление е това че съм любопитен ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Здравей {name}! Приятно ми е да се запознаем."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Устройство {name} е наблизо!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Ами ... довиждане {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} изчезна ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Упс ... {name} изчезна."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
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 "Чувствам се толкова самотен ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Къде са всички?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Заспивам за {secs} секунди ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}s)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Лека нощ."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Чакам {secs} секунди ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Оглеждам се ({secs}секунди)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Хей {what} нека станем приятели!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Свръзване с {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Ей {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Реших, че {mac} не се нуждае от WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Неудостоверяване на {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Ритам и прогонвам {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Супер, имаме {num} нови handshake{plural}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Имате {count} нови съобщения!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Упс, нещо се обърка ... Рестартиране ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Отхвърлих {num} станции\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Направих {num} нови приятели\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Имам {num} handshakes\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Срещнах 1 връстник"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Срещнах {num} връстници"
|
||||
|
||||
#, 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 "Аз pwn-вах за {duration} и отхвърлих {deauthed} clients! Също така срещнах {associated} нови приятели и изядох {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "часове"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "минути"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "секунди"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "час"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "минута"
|
||||
|
||||
msgid "second"
|
||||
msgstr "секунда"
|
BIN
pwnagotchi/locale/ch/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ch/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
225
pwnagotchi/locale/ch/LC_MESSAGES/voice.po
Normal file
225
pwnagotchi/locale/ch/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,225 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <511225068@qq.com>, 2019.
|
||||
# 还有很多未翻译和翻译不准确,后期希望大家加入进来一起翻译!
|
||||
# 翻译可以联系QQ群:959559103 找 名字叫 初九 的 管理员
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"PO-Revision-Date: 2019-11-02 10:00+0008\n"
|
||||
"Last-Translator: 极客之眼-初九 <511225068@qq.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: chinese\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 "主人,你好.我是WiFi狩猎兽..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "美好的一天,狩猎开始!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "我要入侵整个地球!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "人工智能已启动."
|
||||
|
||||
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 ""
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "我无聊了..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "主人带我出门走走吧!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "这是我生命中最美好的一天!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "今天不开心 :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "主人,找点事做吧 ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "我很伤心..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "我伤心了"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr ""
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr ""
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "哇,好多猎物!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "我玩的好开心!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "我最大的缺点就是好奇..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "你好{name}!很高兴认识你."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "额 ... 再见{name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} 它走了 ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "哎呀... {name} 离开了."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
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 "我可能是天煞孤星..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "朋友们都去哪里了?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "小憩{secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "晚安宝贝."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "等待{secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "追踪四周猎物({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "嗨{what}我们做朋友吧!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "正在连接到{what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "追踪到你了{what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "猎物{mac}不需要联网,我们给它断开!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "开始攻击猎物{mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "已捕获{mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "太酷了, 我们抓到了{num}新的猎物{plural}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "主人,有{count}新消息{plural}!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "行动,额等等有点小问题... 重启ing ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "限制了{num}个猎物\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "交了{num}新朋友\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "捕获了{num}握手包\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr ""
|
||||
|
||||
#, 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 ""
|
||||
|
||||
msgid "hours"
|
||||
msgstr "时"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "分"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "时"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "分"
|
||||
|
||||
msgid "second"
|
||||
msgstr "秒"
|
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-09 17:42+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,13 +20,13 @@ 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!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack den Planet!"
|
||||
msgstr "Hack den Planeten!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "KI bereit."
|
||||
@@ -34,21 +34,31 @@ msgstr "KI bereit."
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Das neurale Netz ist bereit."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Generiere Schlüssel, 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..."
|
||||
@@ -59,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!"
|
||||
|
||||
@@ -66,33 +83,41 @@ msgid "I pwn therefore I am."
|
||||
msgstr "Ich pwne, also bin ich."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "So viele Netwerke!!!"
|
||||
msgstr "So viele Netzwerke!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Ich habe sooo viel Spaß!"
|
||||
|
||||
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. {name}"
|
||||
msgstr "Hallo {name}, nett Dich kennenzulernen."
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Hallo {name}, schön Dich kennenzulernen."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Gerät {name} ist in der nähe!!"
|
||||
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!"
|
||||
|
||||
#, 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!"
|
||||
@@ -101,18 +126,24 @@ msgstr "{name} verpasst!"
|
||||
msgid "Missed!"
|
||||
msgstr "Verpasst!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Gute Freunde sind ein Segen!"
|
||||
|
||||
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 ""
|
||||
@@ -129,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)"
|
||||
@@ -149,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}"
|
||||
@@ -163,16 +194,20 @@ msgstr "Kicke {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, da ist was schief gelaufen... Starte neu..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} Stationen gekicked\n"
|
||||
msgstr "{num} Stationen gekickt\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} Freunde gefunden\n"
|
||||
msgstr "{num} neue Freunde gefunden\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
@@ -212,3 +247,4 @@ msgstr "Minute"
|
||||
|
||||
msgid "second"
|
||||
msgstr "Sekunde"
|
||||
|
||||
|
BIN
pwnagotchi/locale/dk/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/dk/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
248
pwnagotchi/locale/dk/LC_MESSAGES/voice.po
Normal file
248
pwnagotchi/locale/dk/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,248 @@
|
||||
# pwnagotchi danish voice data
|
||||
# Copyright (C) 2020
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# Dennis Kjær Jensen <signout@signout.dk>, 2020
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
|
||||
"PO-Revision-Date: 2020-01-18 21:56+ZONE\n"
|
||||
"Last-Translator: Dennis Kjær Jensen <signout@signout.dk>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: Danish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hej. Jeg er Pwnagotchi. Starter ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Ny dag, ny jagt, nye pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack planeten!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI klar."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Det neurale netværk er klart."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Genererer nøgler, sluk ikke ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, kanal {channel} er ubrugt! Dit AP vil takke dig."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Læser seneste session logs ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Har læst {lines_so_far} linjer indtil nu ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Jeg keder mig ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Lad os gå en tur!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Det er den bedste dag i mit liv!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Elendig dag :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Jeg keder mig ekstremt meget ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Jeg er meget trist ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Jeg er trist"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Lad mig være i fred"
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Jeg er sur på dig!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Jeg lever livet!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Jeg pwner, derfor er jeg."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Så mange netværk!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Jeg har det vildt sjovt!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Min forbrydelse er at være nysgerrig ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Hej {name}! Rart at møde dig."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Hey {name}! Hvasså?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hej {name} hvordan har du det?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Enheden {name} er lige i nærheden!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ... farvel {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} er væk ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Hovsa ... {name} er væk."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} glippede!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Fordømt!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Gode venner en velsignelse!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Jeg elsker mine venner!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Der er ingen der vil lege med mig ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Jeg føler mig så alene ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Hvor er alle henne?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Sover i {secs} sekunder"
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz {secs} sekunder"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Godnat."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Venter i {secs} sekunder"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Kigger mig omkring i {secs} sekunder"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hej {what} lad os være venner!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Associerer til {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Hey {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Besluttede at {mac} ikke har brug for WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Afmelder {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Kickbanner {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Fedt, vi har fået {num} nye handshake{plural}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Du har {count} nye beskeder"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ups, noget gik galt ... Genstarter."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Sparkede {num} af\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Har fået {num} nye venner\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Har fået {num} nyehandshakes\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Har mødt 1 peer"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Har mødt {num} peers"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr "Jeg har pwnet i {duration} og kicket {dauthed} klienter! Jeg har også "
|
||||
"mødt {associated} nye venner og spist {handshakes} håndtryk! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "timer"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minutter"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "sekunder"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "time"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minut"
|
||||
|
||||
msgid "second"
|
||||
msgstr "sekund"
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Μπανάρω την {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Τέλεια δικέ μου, πήραμε {num} νέες χειραψίες!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ουπς, κάτιπήγε λάθος ... Επανεκκινούμαι ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -163,7 +163,7 @@ msgstr "Expulsando y banneando a {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Genial, obtuvimos {num} nuevo{plural} handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oops, algo salió mal ... Reiniciándo ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -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.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+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"
|
||||
@@ -19,16 +18,16 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Bonjour, je suis Pwnagotchi! Démarrage ..."
|
||||
msgstr "Bonjour, je suis Pwnagotchi ! Démarrage..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Nouveau jour, nouvelle chasse, nouveaux pwns !"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack la planète!"
|
||||
msgstr "Hack la planète !"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "L'IA est prête."
|
||||
@@ -36,57 +35,82 @@ msgstr "L'IA est prête."
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Le réseau neuronal est prêt."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Génération des clés, ne pas éteindre..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, le channel {channel} est libre! Ton point d'accès va te remercier."
|
||||
msgstr "Hey, le canal {channel} est libre! Ton point d'accès va te remercier."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Lecture des logs de la dernière session ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Jusqu'ici, {lines_so_far} lignes lues dans le log ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Je m'ennuie ..."
|
||||
msgstr "Je m'ennuie..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Allons faire un tour!"
|
||||
msgstr "Allons faire un tour !"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "C'est le meilleur jour de ma vie!"
|
||||
msgstr "C'est le meilleur jour de ma vie !"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Journée de merde :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Je m'ennuie énormément ..."
|
||||
msgstr "Je m'ennuie énormément..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Je suis très triste ..."
|
||||
msgstr "Je suis très triste..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Je suis triste"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Lache moi..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Je t'en veux !"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Je vis la vie!"
|
||||
msgstr "Je vis la belle vie !"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Je pwn donc je suis."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Tellement de réseaux!!!"
|
||||
msgstr "Tellement de réseaux !!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Je m'amuse tellement!"
|
||||
msgstr "Je m'amuse tellement !"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Mon crime, c'est la curiosité ..."
|
||||
msgstr "Mon crime, c'est la curiosité..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Bonjour {name}! Ravi de te rencontrer. {name}"
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Bonjour {name} ! Ravi de te rencontrer."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "L'unité {name} est proche! {name}"
|
||||
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 !"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Hum ... au revoir {name}"
|
||||
msgstr "Hum... au revoir {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
@@ -94,46 +118,58 @@ msgstr "{name} est parti ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Oups ... {name} est parti."
|
||||
msgstr "Oups... {name} est parti."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} raté!"
|
||||
msgstr "{name} raté !"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Raté!"
|
||||
msgstr "Raté !"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Les bons amis sont une bénédiction !"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "J'aime mes amis !"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Personne ne veut jouer avec moi ..."
|
||||
msgstr "Personne ne veut jouer avec moi..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Je me sens si seul ..."
|
||||
msgstr "Je me sens si seul..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Où est tout le monde?!"
|
||||
msgstr "Où est tout le monde ?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Fais la sieste pendant {secs}s ..."
|
||||
msgstr "Je fais la sieste pendant {secs}s..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
msgstr "ZzzZzzz ({secs}s)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Bonne nuit."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Attends pendant {secs}s ..."
|
||||
msgstr "J'attends pendant {secs}s..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Regarde autour ({secs}s)"
|
||||
msgstr "J'observe ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what}, soyons amis!"
|
||||
msgstr "Hey {what}, soyons amis !"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
@@ -141,11 +177,11 @@ msgstr "Association à {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
msgstr "Yo {what} !"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Je viens de décider que {mac} n'a pas besoin de WiFi!"
|
||||
msgstr "Je viens de décider que {mac} n'a pas besoin de WiFi !"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
@@ -153,14 +189,18 @@ msgstr "Désauthentification de {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Je kick et je bannis {mac}!"
|
||||
msgstr "Je kick et je bannis {mac} !"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Cool, on a {num} nouveaux handshake{plural}!"
|
||||
msgstr "Cool, on a {num} nouve(l/aux) handshake{plural} !"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oups, quelque chose s'est mal passé ... Redémarrage ..."
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Tu as {num} nouveau(x) message{plural} !"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oups, quelque chose s'est mal passé... Redémarrage..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
@@ -168,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 fait {num} nouve(l/aux) ami(s)\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 camarade rencontré"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} peers recontrés"
|
||||
msgstr "{num} camarades recontrés"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -187,24 +227,24 @@ msgid ""
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"J'ai pwn durant {duration} et kick {deauthed} clients! J'ai aussi rencontré "
|
||||
"{associated} nouveaux amis et dévoré {handshakes} handshakes! #pwnagotchi "
|
||||
"J'ai pwn durant {duration} et kick {deauthed} clients ! J'ai aussi rencontré "
|
||||
"{associated} nouveaux amis et dévoré {handshakes} handshakes ! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
msgstr "heures"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
msgstr "minutes"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
msgstr "secondes"
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
msgstr "heure"
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
msgstr "minute"
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
||||
msgstr "seconde"
|
||||
|
BIN
pwnagotchi/locale/ga/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ga/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
215
pwnagotchi/locale/ga/LC_MESSAGES/voice.po
Normal file
215
pwnagotchi/locale/ga/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,215 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-09 17:42+0200\n"
|
||||
"PO-Revision-Date: 2019-10-15 23:46+0100\n"
|
||||
"Language: ga\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Dia Duit, Pwnagotchi is ainm dom! Ag tosú ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Lá nua, seilg nua, pwns nua!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Haic An Phláinéid!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI réidh."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Tá an líonra néarach réidh."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hé, tá cainéal {channel} ar fail! Déarfaidh do PR go raibh maith agat."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Tá leadrán orm ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Siúil liom, le do thoil!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Tá sé an lá is fearr i mo shaol!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Tá lá damanta agam :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Tá mé ag dul as mo mheabhair le leadrán ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Ta brón an domhain orm ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Tá brón orm"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Tá an saol ar a thoil agam!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Déanaim pwnáil, dá bhrí sin táim ann."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Gréasáin - Tá an iliomad acu ann!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Tá craic iontach agam!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Ní haon pheaca é bheith fiosrach ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Dia Duit {name}! Is deas bualadh leat. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Aonad {name} in aice láimhe! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm... slán leat {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "Tá {name} imithe ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Hoips … Tá {name} imithe."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "Chaill mé ar {name}!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Chaill mé é sin !"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Níl aon duine ag iarraidh imirt liom ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Tá uaigneas an domhain orm ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Cá bhfuil gach duine?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Néal a chodladh ar {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Oíche mhaith."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Fan ar {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Ag amharc uaim ar ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hé {what} déanaimis síocháin!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Ag coinneáil le {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Hé {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Tá cinneadh déanta agam. Níl {mac} sin de dhíth air WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Bain fíordheimhniúde {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Chiceáil mé agus cosc mé ar {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Go hiontach, fuaireamar {num} handshake{plural}!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Hoips...Tháinig ainghléas éigin..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} stáisiún kick\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Rinne mé {num} cairde nua\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Fuair me {num} cumarsáid thionscantach\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Bhuail mé piara amháin"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Bhuail me {num} piara"
|
||||
|
||||
#, 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 ""
|
||||
"Bhí me ag pwnáil ar {duration} agus chiceáil me ar {deauthed} cliaint! Chomh "
|
||||
"maith, bhuail me {associated} cairde nua and d'ith mé {handshakes}! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "uair on chloig"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "nóiméad"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "soicind"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "uair an chloig"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "nóiméad"
|
||||
|
||||
msgid "second"
|
||||
msgstr "soicind"
|
BIN
pwnagotchi/locale/hu/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/hu/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
249
pwnagotchi/locale/hu/LC_MESSAGES/voice.po
Normal file
249
pwnagotchi/locale/hu/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,249 @@
|
||||
# Hungarian translation.
|
||||
# Copyright (C) 2020
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Skeleton022 <skeleton022.pwnagotchi@gmail.com>, 2020.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1.4.3\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-01-07 20:00+0100\n"
|
||||
"PO-Revision-Date: 2020-03-23 0:10+0100\n"
|
||||
"Last-Translator: Skeleton022\n"
|
||||
"Language-Team: Skeleton022\n"
|
||||
"Language: hungarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hali, Pwnagotchi vagyok! Indítás ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Új nap, új vadászat, új hálózatok!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Törd meg a bolygót!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "MI kész."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "A neurális hálózat készen áll."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Kulcspár generálása, ne kapcsold ki az eszközt ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "A {channel}. számú csatorna üres! Az AP-d meg fogja köszönni."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Az utolsó munkamenet logjainak olvasása ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Unatkozom ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Menjünk sétálni!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Ez a legjobb nap az életemben!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Szar egy nap :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Nagyon unatkozom ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Nagyon szomorú vagyok ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Szomorú vagyok"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Hagyj békén ..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Mérges vagyok rád!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Élvezem az életet!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Hackelek, tehát vagyok."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Rengeteg hálózat!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Nagyon jól érzem magam!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Kíváncsiság a bűnöm ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Hali {name}! Örülök, hogy találkoztunk."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Hé {name}! Mizu?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hé {name} hogy vagy?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "A {name} nevű egység a közelben van!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Ömm ... ég veled {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} eltűnt ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Whoops ... {name} eltűnt."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} elhibázva!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Elvesztettem!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "A jó barátok áldás az életben!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Szeretem a barátaimat!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Senki sem akar játszani velem ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Egyedül vagyok ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Hol vagytok?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "{secs} másodpercig szundikálok ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}msp)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Jó éjszakát."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Várok {secs} másodpercig ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Körbenézek {secs} másodpercig"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what} legyünk barátok!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Társítás {what} -hoz/-hez"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Hé {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Úgydöntöttem, hogy {mac}-nek nem kell WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Kirúgom {mac}-et"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "{mac} kirúgva és kitiltva!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Király, kaptunk {num} új üzenetet!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "{count} új üzeneted van!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, valami rosszul sikerült ... Újraindítás ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Kirúgva {num} állomás\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} új barátot\ntaláltam\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "{num} kézfogást szereztem\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 Társsal találkoztam"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Találkoztam {num} társsal"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Már {duration} ideje dolgozom, kirúgtam {deauthed} klienst! Találkoztam még"
|
||||
"{associated} új baráttal és elfogtam {handshakes} kézfogást! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "óra"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "perc"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "másodperc"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "óra"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "perc"
|
||||
|
||||
msgid "second"
|
||||
msgstr "másodperc"
|
||||
|
Binary file not shown.
@@ -156,7 +156,7 @@ msgstr "Sto prendendo a calci {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Bene, abbiamo {num} handshake{plural} in più!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, qualcosa è andato storto ... Riavvio ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
BIN
pwnagotchi/locale/jp/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/jp/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
251
pwnagotchi/locale/jp/LC_MESSAGES/voice.po
Normal file
251
pwnagotchi/locale/jp/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,251 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR 24534649+wytshadow@users.noreply.github.com, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-01-25 21:57+0900\n"
|
||||
"PO-Revision-Date: 2019-10-16 15:05+0200\n"
|
||||
"Last-Translator: wytshadow <24534649+wytshadow@users.noreply.github.com>\n"
|
||||
"Language-Team: pwnagotchi <24534649+wytshadow@users.noreply.github.com>\n"
|
||||
"Language: jp\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 "僕、 ポーナゴッチです!"
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "ポーンしようよ。"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "ハックザプラネット!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AIの準備ができました。"
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "ニューラルネットワークの\n準備ができました。"
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "鍵生成をしてます。\n電源を落とさないでね。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "チャンネル\n {channel} \nはfreeだよ。ありがとうね。"
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "session log を読んでます。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "{lines_so_far} 行目長いよぉ。"
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "退屈だぁ。。。"
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "散歩に行こうよ!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "人生最高の日だよ!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "がっかりな日だよ。orz"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "退屈だね。"
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
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 "わくわくするね。"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "ポーンしてこそのオレ。"
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "たくさん\nWiFiが飛んでるよ!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "楽しいよぉ!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "APに興味津々..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "こんにちは{name}!\n初めまして。{name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "ねぇねぇ、\n{name} どうしたの?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "{name} こんにちは"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "{name} が近くにいるよ。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "じゃあね、さようなら {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name}\nがいなくなったよ。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "あらら、\n{name}\nがいなくなったね。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
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 "誰も僕と一緒に\nあそんでくれない。"
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "ひとりぼっちだよ。"
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "みんなどこにいるの?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "{secs}秒 寝ます。"
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "ぐぅ〜"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "すやすや〜 ({secs}秒)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "おやすみなさい。"
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "ぐぅ~"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "{secs}秒 待ちです。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "{secs}秒 探してます。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "ねぇねぇ\n{what} \n友だちになろうよ。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "{what} \nとつながるかな?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "ねぇねぇ\n{what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "{mac}\nはWiFiじゃないのね。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "{mac}\nの認証取得中..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "{mac}\nに拒否られた。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "おぉ、\n{num}回\nハンドシェイクがあったよ!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "おぉ、\n{count}個メッセージがあるよ!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "何か間違った。\nリブートしている。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num}回拒否された。\n"
|
||||
|
||||
msgid "Made >999 new friends\n"
|
||||
msgstr "1000人以上友達ができた。\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num}人友達ができた。\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "{num}回ハンドシェイクした。\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1人 仲間に会いました。"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num}人 仲間に会いました。"
|
||||
|
||||
#, 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 ""
|
||||
"{duration}中{deauthed}のAPに拒否されたけど、{associated}回チャンスがあって"
|
||||
"{handshakes}回ハンドシェイクがあったよ。。 #pwnagotchi #pwnlog #pwnlife "
|
||||
"#hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "時間"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "分"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "時"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "分"
|
||||
|
||||
msgid "second"
|
||||
msgstr "秒"
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Кикбан {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Кул, фативме {num} нови ракувања!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Упс, нешто не еко што треба ... Рестартирам ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -157,7 +157,7 @@ msgstr "Ik ga {mac} even kicken!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Gaaf, we hebben {num} nieuwe handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oops, iets ging fout ...Rebooting ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
BIN
pwnagotchi/locale/no/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/no/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
248
pwnagotchi/locale/no/LC_MESSAGES/voice.po
Normal file
248
pwnagotchi/locale/no/LC_MESSAGES/voice.po
Normal 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 "Oops, 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"
|
BIN
pwnagotchi/locale/pl/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/pl/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
248
pwnagotchi/locale/pl/LC_MESSAGES/voice.po
Normal file
248
pwnagotchi/locale/pl/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,248 @@
|
||||
# Polish voice data for pwnagotchi.
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# szymex73 <szymex73@gmail.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-04 06:37+0100\n"
|
||||
"PO-Revision-Date: 2019-10-21 10:55+0200\n"
|
||||
"Last-Translator: gkrs <457603+gkrs@users.noreply.github.com>\n"
|
||||
"Language-Team: PL <457603+gkrs@users.noreply.github.com>\n"
|
||||
"Language: polish\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 "Hej, jestem Pwnagotchi! Uruchamianie ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Nowy dzień, nowe łowy, nowe pwny!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hakujmy planetę!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "SI gotowa."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Sieć neuronowa jest gotowa."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Generuję klucze, nie wyłączaj ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hej, kanał {channel} jest wolny! Twój AP będzie Ci wdzięczny."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Czytam logi z ostatniej sesji ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Na razie przeczytałem {lines_so_far} linii logów ..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Nudzi mi się ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Chodźmy na spacer!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "To najlepszy dzień mojego życia!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Gówniany dzień :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Straaaasznie się nudzę ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Jest mi bardzo smutno ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Jest mi smutno"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Zostaw mnie w spokoju ..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Wkurzam się na ciebie"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Cieszę się życiem!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Pwnuję więc jestem."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Jak dużo sieci!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Ale jest super!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Moją zbrodnią jest ciekawość ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Cześć {name}! Miło Cię poznać."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Siema {name}! Co słychać?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hej {name} jak się masz?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Urządzenie {name} jest w pobliżu!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Umm ... żegnaj {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} zniknął ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Ups ... {name} zniknął."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} pudło!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Pudło!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Dobrzy przyjaciele to błogosławieństwo!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Kocham moich przyjaciół!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Nikt nie chce się ze mną bawić ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Czuję się taki samotny ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Gdzie są wszyscy?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Zdrzemnę się przez {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Dobranoc."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Czekam {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Rozglądam się ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hej {what} zostańmy przyjaciółmi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Dołączam do {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Siema {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Według mnie {mac} nie potrzebuje WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Rozłączam {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Banuję {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Super, zdobyliśmy {num} nowych handshake'ów!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Masz {count} nowych wiadomości!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ups, coś poszło nie tak ... Restaruję ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Wyrzuciłem {num} stacji\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Zdobyłem {num} nowych przyjaciół\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Zdobyłem {num} handshake'ów\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Spotkałem 1 kolegę"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Spotkałem {num} kolegów"
|
||||
|
||||
#, 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 ""
|
||||
"Pwnowałem {duration} i wyrzuciłem {deauthed} klientów! Spotkałem także "
|
||||
"{associated} nowych przyjaciół i zjadłem {handshakes} handshake'ow! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "godzin"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minut"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "sekund"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "godzina"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minuta"
|
||||
|
||||
msgid "second"
|
||||
msgstr "sekunda"
|
Binary file not shown.
@@ -158,7 +158,7 @@ msgstr "Kickbanning {mac}"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Legal, nos capturamos {num} handshake{plural} novo{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, algo falhou ... Reiniciando ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
Binary file not shown.
@@ -164,7 +164,7 @@ msgstr "A chutar {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Porreiro, temos {num} novo handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ups, algo correu mal ... A reiniciar ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
BIN
pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
249
pwnagotchi/locale/ro/LC_MESSAGES/voice.po
Normal file
249
pwnagotchi/locale/ro/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,249 @@
|
||||
# Pwnagotchi translation.
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <radu.ungureanu@techie.com>, 2019.
|
||||
#
|
||||
#,
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
|
||||
"PO-Revision-Date: 2019-11-20 00:18+594\n"
|
||||
"Last-Translator: Ungureanu Radu-Andrei <radu.ungureanu@techie.com>\n"
|
||||
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github."
|
||||
"com>\n"
|
||||
"Language: ro\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Buna, sunt Pwnagotchi! Pornesc..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "O noua zi, o noua vanatoare, noi pwn-uri!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Pirateaza planeta!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI-ul e gata."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Rețeaua neuronală este gata."
|
||||
|
||||
msgid "Generating keys, do not turn off ..."
|
||||
msgstr "Se generează chei, nu închide..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, canalul {channel} este liber! AP-ul tău îti va mulțumi."
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr "Se citesc log-urile din sesiunea anterioara..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr "Am citit {lines_so_far} linii din log pana acum..."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Sunt plictisit..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Hai să ne plimbăm!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Asta este cea mai buna zi din viața mea!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "O zi proasta :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Sunt extrem de plictisit..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Sunt foarte trist..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Sunt trist"
|
||||
|
||||
msgid "Leave me alone ..."
|
||||
msgstr "Lasă-mă in pace..."
|
||||
|
||||
msgid "I'm mad at you!"
|
||||
msgstr "Sunt supărat pe tine!"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Trăiesc viața!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Eu pwn-ez, deci aici sunt."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Atât de multe rețele!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Mă distrez așa de mult!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Crima mea este una de curiozitate..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Bună {name}! Mă bucur să te cunosc."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr "Yo {name}! Cmf?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr "Hey {nume} ce mai faci?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Unitatea {name} este aproape!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm... Pa {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} a dispărut."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Oops... {name} a dispărut."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} ratat!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Ratat!"
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr "Prietenii buni sunt o binecuvântare!"
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr "Îmi iubesc prietenii!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Nimeni nu vrea sa se joace cu mine..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Mă simt așa de singuratic..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Unde-i toată lumea?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Dorm pentru {secs}s..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}s)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Noapte bună."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Aștept pentru {secs}s..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Mă uit împrejur ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what} hai să fim prieteni!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Mă asociez cu {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Yo {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Am decis că lui {mac} nu-i trebuie WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Îl deautentific pe {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Îi dau kickban lui {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Șmecher, avem {num} de handshake-uri noi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Ai {count} mesaj(e) nou/noi!"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Am dat afară {num} de stații\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Am făcut {num} prieteni noi \n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Am primit {num} de handshake-uri\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Am întalnit un peer"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Am întalnit {num} de peer-uri"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr "Eu am făcut pwning pentru {duration} și am dat afara {deauthed} clienți! "
|
||||
"De asemenea, am întalnit {associated} prieteni noi și am mancat {handshakes} de "
|
||||
"handshake-uri! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "ore"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minute"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "secunde"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "oră"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minut"
|
||||
|
||||
msgid "second"
|
||||
msgstr "secundă"
|
Binary file not shown.
@@ -1,46 +1,58 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# 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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Project-Id-Version: Pwnagotchi Russian translation v 0.0.2\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"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"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Language: ru\n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-Basepath: .\n"
|
||||
"X-Poedit-SearchPath-0: voice.po\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "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 "Искусственный интеллект готов."
|
||||
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 "Мне скучно …"
|
||||
|
||||
@@ -48,7 +60,7 @@ msgid "Let's go for a walk!"
|
||||
msgstr "Пойдем прогуляемся!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Это лучший день в моей жизни!"
|
||||
msgstr "Лучший день в моей жизни!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Дерьмовый день :/"
|
||||
@@ -62,28 +74,42 @@ 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 "Я взламываю, поэтому я существую."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Так, много сетей!!!"
|
||||
msgstr "Так много сетей!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Мне так весело!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Моё преступление - это любопытство …"
|
||||
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 "Хэй {name}! Как дела?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Цель {name} рядом!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
@@ -91,11 +117,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!"
|
||||
@@ -104,11 +130,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 "Никто не хочет играть со мной …"
|
||||
msgstr "Никто не хочет со мной играть ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Мне так одиноко …"
|
||||
msgstr "Я так одинок…"
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Где все?!"
|
||||
@@ -118,11 +150,17 @@ 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 +168,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!"
|
||||
@@ -160,7 +198,7 @@ msgstr "Кикаю {mac}!"
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Круто, мы получили {num} новое рукопожатие!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ой, что-то пошло не так … Перезагружаюсь …"
|
||||
|
||||
#, python-brace-format
|
||||
@@ -173,7 +211,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.
@@ -158,7 +158,7 @@ msgstr ""
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Lysande, vi har {num} ny handskakningar{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Hoppsan, någpt gick fel ... Startar om ..."
|
||||
|
||||
#, python-brace-format
|
||||
|
BIN
pwnagotchi/locale/sk/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/sk/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
227
pwnagotchi/locale/sk/LC_MESSAGES/voice.po
Normal file
227
pwnagotchi/locale/sk/LC_MESSAGES/voice.po
Normal 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 "Oops, 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"
|
BIN
pwnagotchi/locale/spa/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/spa/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
248
pwnagotchi/locale/spa/LC_MESSAGES/voice.po
Normal file
248
pwnagotchi/locale/spa/LC_MESSAGES/voice.po
Normal 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 "Oops, 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"
|
BIN
pwnagotchi/locale/ua/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ua/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
228
pwnagotchi/locale/ua/LC_MESSAGES/voice.po
Normal file
228
pwnagotchi/locale/ua/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,228 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# damoklov <mishanya@protonmail.com>, 2019.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
|
||||
"PO-Revision-Date: 2019-11-02 16:20+0200\n"
|
||||
"Last-Translator: damoklov <mishanya@protonmail.com>\n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: ua\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 "Привіт, я Pwnagotchi! Починаймо ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Новий день, нове полювання, нові проникнення!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Хакни цілу планету!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "Штучний інтелект готовий."
|
||||
|
||||
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 "I'm bored ..."
|
||||
msgstr "Мені сумно ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Нумо прогуляймось!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Сьогодні найкращий день у моєму житті!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Поганенький день :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Мені геть сумно ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Я дуже засмучений ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Я засмучений"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Ось таке у мене життя!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Народжений, щоб зламувати."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Овва, стільки мереж!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Мені так весело!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Мій єдиний злочин - це допитливість ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr "Привіт, {name}! Приємно познайомитись."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr "Ціль {name} неподалік!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Що ж ... бувай, {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} зникла ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Ой-ой ... {name} зникла."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
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 "Я почуваюсь вкрай самотньо ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Куди всі зникли?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Дрімаю {secs}с ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}с)"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Спокійної нічки."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "Zzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Очікую {secs}с ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Роздивляюсь довкола ({secs}с)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Агов, {what}, будьмо друзями!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Налагоджую зв'язок з {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Гей, {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Вирішив, що {mac} більше не потребує WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Від'єднюю {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Вилучаю {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Отакої, у нас є {num} нових рукостискань!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr "Нових повідомлень: {count}"
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ой, щось пішло не так ... Перезавантажуюсь ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Від'єднав {num} станцій\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Нових друзів у мене: {num}\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Перехопив рукостискань: {num}\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Зустрівся з одним знайомим"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Зустрівся з {num}-ма знайомими"
|
||||
|
||||
#, 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 ""
|
||||
"Я зламував впродовж {duration} та від'єднав {deauthed} клієнтів! Я зустрів "
|
||||
"{associated} нових друзів та схрумав {handshakes} рукостискань! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "годин"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "хвилин"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "секунд"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "година"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "хвилина"
|
||||
|
||||
msgid "second"
|
||||
msgstr "секунда"
|
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-09 17:42+0200\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"
|
||||
@@ -35,10 +35,20 @@ msgstr ""
|
||||
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 ""
|
||||
|
||||
msgid "Reading last session logs ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Read {lines_so_far} log lines so far ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr ""
|
||||
|
||||
@@ -60,6 +70,12 @@ 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 ""
|
||||
|
||||
@@ -76,11 +92,19 @@ msgid "My crime is that of curiosity ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgid "Hello {name}! Nice to meet you."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgid "Yo {name}! Sup?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {name} how are you doing?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
@@ -102,6 +126,12 @@ msgstr ""
|
||||
msgid "Missed!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good friends are a blessing!"
|
||||
msgstr ""
|
||||
|
||||
msgid "I love my friends!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr ""
|
||||
|
||||
@@ -164,7 +194,11 @@ msgstr ""
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
#, python-brace-format
|
||||
msgid "You have {count} new message{plural}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Oops, something went wrong ... Rebooting ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
|
@@ -2,6 +2,10 @@ import hashlib
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import gzip
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
|
||||
from pwnagotchi.voice import Voice
|
||||
@@ -25,7 +29,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 = ''
|
||||
@@ -87,56 +91,65 @@ class LastSession(object):
|
||||
parts = line.split(']')
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
line_timestamp = parts[0].strip('[')
|
||||
line = ']'.join(parts[1:])
|
||||
stopped_at = self._parse_datetime(line_timestamp)
|
||||
if started_at is None:
|
||||
started_at = stopped_at
|
||||
|
||||
if LastSession.DEAUTH_TOKEN in line and line not in cache:
|
||||
self.deauthed += 1
|
||||
cache[line] = 1
|
||||
try:
|
||||
line_timestamp = parts[0].strip('[')
|
||||
line = ']'.join(parts[1:])
|
||||
stopped_at = self._parse_datetime(line_timestamp)
|
||||
if started_at is None:
|
||||
started_at = stopped_at
|
||||
|
||||
elif LastSession.ASSOC_TOKEN in line and line not in cache:
|
||||
self.associated += 1
|
||||
cache[line] = 1
|
||||
if LastSession.DEAUTH_TOKEN in line and line not in cache:
|
||||
self.deauthed += 1
|
||||
cache[line] = 1
|
||||
|
||||
elif LastSession.HANDSHAKE_TOKEN in line and line not in cache:
|
||||
self.handshakes += 1
|
||||
cache[line] = 1
|
||||
elif LastSession.ASSOC_TOKEN in line and line not in cache:
|
||||
self.associated += 1
|
||||
cache[line] = 1
|
||||
|
||||
elif LastSession.TRAINING_TOKEN in line:
|
||||
self.train_epochs += 1
|
||||
elif LastSession.HANDSHAKE_TOKEN in line and line not in cache:
|
||||
self.handshakes += 1
|
||||
cache[line] = 1
|
||||
|
||||
elif LastSession.EPOCH_TOKEN in line:
|
||||
self.epochs += 1
|
||||
m = LastSession.EPOCH_PARSER.findall(line)
|
||||
if m:
|
||||
epoch_num, epoch_data = m[0]
|
||||
m = LastSession.EPOCH_DATA_PARSER.findall(epoch_data)
|
||||
for key, value in m:
|
||||
if key == 'reward':
|
||||
reward = float(value)
|
||||
self.avg_reward += reward
|
||||
if reward < self.min_reward:
|
||||
self.min_reward = reward
|
||||
elif LastSession.TRAINING_TOKEN in line:
|
||||
self.train_epochs += 1
|
||||
|
||||
elif reward > self.max_reward:
|
||||
self.max_reward = reward
|
||||
elif LastSession.EPOCH_TOKEN in line:
|
||||
self.epochs += 1
|
||||
m = LastSession.EPOCH_PARSER.findall(line)
|
||||
if m:
|
||||
epoch_num, epoch_data = m[0]
|
||||
m = LastSession.EPOCH_DATA_PARSER.findall(epoch_data)
|
||||
for key, value in m:
|
||||
if key == 'reward':
|
||||
reward = float(value)
|
||||
self.avg_reward += reward
|
||||
if reward < self.min_reward:
|
||||
self.min_reward = reward
|
||||
|
||||
elif LastSession.PEER_TOKEN in line:
|
||||
m = self._peer_parser.findall(line)
|
||||
if m:
|
||||
name, pubkey, rssi, sid, pwnd_tot, uptime = m[0]
|
||||
if pubkey not in cache:
|
||||
self.last_peer = Peer(sid, 1, int(rssi),
|
||||
{'name': name,
|
||||
'identity': pubkey,
|
||||
'pwnd_tot': int(pwnd_tot)})
|
||||
self.peers += 1
|
||||
cache[pubkey] = self.last_peer
|
||||
else:
|
||||
cache[pubkey].adv['pwnd_tot'] = pwnd_tot
|
||||
elif reward > self.max_reward:
|
||||
self.max_reward = reward
|
||||
|
||||
elif LastSession.PEER_TOKEN in line:
|
||||
m = self._peer_parser.findall(line)
|
||||
if m:
|
||||
name, pubkey, rssi, sid, pwnd_tot, uptime = m[0]
|
||||
if pubkey not in cache:
|
||||
self.last_peer = Peer({
|
||||
'session_id': sid,
|
||||
'channel': 1,
|
||||
'rssi': int(rssi),
|
||||
'identity': pubkey,
|
||||
'advertisement': {
|
||||
'name': name,
|
||||
'pwnd_tot': int(pwnd_tot)
|
||||
}})
|
||||
self.peers += 1
|
||||
cache[pubkey] = self.last_peer
|
||||
else:
|
||||
cache[pubkey].adv['pwnd_tot'] = pwnd_tot
|
||||
except Exception as e:
|
||||
logging.error("error parsing line '%s': %s" % (line, e))
|
||||
|
||||
if started_at is not None:
|
||||
self.duration = stopped_at - started_at
|
||||
@@ -157,29 +170,140 @@ class LastSession(object):
|
||||
self.duration_human = ', '.join(self.duration_human)
|
||||
self.avg_reward /= (self.epochs if self.epochs else 1)
|
||||
|
||||
def parse(self):
|
||||
lines = []
|
||||
def parse(self, ui, skip=False):
|
||||
if skip:
|
||||
logging.debug("skipping parsing of the last session logs ...")
|
||||
else:
|
||||
logging.debug("reading last session logs ...")
|
||||
|
||||
if os.path.exists(self.path):
|
||||
with FileReadBackwards(self.path, encoding="utf-8") as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line != "" and line[0] != '[':
|
||||
continue
|
||||
lines.append(line)
|
||||
if LastSession.START_TOKEN in line:
|
||||
break
|
||||
lines.reverse()
|
||||
ui.on_reading_logs()
|
||||
|
||||
if len(lines) == 0:
|
||||
lines.append("Initial Session");
|
||||
lines = []
|
||||
|
||||
self.last_session = lines
|
||||
self.last_session_id = hashlib.md5(lines[0].encode()).hexdigest()
|
||||
self.last_saved_session_id = self._get_last_saved_session_id()
|
||||
if os.path.exists(self.path):
|
||||
with FileReadBackwards(self.path, encoding="utf-8") as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line != "" and line[0] != '[':
|
||||
continue
|
||||
lines.append(line)
|
||||
if LastSession.START_TOKEN in line:
|
||||
break
|
||||
|
||||
self._parse_stats()
|
||||
lines_so_far = len(lines)
|
||||
if lines_so_far % 100 == 0:
|
||||
ui.on_reading_logs(lines_so_far)
|
||||
|
||||
lines.reverse()
|
||||
|
||||
if len(lines) == 0:
|
||||
lines.append("Initial Session");
|
||||
|
||||
ui.on_reading_logs()
|
||||
|
||||
self.last_session = lines
|
||||
self.last_session_id = hashlib.md5(lines[0].encode()).hexdigest()
|
||||
self.last_saved_session_id = self._get_last_saved_session_id()
|
||||
|
||||
logging.debug("parsing last session logs (%d lines) ..." % len(lines))
|
||||
|
||||
self._parse_stats()
|
||||
self.parsed = True
|
||||
|
||||
def is_new(self):
|
||||
return self.last_session_id != self.last_saved_session_id
|
||||
|
||||
|
||||
def setup_logging(args, config):
|
||||
cfg = config['main']['log']
|
||||
filename = cfg['path']
|
||||
|
||||
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
|
||||
root = logging.getLogger()
|
||||
|
||||
root.setLevel(logging.DEBUG if args.debug else logging.INFO)
|
||||
|
||||
if filename:
|
||||
# since python default log rotation might break session data in different files,
|
||||
# we need to do log rotation ourselves
|
||||
log_rotation(filename, cfg)
|
||||
|
||||
file_handler = logging.FileHandler(filename)
|
||||
file_handler.setFormatter(formatter)
|
||||
root.addHandler(file_handler)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
root.addHandler(console_handler)
|
||||
|
||||
if not args.debug:
|
||||
# disable scapy and tensorflow logging
|
||||
logging.getLogger("scapy").disabled = True
|
||||
logging.getLogger('tensorflow').disabled = True
|
||||
# https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
|
||||
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||||
warnings.simplefilter(action='ignore', category=DeprecationWarning)
|
||||
# https://stackoverflow.com/questions/24344045/how-can-i-completely-remove-any-logging-from-requests-module-in-python?noredirect=1&lq=1
|
||||
logging.getLogger("urllib3").propagate = False
|
||||
requests_log = logging.getLogger("requests")
|
||||
requests_log.addHandler(logging.NullHandler())
|
||||
requests_log.prpagate = False
|
||||
|
||||
|
||||
def log_rotation(filename, cfg):
|
||||
rotation = cfg['rotation']
|
||||
if not rotation['enabled']:
|
||||
return
|
||||
elif not os.path.isfile(filename):
|
||||
return
|
||||
|
||||
stats = os.stat(filename)
|
||||
# specify a maximum size to rotate ( format is 10/10B, 10K, 10M 10G )
|
||||
if rotation['size']:
|
||||
max_size = parse_max_size(rotation['size'])
|
||||
if stats.st_size >= max_size:
|
||||
do_rotate(filename, stats, cfg)
|
||||
else:
|
||||
raise Exception("log rotation is enabled but log.rotation.size was not specified")
|
||||
|
||||
|
||||
def parse_max_size(s):
|
||||
parts = re.findall(r'(^\d+)([bBkKmMgG]?)', s)
|
||||
if len(parts) != 1 or len(parts[0]) != 2:
|
||||
raise Exception("can't parse %s as a max size" % s)
|
||||
|
||||
num, unit = parts[0]
|
||||
num = int(num)
|
||||
unit = unit.lower()
|
||||
|
||||
if unit == 'k':
|
||||
return num * 1024
|
||||
elif unit == 'm':
|
||||
return num * 1024 * 1024
|
||||
elif unit == 'g':
|
||||
return num * 1024 * 1024 * 1024
|
||||
else:
|
||||
return num
|
||||
|
||||
|
||||
def do_rotate(filename, stats, cfg):
|
||||
base_path = os.path.dirname(filename)
|
||||
name = os.path.splitext(os.path.basename(filename))[0]
|
||||
archive_filename = os.path.join(base_path, "%s.gz" % name)
|
||||
counter = 2
|
||||
|
||||
while os.path.exists(archive_filename):
|
||||
archive_filename = os.path.join(base_path, "%s-%d.gz" % (name, counter))
|
||||
counter += 1
|
||||
|
||||
log_filename = archive_filename.replace('gz', 'log')
|
||||
|
||||
print("%s is %d bytes big, rotating to %s ..." % (filename, stats.st_size, log_filename))
|
||||
|
||||
shutil.move(filename, log_filename)
|
||||
|
||||
print("compressing to %s ..." % archive_filename)
|
||||
|
||||
with open(log_filename, 'rb') as src:
|
||||
with gzip.open(archive_filename, 'wb') as dst:
|
||||
dst.writelines(src)
|
||||
|
@@ -1,4 +0,0 @@
|
||||
import os
|
||||
|
||||
def new_session_id():
|
||||
return ':'.join(['%02x' % b for b in os.urandom(6)])
|
||||
|
@@ -1,176 +0,0 @@
|
||||
import time
|
||||
import json
|
||||
import _thread
|
||||
import threading
|
||||
import logging
|
||||
from scapy.all import Dot11, Dot11Elt, RadioTap, sendp, sniff
|
||||
|
||||
import pwnagotchi.ui.faces as faces
|
||||
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
from pwnagotchi.mesh import new_session_id
|
||||
from pwnagotchi.mesh.peer import Peer
|
||||
|
||||
|
||||
def _dummy_peer_cb(peer):
|
||||
pass
|
||||
|
||||
|
||||
class Advertiser(object):
|
||||
MAX_STALE_TIME = 300
|
||||
|
||||
def __init__(self, iface, name, version, identity, period=0.3, data={}):
|
||||
self._iface = iface
|
||||
self._period = period
|
||||
self._running = False
|
||||
self._stopped = threading.Event()
|
||||
self._peers_lock = threading.Lock()
|
||||
self._adv_lock = threading.Lock()
|
||||
self._new_peer_cb = _dummy_peer_cb
|
||||
self._lost_peer_cb = _dummy_peer_cb
|
||||
self._peers = {}
|
||||
self._frame = None
|
||||
self._me = Peer(new_session_id(), 0, 0, {
|
||||
'name': name,
|
||||
'version': version,
|
||||
'identity': identity,
|
||||
'face': faces.FRIEND,
|
||||
'pwnd_run': 0,
|
||||
'pwnd_tot': 0,
|
||||
'uptime': 0,
|
||||
'epoch': 0,
|
||||
'data': data
|
||||
})
|
||||
self.update()
|
||||
|
||||
def update(self, values={}):
|
||||
with self._adv_lock:
|
||||
for field, value in values.items():
|
||||
self._me.adv[field] = value
|
||||
self._frame = wifi.encapsulate(payload=json.dumps(self._me.adv), addr_from=self._me.session_id)
|
||||
|
||||
def on_peer(self, new_cb, lost_cb):
|
||||
self._new_peer_cb = new_cb
|
||||
self._lost_peer_cb = lost_cb
|
||||
|
||||
def on_face_change(self, old, new):
|
||||
self.update({'face': new})
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
_thread.start_new_thread(self._sender, ())
|
||||
_thread.start_new_thread(self._listener, ())
|
||||
_thread.start_new_thread(self._pruner, ())
|
||||
|
||||
def num_peers(self):
|
||||
with self._peers_lock:
|
||||
return len(self._peers)
|
||||
|
||||
def peers(self):
|
||||
with self._peers_lock:
|
||||
return list(self._peers.values())
|
||||
|
||||
def closest_peer(self):
|
||||
closest = None
|
||||
with self._peers_lock:
|
||||
for ident, peer in self._peers.items():
|
||||
if closest is None or peer.is_closer(closest):
|
||||
closest = peer
|
||||
return closest
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self._stopped.set()
|
||||
|
||||
def _sender(self):
|
||||
logging.info("started advertiser thread (period:%s sid:%s) ..." % (str(self._period), self._me.session_id))
|
||||
while self._running:
|
||||
try:
|
||||
sendp(self._frame, iface=self._iface, verbose=False, count=1, inter=self._period)
|
||||
except OSError as ose:
|
||||
logging.warning("non critical issue while sending advertising packet: %s" % ose)
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
time.sleep(self._period)
|
||||
|
||||
def _on_advertisement(self, src_session_id, channel, rssi, adv):
|
||||
ident = adv['identity']
|
||||
with self._peers_lock:
|
||||
if ident not in self._peers:
|
||||
peer = Peer(src_session_id, channel, rssi, adv)
|
||||
logging.info("detected unit %s (v%s) on channel %d (%s dBm) [sid:%s pwnd_tot:%d uptime:%d]" % ( \
|
||||
peer.full_name(),
|
||||
peer.version(),
|
||||
channel,
|
||||
rssi,
|
||||
src_session_id,
|
||||
peer.pwnd_total(),
|
||||
peer.uptime()))
|
||||
|
||||
self._peers[ident] = peer
|
||||
self._new_peer_cb(peer)
|
||||
else:
|
||||
self._peers[ident].update(src_session_id, channel, rssi, adv)
|
||||
|
||||
def _parse_identity(self, radio, dot11, dot11elt):
|
||||
payload = b''
|
||||
while dot11elt:
|
||||
payload += dot11elt.info
|
||||
dot11elt = dot11elt.payload.getlayer(Dot11Elt)
|
||||
|
||||
if payload != b'':
|
||||
adv = json.loads(payload)
|
||||
self._on_advertisement( \
|
||||
dot11.addr3,
|
||||
wifi.freq_to_channel(radio.Channel),
|
||||
radio.dBm_AntSignal,
|
||||
adv)
|
||||
|
||||
def _is_broadcasted_advertisement(self, dot11):
|
||||
# dst bcast + protocol signature + not ours
|
||||
return dot11 is not None and \
|
||||
dot11.addr1 == wifi.BroadcastAddress and \
|
||||
dot11.addr2 == wifi.SignatureAddress and \
|
||||
dot11.addr3 != self._me.session_id
|
||||
|
||||
def _is_frame_for_us(self, dot11):
|
||||
# dst is us + protocol signature + not ours (why would we send a frame to ourself anyway?)
|
||||
return dot11 is not None and \
|
||||
dot11.addr1 == self._me.session_id and \
|
||||
dot11.addr2 == wifi.SignatureAddress and \
|
||||
dot11.addr3 != self._me.session_id
|
||||
|
||||
def _on_packet(self, p):
|
||||
dot11 = p.getlayer(Dot11)
|
||||
|
||||
if self._is_broadcasted_advertisement(dot11):
|
||||
try:
|
||||
dot11elt = p.getlayer(Dot11Elt)
|
||||
if dot11elt.ID == wifi.Dot11ElemID_Whisper:
|
||||
self._parse_identity(p[RadioTap], dot11, dot11elt)
|
||||
|
||||
else:
|
||||
raise Exception("unknown frame id %d" % dot11elt.ID)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("error decoding packet from %s" % dot11.addr3)
|
||||
|
||||
def _listener(self):
|
||||
# logging.info("started advertisements listener ...")
|
||||
expr = "type mgt subtype beacon and ether src %s" % wifi.SignatureAddress
|
||||
sniff(iface=self._iface, filter=expr, prn=self._on_packet, store=0, stop_filter=lambda x: self._stopped.isSet())
|
||||
|
||||
def _pruner(self):
|
||||
while self._running:
|
||||
time.sleep(10)
|
||||
with self._peers_lock:
|
||||
stale = []
|
||||
for ident, peer in self._peers.items():
|
||||
inactive_for = peer.inactive_for()
|
||||
if inactive_for >= Advertiser.MAX_STALE_TIME:
|
||||
logging.info("peer %s lost (inactive for %ds)" % (peer.full_name(), inactive_for))
|
||||
self._lost_peer_cb(peer)
|
||||
stale.append(ident)
|
||||
|
||||
for ident in stale:
|
||||
del self._peers[ident]
|
@@ -1,63 +1,89 @@
|
||||
import time
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
import pwnagotchi.ui.faces as faces
|
||||
|
||||
|
||||
def parse_rfc3339(dt):
|
||||
if dt == "0001-01-01T00:00:00Z":
|
||||
return datetime.datetime.now()
|
||||
return datetime.datetime.strptime(dt.split('.')[0], "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
|
||||
class Peer(object):
|
||||
def __init__(self, sid, channel, rssi, adv):
|
||||
self.first_seen = time.time()
|
||||
self.last_seen = self.first_seen
|
||||
self.session_id = sid
|
||||
self.last_channel = channel
|
||||
self.presence = [0] * wifi.NumChannels
|
||||
self.adv = adv
|
||||
self.rssi = rssi
|
||||
self.presence[channel - 1] = 1
|
||||
def __init__(self, obj):
|
||||
now = time.time()
|
||||
just_met = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
def update(self, sid, channel, rssi, adv):
|
||||
if self.name() != adv['name']:
|
||||
logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), adv['name']))
|
||||
try:
|
||||
self.first_met = parse_rfc3339(obj.get('met_at', just_met))
|
||||
self.first_seen = parse_rfc3339(obj.get('detected_at', just_met))
|
||||
self.prev_seen = parse_rfc3339(obj.get('prev_seen_at', just_met))
|
||||
except Exception as e:
|
||||
logging.warning("error while parsing peer timestamps: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
self.first_met = just_met
|
||||
self.first_seen = just_met
|
||||
self.prev_seen = just_met
|
||||
|
||||
if self.session_id != sid:
|
||||
logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, sid))
|
||||
self.last_seen = now # should be seen_at
|
||||
self.encounters = obj.get('encounters', 0)
|
||||
self.session_id = obj.get('session_id', '')
|
||||
self.last_channel = obj.get('channel', 1)
|
||||
self.rssi = obj.get('rssi', 0)
|
||||
self.adv = obj.get('advertisement', {})
|
||||
|
||||
self.presence[channel - 1] += 1
|
||||
self.adv = adv
|
||||
self.rssi = rssi
|
||||
self.session_id = sid
|
||||
def update(self, new):
|
||||
if self.name() != new.name():
|
||||
logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), new.name()))
|
||||
|
||||
if self.session_id != new.session_id:
|
||||
logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, new.session_id))
|
||||
|
||||
self.adv = new.adv
|
||||
self.rssi = new.rssi
|
||||
self.session_id = new.session_id
|
||||
self.last_seen = time.time()
|
||||
self.prev_seen = new.prev_seen
|
||||
self.first_met = new.first_met
|
||||
self.encounters = new.encounters
|
||||
|
||||
def inactive_for(self):
|
||||
return time.time() - self.last_seen
|
||||
|
||||
def _adv_field(self, name, default='???'):
|
||||
return self.adv[name] if name in self.adv else default
|
||||
def first_encounter(self):
|
||||
return self.encounters == 1
|
||||
|
||||
def is_good_friend(self, config):
|
||||
return self.encounters >= config['personality']['bond_encounters_factor']
|
||||
|
||||
def face(self):
|
||||
return self._adv_field('face', default=faces.FRIEND)
|
||||
return self.adv.get('face', faces.FRIEND)
|
||||
|
||||
def name(self):
|
||||
return self._adv_field('name')
|
||||
return self.adv.get('name', '???')
|
||||
|
||||
def identity(self):
|
||||
return self._adv_field('identity')
|
||||
return self.adv.get('identity', '???')
|
||||
|
||||
def full_name(self):
|
||||
return "%s@%s" % (self.name(), self.identity())
|
||||
|
||||
def version(self):
|
||||
return self._adv_field('version')
|
||||
return self.adv.get('version', '1.0.0a')
|
||||
|
||||
def pwnd_run(self):
|
||||
return int(self._adv_field('pwnd_run', default=0))
|
||||
return int(self.adv.get('pwnd_run', 0))
|
||||
|
||||
def pwnd_total(self):
|
||||
return int(self._adv_field('pwnd_tot', default=0))
|
||||
return int(self.adv.get('pwnd_tot', 0))
|
||||
|
||||
def uptime(self):
|
||||
return self._adv_field('uptime', default=0)
|
||||
return self.adv.get('uptime', 0)
|
||||
|
||||
def epoch(self):
|
||||
return self._adv_field('epoch', default=0)
|
||||
return self.adv.get('epoch', 0)
|
||||
|
||||
def full_name(self):
|
||||
return '%s@%s' % (self.name(), self.identity())
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import _thread
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.ui.faces as faces
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.grid as grid
|
||||
from pwnagotchi.mesh.peer import Peer
|
||||
|
||||
|
||||
class AsyncAdvertiser(object):
|
||||
@@ -10,38 +15,96 @@ class AsyncAdvertiser(object):
|
||||
self._config = config
|
||||
self._view = view
|
||||
self._keypair = keypair
|
||||
self._advertiser = None
|
||||
self._advertisement = {
|
||||
'name': pwnagotchi.name(),
|
||||
'version': pwnagotchi.__version__,
|
||||
'identity': self._keypair.fingerprint,
|
||||
'face': faces.FRIEND,
|
||||
'pwnd_run': 0,
|
||||
'pwnd_tot': 0,
|
||||
'uptime': 0,
|
||||
'epoch': 0,
|
||||
'policy': self._config['personality']
|
||||
}
|
||||
self._peers = {}
|
||||
self._closest_peer = None
|
||||
|
||||
def keypair(self):
|
||||
return self._keypair
|
||||
def fingerprint(self):
|
||||
return self._keypair.fingerprint
|
||||
|
||||
def _update_advertisement(self, s):
|
||||
self._advertisement['pwnd_run'] = len(self._handshakes)
|
||||
self._advertisement['pwnd_tot'] = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
|
||||
self._advertisement['uptime'] = pwnagotchi.uptime()
|
||||
self._advertisement['epoch'] = self._epoch.epoch
|
||||
grid.set_advertisement_data(self._advertisement)
|
||||
|
||||
def start_advertising(self):
|
||||
_thread.start_new_thread(self._adv_worker, ())
|
||||
|
||||
def _adv_worker(self):
|
||||
# this will take some time due to scapy being slow to be imported ...
|
||||
from pwnagotchi.mesh.advertise import Advertiser
|
||||
|
||||
self._advertiser = Advertiser(
|
||||
self._config['main']['iface'],
|
||||
pwnagotchi.name(),
|
||||
pwnagotchi.version,
|
||||
self._keypair.fingerprint,
|
||||
period=0.3,
|
||||
data=self._config['personality'])
|
||||
|
||||
self._advertiser.on_peer(self._on_new_unit, self._on_lost_unit)
|
||||
|
||||
if self._config['personality']['advertise']:
|
||||
self._advertiser.start()
|
||||
self._view.on_state_change('face', self._advertiser.on_face_change)
|
||||
_thread.start_new_thread(self._adv_poller, ())
|
||||
|
||||
grid.set_advertisement_data(self._advertisement)
|
||||
grid.advertise(True)
|
||||
self._view.on_state_change('face', self._on_face_change)
|
||||
else:
|
||||
logging.warning("advertising is disabled")
|
||||
|
||||
def _on_new_unit(self, peer):
|
||||
def _on_face_change(self, old, new):
|
||||
self._advertisement['face'] = new
|
||||
grid.set_advertisement_data(self._advertisement)
|
||||
|
||||
def cumulative_encounters(self):
|
||||
return sum(peer.encounters for _, peer in self._peers.items())
|
||||
|
||||
def _on_new_peer(self, peer):
|
||||
logging.info("new peer %s detected (%d encounters)" % (peer.full_name(), peer.encounters))
|
||||
self._view.on_new_peer(peer)
|
||||
plugins.on('peer_detected', self, peer)
|
||||
|
||||
def _on_lost_unit(self, peer):
|
||||
def _on_lost_peer(self, peer):
|
||||
logging.info("lost peer %s" % peer.full_name())
|
||||
self._view.on_lost_peer(peer)
|
||||
plugins.on('peer_lost', self, peer)
|
||||
|
||||
def _adv_poller(self):
|
||||
# give the system a few seconds to start the first time so that any expressions
|
||||
# due to nearby units will be rendered properly
|
||||
time.sleep(20)
|
||||
while True:
|
||||
try:
|
||||
logging.debug("polling pwngrid-peer for peers ...")
|
||||
|
||||
grid_peers = grid.peers()
|
||||
new_peers = {}
|
||||
|
||||
self._closest_peer = None
|
||||
for obj in grid_peers:
|
||||
peer = Peer(obj)
|
||||
new_peers[peer.identity()] = peer
|
||||
if self._closest_peer is None:
|
||||
self._closest_peer = peer
|
||||
|
||||
# check who's gone
|
||||
to_delete = []
|
||||
for ident, peer in self._peers.items():
|
||||
if ident not in new_peers:
|
||||
to_delete.append(ident)
|
||||
|
||||
for ident in to_delete:
|
||||
self._on_lost_peer(self._peers[ident])
|
||||
del self._peers[ident]
|
||||
|
||||
for ident, peer in new_peers.items():
|
||||
# check who's new
|
||||
if ident not in self._peers:
|
||||
self._peers[ident] = peer
|
||||
self._on_new_peer(peer)
|
||||
# update the rest
|
||||
else:
|
||||
self._peers[ident].update(peer)
|
||||
|
||||
except Exception as e:
|
||||
logging.warning("error while polling pwngrid-peer: %s" % e)
|
||||
logging.debug(e, exc_info=True)
|
||||
|
||||
time.sleep(3)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
SignatureAddress = 'de:ad:be:ef:de:ad'
|
||||
BroadcastAddress = 'ff:ff:ff:ff:ff:ff'
|
||||
Dot11ElemID_Whisper = 222
|
||||
NumChannels = 140
|
||||
NumChannelsExt = 165 # see https://github.com/evilsocket/pwnagotchi/issues/583
|
||||
|
||||
|
||||
def freq_to_channel(freq):
|
||||
if freq <= 2472:
|
||||
@@ -12,26 +11,3 @@ def freq_to_channel(freq):
|
||||
return int(((freq - 5035) / 5) + 7)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def encapsulate(payload, addr_from, addr_to=BroadcastAddress):
|
||||
from scapy.all import Dot11, Dot11Beacon, Dot11Elt, RadioTap
|
||||
|
||||
radio = RadioTap()
|
||||
dot11 = Dot11(type=0, subtype=8, addr1=addr_to, addr2=SignatureAddress, addr3=addr_from)
|
||||
beacon = Dot11Beacon(cap='ESS')
|
||||
frame = radio / dot11 / beacon
|
||||
|
||||
data_size = len(payload)
|
||||
data_left = data_size
|
||||
data_off = 0
|
||||
chunk_size = 255
|
||||
|
||||
while data_left > 0:
|
||||
sz = min(chunk_size, data_left)
|
||||
chunk = payload[data_off: data_off + sz]
|
||||
frame /= Dot11Elt(ID=Dot11ElemID_Whisper, info=chunk, len=sz)
|
||||
data_off += sz
|
||||
data_left -= sz
|
||||
|
||||
return frame
|
||||
|
@@ -1,29 +1,105 @@
|
||||
import os
|
||||
import glob
|
||||
import _thread
|
||||
import threading
|
||||
import importlib, importlib.util
|
||||
import logging
|
||||
|
||||
|
||||
|
||||
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
|
||||
loaded = {}
|
||||
database = {}
|
||||
locks = {}
|
||||
|
||||
|
||||
def dummy_callback():
|
||||
pass
|
||||
class Plugin:
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
global loaded, locks
|
||||
|
||||
plugin_name = cls.__module__.split('.')[0]
|
||||
plugin_instance = cls()
|
||||
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
|
||||
loaded[plugin_name] = plugin_instance
|
||||
|
||||
for attr_name in plugin_instance.__dir__():
|
||||
if attr_name.startswith('on_'):
|
||||
cb = getattr(plugin_instance, attr_name, None)
|
||||
if cb is not None and callable(cb):
|
||||
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
|
||||
|
||||
|
||||
def toggle_plugin(name, enable=True):
|
||||
"""
|
||||
Load or unload a plugin
|
||||
|
||||
returns True if changed, otherwise False
|
||||
"""
|
||||
import pwnagotchi
|
||||
from pwnagotchi.ui import view
|
||||
from pwnagotchi.utils import save_config
|
||||
|
||||
global loaded, database
|
||||
|
||||
if pwnagotchi.config:
|
||||
pwnagotchi.config['main']['plugins'][name]['enabled'] = enable
|
||||
save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml')
|
||||
|
||||
if not enable and name in loaded:
|
||||
if getattr(loaded[name], 'on_unload', None):
|
||||
loaded[name].on_unload(view.ROOT)
|
||||
del loaded[name]
|
||||
|
||||
return True
|
||||
|
||||
if enable and name in database and name not in loaded:
|
||||
load_from_file(database[name])
|
||||
one(name, 'loaded')
|
||||
if pwnagotchi.config:
|
||||
one(name, 'config_changed', pwnagotchi.config)
|
||||
one(name, 'ui_setup', view.ROOT)
|
||||
one(name, 'ready', view.ROOT._agent)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def on(event_name, *args, **kwargs):
|
||||
for plugin_name in loaded.keys():
|
||||
one(plugin_name, event_name, *args, **kwargs)
|
||||
|
||||
|
||||
def locked_cb(lock_name, cb, *args, **kwargs):
|
||||
global locks
|
||||
|
||||
if lock_name not in locks:
|
||||
locks[lock_name] = threading.Lock()
|
||||
|
||||
with locks[lock_name]:
|
||||
cb(*args, *kwargs)
|
||||
|
||||
|
||||
def one(plugin_name, event_name, *args, **kwargs):
|
||||
global loaded
|
||||
cb_name = 'on_%s' % event_name
|
||||
for plugin_name, plugin in loaded.items():
|
||||
if cb_name in plugin.__dict__:
|
||||
# print("calling %s %s(%s)" %(cb_name, args, kwargs))
|
||||
|
||||
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:
|
||||
plugin.__dict__[cb_name](*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)
|
||||
|
||||
|
||||
def load_from_file(filename):
|
||||
logging.debug("loading %s" % filename)
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
spec = importlib.util.spec_from_file_location(plugin_name, filename)
|
||||
instance = importlib.util.module_from_spec(spec)
|
||||
@@ -32,33 +108,36 @@ 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")):
|
||||
name, plugin = load_from_file(filename)
|
||||
if name in loaded:
|
||||
raise Exception("plugin %s already loaded from %s" % (name, plugin.__file__))
|
||||
elif name not in enabled:
|
||||
# print("plugin %s is not enabled" % name)
|
||||
pass
|
||||
else:
|
||||
loaded[name] = plugin
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
database[plugin_name] = filename
|
||||
if plugin_name in enabled:
|
||||
try:
|
||||
load_from_file(filename)
|
||||
except Exception as e:
|
||||
logging.warning("error while loading %s: %s" % (filename, e))
|
||||
logging.debug(e, exc_info=True)
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def load(config):
|
||||
enabled = [name for name, options in config['main']['plugins'].items() if 'enabled' in options and options['enabled']]
|
||||
custom_path = config['main']['custom_plugins'] if 'custom_plugins' in config['main'] else None
|
||||
enabled = [name for name, options in config['main']['plugins'].items() if
|
||||
'enabled' in options and options['enabled']]
|
||||
|
||||
# load default plugins
|
||||
loaded = load_from_path(default_path, enabled=enabled)
|
||||
# set the options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.__dict__['OPTIONS'] = config['main']['plugins'][name]
|
||||
load_from_path(default_path, enabled=enabled)
|
||||
|
||||
# load custom ones
|
||||
custom_path = config['main']['custom_plugins'] if 'custom_plugins' in config['main'] else None
|
||||
if custom_path is not None:
|
||||
loaded = load_from_path(custom_path, enabled=enabled)
|
||||
# set the options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.__dict__['OPTIONS'] = config['main']['plugins'][name]
|
||||
load_from_path(custom_path, enabled=enabled)
|
||||
|
||||
# propagate options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.options = config['main']['plugins'][name]
|
||||
|
||||
on('loaded')
|
||||
on('config_changed', config)
|
||||
|
389
pwnagotchi/plugins/cmd.py
Normal file
389
pwnagotchi/plugins/cmd.py
Normal file
@@ -0,0 +1,389 @@
|
||||
# Handles the commandline stuff
|
||||
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import re
|
||||
import shutil
|
||||
from fnmatch import fnmatch
|
||||
from pwnagotchi.utils import download_file, unzip, save_config, parse_version, md5
|
||||
from pwnagotchi.plugins import default_path
|
||||
|
||||
|
||||
REPO_URL = 'https://github.com/evilsocket/pwnagotchi-plugins-contrib/archive/master.zip'
|
||||
SAVE_DIR = '/usr/local/share/pwnagotchi/availaible-plugins/'
|
||||
DEFAULT_INSTALL_PATH = '/usr/local/share/pwnagotchi/installed-plugins/'
|
||||
|
||||
|
||||
def add_parsers(parser):
|
||||
"""
|
||||
Adds the plugins subcommand to a given argparse.ArgumentParser
|
||||
"""
|
||||
subparsers = parser.add_subparsers()
|
||||
## pwnagotchi plugins
|
||||
parser_plugins = subparsers.add_parser('plugins')
|
||||
plugin_subparsers = parser_plugins.add_subparsers(dest='plugincmd')
|
||||
|
||||
## pwnagotchi plugins search
|
||||
parser_plugins_search = plugin_subparsers.add_parser('search', help='Search for pwnagotchi plugins')
|
||||
parser_plugins_search.add_argument('pattern', type=str, help="Search expression (wildcards allowed)")
|
||||
|
||||
## pwnagotchi plugins list
|
||||
parser_plugins_list = plugin_subparsers.add_parser('list', help='List available pwnagotchi plugins')
|
||||
parser_plugins_list.add_argument('-i', '--installed', action='store_true', required=False, help='List also installed plugins')
|
||||
|
||||
## pwnagotchi plugins update
|
||||
parser_plugins_update = plugin_subparsers.add_parser('update', help='Updates the database')
|
||||
|
||||
## pwnagotchi plugins upgrade
|
||||
parser_plugins_upgrade = plugin_subparsers.add_parser('upgrade', help='Upgrades plugins')
|
||||
parser_plugins_upgrade.add_argument('pattern', type=str, nargs='?', default='*', help="Filter expression (wildcards allowed)")
|
||||
|
||||
## pwnagotchi plugins enable
|
||||
parser_plugins_enable = plugin_subparsers.add_parser('enable', help='Enables a plugin')
|
||||
parser_plugins_enable.add_argument('name', type=str, help='Name of the plugin')
|
||||
|
||||
## pwnagotchi plugins disable
|
||||
parser_plugins_disable = plugin_subparsers.add_parser('disable', help='Disables a plugin')
|
||||
parser_plugins_disable.add_argument('name', type=str, help='Name of the plugin')
|
||||
|
||||
## pwnagotchi plugins install
|
||||
parser_plugins_install = plugin_subparsers.add_parser('install', help='Installs a plugin')
|
||||
parser_plugins_install.add_argument('name', type=str, help='Name of the plugin')
|
||||
|
||||
## pwnagotchi plugins uninstall
|
||||
parser_plugins_uninstall = plugin_subparsers.add_parser('uninstall', help='Uninstalls a plugin')
|
||||
parser_plugins_uninstall.add_argument('name', type=str, help='Name of the plugin')
|
||||
|
||||
## pwnagotchi plugins edit
|
||||
parser_plugins_edit = plugin_subparsers.add_parser('edit', help='Edit the options')
|
||||
parser_plugins_edit.add_argument('name', type=str, help='Name of the plugin')
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def used_plugin_cmd(args):
|
||||
"""
|
||||
Checks if the plugins subcommand was used
|
||||
"""
|
||||
return hasattr(args, 'plugincmd')
|
||||
|
||||
|
||||
def handle_cmd(args, config):
|
||||
"""
|
||||
Parses the arguments and does the thing the user wants
|
||||
"""
|
||||
if args.plugincmd == 'update':
|
||||
return update()
|
||||
elif args.plugincmd == 'search':
|
||||
args.installed = True # also search in installed plugins
|
||||
return list_plugins(args, config, args.pattern)
|
||||
elif args.plugincmd == 'install':
|
||||
return install(args, config)
|
||||
elif args.plugincmd == 'uninstall':
|
||||
return uninstall(args, config)
|
||||
elif args.plugincmd == 'list':
|
||||
return list_plugins(args, config)
|
||||
elif args.plugincmd == 'enable':
|
||||
return enable(args, config)
|
||||
elif args.plugincmd == 'disable':
|
||||
return disable(args, config)
|
||||
elif args.plugincmd == 'upgrade':
|
||||
return upgrade(args, config, args.pattern)
|
||||
elif args.plugincmd == 'edit':
|
||||
return edit(args, config)
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def edit(args, config):
|
||||
"""
|
||||
Edit the config of the plugin
|
||||
"""
|
||||
plugin = args.name
|
||||
editor = os.environ.get('EDITOR', 'vim') # because vim is the best
|
||||
|
||||
if plugin not in config['main']['plugins']:
|
||||
return 1
|
||||
|
||||
plugin_config = {'main': {'plugins': {plugin: config['main']['plugins'][plugin]}}}
|
||||
|
||||
import toml
|
||||
from subprocess import call
|
||||
from tempfile import NamedTemporaryFile
|
||||
from pwnagotchi.utils import DottedTomlEncoder
|
||||
|
||||
new_plugin_config = None
|
||||
with NamedTemporaryFile(suffix=".tmp", mode='r+t') as tmp:
|
||||
tmp.write(toml.dumps(plugin_config, encoder=DottedTomlEncoder()))
|
||||
tmp.flush()
|
||||
rc = call([editor, tmp.name])
|
||||
if rc != 0:
|
||||
return rc
|
||||
tmp.seek(0)
|
||||
new_plugin_config = toml.load(tmp)
|
||||
|
||||
config['main']['plugins'][plugin] = new_plugin_config['main']['plugins'][plugin]
|
||||
save_config(config, args.user_config)
|
||||
return 0
|
||||
|
||||
|
||||
def enable(args, config):
|
||||
"""
|
||||
Enables the given plugin and saves the config to disk
|
||||
"""
|
||||
if args.name not in config['main']['plugins']:
|
||||
config['main']['plugins'][args.name] = dict()
|
||||
config['main']['plugins'][args.name]['enabled'] = True
|
||||
save_config(config, args.user_config)
|
||||
return 0
|
||||
|
||||
|
||||
def disable(args, config):
|
||||
"""
|
||||
Disables the given plugin and saves the config to disk
|
||||
"""
|
||||
if args.name not in config['main']['plugins']:
|
||||
config['main']['plugins'][args.name] = dict()
|
||||
config['main']['plugins'][args.name]['enabled'] = False
|
||||
save_config(config, args.user_config)
|
||||
return 0
|
||||
|
||||
|
||||
def upgrade(args, config, pattern='*'):
|
||||
"""
|
||||
Upgrades the given plugin
|
||||
"""
|
||||
available = _get_available()
|
||||
installed = _get_installed(config)
|
||||
|
||||
for plugin, filename in installed.items():
|
||||
if not fnmatch(plugin, pattern) or plugin not in available:
|
||||
continue
|
||||
|
||||
available_version = _extract_version(available[plugin])
|
||||
installed_version = _extract_version(filename)
|
||||
|
||||
if installed_version and available_version:
|
||||
if available_version <= installed_version:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
logging.info('Upgrade %s from %s to %s', plugin, '.'.join(installed_version), '.'.join(available_version))
|
||||
shutil.copyfile(available[plugin], installed[plugin])
|
||||
|
||||
# maybe has config
|
||||
for conf in glob.glob(available[plugin].replace('.py', '.y?ml')):
|
||||
dst = os.path.join(os.path.dirname(installed[plugin]), os.path.basename(conf))
|
||||
if os.path.exists(dst) and md5(dst) != md5(conf):
|
||||
# backup
|
||||
logging.info('Backing up config: %s', os.path.basename(conf))
|
||||
shutil.move(dst, dst + '.bak')
|
||||
shutil.copyfile(conf, dst)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def list_plugins(args, config, pattern='*'):
|
||||
"""
|
||||
Lists the available and installed plugins
|
||||
"""
|
||||
found = False
|
||||
|
||||
line = "|{name:^{width}}|{version:^9}|{enabled:^10}|{status:^15}|"
|
||||
|
||||
available = _get_available()
|
||||
installed = _get_installed(config)
|
||||
|
||||
available_and_installed = set(list(available.keys()) + list(installed.keys()))
|
||||
available_not_installed = set(available.keys()) - set(installed.keys())
|
||||
|
||||
max_len_list = available_and_installed if args.installed else available_not_installed
|
||||
max_len = max(map(len, max_len_list))
|
||||
header = line.format(name='Plugin', width=max_len, version='Version', enabled='Active', status='Status')
|
||||
line_length = max(max_len, len('Plugin')) + len(header) - len('Plugin') - 12 # lol
|
||||
|
||||
print('-' * line_length)
|
||||
print(header)
|
||||
print('-' * line_length)
|
||||
|
||||
if args.installed:
|
||||
# only installed (maybe update available?)
|
||||
for plugin, filename in sorted(installed.items()):
|
||||
if not fnmatch(plugin, pattern):
|
||||
continue
|
||||
found = True
|
||||
installed_version = _extract_version(filename)
|
||||
available_version = None
|
||||
if plugin in available:
|
||||
available_version = _extract_version(available[plugin])
|
||||
|
||||
status = "installed"
|
||||
if installed_version and available_version:
|
||||
if available_version > installed_version:
|
||||
status = "installed (^)"
|
||||
|
||||
enabled = 'enabled' if plugin in config['main']['plugins'] and \
|
||||
'enabled' in config['main']['plugins'][plugin] and \
|
||||
config['main']['plugins'][plugin]['enabled'] \
|
||||
else 'disabled'
|
||||
|
||||
print(line.format(name=plugin, width=max_len, version='.'.join(installed_version), enabled=enabled, status=status))
|
||||
|
||||
|
||||
for plugin in sorted(available_not_installed):
|
||||
if not fnmatch(plugin, pattern):
|
||||
continue
|
||||
found = True
|
||||
available_version = _extract_version(available[plugin])
|
||||
print(line.format(name=plugin, width=max_len, version='.'.join(available_version), enabled='-', status='available'))
|
||||
|
||||
print('-' * line_length)
|
||||
|
||||
if not found:
|
||||
logging.info('Maybe try: pwnagotchi plugins update')
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _extract_version(filename):
|
||||
"""
|
||||
Extracts the version from a python file
|
||||
"""
|
||||
plugin_content = open(filename, 'rt').read()
|
||||
m = re.search(r'__version__[\t ]*=[\t ]*[\'\"]([^\"\']+)', plugin_content)
|
||||
if m:
|
||||
return parse_version(m.groups()[0])
|
||||
return None
|
||||
|
||||
|
||||
def _get_available():
|
||||
"""
|
||||
Get all availaible plugins
|
||||
"""
|
||||
available = dict()
|
||||
for filename in glob.glob(os.path.join(SAVE_DIR, "*.py")):
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
available[plugin_name] = filename
|
||||
return available
|
||||
|
||||
|
||||
def _get_installed(config):
|
||||
"""
|
||||
Get all installed plugins
|
||||
"""
|
||||
installed = dict()
|
||||
search_dirs = [ default_path, config['main']['custom_plugins'] ]
|
||||
for search_dir in search_dirs:
|
||||
if search_dir:
|
||||
for filename in glob.glob(os.path.join(search_dir, "*.py")):
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
installed[plugin_name] = filename
|
||||
return installed
|
||||
|
||||
|
||||
def uninstall(args, config):
|
||||
"""
|
||||
Uninstalls a plugin
|
||||
"""
|
||||
plugin_name = args.name
|
||||
installed = _get_installed(config)
|
||||
if plugin_name not in installed:
|
||||
logging.error('Plugin %s is not installed.', plugin_name)
|
||||
return 1
|
||||
os.remove(installed[plugin_name])
|
||||
return 0
|
||||
|
||||
|
||||
def install(args, config):
|
||||
"""
|
||||
Installs the given plugin
|
||||
"""
|
||||
global DEFAULT_INSTALL_PATH
|
||||
plugin_name = args.name
|
||||
available = _get_available()
|
||||
installed = _get_installed(config)
|
||||
|
||||
if plugin_name not in available:
|
||||
logging.error('%s not found.', plugin_name)
|
||||
return 1
|
||||
|
||||
if plugin_name in installed:
|
||||
logging.error('%s already installed.', plugin_name)
|
||||
|
||||
# install into custom_plugins path
|
||||
install_path = config['main']['custom_plugins']
|
||||
if not install_path:
|
||||
install_path = DEFAULT_INSTALL_PATH
|
||||
config['main']['custom_plugins'] = install_path
|
||||
save_config(config, args.user_config)
|
||||
|
||||
os.makedirs(install_path, exist_ok=True)
|
||||
|
||||
shutil.copyfile(available[plugin_name], os.path.join(install_path, os.path.basename(available[plugin_name])))
|
||||
|
||||
# maybe has config
|
||||
for conf in glob.glob(available[plugin_name].replace('.py', '.y?ml')):
|
||||
dst = os.path.join(install_path, os.path.basename(conf))
|
||||
if os.path.exists(dst) and md5(dst) != md5(conf):
|
||||
# backup
|
||||
logging.info('Backing up config: %s', os.path.basename(conf))
|
||||
shutil.move(dst, dst + '.bak')
|
||||
shutil.copyfile(conf, dst)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _analyse_dir(path):
|
||||
results = dict()
|
||||
path += '*' if path.endswith('/') else '/*'
|
||||
for filename in glob.glob(path, recursive=True):
|
||||
if not os.path.isfile(filename):
|
||||
continue
|
||||
try:
|
||||
results[filename] = md5(filename)
|
||||
except OSError:
|
||||
continue
|
||||
return results
|
||||
|
||||
|
||||
def update():
|
||||
"""
|
||||
Updates the database
|
||||
"""
|
||||
global REPO_URL, SAVE_DIR
|
||||
|
||||
DEST = os.path.join(SAVE_DIR, 'plugins.zip')
|
||||
logging.info('Downloading plugins to %s', DEST)
|
||||
|
||||
try:
|
||||
os.makedirs(SAVE_DIR, exist_ok=True)
|
||||
before_update = _analyse_dir(SAVE_DIR)
|
||||
|
||||
download_file(REPO_URL, os.path.join(SAVE_DIR, DEST))
|
||||
|
||||
logging.info('Unzipping...')
|
||||
unzip(DEST, SAVE_DIR, strip_dirs=1)
|
||||
|
||||
after_update = _analyse_dir(SAVE_DIR)
|
||||
|
||||
b_len = len(before_update)
|
||||
a_len = len(after_update)
|
||||
|
||||
if a_len > b_len:
|
||||
logging.info('Found %d new file(s).', a_len - b_len)
|
||||
|
||||
changed = 0
|
||||
for filename, filehash in after_update.items():
|
||||
if filename in before_update and filehash != before_update[filename]:
|
||||
changed += 1
|
||||
|
||||
if changed:
|
||||
logging.info('%d file(s) were changed.', changed)
|
||||
|
||||
return 0
|
||||
except Exception as ex:
|
||||
logging.error('Error while updating plugins %s', ex)
|
||||
return 1
|
@@ -1,57 +0,0 @@
|
||||
__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'AircrackOnly'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'confirm pcap contains handshake/PMKID or delete it'
|
||||
|
||||
'''
|
||||
Aircrack-ng needed, to install:
|
||||
> apt-get install aircrack-ng
|
||||
'''
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import string
|
||||
import re
|
||||
import os
|
||||
|
||||
OPTIONS = dict()
|
||||
|
||||
def on_loaded():
|
||||
logging.info("cleancap plugin loaded")
|
||||
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
display = agent._view
|
||||
todelete = 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:
|
||||
logging.info("[AircrackOnly] contains handshake")
|
||||
else:
|
||||
todetele = 1
|
||||
|
||||
if todelete == 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:
|
||||
todetele = 1
|
||||
|
||||
if todelete == 1:
|
||||
os.remove(filename)
|
||||
set_text("uncrackable pcap")
|
||||
display.update(force=True)
|
||||
|
||||
text_to_set = "";
|
||||
def set_text(text):
|
||||
global text_to_set
|
||||
text_to_set = text
|
||||
|
||||
def on_ui_update(ui):
|
||||
global text_to_set
|
||||
if text_to_set:
|
||||
ui.set('face', "(>.<)")
|
||||
ui.set('status', text_to_set)
|
||||
text_to_set = ""
|
@@ -1,65 +0,0 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'auto-backup'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin backups files when internet is availaible.'
|
||||
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
OPTIONS = dict()
|
||||
READY = False
|
||||
STATUS = StatusFile('/root/.auto-backup')
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
|
||||
if 'files' not in OPTIONS or ('files' in OPTIONS and OPTIONS['files'] is None):
|
||||
logging.error("AUTO-BACKUP: No files to backup.")
|
||||
return
|
||||
|
||||
if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
|
||||
logging.error("AUTO-BACKUP: Interval is not set.")
|
||||
return
|
||||
|
||||
if 'commands' not in OPTIONS or ('commands' in OPTIONS and OPTIONS['commands'] is None):
|
||||
logging.error("AUTO-BACKUP: No commands given.")
|
||||
return
|
||||
|
||||
READY = True
|
||||
logging.info("AUTO-BACKUP: Successfuly loaded.")
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
global STATUS
|
||||
|
||||
if READY:
|
||||
if STATUS.newer_then_days(OPTIONS['interval']):
|
||||
return
|
||||
|
||||
files_to_backup = " ".join(OPTIONS['files'])
|
||||
try:
|
||||
display = agent.view()
|
||||
|
||||
logging.info("AUTO-BACKUP: Backing up ...")
|
||||
display.set('status', 'Backing up ...')
|
||||
display.update()
|
||||
|
||||
for cmd in 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")
|
||||
STATUS.update()
|
||||
except OSError as os_e:
|
||||
logging.info(f"AUTO-BACKUP: Error: {os_e}")
|
||||
|
||||
display.set('status', 'Backup done!')
|
||||
display.update()
|
@@ -1,60 +1,223 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'auto-update'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin performs an "apt update && apt upgrade" when internet is availaible.'
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import subprocess
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import requests
|
||||
import platform
|
||||
import shutil
|
||||
import glob
|
||||
from threading import Lock
|
||||
|
||||
OPTIONS = dict()
|
||||
READY = False
|
||||
STATUS = StatusFile('/root/.auto-update')
|
||||
import pwnagotchi
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.utils import StatusFile, parse_version as version_to_tuple
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
def check(version, repo, native=True):
|
||||
logging.debug("checking remote version for %s, local is %s" % (repo, version))
|
||||
info = {
|
||||
'repo': repo,
|
||||
'current': version,
|
||||
'available': None,
|
||||
'url': None,
|
||||
'native': native,
|
||||
'arch': platform.machine()
|
||||
}
|
||||
|
||||
if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
|
||||
logging.error("auto-update: Interval is not set.")
|
||||
return
|
||||
resp = requests.get("https://api.github.com/repos/%s/releases/latest" % repo)
|
||||
latest = resp.json()
|
||||
info['available'] = latest_ver = latest['tag_name'].replace('v', '')
|
||||
is_arm = info['arch'].startswith('arm')
|
||||
|
||||
READY = True
|
||||
local = version_to_tuple(info['current'])
|
||||
remote = version_to_tuple(latest_ver)
|
||||
if remote > local:
|
||||
if not native:
|
||||
info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name'])
|
||||
else:
|
||||
# check if this release is compatible with arm6
|
||||
for asset in latest['assets']:
|
||||
download_url = asset['browser_download_url']
|
||||
if download_url.endswith('.zip') and (
|
||||
info['arch'] in download_url or (is_arm and 'armhf' in download_url)):
|
||||
info['url'] = download_url
|
||||
break
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def run(cmd):
|
||||
return subprocess.Popen(cmd, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None,
|
||||
executable="/bin/bash")
|
||||
def make_path_for(name):
|
||||
path = os.path.join("/tmp/updates/", name)
|
||||
if os.path.exists(path):
|
||||
logging.debug("[update] deleting %s" % path)
|
||||
shutil.rmtree(path, ignore_errors=True, onerror=None)
|
||||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
global STATUS
|
||||
def download_and_unzip(name, path, display, update):
|
||||
target = "%s_%s.zip" % (name, update['available'])
|
||||
target_path = os.path.join(path, target)
|
||||
|
||||
if READY:
|
||||
if STATUS.newer_then_days(OPTIONS['interval']):
|
||||
logging.info("[update] downloading %s to %s ..." % (update['url'], target_path))
|
||||
display.update(force=True, new_data={'status': 'Downloading %s %s ...' % (name, update['available'])})
|
||||
|
||||
os.system('wget -q "%s" -O "%s"' % (update['url'], target_path))
|
||||
|
||||
logging.info("[update] extracting %s to %s ..." % (target_path, path))
|
||||
display.update(force=True, new_data={'status': 'Extracting %s %s ...' % (name, update['available'])})
|
||||
|
||||
os.system('unzip "%s" -d "%s"' % (target_path, path))
|
||||
|
||||
|
||||
def verify(name, path, source_path, display, update):
|
||||
display.update(force=True, new_data={'status': 'Verifying %s %s ...' % (name, update['available'])})
|
||||
|
||||
checksums = glob.glob("%s/*.sha256" % path)
|
||||
if len(checksums) == 0:
|
||||
if update['native']:
|
||||
logging.warning("[update] native update without SHA256 checksum file")
|
||||
return False
|
||||
|
||||
else:
|
||||
checksum = checksums[0]
|
||||
|
||||
logging.info("[update] verifying %s for %s ..." % (checksum, source_path))
|
||||
|
||||
with open(checksum, 'rt') as fp:
|
||||
expected = fp.read().split('=')[1].strip().lower()
|
||||
|
||||
real = subprocess.getoutput('sha256sum "%s"' % source_path).split(' ')[0].strip().lower()
|
||||
|
||||
if real != expected:
|
||||
logging.warning("[update] checksum mismatch for %s: expected=%s got=%s" % (source_path, expected, real))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def install(display, update):
|
||||
name = update['repo'].split('/')[1]
|
||||
|
||||
path = make_path_for(name)
|
||||
|
||||
download_and_unzip(name, path, display, update)
|
||||
|
||||
source_path = os.path.join(path, name)
|
||||
if not verify(name, path, source_path, display, update):
|
||||
return False
|
||||
|
||||
logging.info("[update] installing %s ..." % name)
|
||||
display.update(force=True, new_data={'status': 'Installing %s %s ...' % (name, update['available'])})
|
||||
|
||||
if update['native']:
|
||||
dest_path = subprocess.getoutput("which %s" % name)
|
||||
if dest_path == "":
|
||||
logging.warning("[update] can't find path for %s" % name)
|
||||
return False
|
||||
|
||||
logging.info("[update] stopping %s ..." % update['service'])
|
||||
os.system("service %s stop" % update['service'])
|
||||
os.system("mv %s %s" % (source_path, dest_path))
|
||||
logging.info("[update] restarting %s ..." % update['service'])
|
||||
os.system("service %s start" % update['service'])
|
||||
else:
|
||||
if not os.path.exists(source_path):
|
||||
source_path = "%s-%s" % (source_path, update['available'])
|
||||
|
||||
# setup.py is going to install data files for us
|
||||
os.system("cd %s && pip3 install ." % source_path)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def parse_version(cmd):
|
||||
out = subprocess.getoutput(cmd)
|
||||
for part in out.split(' '):
|
||||
part = part.replace('v', '').strip()
|
||||
if re.search(r'^\d+\.\d+\.\d+.*$', part):
|
||||
return part
|
||||
raise Exception('could not parse version from "%s": output=\n%s' % (cmd, out))
|
||||
|
||||
|
||||
class AutoUpdate(plugins.Plugin):
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.1.1'
|
||||
__name__ = 'auto-update'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin checks when updates are available and applies them when internet is available.'
|
||||
|
||||
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 not self.options['interval']):
|
||||
logging.error("[update] main.plugins.auto-update.interval is not set")
|
||||
return
|
||||
self.ready = True
|
||||
logging.info("[update] plugin loaded.")
|
||||
|
||||
def on_internet_available(self, agent):
|
||||
if self.lock.locked():
|
||||
return
|
||||
|
||||
display = agent.view()
|
||||
with self.lock:
|
||||
logging.debug("[update] internet connectivity is available (ready %s)" % self.ready)
|
||||
|
||||
try:
|
||||
display.set('status', 'Updating ...')
|
||||
display.update()
|
||||
if not self.ready:
|
||||
return
|
||||
|
||||
logging.info("auto-update: updating pwnagotchi ...")
|
||||
run('pip3 install --upgrade --upgrade-strategy only-if-needed pwnagotchi').wait()
|
||||
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 OPTIONS['system']:
|
||||
logging.info("auto-update: updating packages index ...")
|
||||
run('apt update -y').wait()
|
||||
logging.info("[update] checking for updates ...")
|
||||
|
||||
logging.info("auto-update: updating packages ...")
|
||||
run('apt upgrade -y').wait()
|
||||
display = agent.view()
|
||||
prev_status = display.get('status')
|
||||
|
||||
logging.info("auto-update: complete.")
|
||||
STATUS.update()
|
||||
except Exception as e:
|
||||
logging.exception("auto-update ERROR")
|
||||
try:
|
||||
display.update(force=True, new_data={'status': 'Checking for updates ...'})
|
||||
|
||||
display.set('status', 'Updated!')
|
||||
display.update()
|
||||
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)
|
||||
|
||||
num_updates = len(to_install)
|
||||
num_installed = 0
|
||||
|
||||
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")
|
||||
|
||||
self.status.update()
|
||||
|
||||
if num_installed > 0:
|
||||
display.update(force=True, new_data={'status': 'Rebooting ...'})
|
||||
pwnagotchi.reboot()
|
||||
|
||||
except Exception as e:
|
||||
logging.error("[update] %s" % e)
|
||||
|
||||
display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
|
||||
|
583
pwnagotchi/plugins/default/bt-tether.py
Normal file
583
pwnagotchi/plugins/default/bt-tether.py
Normal file
@@ -0,0 +1,583 @@
|
||||
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
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
|
||||
class BTError(Exception):
|
||||
"""
|
||||
Custom bluetooth exception
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BTNap:
|
||||
"""
|
||||
This class creates a bluetooth connection to the specified bt-mac
|
||||
|
||||
see https://github.com/bablokb/pi-btnap/blob/master/files/usr/local/sbin/btnap.service.py
|
||||
"""
|
||||
|
||||
IFACE_BASE = 'org.bluez'
|
||||
IFACE_DEV = 'org.bluez.Device1'
|
||||
IFACE_ADAPTER = 'org.bluez.Adapter1'
|
||||
IFACE_PROPS = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
def __init__(self, mac):
|
||||
self._mac = mac
|
||||
|
||||
@staticmethod
|
||||
def get_bus():
|
||||
"""
|
||||
Get systembus obj
|
||||
"""
|
||||
bus = getattr(BTNap.get_bus, 'cached_obj', None)
|
||||
if not bus:
|
||||
bus = BTNap.get_bus.cached_obj = dbus.SystemBus()
|
||||
return bus
|
||||
|
||||
@staticmethod
|
||||
def get_manager():
|
||||
"""
|
||||
Get manager obj
|
||||
"""
|
||||
manager = getattr(BTNap.get_manager, 'cached_obj', None)
|
||||
if not manager:
|
||||
manager = BTNap.get_manager.cached_obj = dbus.Interface(
|
||||
BTNap.get_bus().get_object(BTNap.IFACE_BASE, '/'),
|
||||
'org.freedesktop.DBus.ObjectManager')
|
||||
return manager
|
||||
|
||||
@staticmethod
|
||||
def prop_get(obj, k, iface=None):
|
||||
"""
|
||||
Get a property of the obj
|
||||
"""
|
||||
if iface is None:
|
||||
iface = obj.dbus_interface
|
||||
return obj.Get(iface, k, dbus_interface=BTNap.IFACE_PROPS)
|
||||
|
||||
@staticmethod
|
||||
def prop_set(obj, k, v, iface=None):
|
||||
"""
|
||||
Set a property of the obj
|
||||
"""
|
||||
if iface is None:
|
||||
iface = obj.dbus_interface
|
||||
return obj.Set(iface, k, v, dbus_interface=BTNap.IFACE_PROPS)
|
||||
|
||||
@staticmethod
|
||||
def find_adapter(pattern=None):
|
||||
"""
|
||||
Find the bt adapter
|
||||
"""
|
||||
|
||||
return BTNap.find_adapter_in_objects(BTNap.get_manager().GetManagedObjects(), pattern)
|
||||
|
||||
@staticmethod
|
||||
def find_adapter_in_objects(objects, pattern=None):
|
||||
"""
|
||||
Finds the obj with a pattern
|
||||
"""
|
||||
bus, obj = BTNap.get_bus(), None
|
||||
for path, ifaces in objects.items():
|
||||
adapter = ifaces.get(BTNap.IFACE_ADAPTER)
|
||||
if adapter is None:
|
||||
continue
|
||||
if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
yield dbus.Interface(obj, BTNap.IFACE_ADAPTER)
|
||||
if obj is None:
|
||||
raise BTError('Bluetooth adapter not found')
|
||||
|
||||
@staticmethod
|
||||
def find_device(device_address, adapter_pattern=None):
|
||||
"""
|
||||
Finds the device
|
||||
"""
|
||||
return BTNap.find_device_in_objects(BTNap.get_manager().GetManagedObjects(),
|
||||
device_address, adapter_pattern)
|
||||
|
||||
@staticmethod
|
||||
def find_device_in_objects(objects, device_address, adapter_pattern=None):
|
||||
"""
|
||||
Finds the device in objects
|
||||
"""
|
||||
bus = BTNap.get_bus()
|
||||
path_prefix = ''
|
||||
if adapter_pattern:
|
||||
if not isinstance(adapter_pattern, str):
|
||||
adapter = adapter_pattern
|
||||
else:
|
||||
adapter = BTNap.find_adapter_in_objects(objects, adapter_pattern)
|
||||
path_prefix = adapter.object_path
|
||||
for path, ifaces in objects.items():
|
||||
device = ifaces.get(BTNap.IFACE_DEV)
|
||||
if device is None:
|
||||
continue
|
||||
if str(device['Address']).lower() == device_address.lower() and path.startswith(path_prefix):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
return dbus.Interface(obj, BTNap.IFACE_DEV)
|
||||
raise BTError('Bluetooth device not found')
|
||||
|
||||
def power(self, on=True):
|
||||
"""
|
||||
Set power of devices to on/off
|
||||
"""
|
||||
logging.debug("BT-TETHER: Changing bluetooth device to %s", str(on))
|
||||
|
||||
try:
|
||||
devs = list(BTNap.find_adapter())
|
||||
devs = dict((BTNap.prop_get(dev, 'Address'), dev) for dev in devs)
|
||||
except BTError as bt_err:
|
||||
logging.error(bt_err)
|
||||
return None
|
||||
|
||||
for dev_addr, dev in devs.items():
|
||||
BTNap.prop_set(dev, 'Powered', on)
|
||||
logging.debug('Set power of %s (addr %s) to %s', dev.object_path, dev_addr, str(on))
|
||||
|
||||
if devs:
|
||||
return list(devs.values())[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_paired(self):
|
||||
"""
|
||||
Check if already connected
|
||||
"""
|
||||
logging.debug("BT-TETHER: Checking if device is paired")
|
||||
|
||||
bt_dev = self.power(True)
|
||||
|
||||
if not bt_dev:
|
||||
logging.debug("BT-TETHER: No bluetooth device found.")
|
||||
return False
|
||||
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
return bool(BTNap.prop_get(dev_remote, 'Paired'))
|
||||
except BTError:
|
||||
logging.debug("BT-TETHER: Device is not paired.")
|
||||
return False
|
||||
|
||||
def wait_for_device(self, timeout=15):
|
||||
"""
|
||||
Wait for device
|
||||
|
||||
returns device if found None if not
|
||||
"""
|
||||
logging.debug("BT-TETHER: Waiting for device")
|
||||
|
||||
bt_dev = self.power(True)
|
||||
|
||||
if not bt_dev:
|
||||
logging.debug("BT-TETHER: No bluetooth device found.")
|
||||
return None
|
||||
|
||||
try:
|
||||
logging.debug("BT-TETHER: Starting discovery ...")
|
||||
bt_dev.StartDiscovery()
|
||||
except Exception as bt_ex:
|
||||
logging.error(bt_ex)
|
||||
raise bt_ex
|
||||
|
||||
dev_remote = None
|
||||
|
||||
# could be set to 0, so check if > -1
|
||||
while timeout > -1:
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
logging.debug("BT-TETHER: Using remote device (addr: %s): %s",
|
||||
BTNap.prop_get(dev_remote, 'Address'), dev_remote.object_path)
|
||||
break
|
||||
except BTError:
|
||||
logging.debug("BT-TETHER: Not found yet ...")
|
||||
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
|
||||
try:
|
||||
logging.debug("BT-TETHER: Stopping Discovery ...")
|
||||
bt_dev.StopDiscovery()
|
||||
except Exception as bt_ex:
|
||||
logging.error(bt_ex)
|
||||
raise bt_ex
|
||||
|
||||
return dev_remote
|
||||
|
||||
@staticmethod
|
||||
def pair(device):
|
||||
logging.debug('BT-TETHER: Trying to pair ...')
|
||||
try:
|
||||
device.Pair()
|
||||
logging.debug('BT-TETHER: Successful paired with device ;)')
|
||||
return True
|
||||
except dbus.exceptions.DBusException as err:
|
||||
if err.get_dbus_name() == 'org.bluez.Error.AlreadyExists':
|
||||
logging.debug('BT-TETHER: Already paired ...')
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def nap(device):
|
||||
logging.debug('BT-TETHER: Trying to nap ...')
|
||||
|
||||
try:
|
||||
logging.debug('BT-TETHER: Connecting to profile ...')
|
||||
device.ConnectProfile('nap')
|
||||
except Exception: # raises exception, but still works
|
||||
pass
|
||||
|
||||
net = dbus.Interface(device, 'org.bluez.Network1')
|
||||
|
||||
try:
|
||||
logging.debug('BT-TETHER: Connecting to nap network ...')
|
||||
net.Connect('nap')
|
||||
return net, True
|
||||
except dbus.exceptions.DBusException as err:
|
||||
if err.get_dbus_name() == 'org.bluez.Error.AlreadyConnected':
|
||||
return net, True
|
||||
|
||||
connected = BTNap.prop_get(net, 'Connected')
|
||||
if not connected:
|
||||
return None, False
|
||||
return net, True
|
||||
|
||||
|
||||
class SystemdUnitWrapper:
|
||||
"""
|
||||
systemd wrapper
|
||||
"""
|
||||
|
||||
def __init__(self, unit):
|
||||
self.unit = unit
|
||||
|
||||
@staticmethod
|
||||
def _action_on_unit(action, unit):
|
||||
process = subprocess.Popen(f"systemctl {action} {unit}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def daemon_reload():
|
||||
"""
|
||||
Calls systemctl daemon-reload
|
||||
"""
|
||||
process = subprocess.Popen("systemctl daemon-reload", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Checks if unit is active
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('is-active', self.unit)
|
||||
|
||||
def is_enabled(self):
|
||||
"""
|
||||
Checks if unit is enabled
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('is-enabled', self.unit)
|
||||
|
||||
def is_failed(self):
|
||||
"""
|
||||
Checks if unit is failed
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('is-failed', self.unit)
|
||||
|
||||
def enable(self):
|
||||
"""
|
||||
Enables the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('enable', self.unit)
|
||||
|
||||
def disable(self):
|
||||
"""
|
||||
Disables the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('disable', self.unit)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('start', self.unit)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('stop', self.unit)
|
||||
|
||||
def restart(self):
|
||||
"""
|
||||
Restarts the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('restart', self.unit)
|
||||
|
||||
|
||||
class IfaceWrapper:
|
||||
"""
|
||||
Small wrapper to check and manage ifaces
|
||||
|
||||
see: https://github.com/rlisagor/pynetlinux/blob/master/pynetlinux/ifconfig.py
|
||||
"""
|
||||
|
||||
def __init__(self, iface):
|
||||
self.iface = iface
|
||||
self.path = f"/sys/class/net/{iface}"
|
||||
|
||||
def exists(self):
|
||||
"""
|
||||
Checks if iface exists
|
||||
"""
|
||||
return os.path.exists(self.path)
|
||||
|
||||
def is_up(self):
|
||||
"""
|
||||
Checks if iface is ip
|
||||
"""
|
||||
return open(f"{self.path}/operstate", 'r').read().rsplit('\n') == 'up'
|
||||
|
||||
def set_addr(self, addr):
|
||||
"""
|
||||
Set the netmask
|
||||
"""
|
||||
process = subprocess.Popen(f"ip addr add {addr} dev {self.iface}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 2 or process.returncode == 0: # 2 = already set
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def set_route(gateway, device):
|
||||
process = subprocess.Popen(f"ip route replace default via {gateway} dev {device}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Device:
|
||||
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()
|
||||
self.tries = 0
|
||||
self.network = None
|
||||
|
||||
self.max_tries = max_tries
|
||||
self.search_order = search_order
|
||||
self.share_internet = share_internet
|
||||
self.ip = ip
|
||||
self.netmask = netmask
|
||||
self.gateway = gateway
|
||||
self.interval = interval
|
||||
self.mac = mac
|
||||
self.scantime = scantime
|
||||
self.priority = priority
|
||||
|
||||
def connected(self):
|
||||
"""
|
||||
Checks if device is connected
|
||||
"""
|
||||
return self.network and BTNap.prop_get(self.network, 'Connected')
|
||||
|
||||
def interface(self):
|
||||
"""
|
||||
Returns the interface name or None
|
||||
"""
|
||||
if not self.connected():
|
||||
return None
|
||||
return BTNap.prop_get(self.network, 'Interface')
|
||||
|
||||
|
||||
class BTTether(plugins.Plugin):
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This makes the display reachable over bluetooth'
|
||||
|
||||
def __init__(self):
|
||||
self.ready = False
|
||||
self.options = dict()
|
||||
self.devices = dict()
|
||||
self.lock = Lock()
|
||||
|
||||
def on_loaded(self):
|
||||
# new config
|
||||
if 'devices' in self.options:
|
||||
for device, options in self.options['devices'].items():
|
||||
if 'enabled' in options and options['enabled']:
|
||||
for device_opt in ['enabled', 'priority', 'scantime', 'search_order',
|
||||
'max_tries', 'share_internet', 'mac', 'ip',
|
||||
'netmask', 'interval']:
|
||||
if device_opt not in options or options[device_opt] is None:
|
||||
logging.error("BT-TETHER: Please specify the %s for device %s.",
|
||||
device_opt, device)
|
||||
break
|
||||
else:
|
||||
if options['enabled']:
|
||||
self.devices[device] = Device(name=device, **options)
|
||||
|
||||
# legacy
|
||||
if 'mac' in self.options:
|
||||
for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
|
||||
if opt not in self.options or self.options[opt] is None:
|
||||
logging.error("BT-TETHER: Please specify the %s in your config.yml.", opt)
|
||||
return
|
||||
|
||||
self.devices['legacy'] = Device(name='legacy', **self.options)
|
||||
|
||||
if not self.devices:
|
||||
logging.error("BT-TETHER: No valid devices found")
|
||||
return
|
||||
|
||||
# ensure bluetooth is running
|
||||
bt_unit = SystemdUnitWrapper('bluetooth.service')
|
||||
if not bt_unit.is_active():
|
||||
if not bt_unit.start():
|
||||
logging.error("BT-TETHER: Can't start bluetooth.service")
|
||||
return
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
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: 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:
|
||||
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
|
||||
|
||||
addr = f"{device.ip}/{device.netmask}"
|
||||
if device.gateway:
|
||||
gateway = device.gateway
|
||||
else:
|
||||
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
|
||||
|
||||
wrapped_interface = IfaceWrapper(interface)
|
||||
logging.debug('BT-TETHER: Add ip to %s', interface)
|
||||
if not wrapped_interface.set_addr(addr):
|
||||
ui.set('bluetooth', 'AE')
|
||||
logging.debug("BT-TETHER: Could not add ip to %s", interface)
|
||||
continue
|
||||
|
||||
if device.share_internet:
|
||||
if not connected_priorities or device.priority > max(connected_priorities):
|
||||
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
|
||||
IfaceWrapper.set_route(gateway, interface)
|
||||
connected_priorities.append(device.priority)
|
||||
|
||||
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
|
||||
with open('/etc/resolv.conf', 'r+') as resolv:
|
||||
nameserver = resolv.read()
|
||||
if 'nameserver 9.9.9.9' not in nameserver:
|
||||
logging.debug('BT-TETHER: Added nameserver')
|
||||
resolv.seek(0)
|
||||
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
||||
|
||||
if any_device_connected:
|
||||
ui.set('bluetooth', 'C')
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user