Compare commits
836 Commits
muwire-0.4
...
github/for
Author | SHA1 | Date | |
---|---|---|---|
52d636039d | |||
483f92836c | |||
83546d68d2 | |||
a891c83518 | |||
aa56cc23c0 | |||
a2b37ef567 | |||
4bc04ae631 | |||
56da9a16b0 | |||
2935ee1a1d | |||
855183397b | |||
e27704c1af | |||
5c18b4a141 | |||
dcd233b7ad | |||
7cee8a28ba | |||
7446fc949a | |||
598ab90f63 | |||
043028c296 | |||
cd1757fac3 | |||
9d4b365e63 | |||
8ac3660b36 | |||
b12d57e30a | |||
f33d1b6db3 | |||
9e451460da | |||
ffa52c129a | |||
b779fb75a0 | |||
fbe6b53278 | |||
b2bd95788d | |||
83d4a2624b | |||
03e20e21aa | |||
8a08955675 | |||
4ec54ebe54 | |||
758af6f48e | |||
a7bdd47fcd | |||
f7caa77a18 | |||
7641f64536 | |||
02baaace48 | |||
ef06cadde3 | |||
d90067ff39 | |||
c910a215f5 | |||
65e073b1b9 | |||
489a7518c3 | |||
3733e48bbd | |||
c3723a1348 | |||
0e0f52bc77 | |||
60b9e990cf | |||
28ad0ae30f | |||
9142de85cd | |||
4eb31c11e3 | |||
e8afe358a5 | |||
3db4317fc1 | |||
5ad2b28527 | |||
3036765f81 | |||
8f9b1e5a8b | |||
e6d59a2438 | |||
32609b4779 | |||
74ac4cfecf | |||
69173c4156 | |||
6283287bee | |||
8e3f76f68c | |||
574294fdc6 | |||
8bd41546cd | |||
ba5425c958 | |||
22580f002c | |||
5c773cec80 | |||
7df00e6709 | |||
5c05bd2562 | |||
9df1d043e4 | |||
6ea1a15641 | |||
c0575facec | |||
09168844e0 | |||
e21d482393 | |||
f5fc3e40c2 | |||
796a0138fa | |||
505b4ddb06 | |||
a35216ff56 | |||
fba92fe9b9 | |||
1cc511b0ae | |||
fa94c8ebfa | |||
88b68a3c5c | |||
b3e0d2ee7a | |||
ce293cbda8 | |||
3abc617e9f | |||
67ee634f20 | |||
503d54927f | |||
5788329e1a | |||
f0ffc68122 | |||
3d710cebe5 | |||
7d67573c92 | |||
3acc676448 | |||
2bf03b6b84 | |||
b8ba6df4d5 | |||
9fa7fa07b4 | |||
1c7253ea0a | |||
d947ad2997 | |||
dd0bd6f5f8 | |||
f05b6d0b40 | |||
906c69a482 | |||
5375b7aec0 | |||
ea5da2431a | |||
14b3a9ac9e | |||
40bbef4583 | |||
f811653247 | |||
f321000071 | |||
6eb85283cd | |||
2973759cd9 | |||
fe945a9941 | |||
5f7e949310 | |||
11edb2cb3c | |||
ff1f801155 | |||
0a98083c64 | |||
75b2852f6e | |||
5774cdee94 | |||
2b0f4e52ca | |||
1d20dc917b | |||
63e3b3710c | |||
0878b89082 | |||
fecf0ecae8 | |||
fec8d4ef9f | |||
067ac8582a | |||
31cac25a23 | |||
6bcc44e01e | |||
31652b34d7 | |||
41a15fc7d5 | |||
da3d7d7a50 | |||
3a079d9f21 | |||
ba0c85fe07 | |||
ecb2283886 | |||
cf9a18cee5 | |||
982a93a04b | |||
58137d11d1 | |||
d87bec927d | |||
dc8dd96495 | |||
add9fb6feb | |||
c500e95ab6 | |||
477c3285d2 | |||
1f5b112bfe | |||
b0d09853e4 | |||
b96d997037 | |||
a631ec1e14 | |||
62a06bc891 | |||
3534b23194 | |||
c561ae9140 | |||
5926457eb5 | |||
37c93e352b | |||
be8fecda39 | |||
7ec6257ac0 | |||
c4ea58c330 | |||
a482fe5c93 | |||
2ee84848c4 | |||
e29d7f6872 | |||
5ded824ef2 | |||
c607560cb8 | |||
8b341bb125 | |||
6bc5a9075b | |||
6b1d2bc5ce | |||
0cbbaf6a63 | |||
3363b99675 | |||
4ab4785539 | |||
e595fa97e8 | |||
65a7088463 | |||
2d5bd653c1 | |||
a864343c05 | |||
696b348469 | |||
b08333c5ea | |||
0cf368c1af | |||
62ab957892 | |||
2b9e722165 | |||
8cf4b23762 | |||
1285c68521 | |||
daa9e0bafc | |||
8efd9c2c88 | |||
918549f164 | |||
e30a4666cb | |||
26167abc08 | |||
93f7c67f37 | |||
f9a0a5e08a | |||
d8ae275df2 | |||
fce879be5d | |||
0b58e22714 | |||
dd230c4dfc | |||
fba0b001c0 | |||
6978c7b992 | |||
7355e76e1b | |||
5147cf21a0 | |||
e8dd7d710d | |||
fc9114eaa5 | |||
20b7104c41 | |||
570616951a | |||
e075bfac55 | |||
b6411a555c | |||
d395475727 | |||
8ae0a16b8a | |||
38fcdfc97a | |||
a0fb07cf99 | |||
3747f9a5d5 | |||
3a738f8f62 | |||
ca56363438 | |||
e06cb05e2a | |||
8ab2dd7900 | |||
26116d313a | |||
738f177d6c | |||
62c4579bbd | |||
18d84685ec | |||
c05a7a021c | |||
a9935eba62 | |||
e3d80bf809 | |||
a59a1d3f30 | |||
37ed75a3e8 | |||
cd4b600ba2 | |||
fcd6dbcfbd | |||
f3ab15bd74 | |||
cddaad0f29 | |||
ecb597e0a0 | |||
ec2a934f73 | |||
e1d630fdee | |||
5807672503 | |||
2fadb314d3 | |||
ec5c15ff64 | |||
c169a7613f | |||
0f762968ae | |||
8e6517e7d8 | |||
6946bff7f9 | |||
37dcedb99b | |||
afb92b0e4e | |||
7c39dff34f | |||
e41c122d2d | |||
117c5eaf67 | |||
10fab2b47f | |||
3f71df3d29 | |||
813e211200 | |||
1adb130fba | |||
f69d4027db | |||
e0d006ec69 | |||
81d8af57ed | |||
42c48a8e37 | |||
3b1349b643 | |||
0250ea329c | |||
b722c64ad8 | |||
effa3b567e | |||
64f198d599 | |||
131b2defbb | |||
df5aab67ac | |||
fdc030904c | |||
2a4fae8de4 | |||
662b065116 | |||
300938fa44 | |||
086e27876d | |||
247c62bfb4 | |||
a13315c324 | |||
65f40ef23a | |||
96a611ff78 | |||
0f4119b74f | |||
6847329093 | |||
9d2bcf70c7 | |||
aa33709f04 | |||
eacaedaf3d | |||
f9c428cfcd | |||
aa1ede46d2 | |||
3c43244631 | |||
b468a6f19b | |||
cfdc750ac0 | |||
6f8b006227 | |||
3f4bf986f3 | |||
bef1033e12 | |||
13061d60a4 | |||
5c6917a7e6 | |||
2ec15cfbbc | |||
1325a8dc65 | |||
b5d8fcf25b | |||
c22ff0678e | |||
07051b813a | |||
5c22af6576 | |||
c3e1298ea3 | |||
949b616fdd | |||
2b1d95e2ef | |||
3d967da110 | |||
66fde32b64 | |||
80a89a5ac0 | |||
c59e038c2a | |||
844bd8fd6e | |||
7d9ebb5b0b | |||
7fd7444dbf | |||
13af6cce22 | |||
458dbec5fd | |||
2137d6d30b | |||
b28de0c119 | |||
0fd4695b7c | |||
74dddc4da4 | |||
8bff987d30 | |||
de8684bafc | |||
905f559aa9 | |||
c7f57c0b15 | |||
0f0f46f425 | |||
d6a3c8b24c | |||
8c661ca1ae | |||
f579c8754f | |||
5c17536683 | |||
8536353c26 | |||
84375c0201 | |||
9c0c187a18 | |||
8ae735e5c0 | |||
8224dda3fd | |||
c852d7474e | |||
71685d2052 | |||
e57e513ca1 | |||
aa4fb14540 | |||
5f74abc944 | |||
c4135389a4 | |||
a6e0834722 | |||
bc628b9c00 | |||
9b2669a8b8 | |||
a0f70f7677 | |||
23b2c912e2 | |||
ecfd4180c0 | |||
42489ba6b2 | |||
61207f893d | |||
4e32359718 | |||
8d4af48eca | |||
693f63534d | |||
b057e848d0 | |||
0114224d1f | |||
beab2be713 | |||
edd4a1ff4b | |||
85814b7544 | |||
d46fbd66f0 | |||
06bd9c80e8 | |||
54b8628435 | |||
b37a548771 | |||
a14689acff | |||
a73bc956bf | |||
d595a768b8 | |||
0fd6421fae | |||
6e9a36461a | |||
d115f54812 | |||
f627f661f2 | |||
0e7ec3dfb3 | |||
0188bd34a9 | |||
a2becfa6e2 | |||
ea32af9b91 | |||
c74c26e4c6 | |||
382e21225b | |||
81c406cbf6 | |||
d9eb46d65c | |||
dadfed20f1 | |||
6dad29a772 | |||
884253fe29 | |||
a5eccbdc2b | |||
d0318e3e83 | |||
d1c308f118 | |||
3871170e44 | |||
95dd5c4a7c | |||
0bff4b55a5 | |||
a2022415c2 | |||
2b8bd8144f | |||
7bf520ac8c | |||
ad8983e889 | |||
d0b62af32e | |||
bc8e259974 | |||
ff0a4661fd | |||
9151df6816 | |||
9c0878408b | |||
61baa53076 | |||
b2841ee9ab | |||
9edea17fb7 | |||
ac17618f0c | |||
e94ed4eafa | |||
8c33a5e62f | |||
f9f1017e5b | |||
5d2d831b9e | |||
562d9a0f4a | |||
b981f9199b | |||
efef0f3734 | |||
cd0b860210 | |||
9cb0655cfa | |||
3775f28af7 | |||
c33b824871 | |||
cf396b739e | |||
631963f43c | |||
06cedb4f41 | |||
7a0c60a164 | |||
4c038ad932 | |||
f6dd38685a | |||
2eab0f0567 | |||
8fedc0c605 | |||
5831b06842 | |||
57d5b5f386 | |||
c0f6b1ed73 | |||
f4cd1c30cd | |||
6b717f560e | |||
e8a3db76bb | |||
5acf7f2953 | |||
e760e9f600 | |||
8a47972b10 | |||
f8e0c9524e | |||
919aeaaed5 | |||
9474512cbd | |||
8c50f6c6d6 | |||
01ee7209c8 | |||
ff7c4eae28 | |||
9373d58b53 | |||
df71ade69f | |||
2ed29be072 | |||
a398ab7d4b | |||
a0125e7195 | |||
cb9a1cfff6 | |||
445e73521a | |||
7bdc922d2c | |||
0c40c8f269 | |||
681ddb99a2 | |||
5dff319746 | |||
57c4a00ac6 | |||
286a0a8678 | |||
17eff7d77f | |||
2e22369ce0 | |||
15c59b440f | |||
8fb015acbf | |||
f7b11c90fd | |||
df93a35062 | |||
ecb19a8412 | |||
b1e5b40800 | |||
daa3a293f2 | |||
907264fc67 | |||
c6becb93dc | |||
2954bd2f1a | |||
35322d2c15 | |||
9f6a7eb368 | |||
fec81808e5 | |||
4db890484d | |||
dfd5e06889 | |||
71da8e14da | |||
7dc37e3e0d | |||
3de058a078 | |||
4d70c7adce | |||
5b41106476 | |||
6240b22e66 | |||
0e26f5afd7 | |||
114bc06dbb | |||
5fa2f2753c | |||
cacdd2a7a9 | |||
d56f7c6184 | |||
f7f4513109 | |||
dd15d893ba | |||
bf5ab9c82e | |||
edd5a29b10 | |||
38eb89f2f7 | |||
73f1d64428 | |||
bc1cae2d75 | |||
a0ab07a7c0 | |||
f875c379ce | |||
0ce9784ccf | |||
be82136e32 | |||
7d25bb9364 | |||
c6e98db9d4 | |||
35a26e2a47 | |||
beef4af329 | |||
cec3c1bc0f | |||
289b958784 | |||
e9c554d717 | |||
1875fcddb2 | |||
bee6154fa9 | |||
1f9b171021 | |||
59c03be35e | |||
621af96bdf | |||
bcb7016202 | |||
b1b2bcaef8 | |||
eec007e83b | |||
3d36351a6b | |||
d57d2ccb71 | |||
d91f15ee54 | |||
6bc61c920d | |||
146ed53e12 | |||
8ebae1600b | |||
18d19ca75e | |||
29e499fe9d | |||
3db167bade | |||
bfe0ab7867 | |||
1fbb1e7932 | |||
0632336cd1 | |||
aa221cd6dc | |||
29b5c55328 | |||
5e7f3587df | |||
8afd387ca6 | |||
5d16963d1c | |||
6080c8b308 | |||
915deb1dee | |||
8afca3dc7f | |||
f072d0343c | |||
a549ad3d8d | |||
b6f5ec7d22 | |||
761bf0a177 | |||
bd873211c0 | |||
036971cfe5 | |||
a2637570b1 | |||
6012adbeab | |||
8f6b6b0caa | |||
8f3b5aea8d | |||
ee098ace8e | |||
5d8401e4bf | |||
fbf9add82a | |||
7379263fef | |||
7d50843754 | |||
f4a2864942 | |||
afaadf65a4 | |||
7bd422d6b4 | |||
3f47274f61 | |||
419e9a0ce6 | |||
ac1068a681 | |||
549457e36f | |||
14d6d10546 | |||
878e397aa0 | |||
27831b488b | |||
449f46c62b | |||
5703b85386 | |||
76d8d847bd | |||
db84d8e5bf | |||
cc9b384907 | |||
72960c24a8 | |||
71298e5e73 | |||
11bc672544 | |||
2f6cd311a0 | |||
0448750491 | |||
800dd1cbba | |||
f95e9450f3 | |||
d842e3f2f2 | |||
2017b53a43 | |||
6e2b3f4f33 | |||
dbb305139b | |||
0801bfec08 | |||
00a8d100fe | |||
e94b7cb0d4 | |||
b0357f2ecd | |||
62e72a7ce0 | |||
26fa757b13 | |||
3b2e1cf98c | |||
5de8a51e47 | |||
f5c07f13c0 | |||
c7b0ae34af | |||
cad5301827 | |||
c998011873 | |||
5802ba7734 | |||
b3f775f59a | |||
739dbc7a24 | |||
af99dee4a3 | |||
07a6c63357 | |||
c4096568f5 | |||
30dda180eb | |||
83ea1bed3e | |||
9181829e4a | |||
94678bad3c | |||
e7072803e9 | |||
e9f7a51e16 | |||
916fad7d9b | |||
9feb891c51 | |||
b865376d24 | |||
8dcba7535c | |||
7e881f1fe6 | |||
a9aad7d9db | |||
e736b42751 | |||
acda64aea7 | |||
d82dc4ce90 | |||
f2ff90795d | |||
49f51a9f5f | |||
6fbd1267fa | |||
149568520f | |||
c672880db0 | |||
6cb1674d14 | |||
dba863a864 | |||
642044b7e2 | |||
47c14f109a | |||
36c1a1a288 | |||
5d51b1c580 | |||
bf3502220f | |||
ff1df88601 | |||
4ed572ba51 | |||
fd3f55ab4d | |||
1358e14467 | |||
e22d5fea11 | |||
7ade4aa10d | |||
a9f623a91a | |||
1ce410e943 | |||
27aad9d75d | |||
24591b10f2 | |||
e4f1ea5c10 | |||
c73c44c5f2 | |||
309cbcc580 | |||
86894f242b | |||
568255140f | |||
f6d2bac5bb | |||
1c396711ed | |||
c154d9538d | |||
8043782446 | |||
00c529cca1 | |||
094b9ac2b0 | |||
0dae0a561b | |||
82eaafc2c3 | |||
a3fc1a62e7 | |||
2fd8f45107 | |||
2429bbf59e | |||
f7e28e04f6 | |||
cc0188f20e | |||
af9b4f4679 | |||
625a559d02 | |||
6e20193d57 | |||
88ac267f99 | |||
9b3a7473d1 | |||
5b0180280e | |||
d0462034fc | |||
f3e4098107 | |||
26e7ca0b21 | |||
11007e5f19 | |||
ae651cb6bd | |||
cad3a88517 | |||
29c81646af | |||
8a0257927b | |||
3b882ae644 | |||
5b61738ca9 | |||
c77d79513e | |||
9f12442897 | |||
477b0a47ad | |||
7f1041dd96 | |||
99393c59bd | |||
a78d8c84ca | |||
fa9c697bfa | |||
e5b12701f5 | |||
f69727ab43 | |||
d7c7afe2c0 | |||
6c806c4441 | |||
c4095abdb4 | |||
8801546854 | |||
f6ee49c0f5 | |||
2320d650f6 | |||
e9e6e6920a | |||
87e5007f39 | |||
8df6715e24 | |||
6d587bf228 | |||
8684452848 | |||
7d652fabcb | |||
5eb8d75bba | |||
9ca8d1738c | |||
2bb9480137 | |||
7a6365f87a | |||
56540ca3ca | |||
eb5a5198b1 | |||
29562c42ea | |||
f5284f9483 | |||
9bd3c4f141 | |||
817dd68faf | |||
5954cdb342 | |||
56d44e6458 | |||
c6fb76610d | |||
5e329dfa2c | |||
742f6da870 | |||
7f46347c0f | |||
b308ac2f37 | |||
9cdabb51d1 | |||
45f0736a5e | |||
fe753ff978 | |||
ac717b5205 | |||
6f624e3afc | |||
623d675ed9 | |||
546b71b632 | |||
804113bb1b | |||
ab9e10f438 | |||
00520acdf0 | |||
8c44d196a7 | |||
9c5fa0a2ce | |||
d7bca05725 | |||
45fcb2209e | |||
7bf0373b80 | |||
5925b42597 | |||
13243b05ad | |||
43987be463 | |||
fcd3414e02 | |||
70913ea8fb | |||
b30e552498 | |||
bae66de4eb | |||
626e145e25 | |||
bf72c76f13 | |||
fce8bbfd97 | |||
1cc7925155 | |||
12b51ceb02 | |||
62811861a4 | |||
837aa6974b | |||
94e7c42d19 | |||
877bf12a93 | |||
224266b2dd | |||
8f16614dc3 | |||
b412f9fb0c | |||
b24d04811d | |||
771f645df0 | |||
b6483ad0f4 | |||
decb72c8ef | |||
439b3bf18b | |||
06679ffee0 | |||
1d5b12e2d7 | |||
4e6e1b6f5b | |||
f0b5361d7b | |||
e0c6bfbf51 | |||
2a0ecd8a47 | |||
fb1804e849 | |||
d4eaa0df8d | |||
ffde6ac86f | |||
7ad677ead2 | |||
ddb0568aab | |||
ff50a84a48 | |||
770396ba41 | |||
b55852e993 | |||
a6945275a4 | |||
7241809e55 | |||
54073af933 | |||
a32903fc8c | |||
e40520be46 | |||
97482b949a | |||
92ee107312 | |||
2e8082af64 | |||
8da5a428c9 | |||
fd46b3c7d6 | |||
eea3b2563b | |||
50719f3828 | |||
01a45a89a8 | |||
66bd249ed3 | |||
265cd6ee15 | |||
1dc88cb96b | |||
3e10d497b1 | |||
9a0b3bb9d6 | |||
a1fe3c01b9 | |||
ab323db62a | |||
d954387e41 | |||
ea9db21a18 | |||
136cf89c9b | |||
46de1baf88 | |||
13f7b8563c | |||
9c15208f3a | |||
a9ce9d96b3 | |||
4d2a5a8018 | |||
8395047386 | |||
cb23aa44f0 | |||
dbcb8508b8 | |||
47d406d93b | |||
e06f1805c2 | |||
2b04374e23 | |||
383addbc37 | |||
cc39cd7f8e | |||
83665d7524 | |||
94340480b4 | |||
8850d49c63 | |||
f0f9d840f0 | |||
7f4cd4f331 | |||
e6162503f6 | |||
7a5d71dc36 | |||
6fa39a5e35 | |||
c5ae804f61 | |||
d7695b448d | |||
946d9c8f32 | |||
02441ca1e3 | |||
5fa21b2360 | |||
d4c08f4fe6 | |||
942de287c6 | |||
d0299f80c6 | |||
1227cf9263 | |||
a05575485f | |||
f5bccd8126 | |||
70fb789abf | |||
feb712c253 | |||
d22b403e2a | |||
a24982e0df | |||
6c26019164 | |||
965fa79bbf | |||
60ddb85461 | |||
c7284623bc | |||
3e7f2aa70a | |||
4f436a636c | |||
b49dbc30c3 | |||
c25d314e1c | |||
b28587a275 | |||
8b8e5d59be | |||
70bbe1f636 | |||
337605dc0f | |||
14bdfa6b2e | |||
ed3f9da773 | |||
251080d08f | |||
f530ab999d | |||
4133384e48 | |||
600fc98868 | |||
129eeb3b88 | |||
20b51b78a0 | |||
33fe755b60 | |||
8b0668a134 | |||
730d2202fd | |||
69906a986d | |||
5bc8fa8633 | |||
7de7c9d8f3 | |||
e943f6019d | |||
2eec7bec5b | |||
c36110cf76 | |||
abe28517bc | |||
15bc4c064d | |||
91d771944b | |||
e09c456a13 | |||
d9c1067226 | |||
eda3e7ad3a | |||
e9798c7eaa | |||
66bb4eef5b | |||
55f260b3f4 | |||
32d4c3965e | |||
de1534d837 | |||
7b58e8a88a | |||
8a03b89985 | |||
1d97374857 | |||
549e8c2d98 | |||
b54d24db0d | |||
fa12e84345 | |||
6430ff2691 | |||
591313c81c | |||
ce7b6a0c65 | |||
5c4d4c4580 | |||
4cb864ff9f | |||
417675ad07 | |||
9513e5ba3c | |||
85610cf169 | |||
e8322384b8 | |||
179279ed30 | |||
ae79f0fded | |||
ed878b3762 | |||
623cca0ef2 | |||
eaa883c3ba | |||
7ae8076865 | |||
b1aa92661c | |||
9ed94c8376 | |||
fa6aea1abe | |||
0de84e704b | |||
a767dda044 | |||
56e9235d7b | |||
2fba9a74ce | |||
2bb6826906 | |||
9f339629a9 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
||||
# Dot directories
|
||||
.gradle/
|
||||
.idea/
|
||||
.git/
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
**/build/
|
||||
|
||||
# We execute COPY . .
|
||||
# Modifying these files would unnecessarily invalidate the build context
|
||||
Dockerfile
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@
|
||||
.gradle
|
||||
.project
|
||||
.classpath
|
||||
**/*.rej
|
||||
**/*.orig
|
||||
|
9
.tx/config
Normal file
9
.tx/config
Normal file
@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = he: iw, id: in, ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, yi: ji, zh_CN: zh
|
||||
|
||||
[I2P.MuWire]
|
||||
file_filter = webui/locale/messages_<lang>.po
|
||||
source_file = webui/locale/messages_en.po
|
||||
source_lang = en
|
||||
minimum_perc = 10
|
64
Dockerfile
Normal file
64
Dockerfile
Normal file
@ -0,0 +1,64 @@
|
||||
FROM jlesage/baseimage-gui:alpine-3.10-glibc
|
||||
|
||||
# Docker image version is provided via build arg.
|
||||
ARG DOCKER_IMAGE_VERSION=unknown
|
||||
|
||||
# JDK version
|
||||
ARG JDK=11
|
||||
|
||||
# Important directories
|
||||
ARG TMP_DIR=/muwire-tmp
|
||||
ENV APP_HOME=/muwire
|
||||
|
||||
# Define working directory.
|
||||
WORKDIR $TMP_DIR
|
||||
|
||||
# Put sources into dir
|
||||
COPY . .
|
||||
|
||||
# Install final dependencies
|
||||
RUN add-pkg openjdk${JDK}-jre
|
||||
|
||||
# Build and untar in future distribution dir
|
||||
RUN add-pkg --virtual openjdk${JDK}-jdk \
|
||||
&& ./gradlew --no-daemon clean assemble \
|
||||
&& mkdir -p ${APP_HOME} \
|
||||
# Extract to ${APP_HOME and ignore the first dir
|
||||
# First dir in tar is the "MuWire-<version>"
|
||||
&& tar -C ${APP_HOME} --strip 1 -xvf gui/build/distributions/MuWire*.tar \
|
||||
# Cleanup
|
||||
&& rm -rf "${TMP_DIR}" /root/.gradle /root/.java \
|
||||
&& del-pkg openjdk${JDK}-jdk
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
|
||||
# Maximize only the main/initial window.
|
||||
RUN \
|
||||
sed-patch 's/<application type="normal">/<application type="normal" title="MuWire">/' \
|
||||
/etc/xdg/openbox/rc.xml
|
||||
|
||||
# Generate and install favicons.
|
||||
RUN \
|
||||
APP_ICON_URL=https://github.com/zlatinb/muwire/raw/master/gui/griffon-app/resources/MuWire-128x128.png && \
|
||||
install_app_icon.sh "$APP_ICON_URL"
|
||||
|
||||
# Add files.
|
||||
COPY docker/rootfs/ /
|
||||
|
||||
# Set environment variables.
|
||||
ENV APP_NAME="MuWire" \
|
||||
S6_KILL_GRACETIME=8000
|
||||
|
||||
# Define mountable directories.
|
||||
VOLUME ["$APP_HOME/.MuWire"]
|
||||
VOLUME ["/incompletes"]
|
||||
VOLUME ["/output"]
|
||||
|
||||
|
||||
# Metadata.
|
||||
LABEL \
|
||||
org.label-schema.name="muwire" \
|
||||
org.label-schema.description="Docker container for MuWire" \
|
||||
org.label-schema.version="$DOCKER_IMAGE_VERSION" \
|
||||
org.label-schema.vcs-url="https://github.com/zlatinb/muwire" \
|
||||
org.label-schema.schema-version="1.0"
|
54
README.md
54
README.md
@ -2,16 +2,16 @@
|
||||
|
||||
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
|
||||
|
||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||
The current stable release - 0.6.8 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
|
||||
|
||||
The current stable release - 0.4.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
|
||||
|
||||
### Building
|
||||
## Building
|
||||
|
||||
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
|
||||
|
||||
```
|
||||
./gradlew clean assemble
|
||||
./gradlew clean assemble
|
||||
```
|
||||
|
||||
If you want to run the unit tests, type
|
||||
@ -19,17 +19,51 @@ If you want to run the unit tests, type
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Some of the UI tests will fail because they haven't been written yet :-/
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
|
||||
|
||||
### Running
|
||||
## Running the GUI
|
||||
|
||||
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt.
|
||||
Type
|
||||
```
|
||||
./gradlew gui:run
|
||||
```
|
||||
|
||||
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
|
||||
|
||||
If you do not have an I2P router, pass the following switch to the Java process: `-DembeddedRouter=true`. This will launch MuWire's embedded router. Be aware that this causes startup to take a lot longer.
|
||||
[Default I2CP port]\: `7654`
|
||||
|
||||
### GPG Fingerprint
|
||||
## Running the CLI
|
||||
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here [cli options]
|
||||
|
||||
The CLI is under active development and doesn't have all the features of the GUI.
|
||||
|
||||
## Running the Web UI / Plugin
|
||||
|
||||
There is a Web-based UI under development. It is intended to be run as a plugin to the Java I2P router. Instructions how to build it are available at the wiki [Plugin] page.
|
||||
|
||||
## Docker
|
||||
|
||||
MuWire is available as a Docker image. For more information see the [Docker] page.
|
||||
|
||||
## Translations
|
||||
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
|
||||
|
||||
## GPG Fingerprint
|
||||
|
||||
```
|
||||
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||
```
|
||||
|
||||
You can find the full key at https://keybase.io/zlatinb
|
||||
|
||||
|
||||
[Default I2CP port]: https://geti2p.net/en/docs/ports
|
||||
[Wiki]: https://github.com/zlatinb/muwire/wiki
|
||||
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
|
||||
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
|
||||
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
[I2P Github]: https://github.com/i2p/i2p.i2p
|
||||
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
|
||||
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
|
||||
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
|
||||
|
41
TODO.md
41
TODO.md
@ -1,8 +1,6 @@
|
||||
# TODO List
|
||||
|
||||
Not in any particular order yet
|
||||
|
||||
### Big Items
|
||||
### Network
|
||||
|
||||
##### Bloom Filters
|
||||
|
||||
@ -12,20 +10,33 @@ This reduces query traffic by not sending last hop queries to peers that definit
|
||||
|
||||
This helps with scalability
|
||||
|
||||
##### Content Control Panel
|
||||
### Core
|
||||
|
||||
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
|
||||
* Metadata parsing and search
|
||||
* Automatic adjustment of number of I2P tunnels
|
||||
* Persist trust immediately
|
||||
* Check if user-selected download and incomplete locations exist and are writeable
|
||||
* Enum i18n
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* Public Feed feature
|
||||
|
||||
##### Web UI, REST Interface, etc.
|
||||
### Chat
|
||||
* echo "unknown/innappropriate command" in the console
|
||||
* break up lines on CR/LF, send multiple messages
|
||||
* Style timestamps and persona names
|
||||
* enforce # in room names or ignore it
|
||||
* auto-create/join channel on server start
|
||||
* jump from notification window to room with message
|
||||
|
||||
Basically any non-gui non-cli user interface
|
||||
### Swing GUI
|
||||
* I2P Status panel - display message when connected to external router
|
||||
* Search box - left identation
|
||||
|
||||
##### Metadata editing and search
|
||||
### Web UI/Plugin
|
||||
* HTML 5 media players
|
||||
* Minimal dependency (break up groovy-all.jar)
|
||||
* Remove versions from jar names
|
||||
* Security: POST nonces, CSP headers
|
||||
|
||||
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
|
||||
|
||||
### Small Items
|
||||
|
||||
* Wrapper of some kind for in-place upgrades
|
||||
* Download file sequentially
|
||||
* Multiple-selection download, Ctrl-A
|
||||
|
||||
|
@ -2,8 +2,9 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile 'net.i2p:i2p:0.9.41'
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
compile 'org.codehaus.groovy:groovy:2.4.15'
|
||||
compile 'org.codehaus.groovy:groovy-jsr223:2.4.15'
|
||||
compile 'org.codehaus.groovy:groovy-json:2.4.15'
|
||||
}
|
||||
|
||||
compileGroovy {
|
||||
|
27
cli-lanterna/build.gradle
Normal file
27
cli-lanterna/build.gradle
Normal file
@ -0,0 +1,27 @@
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin : 'application'
|
||||
application {
|
||||
mainClassName = 'com.muwire.clilanterna.CliLanterna'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
|
||||
applicationName = 'MuWire-cli'
|
||||
}
|
||||
|
||||
apply plugin : 'com.github.johnrengelman.shadow'
|
||||
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
compile 'com.googlecode.lanterna:lanterna:3.0.1'
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class AddCommentView extends BasicWindow {
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
AddCommentView(TextGUI textGUI, Core core, SharedFile sharedFile, TerminalSize terminalSize) {
|
||||
super("Add Comment To "+sharedFile.getFile().getName())
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
String oldComment = sharedFile.getComment()
|
||||
if (oldComment == null)
|
||||
oldComment = ""
|
||||
else
|
||||
oldComment = DataUtil.readi18nString(Base64.decode(oldComment))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize,oldComment,TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
Button saveButton = new Button("Save", {
|
||||
String newComment = textBox.getText()
|
||||
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
|
||||
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
|
||||
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
|
||||
} else {
|
||||
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
|
||||
String encodedOldComment = sharedFile.getComment()
|
||||
sharedFile.setComment(newComment)
|
||||
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
|
||||
close()
|
||||
}
|
||||
})
|
||||
Button cancelButton = new Button("Cancel", {close()})
|
||||
|
||||
buttonsPanel.addComponent(saveButton, layoutData)
|
||||
buttonsPanel.addComponent(cancelButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.BrowseStatus
|
||||
import com.muwire.core.search.BrowseStatusEvent
|
||||
import com.muwire.core.search.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class BrowseModel {
|
||||
private final Persona persona
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
private final TableModel model = new TableModel("Name","Size","Hash","Comment","Certificates")
|
||||
private Map<String, UIResultEvent> rootToResult = new HashMap<>()
|
||||
|
||||
private int totalResults
|
||||
|
||||
private Label status
|
||||
private Label percentage
|
||||
|
||||
BrowseModel(Persona persona, Core core, TextGUIThread guiThread) {
|
||||
this.persona = persona
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
|
||||
core.eventBus.register(BrowseStatusEvent.class, this)
|
||||
core.eventBus.register(UIResultEvent.class, this)
|
||||
core.eventBus.publish(new UIBrowseEvent(host : persona))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(BrowseStatusEvent.class, this)
|
||||
core.eventBus.unregister(UIResultEvent.class, this)
|
||||
}
|
||||
|
||||
void onBrowseStatusEvent(BrowseStatusEvent e) {
|
||||
guiThread.invokeLater {
|
||||
status.setText(e.status.toString())
|
||||
if (e.status == BrowseStatus.FETCHING)
|
||||
totalResults = e.totalResults
|
||||
}
|
||||
}
|
||||
|
||||
void onUIResultEvent(UIResultEvent e) {
|
||||
guiThread.invokeLater {
|
||||
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
|
||||
String infoHash = Base64.encode(e.infohash.getRoot())
|
||||
String comment = String.valueOf(e.comment != null)
|
||||
model.addRow(e.name, size, infoHash, comment, e.certificates)
|
||||
rootToResult.put(infoHash, e)
|
||||
|
||||
String percentageString = ""
|
||||
if (totalResults != 0) {
|
||||
double percentage = Math.round( (model.getRowCount() * 100 / totalResults).toDouble() )
|
||||
percentageString = String.valueOf(percentage)+"%"
|
||||
}
|
||||
percentage.setText(percentageString)
|
||||
}
|
||||
}
|
||||
|
||||
void setStatusLabel(Label status) {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
void setPercentageLabel(Label percentage) {
|
||||
this.percentage = percentage
|
||||
}
|
||||
|
||||
void sort(SortType type) {
|
||||
Comparator<UIResultEvent> chosen
|
||||
switch(type) {
|
||||
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
|
||||
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
|
||||
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
|
||||
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
|
||||
}
|
||||
|
||||
List<UIResultEvent> l = new ArrayList<>(rootToResult.values())
|
||||
Collections.sort(l, chosen)
|
||||
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
l.each { e ->
|
||||
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
|
||||
String infoHash = Base64.encode(e.infohash.getRoot())
|
||||
String comment = String.valueOf(e.comment != null)
|
||||
model.addRow(e.name, size, infoHash, comment, e.certificates)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
|
||||
class BrowseView extends BasicWindow {
|
||||
private final BrowseModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
BrowseView(BrowseModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
super("Browse "+model.persona.getHumanReadableName())
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
Label statusLabel = new Label("")
|
||||
Label percentageLabel = new Label("")
|
||||
model.setStatusLabel(statusLabel)
|
||||
model.setPercentageLabel(percentageLabel)
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
topPanel.addComponent(statusLabel, layoutData)
|
||||
topPanel.addComponent(percentageLabel, layoutData)
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
|
||||
table = new Table("Name","Size","Hash","Comment","Certificates")
|
||||
table.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.model)
|
||||
setVisibleRows(terminalSize.getRows())
|
||||
setSelectAction({rowSelected()})
|
||||
}
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
Button sortButton = new Button("Sort...", {sort()})
|
||||
Button closeButton = new Button("Close",{
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
buttonsPanel.addComponent(sortButton, layoutData)
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
setComponent(contentPanel)
|
||||
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
String infoHash = row[2]
|
||||
boolean comment = Boolean.parseBoolean(row[3])
|
||||
boolean certificates = row[4] > 0
|
||||
if (comment || certificates) {
|
||||
Window prompt = new BasicWindow("Download Or View Comment")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
Button downloadButton = new Button("Download", {download(infoHash)})
|
||||
Button viewButton = new Button("View Comment", {viewComment(infoHash)})
|
||||
Button viewCertificate = new Button("View Certificates",{viewCertificates(infoHash)})
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(downloadButton, layoutData)
|
||||
if (comment)
|
||||
addComponent(viewButton, layoutData)
|
||||
if (certificates)
|
||||
addComponent(viewCertificate, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
downloadButton.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
} else {
|
||||
download(infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
private void download(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
def file = new File(core.muOptions.downloadLocation, result.name)
|
||||
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
|
||||
target : file, sequential : false))
|
||||
MessageDialog.showMessageDialog(textGUI, "Download started", "Started download of "+result.name, MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void viewComment(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void viewCertificates(String infoHash) {
|
||||
UIResultEvent result = model.rootToResult[infoHash]
|
||||
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void sort() {
|
||||
SortPrompt prompt = new SortPrompt(textGUI)
|
||||
SortType type = prompt.prompt()
|
||||
if (type != null)
|
||||
model.sort(type)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.filecert.Certificate
|
||||
|
||||
class CertificateWrapper {
|
||||
private final Certificate certificate
|
||||
CertificateWrapper(Certificate certificate) {
|
||||
this.certificate = certificate
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
certificate.issuer.getHumanReadableName()
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatConnectionEvent
|
||||
import com.muwire.core.chat.ChatLink
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.UIConnectChatEvent
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class ChatConsoleModel {
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
|
||||
volatile ChatLink link
|
||||
volatile Thread poller
|
||||
volatile boolean running
|
||||
|
||||
volatile TextBox textBox
|
||||
|
||||
|
||||
ChatConsoleModel(Core core, TextGUIThread guiThread) {
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (running)
|
||||
return
|
||||
running = true
|
||||
core.chatServer.start()
|
||||
core.eventBus.with {
|
||||
register(ChatConnectionEvent.class, this)
|
||||
publish(new UIConnectChatEvent(host : core.me))
|
||||
}
|
||||
}
|
||||
|
||||
void onChatConnectionEvent(ChatConnectionEvent e) {
|
||||
if (e.persona != core.me)
|
||||
return // can't really happen
|
||||
|
||||
link = e.connection
|
||||
poller = new Thread({eventLoop()} as Runnable)
|
||||
poller.setDaemon(true)
|
||||
poller.start()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!running)
|
||||
return
|
||||
running = false
|
||||
core.chatServer.stop()
|
||||
poller?.interrupt()
|
||||
link = null
|
||||
}
|
||||
|
||||
private void eventLoop() {
|
||||
Thread.sleep(1000)
|
||||
while(running) {
|
||||
ChatLink link = this.link
|
||||
if (link == null || !link.isUp()) {
|
||||
Thread.sleep(100)
|
||||
continue
|
||||
}
|
||||
|
||||
Object event = link.nextEvent()
|
||||
if (event instanceof ChatMessageEvent)
|
||||
handleChatMessage(event)
|
||||
else if (event instanceof Persona)
|
||||
handleLeave(event)
|
||||
else
|
||||
throw new IllegalArgumentException("unknown event type $event")
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChatMessage(ChatMessageEvent e) {
|
||||
String text = DataHelper.formatTime(e.timestamp)+" <"+e.sender.getHumanReadableName()+ "> ["+
|
||||
e.room+"] "+e.payload
|
||||
guiThread.invokeLater({textBox.addLine(text)})
|
||||
}
|
||||
|
||||
private void handleLeave(Persona p) {
|
||||
guiThread.invokeLater({textBox.addLine(p.getHumanReadableName()+ " disconnected")})
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.chat.ChatCommand
|
||||
import com.muwire.core.chat.ChatConnection
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.ChatServer
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class ChatConsoleView extends BasicWindow {
|
||||
private final TextGUI textGUI
|
||||
private final ChatConsoleModel model
|
||||
private final Core core
|
||||
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
private final LayoutData layoutDataFill = GridLayout.createLayoutData(Alignment.FILL, Alignment.FILL, true, false)
|
||||
|
||||
private final TextBox textBox
|
||||
private final TextBox sayField
|
||||
private final TextBox roomField
|
||||
|
||||
ChatConsoleView(Core core, ChatConsoleModel model, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
super("Chat Server Console")
|
||||
this.core = core
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
TextBox textBox = model.textBox == null ? new TextBox(terminalSize,"", TextBox.Style.MULTI_LINE) : model.textBox
|
||||
this.textBox = textBox
|
||||
model.textBox = textBox
|
||||
model.start()
|
||||
TerminalSize textFieldSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), 1)
|
||||
this.sayField = new TextBox(textFieldSize,"", TextBox.Style.SINGLE_LINE)
|
||||
this.roomField = new TextBox(textFieldSize,"__CONSOLE__", TextBox.Style.SINGLE_LINE)
|
||||
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Panel inputPanel = new Panel()
|
||||
inputPanel.with {
|
||||
setLayoutManager(new GridLayout(2))
|
||||
addComponent(new Label("Say something here"), layoutData)
|
||||
addComponent(sayField, layoutDataFill)
|
||||
addComponent(new Label("In room:"), layoutData)
|
||||
addComponent(roomField, layoutDataFill)
|
||||
}
|
||||
contentPanel.addComponent(inputPanel, layoutData)
|
||||
|
||||
Panel bottomPanel = new Panel()
|
||||
bottomPanel.setLayoutManager(new GridLayout(5))
|
||||
|
||||
Button sayButton = new Button("Say",{say()})
|
||||
Button startButton = new Button("Start Server",{model.start()})
|
||||
Button stopButton = new Button("Stop Server", {model.stop()})
|
||||
Button clearButton = new Button("Clear",{textBox.setText("")})
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
|
||||
bottomPanel.with {
|
||||
addComponent(sayButton, layoutData)
|
||||
addComponent(startButton, layoutData)
|
||||
addComponent(stopButton, layoutData)
|
||||
addComponent(clearButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
contentPanel.addComponent(bottomPanel, layoutData)
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void say() {
|
||||
String command = sayField.getText()
|
||||
sayField.setText("")
|
||||
|
||||
ChatCommand chatCommand
|
||||
try {
|
||||
chatCommand = new ChatCommand(command)
|
||||
} catch (Exception e) {
|
||||
chatCommand = new ChatCommand("/SAY $command")
|
||||
}
|
||||
command = chatCommand.source
|
||||
|
||||
String room = roomField.getText()
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long now = System.currentTimeMillis()
|
||||
|
||||
String toAppend = DataHelper.formatTime(now) + " <" + core.me.getHumanReadableName() + "> [$room] " + command
|
||||
textBox.addLine(toAppend)
|
||||
|
||||
byte[] sig = ChatConnection.sign(uuid, now, room, command, core.me, core.me, core.spk)
|
||||
|
||||
def event = new ChatMessageEvent( uuid : uuid,
|
||||
payload : command,
|
||||
sender : core.me,
|
||||
host : core.me,
|
||||
room : room,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
core.eventBus.publish(event)
|
||||
}
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogManager
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Border
|
||||
import com.googlecode.lanterna.gui2.BorderLayout
|
||||
import com.googlecode.lanterna.gui2.Borders
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.MultiWindowTextGUI
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.SeparateTextGUIThread
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.WindowBasedTextGUI
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder
|
||||
import com.googlecode.lanterna.gui2.dialogs.WaitingDialog
|
||||
import com.googlecode.lanterna.screen.Screen
|
||||
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
|
||||
import com.googlecode.lanterna.terminal.Terminal
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.6.8"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
private static WindowBasedTextGUI textGUI
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (System.getProperty("java.util.logging.config.file") == null) {
|
||||
def names = LogManager.getLogManager().getLoggerNames()
|
||||
while(names.hasMoreElements()) {
|
||||
def name = names.nextElement()
|
||||
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
|
||||
}
|
||||
}
|
||||
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
home = new File(home)
|
||||
if (!home.exists())
|
||||
home.mkdirs()
|
||||
|
||||
def propsFile = new File(home,"MuWire.properties")
|
||||
|
||||
|
||||
DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory()
|
||||
Screen screen = terminalFactory.createScreen()
|
||||
textGUI = new MultiWindowTextGUI( new SeparateTextGUIThread.Factory(), screen)
|
||||
textGUI.getGUIThread().start()
|
||||
screen.startScreen()
|
||||
|
||||
def props
|
||||
if (!propsFile.exists()) {
|
||||
String nickname = TextInputDialog.showDialog(textGUI, "Select a nickname", "", "")
|
||||
String defaultDownloadLocation = System.getProperty("user.home")+File.separator+"Downloads"
|
||||
String downloadLocation = TextInputDialog.showDialog(textGUI, "Select download location", "", defaultDownloadLocation)
|
||||
String defaultIncompletesLocation = System.getProperty("user.home")+File.separator+".MuWire"+File.separator+"incompletes"
|
||||
String incompletesLocation = TextInputDialog.showDialog(textGUI, "Select incompletes location", "", defaultIncompletesLocation)
|
||||
|
||||
|
||||
File downloadLocationFile = new File(downloadLocation)
|
||||
if (!downloadLocationFile.exists())
|
||||
downloadLocationFile.mkdirs()
|
||||
File incompletesLocationFile = new File(incompletesLocation)
|
||||
if (!incompletesLocationFile.exists())
|
||||
incompletesLocationFile.mkdirs()
|
||||
|
||||
props = new MuWireSettings()
|
||||
props.setNickname(nickname)
|
||||
props.setDownloadLocation(downloadLocationFile)
|
||||
props.incompleteLocation = incompletesLocationFile
|
||||
|
||||
propsFile.withPrintWriter("UTF-8", {
|
||||
props.write(it)
|
||||
})
|
||||
} else {
|
||||
props = new Properties()
|
||||
propsFile.withReader("UTF-8", {
|
||||
props.load(it)
|
||||
})
|
||||
props = new MuWireSettings(props)
|
||||
}
|
||||
props.updateType = "cli-lanterna"
|
||||
|
||||
def i2pPropsFile = new File(home, "i2p.properties")
|
||||
if (!i2pPropsFile.exists()) {
|
||||
String i2pHost = TextInputDialog.showDialog(textGUI, "I2P router host", "Specifiy the host I2P router is on", "127.0.0.1")
|
||||
int i2pPort = TextInputDialog.showNumberDialog(textGUI, "I2CP port", "Specify the I2CP port", "7654").toInteger()
|
||||
|
||||
Properties i2pProps = new Properties()
|
||||
i2pProps["i2cp.tcp.host"] = i2pHost
|
||||
i2pProps["i2cp.tcp.port"] = String.valueOf(i2pPort)
|
||||
i2pPropsFile.withOutputStream { i2pProps.store(it, "") }
|
||||
}
|
||||
|
||||
def cliProps
|
||||
def cliPropsFile = new File(home, "cli.properties")
|
||||
if (cliPropsFile.exists()) {
|
||||
Properties p = new Properties()
|
||||
cliPropsFile.withInputStream {
|
||||
p.load(it)
|
||||
}
|
||||
cliProps = new CliSettings(p)
|
||||
} else
|
||||
cliProps = new CliSettings(new Properties())
|
||||
|
||||
|
||||
Window window = new BasicWindow("MuWire "+ MW_VERSION)
|
||||
window.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.withBorder(Borders.doubleLine())
|
||||
BorderLayout layout = new BorderLayout()
|
||||
contentPanel.setLayoutManager(layout)
|
||||
|
||||
Panel welcomeNamePanel = new Panel()
|
||||
contentPanel.addComponent(welcomeNamePanel, BorderLayout.Location.CENTER)
|
||||
welcomeNamePanel.setLayoutManager(new GridLayout(1))
|
||||
Label welcomeLabel = new Label("Welcome to MuWire "+ props.nickname)
|
||||
welcomeNamePanel.addComponent(welcomeLabel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
|
||||
|
||||
Panel connectButtonPanel = new Panel()
|
||||
contentPanel.addComponent(connectButtonPanel, BorderLayout.Location.BOTTOM)
|
||||
connectButtonPanel.setLayoutManager(new GridLayout(1))
|
||||
Button connectButton = new Button("Connect", {
|
||||
|
||||
WaitingDialog waiting = new WaitingDialog("Connecting", "Please wait")
|
||||
waiting.showDialog(textGUI, false)
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1)
|
||||
Thread connector = new Thread({
|
||||
try {
|
||||
core = new Core(props, home, MW_VERSION)
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
})
|
||||
connector.start()
|
||||
while(latch.getCount() > 0) {
|
||||
textGUI.updateScreen()
|
||||
Thread.sleep(10)
|
||||
}
|
||||
waiting.close()
|
||||
window.close()
|
||||
} as Runnable)
|
||||
welcomeNamePanel.addComponent(connectButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
|
||||
|
||||
window.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(window)
|
||||
|
||||
if (core == null) {
|
||||
MessageDialog.showMessageDialog(textGUI, "Failed", "MuWire failed to load", MessageDialogButton.Close)
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
window = new MainWindowView("MuWire "+MW_VERSION, core, textGUI, screen, cliProps)
|
||||
core.startServices()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
textGUI.addWindowAndWait(window)
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1)
|
||||
Thread stopper = new Thread({
|
||||
core.shutdown()
|
||||
latch.countDown()
|
||||
} as Runnable)
|
||||
WaitingDialog waitingForShutdown = new WaitingDialog("MuWire is shutting down","Please wait")
|
||||
waitingForShutdown.setHints([Window.Hint.CENTERED])
|
||||
waitingForShutdown.showDialog(textGUI, false)
|
||||
stopper.start()
|
||||
while(latch.getCount() > 0) {
|
||||
textGUI.updateScreen()
|
||||
Thread.sleep(10)
|
||||
}
|
||||
waitingForShutdown.close()
|
||||
|
||||
screen.stopScreen()
|
||||
System.exit(0)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
class CliSettings {
|
||||
|
||||
boolean clearCancelledDownloads
|
||||
boolean clearFinishedDownloads
|
||||
boolean clearUploads
|
||||
|
||||
CliSettings(Properties props) {
|
||||
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","true"))
|
||||
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads", "false"))
|
||||
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads", "false"))
|
||||
}
|
||||
|
||||
void write(OutputStream os) {
|
||||
Properties props = new Properties()
|
||||
props.with {
|
||||
setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
||||
setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
||||
setProperty("clearUploads", String.valueOf(clearUploads))
|
||||
|
||||
store(os, "CLI Properties")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
|
||||
class DownloadDetailsView extends BasicWindow {
|
||||
private final Downloader downloader
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
private Label knownSources, activeSources, donePieces
|
||||
DownloadDetailsView(Downloader downloader) {
|
||||
super("Download details for "+downloader.file.getName())
|
||||
this.downloader = downloader
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
knownSources = new Label("0")
|
||||
activeSources = new Label("0")
|
||||
donePieces = new Label("0")
|
||||
refresh()
|
||||
|
||||
Button refreshButton = new Button("Refresh",{refresh()})
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(new Label("Target Location"), layoutData)
|
||||
addComponent(new Label(downloader.file.getAbsolutePath()), layoutData)
|
||||
addComponent(new Label("Piece Size"), layoutData)
|
||||
addComponent(new Label(String.valueOf(downloader.pieceSize)), layoutData)
|
||||
addComponent(new Label("Total Pieces"), layoutData)
|
||||
addComponent(new Label(String.valueOf(downloader.nPieces)), layoutData)
|
||||
addComponent(new Label("Done Pieces"), layoutData)
|
||||
addComponent(donePieces, layoutData)
|
||||
addComponent(new Label("Known Sources"), layoutData)
|
||||
addComponent(knownSources, layoutData)
|
||||
addComponent(new Label("Active Sources"), layoutData)
|
||||
addComponent(activeSources, layoutData)
|
||||
addComponent(refreshButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
|
||||
setComponent(contentPanel)
|
||||
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
int done = downloader.donePieces()
|
||||
int known = downloader.activeWorkers.size()
|
||||
int active = downloader.activeWorkers()
|
||||
|
||||
knownSources.setText(String.valueOf(known))
|
||||
activeSources.setText(String.valueOf(active))
|
||||
donePieces.setText(String.valueOf(done))
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
class DownloaderWrapper {
|
||||
final Downloader downloader
|
||||
DownloaderWrapper(Downloader downloader) {
|
||||
this.downloader = downloader
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
downloader.file.getName()
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class DownloadsModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final CliSettings props
|
||||
private final List<Downloader> downloaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name", "Status", "Progress", "Speed", "ETA")
|
||||
|
||||
|
||||
private long lastRetryTime
|
||||
|
||||
DownloadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
this.props = props
|
||||
|
||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||
Timer timer = new Timer(true)
|
||||
Runnable guiRunnable = {
|
||||
refreshModel()
|
||||
resumeDownloads()
|
||||
}
|
||||
timer.schedule({
|
||||
if (core.shutdown.get())
|
||||
return
|
||||
guiThread.invokeLater(guiRunnable)
|
||||
} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||
guiThread.invokeLater({
|
||||
downloaders.add(e.downloader)
|
||||
refreshModel()
|
||||
})
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
if (props.clearCancelledDownloads) {
|
||||
downloaders.removeAll { it.cancelled }
|
||||
}
|
||||
if (props.clearFinishedDownloads) {
|
||||
downloaders.removeAll { it.getCurrentState() == Downloader.DownloadState.FINISHED }
|
||||
}
|
||||
|
||||
downloaders.each {
|
||||
String status = it.getCurrentState().toString()
|
||||
int speedInt = it.speed()
|
||||
String speed = DataHelper.formatSize2Decimal(speedInt, false) + "B/sec"
|
||||
|
||||
int pieces = it.nPieces
|
||||
int done = it.donePieces()
|
||||
int percent = -1
|
||||
if (pieces != 0)
|
||||
percent = (done * 100 / pieces)
|
||||
String totalSize = DataHelper.formatSize2Decimal(it.length, false) + "B"
|
||||
String progress = (String.format("%2d", percent) + "% of ${totalSize}".toString())
|
||||
|
||||
String ETA
|
||||
if (speedInt == 0)
|
||||
ETA = "Unknown"
|
||||
else {
|
||||
long remaining = (pieces - done) * it.pieceSize / speedInt
|
||||
ETA = DataHelper.formatDuration(remaining * 1000)
|
||||
}
|
||||
|
||||
model.addRow([new DownloaderWrapper(it), status, progress, speed, ETA])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void resumeDownloads() {
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval == 0)
|
||||
return
|
||||
retryInterval *= 1000
|
||||
long now = System.currentTimeMillis()
|
||||
if (now - lastRetryTime > retryInterval) {
|
||||
lastRetryTime = now
|
||||
downloaders.each {
|
||||
def state = it.getCurrentState()
|
||||
if (state == Downloader.DownloadState.FAILED || state == Downloader.DownloadState.DOWNLOADING)
|
||||
it.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
|
||||
class DownloadsView extends BasicWindow {
|
||||
private final Core core
|
||||
private final DownloadsModel model
|
||||
private final TextGUI textGUI
|
||||
private final Table table
|
||||
|
||||
DownloadsView(Core core, DownloadsModel model, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
this.core = core
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
table = new Table("Name","Status","Progress","Speed","ETA")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
Button clearButton = new Button("Clear Done",{clearDone()})
|
||||
buttonsPanel.addComponent(clearButton, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
Downloader downloader = row[0].downloader
|
||||
|
||||
Window prompt = new BasicWindow("Kill Download?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button killDownload = new Button("Kill Download", {
|
||||
downloader.cancel()
|
||||
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||
MessageDialog.showMessageDialog(textGUI, "Download Killed", downloader.file.getName()+ " has been killed", MessageDialogButton.OK)
|
||||
})
|
||||
Button viewDetails = new Button("View Details", {
|
||||
textGUI.addWindowAndWait(new DownloadDetailsView(downloader))
|
||||
})
|
||||
Button close = new Button("Close", {
|
||||
prompt.close()
|
||||
})
|
||||
|
||||
contentPanel.addComponent(killDownload,layoutData)
|
||||
contentPanel.addComponent(viewDetails, layoutData)
|
||||
contentPanel.addComponent(close, layoutData)
|
||||
prompt.setComponent(contentPanel)
|
||||
close.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void clearDone() {
|
||||
model.downloaders.removeAll {
|
||||
def state = it.getCurrentState()
|
||||
state == Downloader.DownloadState.CANCELLED || state == Downloader.DownloadState.FINISHED
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class FilesModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final List<SharedFile> sharedFiles = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
|
||||
FilesModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
Timer timer = new Timer(true)
|
||||
timer.schedule({
|
||||
guiThread.invokeLater(refreshModel)
|
||||
} as TimerTask, 1000,1000)
|
||||
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.add(e.loadedFile)
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
if (e.sharedFile != null)
|
||||
sharedFiles.add(e.sharedFile)
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.remove(e.unsharedFile)
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
sharedFiles.each {
|
||||
long size = it.getCachedLength()
|
||||
boolean comment = it.comment != null
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
||||
String hits = String.valueOf(it.getHits())
|
||||
String downloaders = String.valueOf(it.getDownloaders().size())
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
||||
}
|
||||
}
|
||||
|
||||
private void sort(SortType type) {
|
||||
Comparator<SharedFile> chosen
|
||||
switch(type) {
|
||||
case SortType.NAME_ASC : chosen = NAME_ASC; break
|
||||
case SortType.NAME_DESC : chosen = NAME_DESC; break
|
||||
case SortType.SIZE_ASC : chosen = SIZE_ASC; break
|
||||
case SortType.SIZE_DESC : chosen = SIZE_DESC; break
|
||||
}
|
||||
|
||||
Collections.sort(sharedFiles, chosen)
|
||||
}
|
||||
|
||||
private static final Comparator<SharedFile> NAME_ASC = new Comparator<SharedFile>() {
|
||||
public int compare(SharedFile a, SharedFile b) {
|
||||
a.getFile().getName().compareTo(b.getFile().getName())
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<SharedFile> NAME_DESC = NAME_ASC.reversed()
|
||||
|
||||
private static final Comparator<SharedFile> SIZE_ASC = new Comparator<SharedFile>() {
|
||||
public int compare(SharedFile a, SharedFile b) {
|
||||
Long.compare(a.getCachedLength(), b.getCachedLength())
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<SharedFile> SIZE_DESC = SIZE_ASC.reversed()
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.FileDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
|
||||
class FilesView extends BasicWindow {
|
||||
private final FilesModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
|
||||
FilesView(FilesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
super("Shared Files")
|
||||
this.model = model
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
table = new Table("Name","Size","Comment","Certified","Search Hits","Downloaders")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(5))
|
||||
|
||||
Button shareFile = new Button("Share File", {shareFile()})
|
||||
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
|
||||
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
|
||||
Button sort = new Button("Sort...",{sort()})
|
||||
Button close = new Button("Close", {close()})
|
||||
|
||||
buttonsPanel.with {
|
||||
addComponent(shareFile, layoutData)
|
||||
addComponent(shareDirectory, layoutData)
|
||||
addComponent(unshareDirectory, layoutData)
|
||||
addComponent(sort, layoutData)
|
||||
addComponent(close, layoutData)
|
||||
}
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
setComponent(contentPanel)
|
||||
close.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
SharedFile sf = row[0].sharedFile
|
||||
|
||||
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
|
||||
Button unshareButton = new Button("Unshare", {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
|
||||
} )
|
||||
Button addCommentButton = new Button("Add Comment", {
|
||||
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
})
|
||||
Button certifyButton = new Button("Certify", {
|
||||
core.eventBus.publish(new UICreateCertificateEvent(sharedFile : sf))
|
||||
MessageDialog.showMessageDialog(textGUI, "Certificate Created", "Certificate has been issued", MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
contentPanel.addComponent(unshareButton, layoutData)
|
||||
contentPanel.addComponent(addCommentButton, layoutData)
|
||||
contentPanel.addComponent(certifyButton, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
|
||||
private void shareFile() {
|
||||
TerminalSize terminalSize = new TerminalSize(terminalSize.getColumns() - 10, terminalSize.getRows() - 10)
|
||||
FileDialog fileDialog = new FileDialog("Share File", "Select a file to share", "Share", terminalSize, false, null)
|
||||
File f = fileDialog.showDialog(textGUI)
|
||||
f = f.getCanonicalFile()
|
||||
core.eventBus.publish(new FileSharedEvent(file : f))
|
||||
MessageDialog.showMessageDialog(textGUI, "File Shared", f.getName()+" has been shared", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void shareDirectory() {
|
||||
String directoryName = TextInputDialog.showDialog(textGUI, "Share a directory", "Enter the directory to share", "")
|
||||
if (directoryName == null)
|
||||
return
|
||||
File directory = new File(directoryName)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new FileSharedEvent(file : directory))
|
||||
MessageDialog.showMessageDialog(textGUI, "Directory Shared", directory.getName()+" has been shared", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void unshareDirectory() {
|
||||
String directoryName = TextInputDialog.showDialog(textGUI, "Unshare a directory", "Enter the directory to unshare", "")
|
||||
if (directoryName == null)
|
||||
return
|
||||
File directory = new File(directoryName)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
|
||||
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void sort() {
|
||||
SortPrompt prompt = new SortPrompt(textGUI)
|
||||
SortType type = prompt.prompt()
|
||||
if (type != null)
|
||||
model.sort(type)
|
||||
}
|
||||
}
|
@ -0,0 +1,321 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalPosition
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.BorderLayout
|
||||
import com.googlecode.lanterna.gui2.Borders
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.Panels
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.screen.Screen
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.update.UpdateAvailableEvent
|
||||
import com.muwire.core.update.UpdateDownloadedEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class MainWindowView extends BasicWindow {
|
||||
|
||||
private final Core core
|
||||
private final TextGUI textGUI
|
||||
private final Screen screen
|
||||
|
||||
private final TextBox searchTextBox
|
||||
|
||||
private final DownloadsModel downloadsModel
|
||||
private final UploadsModel uploadsModel
|
||||
private final FilesModel filesModel
|
||||
private final TrustModel trustModel
|
||||
private final ChatConsoleModel chatModel
|
||||
|
||||
private final Label connectionCount, incoming, outgoing
|
||||
private final Label known, failing, hopeless
|
||||
private final Label sharedFiles
|
||||
private final Label timesBrowsed
|
||||
private final Label updateStatus
|
||||
private final Label usedRam, totalRam, maxRam
|
||||
|
||||
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
|
||||
super(title);
|
||||
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.screen = screen
|
||||
|
||||
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core, props)
|
||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
|
||||
filesModel = new FilesModel(textGUI.getGUIThread(),core)
|
||||
trustModel = new TrustModel(textGUI.getGUIThread(), core)
|
||||
chatModel = new ChatConsoleModel(core, textGUI.getGUIThread())
|
||||
if (core.muOptions.startChatServer)
|
||||
core.chatServer.start()
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
Panel contentPanel = new Panel()
|
||||
setComponent(contentPanel)
|
||||
|
||||
BorderLayout borderLayout = new BorderLayout()
|
||||
contentPanel.setLayoutManager(borderLayout)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
|
||||
|
||||
GridLayout gridLayout = new GridLayout(8)
|
||||
buttonsPanel.setLayoutManager(gridLayout)
|
||||
|
||||
searchTextBox = new TextBox(new TerminalSize(40, 1))
|
||||
Button searchButton = new Button("Search", { search() })
|
||||
Button downloadsButton = new Button("Downloads", {download()})
|
||||
Button uploadsButton = new Button("Uploads", {upload()})
|
||||
Button filesButton = new Button("Files", { files() })
|
||||
Button trustButton = new Button("Trust", {trust()})
|
||||
Button chatButton = new Button("Chat", {chat()})
|
||||
Button quitButton = new Button("Quit", {close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
buttonsPanel.with {
|
||||
addComponent(searchTextBox, layoutData)
|
||||
addComponent(searchButton, layoutData)
|
||||
addComponent(downloadsButton, layoutData)
|
||||
addComponent(uploadsButton, layoutData)
|
||||
addComponent(filesButton, layoutData)
|
||||
addComponent(trustButton, layoutData)
|
||||
addComponent(chatButton, layoutData)
|
||||
addComponent(quitButton, layoutData)
|
||||
}
|
||||
|
||||
Panel bottomPanel = new Panel()
|
||||
contentPanel.addComponent(bottomPanel, BorderLayout.Location.BOTTOM)
|
||||
BorderLayout bottomLayout = new BorderLayout()
|
||||
bottomPanel.setLayoutManager(bottomLayout)
|
||||
|
||||
Label persona = new Label(core.me.getHumanReadableName())
|
||||
bottomPanel.addComponent(persona, BorderLayout.Location.LEFT)
|
||||
|
||||
|
||||
Panel connectionsPanel = new Panel()
|
||||
connectionsPanel.setLayoutManager(new GridLayout(2))
|
||||
Label connections = new Label("Connections:")
|
||||
connectionsPanel.addComponent(connections, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
connectionCount = new Label("0")
|
||||
connectionsPanel.addComponent(connectionCount, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
|
||||
|
||||
bottomPanel.addComponent(connectionsPanel, BorderLayout.Location.RIGHT)
|
||||
|
||||
|
||||
Panel centralPanel = new Panel()
|
||||
centralPanel.setLayoutManager(new GridLayout(1))
|
||||
contentPanel.addComponent(centralPanel, BorderLayout.Location.CENTER)
|
||||
Panel statusPanel = new Panel()
|
||||
statusPanel.setLayoutManager(new GridLayout(2))
|
||||
statusPanel.withBorder(Borders.doubleLine("Stats"))
|
||||
centralPanel.addComponent(statusPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, true))
|
||||
|
||||
incoming = new Label("0")
|
||||
outgoing = new Label("0")
|
||||
known = new Label("0")
|
||||
failing = new Label("0")
|
||||
hopeless = new Label("0")
|
||||
sharedFiles = new Label("0")
|
||||
timesBrowsed = new Label("0")
|
||||
updateStatus = new Label("Unknown")
|
||||
usedRam = new Label("0")
|
||||
maxRam = new Label("0")
|
||||
totalRam = new Label("0")
|
||||
|
||||
statusPanel.with {
|
||||
addComponent(new Label("Incoming Connections: "), layoutData)
|
||||
addComponent(incoming, layoutData)
|
||||
addComponent(new Label("Outgoing Connections: "), layoutData)
|
||||
addComponent(outgoing, layoutData)
|
||||
addComponent(new Label("Known Hosts: "), layoutData)
|
||||
addComponent(known, layoutData)
|
||||
addComponent(new Label("Failing Hosts: "), layoutData)
|
||||
addComponent(failing, layoutData)
|
||||
addComponent(new Label("Hopeless Hosts: "), layoutData)
|
||||
addComponent(hopeless, layoutData)
|
||||
addComponent(new Label("Shared Files: "), layoutData)
|
||||
addComponent(sharedFiles, layoutData)
|
||||
addComponent(new Label("Times Browsed: "), layoutData)
|
||||
addComponent(timesBrowsed, layoutData)
|
||||
addComponent(new Label("Update Status: "), layoutData)
|
||||
addComponent(updateStatus, layoutData)
|
||||
addComponent(new Label("Java Version: "), layoutData)
|
||||
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
|
||||
addComponent(new Label("Used Memory: "), layoutData)
|
||||
addComponent(usedRam, layoutData)
|
||||
addComponent(new Label("Total Memory: "), layoutData)
|
||||
addComponent(totalRam, layoutData)
|
||||
addComponent(new Label("Maximum Memory: "), layoutData)
|
||||
addComponent(maxRam, layoutData)
|
||||
}
|
||||
|
||||
refreshStats()
|
||||
|
||||
searchButton.takeFocus()
|
||||
core.eventBus.register(ConnectionEvent.class, this)
|
||||
core.eventBus.register(HostDiscoveredEvent.class, this)
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileDownloadedEvent.class, this)
|
||||
core.eventBus.register(UpdateAvailableEvent.class, this)
|
||||
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
||||
}
|
||||
|
||||
void onConnectionEvent(ConnectionEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
connectionCount.setText(String.valueOf(core.connectionManager.connections.size()))
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
refreshStats()
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version is available with hash $e.infoHash"
|
||||
updateStatus.setText(label)
|
||||
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
|
||||
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
|
||||
if (button == MessageDialogButton.No)
|
||||
return
|
||||
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
|
||||
textGUI.getGUIThread().invokeLater {
|
||||
String label = "$e.version downloaded"
|
||||
updateStatus.setText(label)
|
||||
String message = "MuWire version $e.version has been downloaded. Show details?."
|
||||
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
|
||||
if (button == MessageDialogButton.No)
|
||||
return
|
||||
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalSize sizeForTables() {
|
||||
TerminalSize full = screen.getTerminalSize()
|
||||
return new TerminalSize(full.getColumns(), full.getRows() - 10)
|
||||
}
|
||||
|
||||
private void search() {
|
||||
String query = searchTextBox.getText()
|
||||
query = query.trim()
|
||||
if (query.length() == 0)
|
||||
return
|
||||
if (query.length() > 128)
|
||||
query = query.substring(0, 128)
|
||||
|
||||
SearchModel model = new SearchModel(query, core, textGUI.getGUIThread())
|
||||
textGUI.addWindowAndWait(new SearchView(model,core, textGUI, sizeForTables()))
|
||||
}
|
||||
|
||||
|
||||
private void download() {
|
||||
textGUI.addWindowAndWait(new DownloadsView(core, downloadsModel, textGUI, sizeForTables()))
|
||||
}
|
||||
|
||||
private void upload() {
|
||||
textGUI.addWindowAndWait(new UploadsView(uploadsModel, sizeForTables()))
|
||||
}
|
||||
|
||||
private void files() {
|
||||
textGUI.addWindowAndWait(new FilesView(filesModel, textGUI, core, sizeForTables()))
|
||||
}
|
||||
|
||||
private void trust() {
|
||||
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
|
||||
}
|
||||
|
||||
private void chat() {
|
||||
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
|
||||
}
|
||||
|
||||
private void refreshStats() {
|
||||
int inCon = 0
|
||||
int outCon = 0
|
||||
core.connectionManager.getConnections().each {
|
||||
if (it.isIncoming())
|
||||
inCon++
|
||||
else
|
||||
outCon++
|
||||
}
|
||||
int knownHosts = core.hostCache.hosts.size()
|
||||
int failingHosts = core.hostCache.countFailingHosts()
|
||||
int hopelessHosts = core.hostCache.countHopelessHosts()
|
||||
int shared = core.fileManager.fileToSharedFile.size()
|
||||
int browsed = core.connectionAcceptor.browsed
|
||||
long freeMemL = Runtime.getRuntime().freeMemory()
|
||||
long totalMemL = Runtime.getRuntime().totalMemory()
|
||||
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
|
||||
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
|
||||
String maxMem
|
||||
long maxMemL = Runtime.getRuntime().maxMemory()
|
||||
if (maxMemL >= Long.MAX_VALUE / 2)
|
||||
maxMem = "Unlimited"
|
||||
else
|
||||
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
|
||||
|
||||
|
||||
incoming.setText(String.valueOf(inCon))
|
||||
outgoing.setText(String.valueOf(outCon))
|
||||
known.setText(String.valueOf(knownHosts))
|
||||
failing.setText(String.valueOf(failingHosts))
|
||||
hopeless.setText(String.valueOf(hopelessHosts))
|
||||
sharedFiles.setText(String.valueOf(shared))
|
||||
timesBrowsed.setText(String.valueOf(browsed))
|
||||
usedRam.setText(usedMem)
|
||||
totalRam.setText(totalMem)
|
||||
maxRam.setText(maxMem)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class PersonaWrapper {
|
||||
private final Persona persona
|
||||
PersonaWrapper(Persona persona) {
|
||||
this.persona = persona
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
persona.getHumanReadableName()
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
class ResultComparators {
|
||||
public static final Comparator<UIResultEvent> NAME_ASC = new Comparator<UIResultEvent>() {
|
||||
public int compare(UIResultEvent a, UIResultEvent b) {
|
||||
a.name.compareTo(b.name)
|
||||
}
|
||||
}
|
||||
|
||||
public static final Comparator<UIResultEvent> NAME_DESC = NAME_ASC.reversed()
|
||||
|
||||
public static final Comparator<UIResultEvent> SIZE_ASC = new Comparator<UIResultEvent>() {
|
||||
public int compare(UIResultEvent a, UIResultEvent b) {
|
||||
Long.compare(a.size, b.size)
|
||||
}
|
||||
}
|
||||
|
||||
public static final Comparator<UIResultEvent> SIZE_DESC = SIZE_ASC.reversed()
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
|
||||
class ResultsModel {
|
||||
private final UIResultBatchEvent results
|
||||
final TableModel model
|
||||
final Map<String, UIResultEvent> rootToResult = new HashMap<>()
|
||||
|
||||
ResultsModel(UIResultBatchEvent results) {
|
||||
this.results = results
|
||||
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
updateModel()
|
||||
}
|
||||
|
||||
void sort(SortType type) {
|
||||
Comparator<UIResultEvent> chosen
|
||||
switch(type) {
|
||||
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
|
||||
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
|
||||
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
|
||||
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
|
||||
}
|
||||
|
||||
Arrays.sort(results.results, chosen)
|
||||
updateModel()
|
||||
}
|
||||
|
||||
private void updateModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
results.results.each {
|
||||
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
|
||||
String infoHash = Base64.encode(it.infohash.getRoot())
|
||||
String sources = String.valueOf(it.sources.size())
|
||||
String comment = String.valueOf(it.comment != null)
|
||||
model.addRow(it.name, size, infoHash, sources, comment, it.certificates)
|
||||
rootToResult.put(infoHash, it)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
class ResultsView extends BasicWindow {
|
||||
|
||||
private final ResultsModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
|
||||
ResultsView(ResultsModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
super(model.results.results[0].sender.getHumanReadableName() + " Results")
|
||||
this.model = model
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
table = new Table("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
Button sortButton = new Button("Sort...",{sort()})
|
||||
buttonsPanel.addComponent(sortButton)
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
buttonsPanel.addComponent(closeButton)
|
||||
contentPanel.addComponent(buttonsPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def rows = model.model.getRow(selectedRow)
|
||||
boolean comment = Boolean.parseBoolean(rows[4])
|
||||
boolean certificates = rows[5] > 0
|
||||
if (comment || certificates) {
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Window prompt = new BasicWindow("Download Or View Comment/Certificates")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
Button downloadButton = new Button("Download", {download(rows[2])})
|
||||
contentPanel.addComponent(downloadButton, layoutData)
|
||||
|
||||
|
||||
if (comment) {
|
||||
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
|
||||
contentPanel.addComponent(viewButton, layoutData)
|
||||
}
|
||||
if (certificates) {
|
||||
Button certsButton = new Button("View Certificates", {viewCertificates(rows[2])})
|
||||
contentPanel.addComponent(certsButton, layoutData)
|
||||
}
|
||||
|
||||
Button closeButton = new Button("Cancel", {prompt.close()})
|
||||
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
prompt.setComponent(contentPanel)
|
||||
downloadButton.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
} else {
|
||||
download(rows[2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void download(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
def file = new File(core.muOptions.downloadLocation, result.name)
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
|
||||
target : file, sequential : false))
|
||||
MessageDialog.showMessageDialog(textGUI, "Download Started", "Started download of "+result.name, MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void viewComment(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void viewCertificates(String infohash) {
|
||||
UIResultEvent result = model.rootToResult[infohash]
|
||||
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void sort() {
|
||||
SortPrompt prompt = new SortPrompt(textGUI)
|
||||
SortType type = prompt.prompt()
|
||||
if (type != null)
|
||||
model.sort(type)
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SplitPattern
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
class SearchModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final String query
|
||||
private final Core core
|
||||
final TableModel model
|
||||
|
||||
private final Map<Persona, UIResultBatchEvent> resultsPerSender = new HashMap<>()
|
||||
|
||||
SearchModel(String query, Core core, TextGUIThread guiThread) {
|
||||
this.query = query
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
this.model = new TableModel("Sender","Results","Browse","Trust")
|
||||
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||
|
||||
|
||||
boolean hashSearch = false
|
||||
byte [] root = null
|
||||
if (query.length() == 44 && query.indexOf(" ") < 0) {
|
||||
try {
|
||||
root = Base64.decode(query)
|
||||
hashSearch = true
|
||||
} catch (Exception e) {
|
||||
// not hash search
|
||||
}
|
||||
}
|
||||
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
|
||||
payload = root
|
||||
} else {
|
||||
def nonEmpty = SplitPattern.termify(query)
|
||||
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(UIResultBatchEvent.class, this)
|
||||
}
|
||||
|
||||
void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||
guiThread.invokeLater {
|
||||
Persona sender = e.results[0].sender
|
||||
|
||||
resultsPerSender.put(sender, e)
|
||||
|
||||
String browse = String.valueOf(e.results[0].browse)
|
||||
String results = String.valueOf(e.results.length)
|
||||
String trust = core.trustService.getLevel(sender.destination).toString()
|
||||
model.addRow([new PersonaWrapper(sender), results, browse, trust])
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
class SearchView extends BasicWindow {
|
||||
private final Core core
|
||||
private final SearchModel model
|
||||
private final Table table
|
||||
private final TextGUI textGUI
|
||||
private final TerminalSize terminalSize
|
||||
|
||||
SearchView(SearchModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
super(model.query)
|
||||
this.core = core
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
table = new Table("Sender","Results","Browse","Trust")
|
||||
table.setCellSelection(false)
|
||||
table.setSelectAction({rowSelected()})
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
Button closeButton = new Button("Close", {
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def rows = model.model.getRow(selectedRow)
|
||||
Persona persona = rows[0].persona
|
||||
boolean browse = Boolean.parseBoolean(rows[2])
|
||||
Window prompt = new BasicWindow("Show Or Browse "+rows[0]+"?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(6))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
Button showResults = new Button("Show Results", {
|
||||
showResults(persona)
|
||||
})
|
||||
Button browseHost = new Button("Browse Host", {
|
||||
BrowseModel model = new BrowseModel(persona, core, textGUI.getGUIThread())
|
||||
BrowseView view = new BrowseView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
})
|
||||
Button trustHost = new Button("Trust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + " has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button neutralHost = new Button("Neutral",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + " has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustHost = new Button("Distrust", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + " has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closePrompt = new Button("Close", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(showResults, layoutData)
|
||||
if (browse)
|
||||
addComponent(browseHost, layoutData)
|
||||
addComponent(trustHost, layoutData)
|
||||
addComponent(neutralHost, layoutData)
|
||||
addComponent(distrustHost, layoutData)
|
||||
addComponent(closePrompt, layoutData)
|
||||
}
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
showResults.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void showResults(Persona persona) {
|
||||
def results = model.resultsPerSender.get(persona)
|
||||
ResultsModel resultsModel = new ResultsModel(results)
|
||||
ResultsView resultsView = new ResultsView(resultsModel, core, textGUI, terminalSize)
|
||||
textGUI.addWindowAndWait(resultsView)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class SharedFileWrapper {
|
||||
private final SharedFile sharedFile
|
||||
|
||||
SharedFileWrapper(SharedFile sharedFile) {
|
||||
this.sharedFile = sharedFile
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
sharedFile.getCachedPath()
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
|
||||
class SortPrompt extends BasicWindow {
|
||||
private final TextGUI textGUI
|
||||
private SortType type
|
||||
SortPrompt(TextGUI textGUI) {
|
||||
super("Select what to sort by")
|
||||
this.textGUI = textGUI
|
||||
}
|
||||
|
||||
SortType prompt() {
|
||||
setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(5))
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button nameAsc = new Button("Name (ascending)",{
|
||||
type = SortType.NAME_ASC
|
||||
close()
|
||||
})
|
||||
Button nameDesc = new Button("Name (descending)",{
|
||||
type = SortType.NAME_DESC
|
||||
close()
|
||||
})
|
||||
Button sizeAsc = new Button("Size (ascending)",{
|
||||
type = SortType.SIZE_ASC
|
||||
close()
|
||||
})
|
||||
Button sizeDesc = new Button("Size (descending)",{
|
||||
type = SortType.SIZE_DESC
|
||||
close()
|
||||
})
|
||||
Button close = new Button("Cancel",{close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(nameAsc, layoutData)
|
||||
addComponent(nameDesc, layoutData)
|
||||
addComponent(sizeAsc, layoutData)
|
||||
addComponent(sizeDesc, layoutData)
|
||||
addComponent(close, layoutData)
|
||||
}
|
||||
|
||||
setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(this)
|
||||
type
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.muwire.clilanterna;
|
||||
|
||||
public enum SortType {
|
||||
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
class TrustEntryWrapper {
|
||||
TrustService.TrustEntry entry
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
|
||||
class TrustListModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final RemoteTrustList trustList
|
||||
private final Core core
|
||||
private final TableModel trustedTableModel, distrustedTableModel
|
||||
|
||||
TrustListModel(RemoteTrustList trustList, Core core) {
|
||||
this.trustList = trustList
|
||||
this.core = core
|
||||
|
||||
trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
|
||||
refreshModels()
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModels() {
|
||||
int trustRows = trustedTableModel.getRowCount()
|
||||
trustRows.times { trustedTableModel.removeRow(0) }
|
||||
int distrustRows = distrustedTableModel.getRowCount()
|
||||
distrustRows.times { distrustedTableModel.removeRow(0) }
|
||||
|
||||
trustList.good.each {
|
||||
trustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
trustList.bad.each {
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(TrustEvent.class, this)
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
class TrustListView extends BasicWindow {
|
||||
private final TrustListModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final TerminalSize terminalSize
|
||||
private final Table trusted, distrusted
|
||||
|
||||
TrustListView(TrustListModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
int tableSize = terminalSize.getRows() - 10
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Label nameLabel = new Label("Trust list for "+model.trustList.persona.getHumanReadableName())
|
||||
Label lastUpdatedLabel = new Label("Last updated "+new Date(model.trustList.timestamp))
|
||||
contentPanel.addComponent(nameLabel, layoutData)
|
||||
contentPanel.addComponent(lastUpdatedLabel, layoutData)
|
||||
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted User","Reason","Your Trust")
|
||||
trusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.trustedTableModel)
|
||||
setVisibleRows(tableSize)
|
||||
}
|
||||
trusted.setSelectAction({ actionsForUser(true) })
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted User","Reason", "Your Trust")
|
||||
distrusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.distrustedTableModel)
|
||||
setVisibleRows(tableSize)
|
||||
}
|
||||
distrusted.setSelectAction({actionsForUser(false)})
|
||||
topPanel.addComponent(distrusted, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void actionsForUser(boolean trustedUser) {
|
||||
def table = trustedUser ? trusted : distrusted
|
||||
def model = trustedUser ? model.trustedTableModel : model.distrustedTableModel
|
||||
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.getRow(selectedRow)
|
||||
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Actions for "+persona.getHumanReadableName())
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button trustButton = new Button("Trust",{
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button neutralButton = new Button("Neutral",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustButton = new Button("Distrust",{
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close",{prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(trustButton,layoutData)
|
||||
addComponent(neutralButton, layoutData)
|
||||
addComponent(distrustButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
|
||||
class TrustListWrapper {
|
||||
private final RemoteTrustList trustList
|
||||
TrustListWrapper(RemoteTrustList trustList) {
|
||||
this.trustList = trustList
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
trustList.persona.getHumanReadableName()
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionUpdatedEvent
|
||||
|
||||
class TrustModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private final TableModel modelTrusted, modelDistrusted, modelSubscriptions
|
||||
|
||||
TrustModel(TextGUIThread guiThread, Core core) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
modelTrusted = new TableModel("Trusted Users","Reason")
|
||||
modelDistrusted = new TableModel("Distrusted Users","Reason")
|
||||
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
void onTrustSubscriptionUpdatedEvent(TrustSubscriptionUpdatedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
refreshModels()
|
||||
}
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModels() {
|
||||
int trustedRows = modelTrusted.getRowCount()
|
||||
trustedRows.times { modelTrusted.removeRow(0) }
|
||||
int distrustedRows = modelDistrusted.getRowCount()
|
||||
distrustedRows.times { modelDistrusted.removeRow(0) }
|
||||
int subsRows = modelSubscriptions.getRowCount()
|
||||
subsRows.times { modelSubscriptions.removeRow(0) }
|
||||
|
||||
core.trustService.good.values().each {
|
||||
modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustService.bad.values().each {
|
||||
modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustSubscriber.remoteTrustLists.values().each {
|
||||
def name = new TrustListWrapper(it)
|
||||
String trusted = String.valueOf(it.good.size())
|
||||
String distrusted = String.valueOf(it.bad.size())
|
||||
String status = it.status
|
||||
String lastUpdated = it.timestamp == 0 ? "Never" : new Date(it.timestamp)
|
||||
|
||||
modelSubscriptions.addRow(name, trusted, distrusted, status, lastUpdated)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
|
||||
class TrustView extends BasicWindow {
|
||||
private final TrustModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final TerminalSize terminalSize
|
||||
private final Table trusted, distrusted, subscriptions
|
||||
|
||||
TrustView(TrustModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.core = core
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
int tableSize = (terminalSize.getRows() / 2 - 10).toInteger()
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted Users","Reason")
|
||||
trusted.setCellSelection(false)
|
||||
trusted.setSelectAction({trustedActions()})
|
||||
trusted.setTableModel(model.modelTrusted)
|
||||
trusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted users","Reason")
|
||||
distrusted.setCellSelection(false)
|
||||
distrusted.setSelectAction({distrustedActions()})
|
||||
distrusted.setTableModel(model.modelDistrusted)
|
||||
distrusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(distrusted, layoutData)
|
||||
|
||||
Panel bottomPanel = new Panel()
|
||||
bottomPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
Label tableName = new Label("Trust List Subscriptions")
|
||||
bottomPanel.addComponent(tableName, layoutData)
|
||||
|
||||
subscriptions = new Table("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
subscriptions.setCellSelection(false)
|
||||
subscriptions.setSelectAction({trustListActions()})
|
||||
subscriptions.setTableModel(model.modelSubscriptions)
|
||||
subscriptions.setVisibleRows(tableSize)
|
||||
bottomPanel.addComponent(subscriptions, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
contentPanel.addComponent(bottomPanel, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void trustedActions() {
|
||||
int selectedRow = trusted.getSelectedRow()
|
||||
def row = model.modelTrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button subscribe = new Button("Subscribe", {
|
||||
core.muOptions.trustSubscriptions.add(persona)
|
||||
saveMuSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
|
||||
MessageDialog.showMessageDialog(textGUI, "Subscribed", "Subscribed from trust list of " + persona.getHumanReadableName(),
|
||||
MessageDialogButton.OK)
|
||||
model.refreshModels()
|
||||
|
||||
})
|
||||
Button markNeutral = new Button("Mark Neutral", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Distrusted", {
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
contentPanel.with {
|
||||
addComponent(subscribe, layoutData)
|
||||
addComponent(markNeutral, layoutData)
|
||||
addComponent(markDistrusted, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void distrustedActions() {
|
||||
int selectedRow = distrusted.getSelectedRow()
|
||||
def row = model.modelDistrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button markNeutral = new Button("Mark Neutral", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Trusted", {
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
contentPanel.with {
|
||||
addComponent(markDistrusted, layoutData)
|
||||
addComponent(markNeutral, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void trustListActions() {
|
||||
int selectedRow = subscriptions.getSelectedRow()
|
||||
def row = model.modelSubscriptions.getRow(selectedRow)
|
||||
|
||||
def trustList = row[0].trustList
|
||||
Persona persona = trustList.persona
|
||||
|
||||
Window prompt = new BasicWindow("Trust List Actions")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(4))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button reviewButton = new Button("Review",{review(trustList)})
|
||||
Button updateButton = new Button("Update",{
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
|
||||
MessageDialog.showMessageDialog(textGUI, "Updating...", "Trust list will update soon", MessageDialogButton.OK)
|
||||
})
|
||||
Button unsubscribeButton = new Button("Unsubscribe", {
|
||||
core.muOptions.trustSubscriptions.remove(persona)
|
||||
saveMuSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : false))
|
||||
MessageDialog.showMessageDialog(textGUI, "Unsubscribed", "Unsubscribed from trust list of " + persona.getHumanReadableName(),
|
||||
MessageDialogButton.OK)
|
||||
model.refreshModels()
|
||||
})
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(reviewButton, layoutData)
|
||||
addComponent(updateButton, layoutData)
|
||||
addComponent(unsubscribeButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
prompt.setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void review(def trustList) {
|
||||
TrustListModel model = new TrustListModel(trustList, core)
|
||||
TrustListView view = new TrustListView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
model.unregister()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File settingsFile = new File(core.home,"MuWire.properties")
|
||||
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
|
||||
class UpdateTextView extends BasicWindow {
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
UpdateTextView(String text, TerminalSize terminalSize) {
|
||||
super("Update Details")
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.upload.UploadEvent
|
||||
import com.muwire.core.upload.UploadFinishedEvent
|
||||
import com.muwire.core.upload.Uploader
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class UploadsModel {
|
||||
private final TextGUIThread guiThread
|
||||
private final Core core
|
||||
private CliSettings props
|
||||
private final List<UploaderWrapper> uploaders = new ArrayList<>()
|
||||
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces", "Speed")
|
||||
|
||||
UploadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
this.props = props
|
||||
|
||||
core.eventBus.register(UploadEvent.class, this)
|
||||
core.eventBus.register(UploadFinishedEvent.class, this)
|
||||
|
||||
Timer timer = new Timer(true)
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
timer.schedule({
|
||||
guiThread.invokeLater(refreshModel)
|
||||
} as TimerTask, 1000, 1000)
|
||||
|
||||
}
|
||||
|
||||
void onUploadEvent(UploadEvent e) {
|
||||
guiThread.invokeLater {
|
||||
UploaderWrapper found = null
|
||||
uploaders.each {
|
||||
if (it.uploader == e.uploader) {
|
||||
found = it
|
||||
return
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
found.uploader = e.uploader
|
||||
found.finished = false
|
||||
} else
|
||||
uploaders << new UploaderWrapper(uploader : e.uploader)
|
||||
}
|
||||
}
|
||||
|
||||
void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
uploaders.each {
|
||||
if (it.uploader == e.uploader) {
|
||||
it.finished = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModel() {
|
||||
int uploadersSize = model.getRowCount()
|
||||
uploadersSize.times { model.removeRow(0) }
|
||||
|
||||
if (props.clearUploads) {
|
||||
uploaders.removeAll { it.finished }
|
||||
}
|
||||
|
||||
uploaders.each {
|
||||
String name = it.uploader.getName()
|
||||
int percent = it.uploader.getProgress()
|
||||
String percentString = "$percent% of piece".toString()
|
||||
String downloader = it.uploader.getDownloader()
|
||||
|
||||
int pieces = it.uploader.getTotalPieces()
|
||||
int done = it.uploader.getDonePieces()
|
||||
if (percent == 100)
|
||||
done++
|
||||
int percentTotal = -1
|
||||
if (pieces != 0)
|
||||
percentTotal = (done * 100) / pieces
|
||||
long size = it.uploader.getTotalSize()
|
||||
String totalSize = ""
|
||||
if (size > 0)
|
||||
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
||||
String remotePieces = String.format("%02d", percentTotal) + "% ${totalSize} ($done/$pieces) pcs".toString()
|
||||
|
||||
String speed = DataHelper.formatSize2Decimal(it.uploader.speed(), false) + "B/sec"
|
||||
|
||||
|
||||
model.addRow([name, percentString, downloader, remotePieces, speed])
|
||||
}
|
||||
}
|
||||
|
||||
private static class UploaderWrapper {
|
||||
Uploader uploader
|
||||
boolean finished
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
uploader.getName()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
|
||||
class UploadsView extends BasicWindow {
|
||||
private final UploadsModel model
|
||||
private final Table table
|
||||
|
||||
UploadsView(UploadsModel model, TerminalSize terminalSize) {
|
||||
this.model = model
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
table = new Table("Name","Progress","Downloader","Remote Pieces","Speed")
|
||||
table.setCellSelection(false)
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
Button clearDoneButton = new Button("Clear Finished",{
|
||||
model.uploaders.removeAll { it.finished }
|
||||
})
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
|
||||
buttonsPanel.addComponent(clearDoneButton, layoutData)
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateFetchEvent
|
||||
import com.muwire.core.filecert.CertificateFetchStatus
|
||||
import com.muwire.core.filecert.CertificateFetchedEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
class ViewCertificatesModel {
|
||||
private final UIResultEvent result
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
|
||||
private final TableModel model = new TableModel("Issuer","Trust Status","File Name","Comment","Timestamp")
|
||||
|
||||
private int totalCerts
|
||||
|
||||
private Label status
|
||||
private Label percentage
|
||||
|
||||
ViewCertificatesModel(UIResultEvent result, Core core, TextGUIThread guiThread) {
|
||||
this.result = result
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
|
||||
core.eventBus.with {
|
||||
register(CertificateFetchEvent.class,this)
|
||||
register(CertificateFetchedEvent.class, this)
|
||||
publish(new UIFetchCertificatesEvent(host : result.sender, infoHash : result.infohash))
|
||||
}
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
core.eventBus.unregister(CertificateFetchEvent.class, this)
|
||||
core.eventBus.unregister(CertificateFetchedEvent.class, this)
|
||||
}
|
||||
|
||||
void onCertificateFetchEvent(CertificateFetchEvent e) {
|
||||
guiThread.invokeLater {
|
||||
status.setText(e.status.toString())
|
||||
if (e.status == CertificateFetchStatus.FETCHING)
|
||||
totalCerts = e.count
|
||||
}
|
||||
}
|
||||
|
||||
void onCertificateFetchedEvent(CertificateFetchedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
Date date = new Date(e.certificate.timestamp)
|
||||
model.addRow(new CertificateWrapper(e.certificate), core.trustService.getLevel(e.certificate.issuer.destination),
|
||||
e.certificate.name.name, e.certificate.comment != null, date)
|
||||
|
||||
String percentageString = ""
|
||||
if (totalCerts > 0) {
|
||||
double percentage = Math.round((model.getRowCount() * 100 / totalCerts).toDouble())
|
||||
percentageString = String.valueOf(percentage) + "%"
|
||||
}
|
||||
percentage.setText(percentageString)
|
||||
}
|
||||
}
|
||||
|
||||
void setStatusLabel(Label status) {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
void setPercentageLabel(Label percentage) {
|
||||
this.percentage = percentage
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
|
||||
class ViewCertificatesView extends BasicWindow {
|
||||
private final ViewCertificatesModel model
|
||||
private final TextGUI textGUI
|
||||
private final Core core
|
||||
private final Table table
|
||||
private final TerminalSize terminalSize
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
ViewCertificatesView(ViewCertificatesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
|
||||
super("Certificates")
|
||||
this.model = model
|
||||
this.core = core
|
||||
this.textGUI = textGUI
|
||||
this.terminalSize = terminalSize
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
Label statusLabel = new Label("")
|
||||
Label percentageLabel = new Label("")
|
||||
model.setStatusLabel(statusLabel)
|
||||
model.setPercentageLabel(percentageLabel)
|
||||
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
topPanel.addComponent(statusLabel, layoutData)
|
||||
topPanel.addComponent(percentageLabel, layoutData)
|
||||
contentPanel.addComponent(topPanel, layoutData)
|
||||
|
||||
table = new Table("Issuer","Trust Status","File Name","Comment","Timestamp")
|
||||
table.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.model)
|
||||
setVisibleRows(terminalSize.getRows())
|
||||
setSelectAction({rowSelected()})
|
||||
}
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close",{
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void rowSelected() {
|
||||
int selectedRow = table.getSelectedRow()
|
||||
def row = model.model.getRow(selectedRow)
|
||||
Certificate certificate = row[0].certificate
|
||||
|
||||
Window prompt = new BasicWindow("Import Certificate?")
|
||||
prompt.setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(3))
|
||||
Button importButton = new Button("Import", {importCert(certificate)})
|
||||
|
||||
Button viewCommentButton = new Button("View Comment", {viewComment(certificate)})
|
||||
|
||||
Button closeButton = new Button("Close", {prompt.close()})
|
||||
contentPanel.addComponent(importButton, layoutData)
|
||||
if (certificate.comment != null)
|
||||
contentPanel.addComponent(viewCommentButton, layoutData)
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
prompt.setComponent(contentPanel)
|
||||
importButton.takeFocus()
|
||||
textGUI.addWindowAndWait(prompt)
|
||||
}
|
||||
|
||||
private void importCert(Certificate certificate) {
|
||||
core.eventBus.publish(new UIImportCertificateEvent(certificate : certificate))
|
||||
MessageDialog.showMessageDialog(textGUI, "Certificate(s) Imported", "", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void viewComment(Certificate certificate) {
|
||||
ViewCommentView view = new ViewCommentView(certificate.comment.name, "Certificate Comment", terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class ViewCommentView extends BasicWindow {
|
||||
private final TextBox textBox
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
ViewCommentView(String text, String title, TerminalSize terminalSize) {
|
||||
super("View Comments For "+title)
|
||||
|
||||
setHints([Window.Hint.CENTERED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
|
||||
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
|
||||
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.10")
|
||||
core = new Core(props, home, "0.5.3")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -53,7 +53,7 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.10")
|
||||
core = new Core(props, home, "0.5.3")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -2,9 +2,10 @@ apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile 'net.i2p:router:0.9.41'
|
||||
compile 'net.i2p.client:mstreaming:0.9.41'
|
||||
compile 'net.i2p.client:streaming:0.9.41'
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
compile "net.i2p:router:${i2pVersion}"
|
||||
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
compile "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
@ -1,13 +0,0 @@
|
||||
package com.muwire.core
|
||||
|
||||
import net.i2p.crypto.SigType
|
||||
|
||||
class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
|
||||
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
||||
public static final int MAX_HEADERS = 16
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
|
||||
}
|
@ -1,8 +1,17 @@
|
||||
package com.muwire.core
|
||||
|
||||
import com.muwire.core.files.PersisterDoneEvent
|
||||
import com.muwire.core.files.PersisterFolderService
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.muwire.core.chat.ChatDisconnectionEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.chat.UIConnectChatEvent
|
||||
import com.muwire.core.chat.UIDisconnectChatEvent
|
||||
import com.muwire.core.connection.ConnectionAcceptor
|
||||
import com.muwire.core.connection.ConnectionEstablisher
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
@ -18,9 +27,13 @@ import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.filecert.CertificateClient
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
@ -28,18 +41,24 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.DirectoryWatcher
|
||||
import com.muwire.core.hostcache.CacheClient
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
import com.muwire.core.search.BrowseManager
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
@ -57,10 +76,8 @@ import net.i2p.client.I2PClientFactory
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.streaming.I2PSocketManager
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||
import net.i2p.client.streaming.I2PSocketOptions
|
||||
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.PrivateKey
|
||||
import net.i2p.data.Signature
|
||||
@ -74,13 +91,16 @@ public class Core {
|
||||
|
||||
final EventBus eventBus
|
||||
final Persona me
|
||||
final String version;
|
||||
final File home
|
||||
final Properties i2pOptions
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final TrustService trustService
|
||||
private final TrustSubscriber trustSubscriber
|
||||
private final I2PSession i2pSession;
|
||||
final TrustService trustService
|
||||
final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final PersisterFolderService persisterFolderService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
private final CacheClient cacheClient
|
||||
@ -93,33 +113,39 @@ public class Core {
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
final ContentManager contentManager
|
||||
final CertificateManager certificateManager
|
||||
final ChatServer chatServer
|
||||
final ChatManager chatManager
|
||||
|
||||
private final Router router
|
||||
|
||||
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||
|
||||
final SigningPrivateKey spk
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
this.version = myVersion
|
||||
this.muOptions = props
|
||||
|
||||
i2pOptions = new Properties()
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
// Read defaults
|
||||
def defaultI2PFile = getClass()
|
||||
.getClassLoader().getResource("defaults/i2p.properties");
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
|
||||
def i2pOptionsFile = new File(home, "i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||
|
||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
} else {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
i2pOptions["inbound.length"] = "3"
|
||||
i2pOptions["inbound.quantity"] = "4"
|
||||
i2pOptions["outbound.length"] = "3"
|
||||
i2pOptions["outbound.quantity"] = "4"
|
||||
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
}
|
||||
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
|
||||
&& i2pOptions.hasProperty("i2np.udp.port")
|
||||
)) {
|
||||
Random r = new Random()
|
||||
int port = r.nextInt(60000) + 4000
|
||||
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||
@ -128,14 +154,18 @@ public class Core {
|
||||
}
|
||||
|
||||
if (!props.embeddedRouter) {
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||
router = null
|
||||
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
|
||||
log.info "Initializing I2P context"
|
||||
I2PAppContext.getGlobalContext().logManager()
|
||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||
router = null
|
||||
}
|
||||
} else {
|
||||
log.info("launching embedded router")
|
||||
Properties routerProps = new Properties()
|
||||
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
|
||||
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
|
||||
routerProps.setProperty("geoip.dir", home.getAbsolutePath() + File.separator + "geoip")
|
||||
routerProps.setProperty("router.excludePeerCaps", "KLM")
|
||||
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
||||
@ -162,7 +192,6 @@ public class Core {
|
||||
|
||||
|
||||
// options like tunnel length and quantity
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
@ -173,7 +202,7 @@ public class Core {
|
||||
i2pSession = socketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
keyDat.withInputStream {
|
||||
destination.readBytes(it)
|
||||
def privateKey = new PrivateKey()
|
||||
@ -184,8 +213,9 @@ public class Core {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.write(Constants.PERSONA_VERSION)
|
||||
daos.writeShort((short)props.getNickname().length())
|
||||
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
|
||||
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
|
||||
daos.writeShort((short)name.length)
|
||||
daos.write(name)
|
||||
destination.writeBytes(daos)
|
||||
daos.flush()
|
||||
byte [] payload = baos.toByteArray()
|
||||
@ -199,6 +229,12 @@ public class Core {
|
||||
|
||||
eventBus = new EventBus()
|
||||
|
||||
log.info("initializing certificate manager")
|
||||
certificateManager = new CertificateManager(eventBus, home, me, spk)
|
||||
eventBus.register(UICreateCertificateEvent.class, certificateManager)
|
||||
eventBus.register(UIImportCertificateEvent.class, certificateManager)
|
||||
|
||||
|
||||
log.info("initializing trust service")
|
||||
File goodTrust = new File(home, "trusted")
|
||||
File badTrust = new File(home, "distrusted")
|
||||
@ -214,15 +250,25 @@ public class Core {
|
||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
|
||||
eventBus.register(UICommentEvent.class, fileManager)
|
||||
eventBus.register(SideCarFileEvent.class, fileManager)
|
||||
|
||||
log.info("initializing mesh manager")
|
||||
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
||||
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
|
||||
log.info "initializing folder persistence service"
|
||||
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
|
||||
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
|
||||
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileLoadedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileHashedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
hostCache = new HostCache(trustService,hostStorage, 30000, props, i2pSession.getMyDestination())
|
||||
@ -241,16 +287,31 @@ public class Core {
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
if (!props.plugin) {
|
||||
log.info("initializing update client")
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
} else
|
||||
log.info("running as plugin, not initializing update client")
|
||||
|
||||
log.info("initializing connector")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
|
||||
log.info("initializing certificate client")
|
||||
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
|
||||
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
|
||||
|
||||
log.info("initializing chat server")
|
||||
chatServer = new ChatServer(eventBus, props, trustService, me, spk)
|
||||
eventBus.with {
|
||||
register(ChatMessageEvent.class, chatServer)
|
||||
register(ChatDisconnectionEvent.class, chatServer)
|
||||
register(TrustEvent.class, chatServer)
|
||||
}
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
@ -268,25 +329,38 @@ public class Core {
|
||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
|
||||
|
||||
log.info("initializing chat manager")
|
||||
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
|
||||
eventBus.with {
|
||||
register(UIConnectChatEvent.class, chatManager)
|
||||
register(UIDisconnectChatEvent.class, chatManager)
|
||||
register(ChatMessageEvent.class, chatManager)
|
||||
register(ChatDisconnectionEvent.class, chatManager)
|
||||
}
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager, chatServer)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
|
||||
eventBus.register(FileSharedEvent.class, hasherService)
|
||||
eventBus.register(FileUnsharedEvent.class, hasherService)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
|
||||
|
||||
log.info("initializing trust subscriber")
|
||||
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
|
||||
@ -297,6 +371,11 @@ public class Core {
|
||||
contentManager = new ContentManager()
|
||||
eventBus.register(ContentControlEvent.class, contentManager)
|
||||
eventBus.register(QueryEvent.class, contentManager)
|
||||
|
||||
log.info("initializing browse manager")
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
@ -309,7 +388,7 @@ public class Core {
|
||||
connectionAcceptor.start()
|
||||
connectionEstablisher.start()
|
||||
hostCache.waitForLoad()
|
||||
updateClient.start()
|
||||
updateClient?.start()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@ -317,11 +396,21 @@ public class Core {
|
||||
log.info("already shutting down")
|
||||
return
|
||||
}
|
||||
log.info("saving settings")
|
||||
saveMuSettings()
|
||||
log.info("shutting down host cache")
|
||||
hostCache.stop()
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
log.info("shutting down download manageer")
|
||||
log.info("shutting down trust service")
|
||||
trustService.stop()
|
||||
log.info("shutting down persister service")
|
||||
persisterService.stop()
|
||||
log.info("shutting down persisterFolder service")
|
||||
persisterFolderService.stop()
|
||||
log.info("shutting down download manager")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceeptor")
|
||||
log.info("shutting down connection acceptor")
|
||||
connectionAcceptor.stop()
|
||||
log.info("shutting down connection establisher")
|
||||
connectionEstablisher.stop()
|
||||
@ -329,12 +418,31 @@ public class Core {
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down chat server")
|
||||
chatServer.stop()
|
||||
log.info("shutting down chat manager")
|
||||
chatManager.shutdown()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
log.info("killing i2p session")
|
||||
i2pSession.destroySession()
|
||||
if (router != null) {
|
||||
log.info("shutting down embedded router")
|
||||
router.shutdown(0)
|
||||
}
|
||||
log.info("shutting down event bus");
|
||||
eventBus.shutdown()
|
||||
log.info("shutdown complete")
|
||||
}
|
||||
|
||||
public void saveMuSettings() {
|
||||
File f = new File(home, "MuWire.properties")
|
||||
f.withPrintWriter("UTF-8", { muOptions.write(it) })
|
||||
}
|
||||
|
||||
public void saveI2PSettings() {
|
||||
File f = new File(home, "i2p.properties")
|
||||
f.withOutputStream { i2pOptions.store(it, "I2P Options") }
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
@ -361,7 +469,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.4.10")
|
||||
Core core = new Core(props, home, "0.6.8")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@ -2,6 +2,7 @@ package com.muwire.core
|
||||
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
@ -12,7 +13,7 @@ import groovy.util.logging.Log
|
||||
class EventBus {
|
||||
|
||||
private Map handlers = new HashMap()
|
||||
private final Executor executor = Executors.newSingleThreadExecutor {r ->
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor {r ->
|
||||
def rv = new Thread(r)
|
||||
rv.setDaemon(true)
|
||||
rv.setName("event-bus")
|
||||
@ -53,4 +54,8 @@ class EventBus {
|
||||
log.info("Unregistering $handler for type $eventType")
|
||||
handlers[eventType]?.remove(handler)
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
executor.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
@ -6,30 +6,46 @@ import com.muwire.core.hostcache.CrawlerResponse
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class MuWireSettings {
|
||||
|
||||
final boolean isLeaf
|
||||
boolean allowUntrusted
|
||||
boolean searchExtraHop
|
||||
boolean allowTrustLists
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int totalUploadSlots
|
||||
int uploadSlotsPerUser
|
||||
int updateCheckInterval
|
||||
long lastUpdateCheck
|
||||
boolean autoDownloadUpdate
|
||||
String updateType
|
||||
String nickname
|
||||
File downloadLocation
|
||||
File incompleteLocation
|
||||
CrawlerResponse crawlerResponse
|
||||
boolean shareDownloadedFiles
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
boolean startChatServer
|
||||
int maxChatConnections
|
||||
boolean advertiseChat
|
||||
File chatWelcomeFile
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||
int meshExpiration
|
||||
int speedSmoothSeconds
|
||||
boolean embeddedRouter
|
||||
boolean plugin
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
Set<String> negativeFileTree
|
||||
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
@ -38,27 +54,48 @@ class MuWireSettings {
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
|
||||
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
||||
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
|
||||
String incompleteLocationProp = props.getProperty("incompleteLocation")
|
||||
if (incompleteLocationProp != null)
|
||||
incompleteLocation = new File(incompleteLocationProp)
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
|
||||
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||
updateType = props.getProperty("updateType","jar")
|
||||
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
|
||||
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
|
||||
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
|
||||
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
||||
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
|
||||
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
|
||||
watchedDirectories = readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = readEncodedSet(props, "watchedRegexes")
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
|
||||
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
|
||||
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
|
||||
String chatWelcomeProp = props.getProperty("chatWelcomeFile")
|
||||
if (chatWelcomeProp != null)
|
||||
chatWelcomeFile = new File(chatWelcomeProp)
|
||||
|
||||
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
|
||||
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
|
||||
|
||||
trustSubscriptions = new HashSet<>()
|
||||
if (props.containsKey("trustSubscriptions")) {
|
||||
@ -70,30 +107,49 @@ class MuWireSettings {
|
||||
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
void write(Writer out) throws IOException {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("leaf", isLeaf.toString())
|
||||
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
||||
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
|
||||
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
|
||||
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
|
||||
props.setProperty("crawlerResponse", crawlerResponse.toString())
|
||||
props.setProperty("nickname", nickname)
|
||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||
if (incompleteLocation != null)
|
||||
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
|
||||
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||
props.setProperty("updateType",String.valueOf(updateType))
|
||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
|
||||
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
|
||||
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
||||
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||
props.setProperty("plugin", String.valueOf(plugin))
|
||||
props.setProperty("inBw", String.valueOf(inBw))
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
props.setProperty("startChatServer", String.valueOf(startChatServer))
|
||||
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
|
||||
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
|
||||
if (chatWelcomeFile != null)
|
||||
props.setProperty("chatWelcomeFile", chatWelcomeFile.getAbsolutePath())
|
||||
|
||||
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
|
||||
|
||||
if (!trustSubscriptions.isEmpty()) {
|
||||
String encoded = trustSubscriptions.stream().
|
||||
@ -102,25 +158,7 @@ class MuWireSettings {
|
||||
props.setProperty("trustSubscriptions", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
}
|
||||
|
||||
private static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new HashSet<>()
|
||||
if (props.containsKey(property)) {
|
||||
String[] encoded = props.getProperty(property).split(",")
|
||||
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
|
||||
if (set.isEmpty())
|
||||
return
|
||||
String encoded = set.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty(property, encoded)
|
||||
props.store(out, "This file is UTF-8")
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
|
@ -1,45 +0,0 @@
|
||||
package com.muwire.core
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* A name of persona, file or search term
|
||||
*/
|
||||
public class Name {
|
||||
final String name
|
||||
|
||||
Name(String name) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
Name(InputStream nameStream) throws IOException {
|
||||
DataInputStream dis = new DataInputStream(nameStream)
|
||||
int length = dis.readUnsignedShort()
|
||||
byte [] nameBytes = new byte[length]
|
||||
dis.readFully(nameBytes)
|
||||
this.name = new String(nameBytes, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
public void write(OutputStream out) throws IOException {
|
||||
DataOutputStream dos = new DataOutputStream(out)
|
||||
dos.writeShort(name.length())
|
||||
dos.write(name.getBytes(StandardCharsets.UTF_8))
|
||||
}
|
||||
|
||||
public getName() {
|
||||
name
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
name.hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Name))
|
||||
return false
|
||||
Name other = (Name)o
|
||||
name.equals(other.name)
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package com.muwire.core
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPublicKey
|
||||
|
||||
public class Persona {
|
||||
private static final int SIG_LEN = Constants.SIG_TYPE.getSigLen()
|
||||
|
||||
private final byte version
|
||||
private final Name name
|
||||
private final Destination destination
|
||||
private final byte[] sig
|
||||
private volatile String humanReadableName
|
||||
private volatile String base64
|
||||
private volatile byte[] payload
|
||||
|
||||
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
|
||||
version = (byte) (personaStream.read() & 0xFF)
|
||||
if (version != Constants.PERSONA_VERSION)
|
||||
throw new IOException("Unknown version "+version)
|
||||
|
||||
name = new Name(personaStream)
|
||||
destination = Destination.create(personaStream)
|
||||
sig = new byte[SIG_LEN]
|
||||
DataInputStream dis = new DataInputStream(personaStream)
|
||||
dis.readFully(sig)
|
||||
if (!verify(version, name, destination, sig))
|
||||
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify")
|
||||
}
|
||||
|
||||
private static boolean verify(byte version, Name name, Destination destination, byte [] sig) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
baos.write(version)
|
||||
name.write(baos)
|
||||
destination.writeBytes(baos)
|
||||
byte[] payload = baos.toByteArray()
|
||||
SigningPublicKey spk = destination.getSigningPublicKey()
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
public void write(OutputStream out) throws IOException {
|
||||
if (payload == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
baos.write(version)
|
||||
name.write(baos)
|
||||
destination.writeBytes(baos)
|
||||
baos.write(sig)
|
||||
payload = baos.toByteArray()
|
||||
}
|
||||
out.write(payload)
|
||||
}
|
||||
|
||||
public String getHumanReadableName() {
|
||||
if (humanReadableName == null)
|
||||
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32)
|
||||
humanReadableName
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
if (base64 == null) {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
write(baos)
|
||||
base64 = Base64.encode(baos.toByteArray())
|
||||
}
|
||||
base64
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
name.hashCode() ^ destination.hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Persona))
|
||||
return false
|
||||
Persona other = (Persona)o
|
||||
name.equals(other.name) && destination.equals(other.destination)
|
||||
}
|
||||
|
||||
public static void main(String []args) {
|
||||
if (args.length != 1) {
|
||||
println "This utility decodes a bas64-encoded persona"
|
||||
System.exit(1)
|
||||
}
|
||||
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(args[0])))
|
||||
println p.getHumanReadableName()
|
||||
}
|
||||
}
|
91
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
91
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
@ -0,0 +1,91 @@
|
||||
package com.muwire.core
|
||||
|
||||
class SplitPattern {
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?\r\n]";
|
||||
|
||||
private static final Set<Character> SPLIT_CHARS = new HashSet<>()
|
||||
static {
|
||||
SPLIT_CHARS.with {
|
||||
add(' '.toCharacter())
|
||||
add('*'.toCharacter())
|
||||
add('+'.toCharacter())
|
||||
add('-'.toCharacter())
|
||||
add(','.toCharacter())
|
||||
add('.'.toCharacter())
|
||||
add(':'.toCharacter())
|
||||
add(';'.toCharacter())
|
||||
add('('.toCharacter())
|
||||
add(')'.toCharacter())
|
||||
add('='.toCharacter())
|
||||
add('_'.toCharacter())
|
||||
add('/'.toCharacter())
|
||||
add('\\'.toCharacter())
|
||||
add('!'.toCharacter())
|
||||
add('\''.toCharacter())
|
||||
add('$'.toCharacter())
|
||||
add('%'.toCharacter())
|
||||
add('|'.toCharacter())
|
||||
add('['.toCharacter())
|
||||
add(']'.toCharacter())
|
||||
add('{'.toCharacter())
|
||||
add('}'.toCharacter())
|
||||
add('?'.toCharacter())
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] termify(final String source) {
|
||||
String lowercase = source.toLowerCase().trim()
|
||||
|
||||
def rv = []
|
||||
int pos = 0
|
||||
int quote = -1
|
||||
|
||||
StringBuilder tmp = new StringBuilder()
|
||||
while(pos < lowercase.length()) {
|
||||
char c = lowercase.charAt(pos++)
|
||||
if (quote < 0 && c == '"') {
|
||||
quote = pos - 1
|
||||
continue
|
||||
}
|
||||
if (quote >= 0) {
|
||||
if (c == '"') {
|
||||
quote = -1
|
||||
if (tmp.length() != 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append(c)
|
||||
} else if (SPLIT_CHARS.contains(c)) {
|
||||
if (tmp.length() != 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append c
|
||||
}
|
||||
|
||||
// check if odd number of quotes and re-tokenize from last quote
|
||||
if (quote >= 0) {
|
||||
tmp = new StringBuilder()
|
||||
pos = quote + 1
|
||||
while(pos < lowercase.length()) {
|
||||
char c = lowercase.charAt(pos++)
|
||||
if (SPLIT_CHARS.contains(c)) {
|
||||
if (tmp.length() > 0) {
|
||||
rv << tmp.toString()
|
||||
tmp = new StringBuilder()
|
||||
}
|
||||
} else
|
||||
tmp.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp.length() > 0)
|
||||
rv << tmp.toString()
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
}
|
24
core/src/main/groovy/com/muwire/core/chat/ChatAction.java
Normal file
24
core/src/main/groovy/com/muwire/core/chat/ChatAction.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.muwire.core.chat;
|
||||
|
||||
enum ChatAction {
|
||||
JOIN(true, false, true, false),
|
||||
LEAVE(false, false, true, false),
|
||||
SAY(false, false, true, false),
|
||||
LIST(true, true, true, false),
|
||||
HELP(true, true, true, false),
|
||||
INFO(true, true, true, false),
|
||||
JOINED(true, true, false, false),
|
||||
TRUST(true, false, true, true),
|
||||
DISTRUST(true, false, true, true);
|
||||
|
||||
final boolean console;
|
||||
final boolean stateless;
|
||||
final boolean user;
|
||||
final boolean local;
|
||||
ChatAction(boolean console, boolean stateless, boolean user, boolean local) {
|
||||
this.console = console;
|
||||
this.stateless = stateless;
|
||||
this.user = user;
|
||||
this.local = local;
|
||||
}
|
||||
}
|
141
core/src/main/groovy/com/muwire/core/chat/ChatClient.groovy
Normal file
141
core/src/main/groovy/com/muwire/core/chat/ChatClient.groovy
Normal file
@ -0,0 +1,141 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class ChatClient implements Closeable {
|
||||
|
||||
private static final long REJECTION_BACKOFF = 60 * 1000
|
||||
|
||||
private static final Executor CONNECTOR = Executors.newCachedThreadPool()
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
private final Persona host, me
|
||||
private final TrustService trustService
|
||||
private final MuWireSettings settings
|
||||
|
||||
private ChatConnection connection
|
||||
private boolean connectInProgress
|
||||
private long lastRejectionTime
|
||||
private Thread connectThread
|
||||
|
||||
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
|
||||
MuWireSettings settings) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
this.host = host
|
||||
this.me = me
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
synchronized void connectIfNeeded() {
|
||||
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
|
||||
return
|
||||
connectInProgress = true
|
||||
CONNECTOR.execute({connect()})
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
synchronized(this) {
|
||||
if (!connectInProgress)
|
||||
return
|
||||
connectThread = Thread.currentThread()
|
||||
}
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
|
||||
endpoint = connector.connect(host.destination)
|
||||
DataOutputStream dos = new DataOutputStream(endpoint.getOutputStream())
|
||||
DataInputStream dis = new DataInputStream(endpoint.getInputStream())
|
||||
|
||||
dos.with {
|
||||
write("IRC\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("Version:${Constants.CHAT_VERSION}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
flush()
|
||||
}
|
||||
|
||||
String codeString = DataUtil.readTillRN(dis)
|
||||
int code = Integer.parseInt(codeString.split(" ")[0])
|
||||
|
||||
if (code == 429) {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
|
||||
try { dos.close() } catch (IOException ignore) {}
|
||||
endpoint.close()
|
||||
synchronized(this) {
|
||||
lastRejectionTime = System.currentTimeMillis()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (code != 200)
|
||||
throw new Exception("unknown code $code")
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis)
|
||||
if (!headers.containsKey('Version'))
|
||||
throw new Exception("Version header missing")
|
||||
|
||||
int version = Integer.parseInt(headers['Version'])
|
||||
if (version != Constants.CHAT_VERSION)
|
||||
throw new Exception("Unknown chat version $version")
|
||||
|
||||
synchronized(this) {
|
||||
if (!connectInProgress)
|
||||
return
|
||||
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
|
||||
connection.start()
|
||||
}
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
|
||||
connection : connection))
|
||||
} catch (Exception e) {
|
||||
log.log(java.util.logging.Level.WARNING, "connect failed", e)
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
|
||||
if (endpoint != null) {
|
||||
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
|
||||
endpoint.close()
|
||||
}
|
||||
} finally {
|
||||
synchronized(this) {
|
||||
connectInProgress = false
|
||||
connectThread = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void disconnected() {
|
||||
connectInProgress = false
|
||||
connection = null
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void close() {
|
||||
connectInProgress = false
|
||||
connectThread?.interrupt()
|
||||
connection?.close()
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
|
||||
}
|
||||
|
||||
synchronized void ping() {
|
||||
connection?.sendPing()
|
||||
}
|
||||
|
||||
synchronized void sendChat(ChatMessageEvent e) {
|
||||
connection?.sendChat(e)
|
||||
}
|
||||
}
|
28
core/src/main/groovy/com/muwire/core/chat/ChatCommand.groovy
Normal file
28
core/src/main/groovy/com/muwire/core/chat/ChatCommand.groovy
Normal file
@ -0,0 +1,28 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
class ChatCommand {
|
||||
private final ChatAction action
|
||||
private final String payload
|
||||
final String source
|
||||
ChatCommand(String source) {
|
||||
if (source.charAt(0) != '/')
|
||||
throw new Exception("command doesn't start with / $source")
|
||||
|
||||
int position = 1
|
||||
StringBuilder sb = new StringBuilder()
|
||||
while(position < source.length()) {
|
||||
char c = source.charAt(position)
|
||||
if (c == ' ')
|
||||
break
|
||||
sb.append(c)
|
||||
position++
|
||||
}
|
||||
String command = sb.toString().toUpperCase()
|
||||
action = ChatAction.valueOf(command)
|
||||
if (position < source.length())
|
||||
payload = source.substring(position + 1)
|
||||
else
|
||||
payload = ""
|
||||
this.source = source
|
||||
}
|
||||
}
|
280
core/src/main/groovy/com/muwire/core/chat/ChatConnection.groovy
Normal file
280
core/src/main/groovy/com/muwire/core/chat/ChatConnection.groovy
Normal file
@ -0,0 +1,280 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
@Log
|
||||
class ChatConnection implements ChatLink {
|
||||
|
||||
private static final long PING_INTERVAL = 20000
|
||||
private static final long MAX_CHAT_AGE = 5 * 60 * 1000
|
||||
|
||||
private final EventBus eventBus
|
||||
private final Endpoint endpoint
|
||||
private final Persona persona
|
||||
private final boolean incoming
|
||||
private final TrustService trustService
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
private final Thread reader, writer
|
||||
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||
private final BlockingQueue incomingEvents = new LinkedBlockingQueue()
|
||||
|
||||
private final DataInputStream dis
|
||||
private final DataOutputStream dos
|
||||
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
private volatile long lastPingSentTime
|
||||
|
||||
ChatConnection(EventBus eventBus, Endpoint endpoint, Persona persona, boolean incoming,
|
||||
TrustService trustService, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.endpoint = endpoint
|
||||
this.persona = persona
|
||||
this.incoming = incoming
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
|
||||
this.dis = new DataInputStream(endpoint.getInputStream())
|
||||
this.dos = new DataOutputStream(endpoint.getOutputStream())
|
||||
|
||||
this.reader = new Thread({readLoop()} as Runnable)
|
||||
this.reader.setName("reader-${persona.getHumanReadableName()}")
|
||||
this.reader.setDaemon(true)
|
||||
|
||||
this.writer = new Thread({writeLoop()} as Runnable)
|
||||
this.writer.setName("writer-${persona.getHumanReadableName()}")
|
||||
this.writer.setDaemon(true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"${persona.getHumanReadableName()} already running", new Exception())
|
||||
return
|
||||
}
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUp() {
|
||||
running.get()
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persona getPersona() {
|
||||
persona
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
log.log(Level.WARNING,"${persona.getHumanReadableName()} already closed", new Exception())
|
||||
return
|
||||
}
|
||||
log.info("Closing "+persona.getHumanReadableName())
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
eventBus.publish(new ChatDisconnectionEvent(persona : persona))
|
||||
}
|
||||
|
||||
private void readLoop() {
|
||||
try {
|
||||
while(running.get())
|
||||
read()
|
||||
} catch( InterruptedException | SocketTimeoutException ignored) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader", e)
|
||||
} finally {
|
||||
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (InterruptedException ignore) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in writer",e)
|
||||
} finally {
|
||||
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private void read() {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte [] payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : break // just ignore
|
||||
case "Chat" : handleChat(json); break
|
||||
case "Leave": handleLeave(json); break
|
||||
default :
|
||||
throw new Exception("unknown json type ${json.type}")
|
||||
}
|
||||
}
|
||||
|
||||
private void write(Object message) {
|
||||
byte [] payload = JsonOutput.toJson(message).bytes
|
||||
dos.with {
|
||||
writeShort(payload.length)
|
||||
write(payload)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
void sendPing() {
|
||||
long now = System.currentTimeMillis()
|
||||
if (now - lastPingSentTime < PING_INTERVAL)
|
||||
return
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
messages.put(ping)
|
||||
lastPingSentTime = now
|
||||
}
|
||||
|
||||
private void handleChat(def json) {
|
||||
UUID uuid = UUID.fromString(json.uuid)
|
||||
Persona host = fromString(json.host)
|
||||
Persona sender = fromString(json.sender)
|
||||
long chatTime = json.chatTime
|
||||
String room = json.room
|
||||
String payload = json.payload
|
||||
byte [] sig = Base64.decode(json.sig)
|
||||
|
||||
if (!verify(uuid,host,sender,chatTime,room,payload,sig)) {
|
||||
log.warning("chat didn't verify")
|
||||
return
|
||||
}
|
||||
if (incoming) {
|
||||
if (sender.destination != endpoint.destination) {
|
||||
log.warning("Sender destination mismatch, dropping message")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (host.destination != endpoint.destination) {
|
||||
log.warning("Host destination mismatch, dropping message")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (System.currentTimeMillis() - chatTime > MAX_CHAT_AGE) {
|
||||
log.warning("Chat too old, dropping")
|
||||
return
|
||||
}
|
||||
switch(trustService.getLevel(sender.destination)) {
|
||||
case TrustLevel.TRUSTED : break
|
||||
case TrustLevel.NEUTRAL :
|
||||
if (!settings.allowUntrusted)
|
||||
return
|
||||
else
|
||||
break
|
||||
case TrustLevel.DISTRUSTED :
|
||||
return
|
||||
}
|
||||
def event = new ChatMessageEvent( uuid : uuid, payload : payload, sender : sender,
|
||||
host : host, room : room, chatTime : chatTime, sig : sig)
|
||||
eventBus.publish(event)
|
||||
if (!incoming)
|
||||
incomingEvents.put(event)
|
||||
}
|
||||
|
||||
private void handleLeave(def json) {
|
||||
Persona leaver = fromString(json.persona)
|
||||
eventBus.publish(new UserDisconnectedEvent(user : leaver, host : persona))
|
||||
incomingEvents.put(leaver)
|
||||
}
|
||||
|
||||
private static Persona fromString(String base64) {
|
||||
new Persona(new ByteArrayInputStream(Base64.decode(base64)))
|
||||
}
|
||||
|
||||
private static boolean verify(UUID uuid, Persona host, Persona sender, long chatTime,
|
||||
String room, String payload, byte []sig) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.write(uuid.toString().bytes)
|
||||
host.write(daos)
|
||||
sender.write(daos)
|
||||
daos.writeLong(chatTime)
|
||||
daos.write(room.getBytes(StandardCharsets.UTF_8))
|
||||
daos.write(payload.getBytes(StandardCharsets.UTF_8))
|
||||
daos.close()
|
||||
byte [] signed = baos.toByteArray()
|
||||
def spk = sender.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, signed, spk)
|
||||
}
|
||||
|
||||
public static byte[] sign(UUID uuid, long chatTime, String room, String words, Persona sender, Persona host, SigningPrivateKey spk) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.with {
|
||||
write(uuid.toString().bytes)
|
||||
host.write(daos)
|
||||
sender.write(daos)
|
||||
writeLong(chatTime)
|
||||
write(room.getBytes(StandardCharsets.UTF_8))
|
||||
write(words.getBytes(StandardCharsets.UTF_8))
|
||||
close()
|
||||
}
|
||||
byte [] payload = baos.toByteArray()
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk)
|
||||
sig.getData()
|
||||
}
|
||||
|
||||
void sendChat(ChatMessageEvent e) {
|
||||
def chat = [:]
|
||||
chat.type = "Chat"
|
||||
chat.uuid = e.uuid.toString()
|
||||
chat.host = e.host.toBase64()
|
||||
chat.sender = e.sender.toBase64()
|
||||
chat.chatTime = e.chatTime
|
||||
chat.room = e.room
|
||||
chat.payload = e.payload
|
||||
chat.sig = Base64.encode(e.sig)
|
||||
messages.put(chat)
|
||||
}
|
||||
|
||||
void sendLeave(Persona p) {
|
||||
def leave = [:]
|
||||
leave.type = "Leave"
|
||||
leave.persona = p.toBase64()
|
||||
messages.put(leave)
|
||||
}
|
||||
|
||||
public Object nextEvent() {
|
||||
incomingEvents.take()
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.muwire.core.chat;
|
||||
|
||||
public enum ChatConnectionAttemptStatus {
|
||||
CONNECTING, SUCCESSFUL, REJECTED, FAILED, DISCONNECTED
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class ChatConnectionEvent extends Event {
|
||||
ChatConnectionAttemptStatus status
|
||||
Persona persona
|
||||
ChatLink connection
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class ChatDisconnectionEvent extends Event {
|
||||
Persona persona
|
||||
}
|
14
core/src/main/groovy/com/muwire/core/chat/ChatLink.java
Normal file
14
core/src/main/groovy/com/muwire/core/chat/ChatLink.java
Normal file
@ -0,0 +1,14 @@
|
||||
package com.muwire.core.chat;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
public interface ChatLink extends Closeable {
|
||||
public Persona getPersona();
|
||||
public boolean isUp();
|
||||
public void sendChat(ChatMessageEvent e);
|
||||
public void sendLeave(Persona p);
|
||||
public void sendPing();
|
||||
public Object nextEvent() throws InterruptedException;
|
||||
}
|
73
core/src/main/groovy/com/muwire/core/chat/ChatManager.groovy
Normal file
73
core/src/main/groovy/com/muwire/core/chat/ChatManager.groovy
Normal file
@ -0,0 +1,73 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
class ChatManager {
|
||||
private final EventBus eventBus
|
||||
private final Persona me
|
||||
private final I2PConnector connector
|
||||
private final TrustService trustService
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final Map<Persona, ChatClient> clients = new ConcurrentHashMap<>()
|
||||
|
||||
ChatManager(EventBus eventBus, Persona me, I2PConnector connector, TrustService trustService,
|
||||
MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.connector = connector
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
|
||||
Timer timer = new Timer("chat-connector", true)
|
||||
timer.schedule({connect()} as TimerTask, 1000, 1000)
|
||||
}
|
||||
|
||||
void onUIConnectChatEvent(UIConnectChatEvent e) {
|
||||
if (e.host == me) {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL,
|
||||
persona : me, connection : LocalChatLink.INSTANCE))
|
||||
} else {
|
||||
ChatClient client = new ChatClient(connector, eventBus, e.host, me, trustService, settings)
|
||||
clients.put(e.host, client)
|
||||
}
|
||||
}
|
||||
|
||||
void onUIDisconnectChatEvent(UIDisconnectChatEvent e) {
|
||||
if (e.host == me)
|
||||
return
|
||||
ChatClient client = clients.remove(e.host)
|
||||
client?.close()
|
||||
}
|
||||
|
||||
void onChatMessageEvent(ChatMessageEvent e) {
|
||||
if (e.host == me)
|
||||
return
|
||||
if (e.sender != me)
|
||||
return
|
||||
clients[e.host]?.sendChat(e)
|
||||
}
|
||||
|
||||
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
||||
clients[e.persona]?.disconnected()
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
clients.each { k, v ->
|
||||
v.connectIfNeeded()
|
||||
v.ping()
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
clients.each { k, v ->
|
||||
v.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class ChatMessageEvent extends Event {
|
||||
UUID uuid
|
||||
String payload
|
||||
Persona sender, host
|
||||
String room
|
||||
long chatTime
|
||||
byte [] sig
|
||||
}
|
330
core/src/main/groovy/com/muwire/core/chat/ChatServer.groovy
Normal file
330
core/src/main/groovy/com/muwire/core/chat/ChatServer.groovy
Normal file
@ -0,0 +1,330 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class ChatServer {
|
||||
public static final String CONSOLE = "__CONSOLE__"
|
||||
private static final String DEFAULT_WELCOME = "Welcome to my chat server! Type /HELP for list of available commands"
|
||||
|
||||
private final EventBus eventBus
|
||||
private final MuWireSettings settings
|
||||
private final TrustService trustService
|
||||
private final Persona me
|
||||
private final SigningPrivateKey spk
|
||||
|
||||
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
|
||||
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
|
||||
private final Map<Persona, Set<String>> memberships = new ConcurrentHashMap<>()
|
||||
private final Map<String, Persona> shortNames = new ConcurrentHashMap<>()
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
|
||||
ChatServer(EventBus eventBus, MuWireSettings settings, TrustService trustService, Persona me, SigningPrivateKey spk) {
|
||||
this.eventBus = eventBus
|
||||
this.settings = settings
|
||||
this.trustService = trustService
|
||||
this.me = me
|
||||
this.spk = spk
|
||||
|
||||
Timer timer = new Timer("chat-server-pinger", true)
|
||||
timer.schedule({sendPings()} as TimerTask, 1000, 1000)
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (!running.compareAndSet(false, true))
|
||||
return
|
||||
connections.put(me.destination, LocalChatLink.INSTANCE)
|
||||
joinRoom(me, CONSOLE)
|
||||
shortNames.put(me.getHumanReadableName(), me)
|
||||
echo(getWelcome(),me.destination)
|
||||
}
|
||||
|
||||
private String getWelcome() {
|
||||
String welcome = DEFAULT_WELCOME
|
||||
if (settings.chatWelcomeFile != null)
|
||||
welcome = settings.chatWelcomeFile.text
|
||||
"/SAY $welcome"
|
||||
}
|
||||
|
||||
private void sendPings() {
|
||||
connections.each { k,v ->
|
||||
v.sendPing()
|
||||
}
|
||||
}
|
||||
|
||||
public void handle(Endpoint endpoint) {
|
||||
InputStream is = endpoint.getInputStream()
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Version"))
|
||||
throw new Exception("Version header missing")
|
||||
|
||||
int version = Integer.parseInt(headers['Version'])
|
||||
if (version != Constants.CHAT_VERSION)
|
||||
throw new Exception("Unknown chat version $version")
|
||||
|
||||
if (!headers.containsKey('Persona'))
|
||||
throw new Exception("Persona header missing")
|
||||
|
||||
Persona client = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
|
||||
if (client.destination != endpoint.destination)
|
||||
throw new Exception("Client destination mismatch")
|
||||
|
||||
if (!running.get()) {
|
||||
os.write("400 Chat Not Enabled\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.close()
|
||||
endpoint.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (connections.containsKey(client.destination) || connections.size() == settings.maxChatConnections) {
|
||||
os.write("429 Rejected\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.close()
|
||||
endpoint.close()
|
||||
return
|
||||
}
|
||||
|
||||
os.with {
|
||||
write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("Version:${Constants.CHAT_VERSION}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
flush()
|
||||
}
|
||||
|
||||
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
|
||||
connections.put(endpoint.destination, connection)
|
||||
joinRoom(client, CONSOLE)
|
||||
shortNames.put(client.getHumanReadableName(), client)
|
||||
connection.start()
|
||||
echo(getWelcome(),connection.endpoint.destination)
|
||||
}
|
||||
|
||||
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
||||
ChatConnection con = connections.remove(e.persona.destination)
|
||||
if (con == null)
|
||||
return
|
||||
|
||||
Set<String> rooms = memberships.get(e.persona)
|
||||
if (rooms != null) {
|
||||
rooms.each {
|
||||
leaveRoom(e.persona, it)
|
||||
}
|
||||
}
|
||||
shortNames.remove(e.persona.getHumanReadableName())
|
||||
connections.each { k, v ->
|
||||
v.sendLeave(e.persona)
|
||||
}
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.TRUSTED)
|
||||
return
|
||||
if (settings.allowUntrusted && e.level == TrustLevel.NEUTRAL)
|
||||
return
|
||||
|
||||
ChatConnection connection = connections.get(e.persona.destination)
|
||||
connection?.close()
|
||||
}
|
||||
|
||||
private void joinRoom(Persona p, String room) {
|
||||
Set<Persona> existing = rooms.get(room)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
rooms.put(room, existing)
|
||||
}
|
||||
existing.add(p)
|
||||
|
||||
Set<String> membership = memberships.get(p)
|
||||
if (membership == null) {
|
||||
membership = new ConcurrentHashSet<>()
|
||||
memberships.put(p, membership)
|
||||
}
|
||||
membership.add(room)
|
||||
}
|
||||
|
||||
private void leaveRoom(Persona p, String room) {
|
||||
Set<Persona> existing = rooms.get(room)
|
||||
if (existing == null) {
|
||||
log.warning(p.getHumanReadableName() + " leaving room they hadn't joined")
|
||||
return
|
||||
}
|
||||
existing.remove(p)
|
||||
if (existing.isEmpty())
|
||||
rooms.remove(room)
|
||||
|
||||
Set<String> membership = memberships.get(p)
|
||||
if (membership == null) {
|
||||
log.warning(p.getHumanReadableName() + " didn't have any memberships")
|
||||
return
|
||||
}
|
||||
membership.remove(room)
|
||||
if (membership.isEmpty())
|
||||
memberships.remove(p)
|
||||
}
|
||||
|
||||
void onChatMessageEvent(ChatMessageEvent e) {
|
||||
if (e.host != me)
|
||||
return
|
||||
|
||||
ChatCommand command
|
||||
try {
|
||||
command = new ChatCommand(e.payload)
|
||||
} catch (Exception badCommand) {
|
||||
log.log(Level.WARNING, "bad chat command",badCommand)
|
||||
return
|
||||
}
|
||||
|
||||
if ((command.action.console && e.room != CONSOLE) ||
|
||||
(!command.action.console && e.room == CONSOLE) ||
|
||||
!command.action.user)
|
||||
return
|
||||
|
||||
if (command.action.local && e.sender != me)
|
||||
return
|
||||
|
||||
switch(command.action) {
|
||||
case ChatAction.JOIN : processJoin(command.payload, e); break
|
||||
case ChatAction.LEAVE : processLeave(e); break
|
||||
case ChatAction.SAY : processSay(e); break
|
||||
case ChatAction.LIST : processList(e.sender.destination); break
|
||||
case ChatAction.INFO : processInfo(e.sender.destination); break
|
||||
case ChatAction.HELP : processHelp(e.sender.destination); break
|
||||
case ChatAction.TRUST : processTrust(command.payload, TrustLevel.TRUSTED); break
|
||||
case ChatAction.DISTRUST : processTrust(command.payload, TrustLevel.DISTRUSTED); break
|
||||
}
|
||||
}
|
||||
|
||||
private void processJoin(String room, ChatMessageEvent e) {
|
||||
joinRoom(e.sender, room)
|
||||
rooms[room].each {
|
||||
if (it == e.sender)
|
||||
return
|
||||
connections[it.destination].sendChat(e)
|
||||
}
|
||||
String payload = rooms[room].stream().filter({it != e.sender}).map({it.toBase64()})
|
||||
.collect(Collectors.joining(","))
|
||||
if (payload.length() == 0) {
|
||||
return
|
||||
}
|
||||
payload = "/JOINED $payload"
|
||||
long now = System.currentTimeMillis()
|
||||
UUID uuid = UUID.randomUUID()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, room, payload, me, me, spk)
|
||||
ChatMessageEvent echo = new ChatMessageEvent(
|
||||
uuid : uuid,
|
||||
payload : payload,
|
||||
sender : me,
|
||||
host : me,
|
||||
room : room,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
connections[e.sender.destination].sendChat(echo)
|
||||
}
|
||||
|
||||
private void processLeave(ChatMessageEvent e) {
|
||||
leaveRoom(e.sender, e.room)
|
||||
rooms.getOrDefault(e.room, []).each {
|
||||
if (it == e.sender)
|
||||
return
|
||||
connections[it.destination].sendChat(e)
|
||||
}
|
||||
}
|
||||
|
||||
private void processSay(ChatMessageEvent e) {
|
||||
if (rooms.containsKey(e.room)) {
|
||||
// not a private message
|
||||
rooms[e.room].each {
|
||||
if (it == e.sender)
|
||||
return
|
||||
connections[it.destination].sendChat(e)
|
||||
}
|
||||
} else {
|
||||
Persona target = new Persona(new ByteArrayInputStream(Base64.decode(e.room)))
|
||||
connections[target.destination]?.sendChat(e)
|
||||
}
|
||||
}
|
||||
|
||||
private void processList(Destination d) {
|
||||
String roomList = rooms.keySet().stream().filter({it != CONSOLE}).collect(Collectors.joining("\n"))
|
||||
roomList = "/SAY \nRoom List:\n"+roomList
|
||||
echo(roomList, d)
|
||||
}
|
||||
|
||||
private void processInfo(Destination d) {
|
||||
String info = "/SAY \nThe address of this server is\n========\n${me.toBase64()}\n========\nCopy/paste the above and share it\n"
|
||||
String connectedUsers = memberships.keySet().stream().map({it.getHumanReadableName()}).collect(Collectors.joining("\n"))
|
||||
info = "${info}\nConnected Users:\n$connectedUsers\n======="
|
||||
echo(info, d)
|
||||
}
|
||||
|
||||
private void processHelp(Destination d) {
|
||||
String help = """/SAY
|
||||
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /TRUST /DISTRUST /HELP
|
||||
/JOIN <room name> - joins a room, or creates one if it does not exist. You must type this in the console
|
||||
/LEAVE - leaves a room. You must type this in the room you want to leave
|
||||
/SAY - optional, says something in the room you're in
|
||||
/LIST - lists the existing rooms on this server. You must type this in the console
|
||||
/INFO - shows information about this server. You must type this in the console
|
||||
/TRUST <user> - marks user as trusted. This is only available to the server owner
|
||||
/DISTRUST <user> - marks user as distrusted. This is only available to the server owner
|
||||
/HELP - prints this help message
|
||||
"""
|
||||
echo(help, d)
|
||||
}
|
||||
|
||||
private void echo(String payload, Destination d) {
|
||||
log.info "echoing $payload"
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long now = System.currentTimeMillis()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, CONSOLE, payload, me, me, spk)
|
||||
ChatMessageEvent echo = new ChatMessageEvent(
|
||||
uuid : uuid,
|
||||
payload : payload,
|
||||
sender : me,
|
||||
host : me,
|
||||
room : CONSOLE,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
connections[d]?.sendChat(echo)
|
||||
}
|
||||
|
||||
private void processTrust(String shortName, TrustLevel level) {
|
||||
Persona p = shortNames.get(shortName)
|
||||
if (p == null)
|
||||
return
|
||||
eventBus.publish(new TrustEvent(persona : p, level : level))
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (running.compareAndSet(true, false)) {
|
||||
connections.each { k, v ->
|
||||
v.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class LocalChatLink implements ChatLink {
|
||||
|
||||
public static final LocalChatLink INSTANCE = new LocalChatLink()
|
||||
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
|
||||
private LocalChatLink() {}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendChat(ChatMessageEvent e) {
|
||||
messages.put(e)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLeave(Persona p) {
|
||||
messages.put(p)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPing() {}
|
||||
|
||||
@Override
|
||||
public Object nextEvent() {
|
||||
messages.take()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUp() {
|
||||
true
|
||||
}
|
||||
|
||||
public Persona getPersona() {
|
||||
null
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIConnectChatEvent extends Event {
|
||||
Persona host
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIDisconnectChatEvent extends Event {
|
||||
Persona host
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UserDisconnectedEvent extends Event {
|
||||
Persona user
|
||||
Persona host
|
||||
}
|
@ -1,10 +1,17 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
@ -16,12 +23,14 @@ import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.Signature
|
||||
|
||||
@Log
|
||||
abstract class Connection implements Closeable {
|
||||
|
||||
|
||||
private static final int SEARCHES = 10
|
||||
private static final long INTERVAL = 1000
|
||||
|
||||
@ -83,6 +92,7 @@ abstract class Connection implements Closeable {
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
log.info("closed $name")
|
||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||
}
|
||||
|
||||
@ -91,6 +101,7 @@ abstract class Connection implements Closeable {
|
||||
while(running.get()) {
|
||||
read()
|
||||
}
|
||||
} catch (InterruptedException ok) {
|
||||
} catch (SocketTimeoutException e) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader",e)
|
||||
@ -107,6 +118,7 @@ abstract class Connection implements Closeable {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (InterruptedException ok) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "unhandled exception in writer",e)
|
||||
} finally {
|
||||
@ -132,11 +144,19 @@ abstract class Connection implements Closeable {
|
||||
query.firstHop = e.firstHop
|
||||
query.keywords = e.searchEvent.getSearchTerms()
|
||||
query.oobInfohash = e.searchEvent.oobInfohash
|
||||
query.searchComments = e.searchEvent.searchComments
|
||||
query.compressedResults = e.searchEvent.compressedResults
|
||||
if (e.searchEvent.searchHash != null)
|
||||
query.infohash = Base64.encode(e.searchEvent.searchHash)
|
||||
query.replyTo = e.replyTo.toBase64()
|
||||
if (e.originator != null)
|
||||
query.originator = e.originator.toBase64()
|
||||
if (e.sig != null)
|
||||
query.sig = Base64.encode(e.sig)
|
||||
if (e.queryTime > 0)
|
||||
query.queryTime = e.queryTime
|
||||
if (e.sig2 != null)
|
||||
query.sig2 = Base64.encode(e.sig2)
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
@ -209,16 +229,73 @@ abstract class Connection implements Closeable {
|
||||
boolean oob = false
|
||||
if (search.oobInfohash != null)
|
||||
oob = search.oobInfohash
|
||||
boolean searchComments = false
|
||||
if (search.searchComments != null)
|
||||
searchComments = search.searchComments
|
||||
boolean compressedResults = false
|
||||
if (search.compressedResults != null)
|
||||
compressedResults = search.compressedResults
|
||||
byte[] sig = null
|
||||
if (search.sig != null) {
|
||||
sig = Base64.decode(search.sig)
|
||||
byte [] payload
|
||||
if (infohash != null)
|
||||
payload = infohash
|
||||
else
|
||||
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("signature didn't match keywords")
|
||||
return
|
||||
} else
|
||||
log.info("query signature verified")
|
||||
} else {
|
||||
log.info("no signature in query")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make this mandatory at some point
|
||||
byte[] sig2 = null
|
||||
long queryTime = 0
|
||||
if (search.sig2 != null) {
|
||||
if (search.queryTime == null) {
|
||||
log.info("extended signature but no timestamp")
|
||||
return
|
||||
}
|
||||
sig2 = Base64.decode(search.sig2)
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
} else {
|
||||
log.info("extended query signature verified")
|
||||
if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) {
|
||||
log.info("query too old")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else
|
||||
log.info("no extended signature in query")
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
uuid : uuid,
|
||||
oobInfohash : oob)
|
||||
oobInfohash : oob,
|
||||
searchComments : searchComments,
|
||||
compressedResults : compressedResults,
|
||||
persona : originator)
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
originator : originator,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : search.firstHop )
|
||||
firstHop : search.firstHop,
|
||||
sig : sig,
|
||||
queryTime : queryTime,
|
||||
sig2 : sig2 )
|
||||
eventBus.publish(event)
|
||||
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
package com.muwire.core.connection
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.attribute.DosFileAttributes
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
@ -17,6 +26,7 @@ import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.search.InvalidSearchResultException
|
||||
import com.muwire.core.search.ResultsParser
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
@ -25,6 +35,7 @@ import com.muwire.core.search.UnexpectedResultsException
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class ConnectionAcceptor {
|
||||
@ -37,17 +48,23 @@ class ConnectionAcceptor {
|
||||
final TrustService trustService
|
||||
final SearchManager searchManager
|
||||
final UploadManager uploadManager
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
final CertificateManager certificateManager
|
||||
final ChatServer chatServer
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
private volatile int browsed
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
ConnectionEstablisher establisher) {
|
||||
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager,
|
||||
ChatServer chatServer) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
@ -55,8 +72,11 @@ class ConnectionAcceptor {
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.searchManager = searchManager
|
||||
this.fileManager = fileManager
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
this.certificateManager = certificateManager
|
||||
this.chatServer = chatServer
|
||||
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
@ -126,14 +146,29 @@ class ConnectionAcceptor {
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
case (byte)'R':
|
||||
processRESULTS(e)
|
||||
break
|
||||
case (byte)'T':
|
||||
processTRUST(e)
|
||||
break
|
||||
case (byte)'B':
|
||||
processBROWSE(e)
|
||||
break
|
||||
case (byte)'C':
|
||||
processCERTIFICATES(e)
|
||||
break
|
||||
case (byte)'I':
|
||||
processIRC(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
try {
|
||||
e.getOutputStream().close()
|
||||
} catch (Exception ignore) {}
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
@ -182,7 +217,9 @@ class ConnectionAcceptor {
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.flush()
|
||||
try {
|
||||
e.outputStream.close()
|
||||
} catch (Exception ignored) {}
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
@ -229,7 +266,7 @@ class ConnectionAcceptor {
|
||||
|
||||
Persona sender = new Persona(dis)
|
||||
if (sender.destination != e.getDestination())
|
||||
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
||||
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
|
||||
int nResults = dis.readUnsignedShort()
|
||||
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||
for (int i = 0; i < nResults; i++) {
|
||||
@ -246,44 +283,246 @@ class ConnectionAcceptor {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processRESULTS(Endpoint e) {
|
||||
InputStream is = e.getInputStream()
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
byte[] esults = new byte[7]
|
||||
dis.readFully(esults)
|
||||
if (esults != "ESULTS ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid RESULTS connection")
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
try {
|
||||
String uuid = DataUtil.readTillRN(dis)
|
||||
UUID resultsUUID = UUID.fromString(uuid)
|
||||
if (!searchManager.hasLocalSearch(resultsUUID))
|
||||
throw new UnexpectedResultsException(resultsUUID.toString())
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is);
|
||||
|
||||
if (!headers.containsKey("Sender"))
|
||||
throw new IOException("No Sender header")
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No Count header")
|
||||
|
||||
boolean chat = false
|
||||
if (headers.containsKey('Chat'))
|
||||
chat = Boolean.parseBoolean(headers['Chat'])
|
||||
|
||||
byte [] personaBytes = Base64.decode(headers['Sender'])
|
||||
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
|
||||
if (sender.destination != e.getDestination())
|
||||
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
|
||||
|
||||
int nResults = Integer.parseInt(headers['Count'])
|
||||
if (nResults > Constants.MAX_RESULTS)
|
||||
throw new IOException("too many results $nResults")
|
||||
|
||||
dis = new DataInputStream(new GZIPInputStream(dis))
|
||||
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||
for (int i = 0; i < nResults; i++) {
|
||||
int jsonSize = dis.readUnsignedShort()
|
||||
byte [] payload = new byte[jsonSize]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||
results[i].chat = chat
|
||||
}
|
||||
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||
} catch (IOException bad) {
|
||||
log.log(Level.WARNING, "failed to process RESULTS", bad)
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void processBROWSE(Endpoint e) {
|
||||
try {
|
||||
byte [] rowse = new byte[7]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(rowse)
|
||||
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid BROWSE connection")
|
||||
|
||||
Persona browser = null
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis);
|
||||
if (headers.containsKey('Persona')) {
|
||||
browser = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
|
||||
if (browser.destination != e.destination)
|
||||
throw new IOException("browser persona mismatch")
|
||||
}
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.browseFiles) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
browsed++
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
def sharedFiles = fileManager.getSharedFiles().values()
|
||||
|
||||
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean chat = chatServer.running.get() && settings.advertiseChat
|
||||
os.write("Chat: ${chat}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
it.hit(browser, System.currentTimeMillis(), "Browse Host");
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
dos.flush()
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processTRUST(Endpoint e) {
|
||||
byte[] RUST = new byte[6]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
try {
|
||||
byte[] RUST = new byte[6]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis)
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
|
||||
|
||||
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
|
||||
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
|
||||
if (!json) {
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
} else {
|
||||
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
good.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
bad.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
} finally {
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private void processCERTIFICATES(Endpoint e) {
|
||||
try {
|
||||
byte [] ERTIFICATES = new byte[12]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(ERTIFICATES)
|
||||
if (ERTIFICATES != "ERTIFICATES ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid CERTIFICATES connection")
|
||||
|
||||
byte [] infoHashStringBytes = new byte[44]
|
||||
dis.readFully(infoHashStringBytes)
|
||||
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||
|
||||
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
List<Persona> good = new ArrayList<>(trustService.good.values())
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.write(dos)
|
||||
byte[] rn = new byte[2]
|
||||
dis.readFully(rn)
|
||||
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Malformed CERTIFICATES request")
|
||||
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
log.info("responding to certificates request for $infoHashString")
|
||||
byte [] root = Base64.decode(infoHashString)
|
||||
|
||||
Set<Certificate> certs = certificateManager.getByInfoHash(new InfoHash(root))
|
||||
if (certs.isEmpty()) {
|
||||
log.info("certs not found")
|
||||
e.getOutputStream().write("404 Certs Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
return
|
||||
}
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: ${certs.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
certs.each {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
it.write(baos)
|
||||
byte [] payload = baos.toByteArray()
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
|
||||
List<Persona> bad = new ArrayList<>(trustService.bad.values())
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
e.close()
|
||||
}
|
||||
|
||||
private void processIRC(Endpoint e) {
|
||||
byte[] IRC = new byte[4]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(IRC)
|
||||
if (IRC != "RC\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new Exception("Invalid IRC connection")
|
||||
chatServer.handle(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class ConnectionEstablisher {
|
||||
final HostCache hostCache
|
||||
|
||||
final Timer timer
|
||||
final ExecutorService executor
|
||||
final ExecutorService executor, closer
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
@ -51,6 +51,8 @@ class ConnectionEstablisher {
|
||||
rv.setName("connector-${System.currentTimeMillis()}")
|
||||
rv
|
||||
} as ThreadFactory)
|
||||
|
||||
closer = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void start() {
|
||||
@ -60,6 +62,7 @@ class ConnectionEstablisher {
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdownNow()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
@ -120,8 +123,12 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
if (!closer.isShutdown()) {
|
||||
closer.execute {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
} as Runnable
|
||||
}
|
||||
}
|
||||
|
||||
private void readK(Endpoint e) {
|
||||
@ -175,7 +182,7 @@ class ConnectionEstablisher {
|
||||
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
||||
} finally {
|
||||
// the end
|
||||
e.close()
|
||||
closer.execute({e.close()} as Runnable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,9 +36,8 @@ abstract class ConnectionManager {
|
||||
timer.schedule({sendPings()} as TimerTask, 1000,1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
void shutdown() {
|
||||
timer.cancel()
|
||||
getConnections().each { it.close() }
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
@ -62,8 +61,6 @@ abstract class ConnectionManager {
|
||||
|
||||
abstract void onDisconnectionEvent(DisconnectionEvent e)
|
||||
|
||||
abstract void shutdown()
|
||||
|
||||
protected void sendPings() {
|
||||
final long now = System.currentTimeMillis()
|
||||
getConnections().each {
|
||||
|
@ -31,9 +31,6 @@ class Endpoint implements Closeable {
|
||||
if (inputStream != null) {
|
||||
try {inputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {outputStream.close()} catch (Exception ignore) {}
|
||||
}
|
||||
if (toClose != null) {
|
||||
try {toClose.reset()} catch (Exception ignore) {}
|
||||
}
|
||||
|
@ -104,6 +104,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
|
||||
@Override
|
||||
void shutdown() {
|
||||
super.shutdown()
|
||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
peerConnections.clear()
|
||||
|
@ -12,6 +12,7 @@ import com.muwire.core.util.DataUtil
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
@ -25,7 +26,9 @@ import com.muwire.core.UILoadedEvent
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
public class DownloadManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
@ -34,7 +37,7 @@ public class DownloadManager {
|
||||
private final MuWireSettings muSettings
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File incompletes, home
|
||||
private final File home
|
||||
private final Persona me
|
||||
|
||||
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
|
||||
@ -46,12 +49,9 @@ public class DownloadManager {
|
||||
this.meshManager = meshManager
|
||||
this.muSettings = muSettings
|
||||
this.connector = connector
|
||||
this.incompletes = new File(home,"incompletes")
|
||||
this.home = home
|
||||
this.me = me
|
||||
|
||||
incompletes.mkdir()
|
||||
|
||||
this.executor = Executors.newCachedThreadPool({ r ->
|
||||
Thread rv = new Thread(r)
|
||||
rv.setName("download-worker")
|
||||
@ -63,6 +63,11 @@ public class DownloadManager {
|
||||
|
||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
||||
|
||||
File incompletes = muSettings.incompleteLocation
|
||||
if (incompletes == null)
|
||||
incompletes = new File(home, "incompletes")
|
||||
incompletes.mkdirs()
|
||||
|
||||
def size = e.result[0].size
|
||||
def infohash = e.result[0].infohash
|
||||
def pieceSize = e.result[0].pieceSize
|
||||
@ -74,7 +79,7 @@ public class DownloadManager {
|
||||
destinations.addAll(e.sources)
|
||||
destinations.remove(me.destination)
|
||||
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize)
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
@ -122,27 +127,48 @@ public class DownloadManager {
|
||||
byte [] root = Base64.decode(json.hashRoot)
|
||||
infoHash = new InfoHash(root)
|
||||
}
|
||||
|
||||
boolean sequential = false
|
||||
if (json.sequential != null)
|
||||
sequential = json.sequential
|
||||
|
||||
File incompletes
|
||||
if (json.incompletes != null)
|
||||
incompletes = new File(DataUtil.readi18nString(Base64.decode(json.incompletes)))
|
||||
else
|
||||
incompletes = new File(home, "incompletes")
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
|
||||
if (json.pieceSizePow2 == null || json.pieceSizePow2 == 0) {
|
||||
log.warning("Skipping $file because pieceSizePow2=$json.pieceSizePow2")
|
||||
return // skip this download as it's corrupt anyway
|
||||
}
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||
if (json.paused != null)
|
||||
downloader.paused = json.paused
|
||||
downloaders.put(infoHash, downloader)
|
||||
downloader.readPieces()
|
||||
if (!downloader.paused)
|
||||
downloader.download()
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
|
||||
try {
|
||||
downloader.readPieces()
|
||||
if (!downloader.paused)
|
||||
downloader.download()
|
||||
downloaders.put(infoHash, downloader)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
} catch (IllegalArgumentException bad) {
|
||||
log.log(Level.WARNING,"cannot start downloader, skipping", bad)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
|
||||
int pieceSize = 0x1 << pieceSizePow2
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
|
||||
long pieceSize = 0x1L << pieceSizePow2
|
||||
int nPieces = (int)(length / pieceSize)
|
||||
if (length % pieceSize != 0)
|
||||
nPieces++
|
||||
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
|
||||
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
|
||||
mesh.pieces
|
||||
}
|
||||
|
||||
@ -188,6 +214,11 @@ public class DownloadManager {
|
||||
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||
|
||||
json.paused = downloader.paused
|
||||
|
||||
json.sequential = downloader.pieces.ratio == 0f
|
||||
|
||||
json.incompletes = Base64.encode(DataUtil.encodei18nString(downloader.incompletes.getAbsolutePath()))
|
||||
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
@ -37,13 +38,12 @@ class DownloadSession {
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
|
||||
private long lastSpeedRead = System.currentTimeMillis()
|
||||
private long dataSinceLastRead
|
||||
private final AtomicLong dataSinceLastRead
|
||||
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available) {
|
||||
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
|
||||
this.eventBus = eventBus
|
||||
this.meB64 = meB64
|
||||
this.pieces = pieces
|
||||
@ -53,6 +53,7 @@ class DownloadSession {
|
||||
this.pieceSize = pieceSize
|
||||
this.fileLength = fileLength
|
||||
this.available = available
|
||||
this.dataSinceLastRead = dataSinceLastRead
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
@ -141,6 +142,8 @@ class DownloadSession {
|
||||
// parse X-Have if present
|
||||
if (headers.containsKey("X-Have")) {
|
||||
DataUtil.decodeXHave(headers["X-Have"]).each {
|
||||
if (it >= pieces.nPieces)
|
||||
throw new IOException("Invalid X-Have header, available piece $it/$pieces.nPieces")
|
||||
available.add(it)
|
||||
}
|
||||
if (!available.contains(piece))
|
||||
@ -188,7 +191,7 @@ class DownloadSession {
|
||||
throw new IOException()
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
dataSinceLastRead += read
|
||||
dataSinceLastRead.addAndGet(read)
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
@ -220,13 +223,4 @@ class DownloadSession {
|
||||
return 0
|
||||
mapped.position()
|
||||
}
|
||||
|
||||
synchronized int speed() {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
@ -27,6 +28,7 @@ import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
public class Downloader {
|
||||
|
||||
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
|
||||
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||
|
||||
@ -48,6 +50,7 @@ public class Downloader {
|
||||
private final I2PConnector connector
|
||||
private final Set<Destination> destinations
|
||||
private final int nPieces
|
||||
private final File incompletes
|
||||
private final File piecesFile
|
||||
private final File incompleteFile
|
||||
final int pieceSizePow2
|
||||
@ -59,10 +62,11 @@ public class Downloader {
|
||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||
private boolean piecesFileClosed
|
||||
|
||||
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
|
||||
private volatile long lastSpeedRead = System.currentTimeMillis()
|
||||
private ArrayList speedArr = new ArrayList<Integer>()
|
||||
private int speedPos = 0
|
||||
private int speedAvg = 0
|
||||
private long timestamp = Instant.now().toEpochMilli()
|
||||
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||
Persona me, File file, long length, InfoHash infoHash,
|
||||
@ -76,21 +80,34 @@ public class Downloader {
|
||||
this.length = length
|
||||
this.connector = connector
|
||||
this.destinations = destinations
|
||||
this.incompletes = incompletes
|
||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||
this.incompleteFile = new File(incompletes, file.getName()+".part")
|
||||
this.pieceSizePow2 = pieceSizePow2
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
this.pieces = pieces
|
||||
this.nPieces = pieces.nPieces
|
||||
|
||||
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
|
||||
// it's easily adjustable by resizing the size of speedArr
|
||||
this.speedArr = [ 0, 0, 0, 0, 0 ]
|
||||
}
|
||||
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
infoHash
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
file
|
||||
}
|
||||
|
||||
public int getNPieces() {
|
||||
nPieces
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
pieceSize
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
length
|
||||
}
|
||||
|
||||
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||
this.infoHash = infoHash
|
||||
@ -140,10 +157,17 @@ public class Downloader {
|
||||
public int speed() {
|
||||
int currSpeed = 0
|
||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING)
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
|
||||
speedArr.clear()
|
||||
downloadManager.muSettings.speedSmoothSeconds.times { speedArr.add(0) }
|
||||
speedPos = 0
|
||||
}
|
||||
|
||||
// normalize to speedArr.size
|
||||
@ -241,6 +265,10 @@ public class Downloader {
|
||||
}
|
||||
active
|
||||
}
|
||||
|
||||
public int getTotalWorkers() {
|
||||
return activeWorkers.size();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused = false
|
||||
@ -268,6 +296,57 @@ public class Downloader {
|
||||
activeWorkers.put(d, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
}
|
||||
|
||||
boolean isSequential() {
|
||||
pieces.ratio == 0f
|
||||
}
|
||||
|
||||
File generatePreview() {
|
||||
int lastCompletePiece = pieces.firstIncomplete() - 1
|
||||
if (lastCompletePiece == -1)
|
||||
return null
|
||||
if (lastCompletePiece < -1)
|
||||
return file
|
||||
long previewableLength = (lastCompletePiece + 1) * ((long)pieceSize)
|
||||
|
||||
// generate name
|
||||
long now = System.currentTimeMillis()
|
||||
File previewFile
|
||||
File parentFile = file.getParentFile()
|
||||
int lastDot = file.getName().lastIndexOf('.')
|
||||
if (lastDot < 0)
|
||||
previewFile = new File(parentFile, file.getName() + "." + String.valueOf(now) + ".mwpreview")
|
||||
else {
|
||||
String name = file.getName().substring(0, lastDot)
|
||||
String extension = file.getName().substring(lastDot + 1)
|
||||
String previewName = name + "." + String.valueOf(now) + ".mwpreview."+extension
|
||||
previewFile = new File(parentFile, previewName)
|
||||
}
|
||||
|
||||
// copy
|
||||
InputStream is = null
|
||||
OutputStream os = null
|
||||
try {
|
||||
is = new BufferedInputStream(new FileInputStream(incompleteFile))
|
||||
os = new BufferedOutputStream(new FileOutputStream(previewFile))
|
||||
byte [] tmp = new byte[0x1 << 13]
|
||||
long totalCopied = 0
|
||||
while(totalCopied < previewableLength) {
|
||||
int read = is.read(tmp, 0, (int)Math.min(tmp.length, previewableLength - totalCopied))
|
||||
if (read < 0)
|
||||
throw new IOException("EOF?")
|
||||
os.write(tmp, 0, read)
|
||||
totalCopied += read
|
||||
}
|
||||
return previewFile
|
||||
} catch (IOException bad) {
|
||||
log.log(Level.WARNING,"Preview failed",bad)
|
||||
return null
|
||||
} finally {
|
||||
try {is?.close() } catch (IOException ignore) {}
|
||||
try {os?.close() } catch (IOException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadWorker implements Runnable {
|
||||
private final Destination destination
|
||||
@ -297,7 +376,7 @@ public class Downloader {
|
||||
boolean requestPerformed
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available)
|
||||
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@ -326,7 +405,7 @@ public class Downloader {
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
|
||||
}
|
||||
@ -334,12 +413,6 @@ public class Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
int speed() {
|
||||
if (currentSession == null)
|
||||
return 0
|
||||
currentSession.speed()
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
downloadThread?.interrupt()
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class Pieces {
|
||||
done = new BitSet(nPieces)
|
||||
claimed = new BitSet(nPieces)
|
||||
}
|
||||
|
||||
|
||||
synchronized int[] claim() {
|
||||
int claimedCardinality = claimed.cardinality()
|
||||
if (claimedCardinality == nPieces) {
|
||||
@ -30,7 +30,7 @@ class Pieces {
|
||||
}
|
||||
|
||||
// if fuller than ratio just do sequential
|
||||
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||
if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
|
||||
int rv = claimed.nextClearBit(0)
|
||||
claimed.set(rv)
|
||||
return [rv, partials.getOrDefault(rv, 0), 0]
|
||||
@ -59,7 +59,8 @@ class Pieces {
|
||||
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||
}
|
||||
List<Integer> toList = availableCopy.toList()
|
||||
Collections.shuffle(toList)
|
||||
if (ratio > 0f)
|
||||
Collections.shuffle(toList)
|
||||
int rv = toList[0]
|
||||
claimed.set(rv)
|
||||
[rv, partials.getOrDefault(rv, 0), 0]
|
||||
@ -74,6 +75,8 @@ class Pieces {
|
||||
}
|
||||
|
||||
synchronized void markDownloaded(int piece) {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
@ -105,6 +108,10 @@ class Pieces {
|
||||
partials.clear()
|
||||
}
|
||||
|
||||
synchronized int firstIncomplete() {
|
||||
done.nextClearBit(0)
|
||||
}
|
||||
|
||||
synchronized void write(PrintWriter writer) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
writer.println(i)
|
||||
|
@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
|
||||
UIResultEvent[] result
|
||||
Set<Destination> sources
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
||||
|
164
core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy
Normal file
164
core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy
Normal file
@ -0,0 +1,164 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.InvalidSignatureException
|
||||
import com.muwire.core.Name
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.data.SigningPublicKey
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class Certificate {
|
||||
private final byte version
|
||||
private final InfoHash infoHash
|
||||
final Name name, comment
|
||||
final long timestamp
|
||||
final Persona issuer
|
||||
private final byte[] sig
|
||||
|
||||
private volatile byte [] payload
|
||||
|
||||
private String base64;
|
||||
|
||||
Certificate(InputStream is) {
|
||||
version = (byte) (is.read() & 0xFF)
|
||||
if (version > Constants.FILE_CERT_VERSION)
|
||||
throw new IOException("Unknown version $version")
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
timestamp = dis.readLong()
|
||||
|
||||
byte [] root = new byte[InfoHash.SIZE]
|
||||
dis.readFully(root)
|
||||
infoHash = new InfoHash(root)
|
||||
|
||||
name = new Name(dis)
|
||||
|
||||
issuer = new Persona(dis)
|
||||
if (version == 2) {
|
||||
byte present = (byte)(dis.read() & 0xFF)
|
||||
if (present != 0) {
|
||||
comment = new Name(dis)
|
||||
}
|
||||
}
|
||||
|
||||
sig = new byte[Constants.SIG_TYPE.getSigLen()]
|
||||
dis.readFully(sig)
|
||||
|
||||
if (!verify(version, infoHash, name, timestamp, issuer, comment, sig))
|
||||
throw new InvalidSignatureException("certificate for $name.name from ${issuer.getHumanReadableName()} didn't verify")
|
||||
}
|
||||
|
||||
Certificate(InfoHash infoHash, String name, long timestamp, Persona issuer, String comment, SigningPrivateKey spk) {
|
||||
this.version = Constants.FILE_CERT_VERSION
|
||||
this.infoHash = infoHash
|
||||
this.name = new Name(name)
|
||||
if (comment != null)
|
||||
this.comment = new Name(comment)
|
||||
else
|
||||
this.comment = null
|
||||
this.timestamp = timestamp
|
||||
this.issuer = issuer
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
|
||||
daos.write(version)
|
||||
daos.writeLong(timestamp)
|
||||
daos.write(infoHash.getRoot())
|
||||
this.name.write(daos)
|
||||
issuer.write(daos)
|
||||
if (this.comment == null) {
|
||||
daos.write((byte) 0)
|
||||
} else {
|
||||
daos.write((byte) 1)
|
||||
this.comment.write(daos)
|
||||
}
|
||||
daos.close()
|
||||
|
||||
byte[] payload = baos.toByteArray()
|
||||
Signature signature = DSAEngine.getInstance().sign(payload, spk)
|
||||
this.sig = signature.getData()
|
||||
}
|
||||
|
||||
private static boolean verify(byte version, InfoHash infoHash, Name name, long timestamp, Persona issuer, Name comment, byte[] sig) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.write(version)
|
||||
daos.writeLong(timestamp)
|
||||
daos.write(infoHash.getRoot())
|
||||
name.write(daos)
|
||||
issuer.write(daos)
|
||||
if (version == 2) {
|
||||
if (comment == null) {
|
||||
daos.write((byte)0)
|
||||
} else {
|
||||
daos.write((byte)1)
|
||||
comment.write(daos)
|
||||
}
|
||||
}
|
||||
daos.close()
|
||||
|
||||
byte [] payload = baos.toByteArray()
|
||||
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
public void write(OutputStream os) {
|
||||
if (payload == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.write(version)
|
||||
daos.writeLong(timestamp)
|
||||
daos.write(infoHash.getRoot())
|
||||
name.write(daos)
|
||||
issuer.write(daos)
|
||||
if (version == 2) {
|
||||
if (comment == null)
|
||||
daos.write((byte) 0)
|
||||
else {
|
||||
daos.write((byte) 1)
|
||||
comment.write(daos)
|
||||
}
|
||||
}
|
||||
daos.write(sig)
|
||||
daos.close()
|
||||
|
||||
payload = baos.toByteArray()
|
||||
}
|
||||
os.write(payload)
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
if (base64 == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
write(baos)
|
||||
base64 = Base64.encode(baos.toByteArray())
|
||||
}
|
||||
return base64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Certificate))
|
||||
return false
|
||||
Certificate other = (Certificate)o
|
||||
|
||||
version == other.version &&
|
||||
infoHash == other.infoHash &&
|
||||
timestamp == other.timestamp &&
|
||||
name == other.name &&
|
||||
issuer == other.issuer &&
|
||||
comment == other.comment
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InvalidSignatureException
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class CertificateClient {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector connector
|
||||
|
||||
private final ExecutorService fetcherThread = Executors.newSingleThreadExecutor()
|
||||
|
||||
CertificateClient(EventBus eventBus, I2PConnector connector) {
|
||||
this.eventBus = eventBus
|
||||
this.connector = connector
|
||||
}
|
||||
|
||||
void onUIFetchCertificatesEvent(UIFetchCertificatesEvent e) {
|
||||
fetcherThread.execute({
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
|
||||
String infoHashString = Base64.encode(e.infoHash.getRoot())
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("CERTIFICATES ${infoHashString}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
InputStream is = endpoint.getInputStream()
|
||||
String code = DataUtil.readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("invalid code $code")
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':')
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("invalid header $header")
|
||||
String key = header.substring(0, colon)
|
||||
String value = header.substring(colon + 1)
|
||||
headers[key] = value.trim()
|
||||
}
|
||||
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No count header")
|
||||
|
||||
int count = Integer.parseInt(headers['Count'])
|
||||
|
||||
// start pulling the certs
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
for (int i = 0; i < count; i++) {
|
||||
int size = dis.readUnsignedShort()
|
||||
byte [] tmp = new byte[size]
|
||||
dis.readFully(tmp)
|
||||
Certificate cert = null
|
||||
try {
|
||||
cert = new Certificate(new ByteArrayInputStream(tmp))
|
||||
} catch (IOException | InvalidSignatureException ignore) {
|
||||
log.log(Level.WARNING, "certificate creation failed",ignore)
|
||||
continue
|
||||
}
|
||||
if (cert.infoHash == e.infoHash)
|
||||
eventBus.publish(new CertificateFetchedEvent(certificate : cert, user : e.host, infoHash : e.infoHash))
|
||||
}
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.DONE, count : count,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Fetching certificates failed", bad)
|
||||
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED,
|
||||
user : e.host, infoHash : e.infoHash))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class CertificateCreatedEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class CertificateFetchEvent extends Event {
|
||||
CertificateFetchStatus status
|
||||
int count
|
||||
Persona user
|
||||
InfoHash infoHash
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.muwire.core.filecert;
|
||||
|
||||
public enum CertificateFetchStatus {
|
||||
CONNECTING, FETCHING, DONE, FAILED
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class CertificateFetchedEvent extends Event {
|
||||
Certificate certificate
|
||||
Persona user
|
||||
InfoHash infoHash
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.InvalidSignatureException
|
||||
import com.muwire.core.Name
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class CertificateManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final File certDir
|
||||
private final Persona me
|
||||
private final SigningPrivateKey spk
|
||||
|
||||
final Map<InfoHash, Set<Certificate>> byInfoHash = new ConcurrentHashMap()
|
||||
final Map<Persona, Set<Certificate>> byIssuer = new ConcurrentHashMap()
|
||||
|
||||
CertificateManager(EventBus eventBus, File home, Persona me, SigningPrivateKey spk) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.spk = spk
|
||||
this.certDir = new File(home, "filecerts")
|
||||
if (!certDir.exists())
|
||||
certDir.mkdirs()
|
||||
else
|
||||
loadCertificates()
|
||||
}
|
||||
|
||||
private void loadCertificates() {
|
||||
certDir.listFiles({ dir, name ->
|
||||
name.endsWith("mwcert")
|
||||
} as FilenameFilter).each { certFile ->
|
||||
Certificate cert = null
|
||||
try {
|
||||
certFile.withInputStream {
|
||||
cert = new Certificate(it)
|
||||
}
|
||||
} catch (IOException | InvalidSignatureException ignore) {
|
||||
log.log(Level.WARNING, "Certificate failed to load from $certFile", ignore)
|
||||
return
|
||||
}
|
||||
|
||||
Set<Certificate> existing = byInfoHash.get(cert.infoHash)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byInfoHash.put(cert.infoHash, existing)
|
||||
}
|
||||
existing.add(cert)
|
||||
|
||||
existing = byIssuer.get(cert.issuer)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byIssuer.put(cert.issuer, existing)
|
||||
}
|
||||
existing.add(cert)
|
||||
|
||||
eventBus.publish(new CertificateCreatedEvent(certificate : cert))
|
||||
}
|
||||
}
|
||||
|
||||
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
|
||||
InfoHash infoHash = e.sharedFile.getInfoHash()
|
||||
String name = e.sharedFile.getFile().getName()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
|
||||
String comment = null
|
||||
if (e.sharedFile.getComment() != null)
|
||||
comment = DataUtil.readi18nString(Base64.decode(e.sharedFile.getComment()))
|
||||
|
||||
Certificate cert = new Certificate(infoHash, name, timestamp, me, comment, spk)
|
||||
|
||||
|
||||
if (addToMaps(cert)) {
|
||||
saveCert(cert)
|
||||
eventBus.publish(new CertificateCreatedEvent(certificate : cert))
|
||||
}
|
||||
}
|
||||
|
||||
void onUIImportCertificateEvent(UIImportCertificateEvent e) {
|
||||
Certificate cert = e.certificate
|
||||
if (!addToMaps(cert))
|
||||
return
|
||||
saveCert(cert)
|
||||
}
|
||||
|
||||
private void saveCert(Certificate cert) {
|
||||
String infoHashString = Base64.encode(cert.infoHash.getRoot())
|
||||
File certFile = new File(certDir, "${infoHashString}_${cert.issuer.getHumanReadableName()}_${cert.timestamp}.mwcert")
|
||||
certFile.withOutputStream { cert.write(it) }
|
||||
}
|
||||
|
||||
private boolean addToMaps(Certificate cert) {
|
||||
boolean added = true
|
||||
|
||||
Set<Certificate> existing = byInfoHash.get(cert.infoHash)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byInfoHash.put(cert.infoHash, existing)
|
||||
}
|
||||
added &= existing.add(cert)
|
||||
|
||||
existing = byIssuer.get(cert.issuer)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
byIssuer.put(cert.issuer, existing)
|
||||
}
|
||||
added &= existing.add(cert)
|
||||
added
|
||||
}
|
||||
|
||||
public boolean hasLocalCertificate(InfoHash infoHash) {
|
||||
if (!byInfoHash.containsKey(infoHash))
|
||||
return false
|
||||
Set<Certificate> set = byInfoHash.get(infoHash)
|
||||
for (Certificate cert : set) {
|
||||
if (cert.issuer == me)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public boolean isImported(Certificate certificate) {
|
||||
Set<Certificate> forInfoHash = byInfoHash.get(certificate.infoHash)
|
||||
if (forInfoHash == null)
|
||||
return false
|
||||
forInfoHash.contains(certificate)
|
||||
}
|
||||
|
||||
Set<Certificate> getByInfoHash(InfoHash infoHash) {
|
||||
Set<Certificate> rv = new HashSet<>()
|
||||
if (byInfoHash.containsKey(infoHash))
|
||||
rv.addAll(byInfoHash.get(infoHash))
|
||||
rv
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UICreateCertificateEvent extends Event {
|
||||
SharedFile sharedFile
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIFetchCertificatesEvent extends Event {
|
||||
Persona host
|
||||
InfoHash infoHash
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filecert
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIImportCertificateEvent extends Event {
|
||||
Certificate certificate
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.util.DataUtil
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
import java.util.stream.Collectors
|
||||
|
||||
abstract class BasePersisterService extends Service{
|
||||
|
||||
protected static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
protected static toJson(SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = sf.getB64EncodedHashRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (!sf.searches.isEmpty()) {
|
||||
Set searchers = new HashSet<>()
|
||||
sf.searches.each {
|
||||
def search = [:]
|
||||
if (it.searcher != null)
|
||||
search.searcher = it.searcher.toBase64()
|
||||
search.timestamp = it.timestamp
|
||||
search.query = it.query
|
||||
searchers.add(search)
|
||||
}
|
||||
json.searchers = searchers
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
}
|
@ -4,4 +4,8 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class DirectoryWatchedEvent extends Event {
|
||||
File directory
|
||||
}
|
@ -13,6 +13,7 @@ import java.nio.file.WatchService
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import groovy.util.logging.Log
|
||||
@ -31,6 +32,8 @@ class DirectoryWatcher {
|
||||
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
|
||||
}
|
||||
|
||||
private final File home
|
||||
private final MuWireSettings muOptions
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
@ -39,7 +42,9 @@ class DirectoryWatcher {
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
|
||||
this.home = home
|
||||
this.muOptions = muOptions
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||
@ -61,18 +66,29 @@ class DirectoryWatcher {
|
||||
watchService?.close()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (!e.file.isDirectory())
|
||||
return
|
||||
Path path = e.file.getCanonicalFile().toPath()
|
||||
void onDirectoryWatchedEvent(DirectoryWatchedEvent e) {
|
||||
File canonical = e.directory.getCanonicalFile()
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.file, wk)
|
||||
|
||||
watchedDirectories.put(canonical, wk)
|
||||
|
||||
if (muOptions.watchedDirectories.add(canonical.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
|
||||
if (muOptions.watchedDirectories.remove(e.directory.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withPrintWriter("UTF-8", {
|
||||
muOptions.write(it)
|
||||
})
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
@ -107,7 +123,8 @@ class DirectoryWatcher {
|
||||
private void processModified(Path parent, Path path) {
|
||||
File f = join(parent, path)
|
||||
log.fine("modified entry $f")
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
if (!fileManager.getNegativeTree().fileToNode.containsKey(f))
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private void processDeleted(Path parent, Path path) {
|
||||
@ -115,7 +132,7 @@ class DirectoryWatcher {
|
||||
log.fine("deleted entry $f")
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
|
@ -13,8 +13,10 @@ import java.security.NoSuchAlgorithmException
|
||||
|
||||
class FileHasher {
|
||||
|
||||
public static final int MIN_PIECE_SIZE_POW2 = 17
|
||||
public static final int MAX_PIECE_SIZE_POW2 = 37
|
||||
/** max size of shared file is 128 GB */
|
||||
public static final long MAX_SIZE = 0x1L << 37
|
||||
public static final long MAX_SIZE = 0x1L << MAX_PIECE_SIZE_POW2
|
||||
|
||||
/**
|
||||
* @param size of the file to be shared
|
||||
@ -24,9 +26,9 @@ class FileHasher {
|
||||
*/
|
||||
static int getPieceSize(long size) {
|
||||
if (size <= 0x1 << 30)
|
||||
return 17
|
||||
return MIN_PIECE_SIZE_POW2
|
||||
|
||||
for (int i = 31; i <= 37; i++) {
|
||||
for (int i = 31; i <= MAX_PIECE_SIZE_POW2; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-13
|
||||
}
|
||||
@ -48,27 +50,28 @@ class FileHasher {
|
||||
|
||||
InfoHash hashFile(File file) {
|
||||
final long length = file.length()
|
||||
final int size = 0x1 << getPieceSize(length)
|
||||
int numPieces = (int) (length / size)
|
||||
final long size = 0x1L << getPieceSize(length)
|
||||
int numPieces = (length / size).toInteger()
|
||||
if (numPieces * size < length)
|
||||
numPieces++
|
||||
|
||||
def output = new ByteArrayOutputStream()
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r")
|
||||
MappedByteBuffer buf = null
|
||||
try {
|
||||
MappedByteBuffer buf
|
||||
for (int i = 0; i < numPieces - 1; i++) {
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size.toInteger())
|
||||
digest.update buf
|
||||
DataUtil.tryUnmap(buf)
|
||||
output.write(digest.digest(), 0, 32)
|
||||
}
|
||||
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||
long lastPieceLength = length - (numPieces - 1) * size
|
||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength.toInteger())
|
||||
digest.update buf
|
||||
output.write(digest.digest(), 0, 32)
|
||||
} finally {
|
||||
raf.close()
|
||||
DataUtil.tryUnmap(buf)
|
||||
}
|
||||
|
||||
byte [] hashList = output.toByteArray()
|
||||
|
@ -0,0 +1,10 @@
|
||||
package com.muwire.core.files;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface FileListCallback<T> {
|
||||
|
||||
public void onFile(File f, T value);
|
||||
|
||||
public void onDirectory(File f);
|
||||
}
|
@ -6,4 +6,5 @@ import com.muwire.core.SharedFile
|
||||
class FileLoadedEvent extends Event {
|
||||
|
||||
SharedFile loadedFile
|
||||
String source
|
||||
}
|
||||
|
@ -8,8 +8,10 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchIndex
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class FileManager {
|
||||
@ -20,16 +22,30 @@ class FileManager {
|
||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree<Void> negativeTree = new FileTree<>()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
this.settings = settings
|
||||
this.eventBus = eventBus
|
||||
|
||||
for (String negative : settings.negativeFileTree) {
|
||||
negativeTree.add(new File(negative), null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
if (e.sharedFile != null)
|
||||
addToIndex(e.sharedFile)
|
||||
if (e.sharedFile == null)
|
||||
return
|
||||
File f = e.sharedFile.getFile()
|
||||
if (sideCarFiles.remove(f)) {
|
||||
File sideCar = new File(f.getParentFile(), f.getName() + ".mwcomment")
|
||||
if (sideCar.exists())
|
||||
e.sharedFile.setComment(Base64.encode(DataUtil.encodei18nString(sideCar.text)))
|
||||
}
|
||||
addToIndex(e.sharedFile)
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
@ -41,6 +57,21 @@ class FileManager {
|
||||
addToIndex(e.downloadedFile)
|
||||
}
|
||||
}
|
||||
|
||||
void onSideCarFileEvent(SideCarFileEvent e) {
|
||||
String name = e.file.getName()
|
||||
name = name.substring(0, name.length() - ".mwcomment".length())
|
||||
File target = new File(e.file.getParentFile(), name)
|
||||
SharedFile existing = fileToSharedFile.get(target)
|
||||
if (existing == null) {
|
||||
sideCarFiles.add(target)
|
||||
return
|
||||
}
|
||||
String comment = Base64.encode(DataUtil.encodei18nString(e.file.text))
|
||||
String oldComment = existing.getComment()
|
||||
existing.setComment(comment)
|
||||
eventBus.publish(new UICommentEvent(oldComment : oldComment, sharedFile : existing))
|
||||
}
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
@ -53,6 +84,13 @@ class FileManager {
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
if (parent != null && settings.watchedDirectories.contains(parent)) {
|
||||
negativeTree.add(sf.file.getParentFile(),null)
|
||||
}
|
||||
saveNegativeTree()
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
@ -62,6 +100,18 @@ class FileManager {
|
||||
}
|
||||
existingFiles.add(sf.getFile())
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
index.add(comment)
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if(existingComment == null) {
|
||||
existingComment = new HashSet<>()
|
||||
commentToFile.put(comment, existingComment)
|
||||
}
|
||||
existingComment.add(sf.getFile())
|
||||
}
|
||||
|
||||
index.add(name)
|
||||
}
|
||||
|
||||
@ -77,6 +127,10 @@ class FileManager {
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file,null)
|
||||
saveNegativeTree()
|
||||
}
|
||||
|
||||
String name = sf.getFile().getName()
|
||||
Set<File> existingFiles = nameToFiles.get(name)
|
||||
@ -86,9 +140,46 @@ class FileManager {
|
||||
nameToFiles.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if (existingComment != null) {
|
||||
existingComment.remove(sf.getFile())
|
||||
if (existingComment.isEmpty()) {
|
||||
commentToFile.remove(comment)
|
||||
index.remove(comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index.remove(name)
|
||||
}
|
||||
|
||||
void onUICommentEvent(UICommentEvent e) {
|
||||
if (e.oldComment != null) {
|
||||
def comment = DataUtil.readi18nString(Base64.decode(e.oldComment))
|
||||
Set<File> existingFiles = commentToFile.get(comment)
|
||||
existingFiles.remove(e.sharedFile.getFile())
|
||||
if (existingFiles.isEmpty()) {
|
||||
commentToFile.remove(comment)
|
||||
index.remove(comment)
|
||||
}
|
||||
}
|
||||
|
||||
String comment = e.sharedFile.getComment()
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
if (comment != null) {
|
||||
index.add(comment)
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if(existingComment == null) {
|
||||
existingComment = new HashSet<>()
|
||||
commentToFile.put(comment, existingComment)
|
||||
}
|
||||
existingComment.add(e.sharedFile.getFile())
|
||||
}
|
||||
}
|
||||
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
synchronized(fileToSharedFile) {
|
||||
@ -107,17 +198,26 @@ class FileManager {
|
||||
Set<SharedFile> found
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty())
|
||||
if (found != null && !found.isEmpty()) {
|
||||
found.each { it.hit(e.persona, e.timestamp, "Hash Search") }
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
names.each {
|
||||
files.addAll nameToFiles.getOrDefault(it, [])
|
||||
if (e.searchComments)
|
||||
files.addAll commentToFile.getOrDefault(it, [])
|
||||
}
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
if (!sharedFiles.isEmpty())
|
||||
|
||||
if (!sharedFiles.isEmpty()) {
|
||||
sharedFiles.each { it.hit(e.persona, e.timestamp, String.join(" ", e.searchTerms)) }
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -130,13 +230,15 @@ class FileManager {
|
||||
return files
|
||||
Set<SharedFile> rv = new HashSet<>()
|
||||
files.each {
|
||||
if (it.getPieceSize() != 0)
|
||||
if (it != null && it.getPieceSize() != 0)
|
||||
rv.add(it)
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
@ -147,4 +249,9 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveNegativeTree() {
|
||||
settings.negativeFileTree.clear()
|
||||
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
|
||||
}
|
||||
}
|
||||
|
115
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
115
core/src/main/groovy/com/muwire/core/files/FileTree.groovy
Normal file
@ -0,0 +1,115 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FileTree<T> {
|
||||
|
||||
private final TreeNode root = new TreeNode()
|
||||
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
||||
|
||||
synchronized void add(File file, T value) {
|
||||
List<File> path = new ArrayList<>()
|
||||
path.add(file)
|
||||
while (file.getParentFile() != null) {
|
||||
path.add(file.getParentFile())
|
||||
file = file.getParentFile()
|
||||
}
|
||||
|
||||
Collections.reverse(path)
|
||||
|
||||
TreeNode current = root
|
||||
for (File element : path) {
|
||||
TreeNode existing = fileToNode.get(element)
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
}
|
||||
current = existing
|
||||
}
|
||||
current.value = value;
|
||||
}
|
||||
|
||||
synchronized boolean remove(File file) {
|
||||
TreeNode node = fileToNode.remove(file)
|
||||
if (node == null) {
|
||||
return false
|
||||
}
|
||||
node.parent.children.remove(node)
|
||||
if (node.parent.children.isEmpty() && node.parent != root)
|
||||
remove(node.parent.file)
|
||||
def copy = new ArrayList(node.children)
|
||||
for (TreeNode child : copy)
|
||||
remove(child.file)
|
||||
true
|
||||
}
|
||||
|
||||
synchronized void traverse(FileTreeCallback<T> callback) {
|
||||
doTraverse(root, callback);
|
||||
}
|
||||
|
||||
synchronized void traverse(File from, FileTreeCallback<T> callback) {
|
||||
if (from == null) {
|
||||
doTraverse(root, callback);
|
||||
} else {
|
||||
TreeNode node = fileToNode.get(from);
|
||||
if (node == null)
|
||||
return
|
||||
doTraverse(node, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
|
||||
boolean leave = false
|
||||
if (node.file != null) {
|
||||
if (node.file.isFile())
|
||||
callback.onFile(node.file, node.value)
|
||||
else {
|
||||
leave = true
|
||||
callback.onDirectoryEnter(node.file)
|
||||
}
|
||||
}
|
||||
|
||||
node.children.each {
|
||||
doTraverse(it, callback)
|
||||
}
|
||||
|
||||
if (leave)
|
||||
callback.onDirectoryLeave()
|
||||
}
|
||||
|
||||
synchronized void list(File parent, FileListCallback<T> callback) {
|
||||
TreeNode<T> node
|
||||
if (parent == null)
|
||||
node = root
|
||||
else
|
||||
node = fileToNode.get(parent)
|
||||
|
||||
node.children.each {
|
||||
if (it.file.isFile())
|
||||
callback.onFile(it.file, it.value)
|
||||
else
|
||||
callback.onDirectory(it.file)
|
||||
}
|
||||
}
|
||||
|
||||
public static class TreeNode<T> {
|
||||
TreeNode parent
|
||||
File file
|
||||
T value;
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
public int hashCode() {
|
||||
Objects.hash(file)
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof TreeNode))
|
||||
return false
|
||||
TreeNode other = (TreeNode)o
|
||||
file == other.file
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user