Compare commits
529 Commits
muwire-0.0
...
muwire-0.4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
58d4207f94 | ||
![]() |
32577a28dc | ||
![]() |
f7b43304d4 | ||
![]() |
dcbe09886d | ||
![]() |
5a54b2dcda | ||
![]() |
581293b24f | ||
![]() |
cd072b9f76 | ||
![]() |
6b74fc5956 | ||
![]() |
3de2f872bb | ||
![]() |
fcde917d08 | ||
![]() |
4ded065010 | ||
![]() |
18a1c7091a | ||
![]() |
46aee19f80 | ||
![]() |
92dd7064c6 | ||
![]() |
b2e4dda677 | ||
![]() |
e77a2c8961 | ||
![]() |
ee2fd2ef68 | ||
![]() |
3f95d2bf1d | ||
![]() |
1390983732 | ||
![]() |
ce660cefe9 | ||
![]() |
72b81eb886 | ||
![]() |
57d593a68a | ||
![]() |
39a81a3376 | ||
![]() |
fd0bf17c24 | ||
![]() |
ac12bff69b | ||
![]() |
feef773bac | ||
![]() |
239d8f12a7 | ||
![]() |
8bbc61a7cb | ||
![]() |
7f31c4477f | ||
![]() |
6bad67c1bf | ||
![]() |
c76e6dc99f | ||
![]() |
acf9db0db3 | ||
![]() |
69b4f0b547 | ||
![]() |
80e165b505 | ||
![]() |
bcce55b873 | ||
![]() |
d5c92560db | ||
![]() |
f827c1c9bf | ||
![]() |
88c5f1a02d | ||
![]() |
d8e44f5f39 | ||
![]() |
72ff47ffe5 | ||
![]() |
066ee2c96d | ||
![]() |
0a8016dea7 | ||
![]() |
db36367b11 | ||
![]() |
b6c9ccb7f6 | ||
![]() |
a9dc636bce | ||
![]() |
3cc0574d11 | ||
![]() |
20fab9b16d | ||
![]() |
4015818323 | ||
![]() |
f569d45c8c | ||
![]() |
3773647869 | ||
![]() |
29cdbf018c | ||
![]() |
94bb7022eb | ||
![]() |
39808302df | ||
![]() |
2d22f9c39e | ||
![]() |
ee8f80bab6 | ||
![]() |
3e6242e583 | ||
![]() |
41181616ee | ||
![]() |
eb2530ca32 | ||
![]() |
b5233780ef | ||
![]() |
78753d7538 | ||
![]() |
4740e8b4f5 | ||
![]() |
ad5b00fc90 | ||
![]() |
d6c6880848 | ||
![]() |
4f948c1b9e | ||
![]() |
2b68c24f9c | ||
![]() |
bcdf0422db | ||
![]() |
f6434b478d | ||
![]() |
e979fdd26f | ||
![]() |
e6bfcaaab9 | ||
![]() |
9780108e8a | ||
![]() |
697c7d2d6d | ||
![]() |
887d10c8bf | ||
![]() |
ef6b8fe458 | ||
![]() |
20ab55d763 | ||
![]() |
eda58c9e0d | ||
![]() |
fb42fc0e35 | ||
![]() |
35cabc47ad | ||
![]() |
5be97d0404 | ||
![]() |
82b0fa253c | ||
![]() |
011a4d5766 | ||
![]() |
5cd1ca88c1 | ||
![]() |
44c880d911 | ||
![]() |
14857cb5ad | ||
![]() |
7daf981f1a | ||
![]() |
b99bc0ea32 | ||
![]() |
1ccf6fbdfa | ||
![]() |
5711979272 | ||
![]() |
9a5e2b1fa3 | ||
![]() |
cafc5f582e | ||
![]() |
a89b423dfc | ||
![]() |
79e8438941 | ||
![]() |
19c2c46491 | ||
![]() |
78f1d54b69 | ||
![]() |
9461649ed4 | ||
![]() |
8573ab2850 | ||
![]() |
8b3d752727 | ||
![]() |
7c54bd8966 | ||
![]() |
5d0fcb7027 | ||
![]() |
3ec9654d3c | ||
![]() |
7c8d64b462 | ||
![]() |
31e30e3d31 | ||
![]() |
8caf6e99b0 | ||
![]() |
624155debd | ||
![]() |
4468a262ae | ||
![]() |
1780901cb0 | ||
![]() |
d830d9261f | ||
![]() |
f5e1833a48 | ||
![]() |
9feb2a3c8f | ||
![]() |
b27665f5dd | ||
![]() |
4465aa4134 | ||
![]() |
ad766ac748 | ||
![]() |
d9e7d67d86 | ||
![]() |
3fefbc94b3 | ||
![]() |
21034209a5 | ||
![]() |
7c04c0f83c | ||
![]() |
f5293d65dd | ||
![]() |
8191bf6066 | ||
![]() |
29b6bfd463 | ||
![]() |
2f3d23bc34 | ||
![]() |
98dd80c4b8 | ||
![]() |
d9edb2e128 | ||
![]() |
de04b40b86 | ||
![]() |
7206a3d926 | ||
![]() |
98b98d8938 | ||
![]() |
294b8fcc2f | ||
![]() |
32f601a1b1 | ||
![]() |
8e3a398080 | ||
![]() |
720b9688b4 | ||
![]() |
e3066161c5 | ||
![]() |
a9aa3a524f | ||
![]() |
92848e818a | ||
![]() |
a7aa3008c0 | ||
![]() |
485325e824 | ||
![]() |
0df2a0e039 | ||
![]() |
fb7b4466c2 | ||
![]() |
53105245f4 | ||
![]() |
b68eab91e0 | ||
![]() |
f72cf91462 | ||
![]() |
a655c4ef50 | ||
![]() |
5d46e9b796 | ||
![]() |
642e6e67b3 | ||
![]() |
2b6b86f903 | ||
![]() |
f2706a4426 | ||
![]() |
1af75413aa | ||
![]() |
adc4077b1a | ||
![]() |
01f4e2453b | ||
![]() |
61267374dd | ||
![]() |
970f814685 | ||
![]() |
4fd9fc1991 | ||
![]() |
26207ffd1b | ||
![]() |
2614cfbe5f | ||
![]() |
f11d461ec0 | ||
![]() |
b2eb2d2755 | ||
![]() |
ea46a54f19 | ||
![]() |
627add45ad | ||
![]() |
d364855459 | ||
![]() |
14ee35e77a | ||
![]() |
8773eb4ee0 | ||
![]() |
51425bbfd9 | ||
![]() |
6a4879bc0b | ||
![]() |
e7fe56439b | ||
![]() |
2886feab4a | ||
![]() |
fb91194026 | ||
![]() |
4527478b0d | ||
![]() |
b0062f146e | ||
![]() |
bf16561170 | ||
![]() |
3b23dc29c4 | ||
![]() |
c0645b670e | ||
![]() |
30613fe530 | ||
![]() |
e7822f6edc | ||
![]() |
7e5c9ba115 | ||
![]() |
647fa3a481 | ||
![]() |
538eca9297 | ||
![]() |
e73a23d4a4 | ||
![]() |
76e41a0383 | ||
![]() |
7045927666 | ||
![]() |
5fb3086b42 | ||
![]() |
2de18227c1 | ||
![]() |
bd12a1de3d | ||
![]() |
a3a91050c8 | ||
![]() |
6c1cc28e49 | ||
![]() |
b6e5b54f05 | ||
![]() |
a6e559ec67 | ||
![]() |
f11badb824 | ||
![]() |
44da44ff6f | ||
![]() |
aae3fc29ca | ||
![]() |
c30aa19d8b | ||
![]() |
c79e8712d0 | ||
![]() |
ed12d78a48 | ||
![]() |
d27872cc8b | ||
![]() |
f794c39760 | ||
![]() |
2be9c425f7 | ||
![]() |
ab5fea9216 | ||
![]() |
d1c8328080 | ||
![]() |
89e761f53b | ||
![]() |
40410eba63 | ||
![]() |
85466a8e80 | ||
![]() |
c210af7870 | ||
![]() |
38ff49d28f | ||
![]() |
710f9f52a8 | ||
![]() |
1b6eda5a40 | ||
![]() |
1ee9ccf098 | ||
![]() |
0f07562de3 | ||
![]() |
6eb1aa07f5 | ||
![]() |
05b02834af | ||
![]() |
56125f6df8 | ||
![]() |
8f9996848b | ||
![]() |
dd655ed60f | ||
![]() |
8923c6ff7d | ||
![]() |
807ab22f8e | ||
![]() |
a26ad229ee | ||
![]() |
5504dd2251 | ||
![]() |
f9777d29f4 | ||
![]() |
b23226e8c6 | ||
![]() |
1249ad29e0 | ||
![]() |
7bb5e5b632 | ||
![]() |
b2e43f9765 | ||
![]() |
2aa73c203a | ||
![]() |
18d2b56563 | ||
![]() |
a455b4ad6e | ||
![]() |
761b683a81 | ||
![]() |
1d41bcd825 | ||
![]() |
f1ac038b55 | ||
![]() |
396c636e42 | ||
![]() |
e32c858e90 | ||
![]() |
821555f3f1 | ||
![]() |
089ab4f0d9 | ||
![]() |
948b6292fe | ||
![]() |
4e2a530a13 | ||
![]() |
03646e2b90 | ||
![]() |
3dce228bbb | ||
![]() |
15a49ad550 | ||
![]() |
3d91c0f4c7 | ||
![]() |
2825a8d9a4 | ||
![]() |
8dcce9bda6 | ||
![]() |
d8d3e2cd58 | ||
![]() |
51d5dbe47e | ||
![]() |
84cee0aa43 | ||
![]() |
162844787f | ||
![]() |
d8a2b59055 | ||
![]() |
67a0939de4 | ||
![]() |
37ca922a2c | ||
![]() |
1d6781819b | ||
![]() |
64d45da94a | ||
![]() |
59c84d8a5e | ||
![]() |
8b55021a4b | ||
![]() |
8bd3ebfaf5 | ||
![]() |
526ec45da3 | ||
![]() |
deb7c0b4b0 | ||
![]() |
e85a0c7b2c | ||
![]() |
7b021a47eb | ||
![]() |
0c21d4d6c1 | ||
![]() |
8e9f79d404 | ||
![]() |
bf33a6ff61 | ||
![]() |
19c8d84afd | ||
![]() |
6a40787863 | ||
![]() |
c698cbd737 | ||
![]() |
9c049b9301 | ||
![]() |
84a9bb9482 | ||
![]() |
0c1008d6b3 | ||
![]() |
c46f1b1ccd | ||
![]() |
7e2c4d48c6 | ||
![]() |
71a919e62b | ||
![]() |
d5eb65bdc2 | ||
![]() |
aef7533bd5 | ||
![]() |
e78016ead4 | ||
![]() |
52ced669dd | ||
![]() |
b52fb38ede | ||
![]() |
5dcef3ca05 | ||
![]() |
eaa0e46ce5 | ||
![]() |
c4f48c02b6 | ||
![]() |
5c16335969 | ||
![]() |
546eb4e9d3 | ||
![]() |
c3d9e852ba | ||
![]() |
0db7077a45 | ||
![]() |
614ecc85fe | ||
![]() |
af66a79376 | ||
![]() |
465171c81d | ||
![]() |
b507361c58 | ||
![]() |
4d001ae74b | ||
![]() |
36a6e2769f | ||
![]() |
69eeb7d77a | ||
![]() |
551982b72a | ||
![]() |
8d808f0b8f | ||
![]() |
7833a83c87 | ||
![]() |
3160c1a8f3 | ||
![]() |
e295aa67d5 | ||
![]() |
a9f5625dc3 | ||
![]() |
cc0af5b9ed | ||
![]() |
041fc3bef3 | ||
![]() |
03c3b1ebf1 | ||
![]() |
aece390daa | ||
![]() |
cf63be68e8 | ||
![]() |
88ece4dc23 | ||
![]() |
13767d58f2 | ||
![]() |
05a1ccd3d8 | ||
![]() |
6807c14a5f | ||
![]() |
684be0c50e | ||
![]() |
6655c262c6 | ||
![]() |
b1ccd55030 | ||
![]() |
a3becd0f7e | ||
![]() |
af2f3e0ebf | ||
![]() |
e2b7ffa1db | ||
![]() |
0e0176acfc | ||
![]() |
7f09bb079c | ||
![]() |
77e48b01bb | ||
![]() |
12db6857c1 | ||
![]() |
acd67733a5 | ||
![]() |
8d3ce7aa8e | ||
![]() |
0eb5870e9b | ||
![]() |
051efbfaba | ||
![]() |
6b38d7bffb | ||
![]() |
5778d537ce | ||
![]() |
93664a7985 | ||
![]() |
edd58e0c90 | ||
![]() |
9ac52b61dc | ||
![]() |
0a4b9c7029 | ||
![]() |
87b366a205 | ||
![]() |
040248560a | ||
![]() |
77caaf83de | ||
![]() |
cc5ece5103 | ||
![]() |
db7e21e343 | ||
![]() |
a388eaec1d | ||
![]() |
8ff39072c7 | ||
![]() |
55d2ac9b24 | ||
![]() |
6ebe492fd8 | ||
![]() |
165cd542ec | ||
![]() |
5ca0c8b00d | ||
![]() |
b6a38e3f23 | ||
![]() |
34d9165bd5 | ||
![]() |
2e52dd5c49 | ||
![]() |
2a315dd734 | ||
![]() |
6b661b99c5 | ||
![]() |
5dacd60bbb | ||
![]() |
f8f7cfe836 | ||
![]() |
0b4f261bc1 | ||
![]() |
042d67d784 | ||
![]() |
800df88f14 | ||
![]() |
4d1eac50a0 | ||
![]() |
c48df7f14b | ||
![]() |
9d04148001 | ||
![]() |
bb4d522572 | ||
![]() |
8052501e52 | ||
![]() |
66cc6d8ab7 | ||
![]() |
a45e57f5ec | ||
![]() |
7d8ca55d87 | ||
![]() |
de22f3c6b9 | ||
![]() |
3b0eb5678d | ||
![]() |
5a1f32e40b | ||
![]() |
ca3f2513e1 | ||
![]() |
658d9cf5a8 | ||
![]() |
e389090b7e | ||
![]() |
04ceaba514 | ||
![]() |
6a01d97a8d | ||
![]() |
747663e1dc | ||
![]() |
e426b3ccbd | ||
![]() |
5172e19627 | ||
![]() |
e826cfd8d5 | ||
![]() |
51004f6fe9 | ||
![]() |
08bb2b614d | ||
![]() |
d0e5d0ce8a | ||
![]() |
9e05802d1b | ||
![]() |
fb4f56eec9 | ||
![]() |
be2083d430 | ||
![]() |
af6275d0a3 | ||
![]() |
5269815329 | ||
![]() |
bd21cf65ea | ||
![]() |
dea592eb27 | ||
![]() |
c81f963e0a | ||
![]() |
dc6b1199f3 | ||
![]() |
42621a2dfb | ||
![]() |
a7125963a7 | ||
![]() |
f39d7f4fa8 | ||
![]() |
b88334f19a | ||
![]() |
81e186ad1f | ||
![]() |
33a45c3835 | ||
![]() |
32b7867e44 | ||
![]() |
5b313276f4 | ||
![]() |
abba4cc6fa | ||
![]() |
15b4804968 | ||
![]() |
942a01a501 | ||
![]() |
502a8d91da | ||
![]() |
5414e8679b | ||
![]() |
14e42dd7c2 | ||
![]() |
1299fb2512 | ||
![]() |
9bafdfe0b1 | ||
![]() |
36eb632756 | ||
![]() |
83ee620402 | ||
![]() |
3fe40d317d | ||
![]() |
e9703a2652 | ||
![]() |
a3fe89851f | ||
![]() |
b9ea0128cd | ||
![]() |
53c6db4ec8 | ||
![]() |
60776829b9 | ||
![]() |
b5cb31c23d | ||
![]() |
5052c0c993 | ||
![]() |
06de007866 | ||
![]() |
7c8a0c9ad9 | ||
![]() |
cda81a89a2 | ||
![]() |
483773422c | ||
![]() |
1e1e6d0bb0 | ||
![]() |
668d6e087d | ||
![]() |
49af412b96 | ||
![]() |
d5513021ed | ||
![]() |
c3154cf717 | ||
![]() |
114940c4c1 | ||
![]() |
d4336e9b5d | ||
![]() |
2c1d5508ed | ||
![]() |
1cebf6c7bd | ||
![]() |
e12924a207 | ||
![]() |
f3b11895e4 | ||
![]() |
1e084820fb | ||
![]() |
2198b4846d | ||
![]() |
a5d442d320 | ||
![]() |
3f9ee887d6 | ||
![]() |
4a9e6d3b6b | ||
![]() |
80f2cc5f99 | ||
![]() |
12283dba9d | ||
![]() |
5c959bc8b7 | ||
![]() |
f3712fe7af | ||
![]() |
3e49b0ec66 | ||
![]() |
f90beb8e3d | ||
![]() |
fbad7b6c7e | ||
![]() |
ec2d89c18c | ||
![]() |
c27fc0a515 | ||
![]() |
14681c2060 | ||
![]() |
1aeb230ea8 | ||
![]() |
d1dfc73f5a | ||
![]() |
0cebe4119c | ||
![]() |
9f21120ec8 | ||
![]() |
7eea8be67d | ||
![]() |
f114302bdb | ||
![]() |
05b9b37488 | ||
![]() |
52f317a5b7 | ||
![]() |
fb8227a1f3 | ||
![]() |
5677d9f46a | ||
![]() |
c5192e3845 | ||
![]() |
43c2a55cb8 | ||
![]() |
94f6de6bea | ||
![]() |
6782849a12 | ||
![]() |
c07d351c5d | ||
![]() |
dc2f675dd3 | ||
![]() |
a8e795ec51 | ||
![]() |
33c5b3b18e | ||
![]() |
581fce4643 | ||
![]() |
7fe78a0719 | ||
![]() |
cdb6e22522 | ||
![]() |
2edeb046be | ||
![]() |
4021f3c244 | ||
![]() |
9008fac24d | ||
![]() |
e2f92c5c5e | ||
![]() |
7b33a16fd8 | ||
![]() |
9a2531b264 | ||
![]() |
9a8dadff57 | ||
![]() |
4a274010f9 | ||
![]() |
1eb930435b | ||
![]() |
9df28552ad | ||
![]() |
ac0204dffc | ||
![]() |
e5c402a400 | ||
![]() |
7704c73b68 | ||
![]() |
a9aa8dd840 | ||
![]() |
de682a802a | ||
![]() |
5435518212 | ||
![]() |
bd01f983c9 | ||
![]() |
8b63864b90 | ||
![]() |
ed3943c1af | ||
![]() |
e195141a27 | ||
![]() |
bb02fdbee9 | ||
![]() |
6e3a2c0d08 | ||
![]() |
bd5fecc19d | ||
![]() |
d5db49fa79 | ||
![]() |
f2ea8619bb | ||
![]() |
b129e79196 | ||
![]() |
404d5b60bc | ||
![]() |
de2753ac50 | ||
![]() |
2d53999c8e | ||
![]() |
5aecf72d6f | ||
![]() |
a574a67ec6 | ||
![]() |
6b5ad969b7 | ||
![]() |
617209c4e4 | ||
![]() |
16b475bd9a | ||
![]() |
3cea1870cd | ||
![]() |
e7240dcb6f | ||
![]() |
c91440cbfc | ||
![]() |
294605f5c7 | ||
![]() |
986caf3a75 | ||
![]() |
8524d5309f | ||
![]() |
48b3ac2b4a | ||
![]() |
18f21dc247 | ||
![]() |
e69a5eac18 | ||
![]() |
6e0f1778b7 | ||
![]() |
abbb741d73 | ||
![]() |
07dfc0a1d1 | ||
![]() |
00c12cfd49 | ||
![]() |
1ee389ff91 | ||
![]() |
3642736cfe | ||
![]() |
b6f7f51476 | ||
![]() |
4c21f2d5ae | ||
![]() |
9e0d52d548 | ||
![]() |
fad01603de | ||
![]() |
da007795fb | ||
![]() |
881d755dd3 |
34
README.md
34
README.md
@@ -1,39 +1,43 @@
|
|||||||
# MuWire - Easy Anonymous File-Sharing
|
# MuWire - Easy Anonymous File-Sharing
|
||||||
|
|
||||||
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net).
|
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.
|
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||||
|
|
||||||
The project is in development. You can find technical documentation in the "doc" folder.
|
The current stable release - 0.4.11 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||||
|
|
||||||
```
|
```
|
||||||
./gradlew assemble
|
./gradlew clean assemble
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to run the unit tests, type
|
If you want to run the unit tests, type
|
||||||
```
|
```
|
||||||
./gradlew build
|
./gradlew clean build
|
||||||
```
|
```
|
||||||
|
|
||||||
Some of the UI tests will fail because they haven't been written yet :-/
|
Some of the UI tests will fail because they haven't been written yet :-/
|
||||||
|
|
||||||
|
If you want to build binary bundles for Windows and Mac that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
|
||||||
|
|
||||||
### Running
|
### Running
|
||||||
|
|
||||||
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside.
|
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.
|
||||||
|
|
||||||
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you.
|
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`
|
||||||
|
|
||||||
At the moment there are very few nodes on the network, so you will see very few connections and search results. It is best to leave MuWire running all the time, just like I2P.
|
[Default I2CP port]\: `7654`
|
||||||
|
|
||||||
|
### GPG Fingerprint
|
||||||
### Known bugs and limitations
|
|
||||||
|
```
|
||||||
* Any shared files get re-hashed on startup
|
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||||
* Sometimes the list of shared files gets lost
|
```
|
||||||
* Many UI features you would expect are not there yet
|
|
||||||
|
You can find the full key at https://keybase.io/zlatinb
|
||||||
|
|
||||||
|
|
||||||
|
[Default I2CP port]: https://geti2p.net/en/docs/ports
|
||||||
|
28
TODO.md
Normal file
28
TODO.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# TODO List
|
||||||
|
|
||||||
|
Not in any particular order yet
|
||||||
|
|
||||||
|
### Big Items
|
||||||
|
|
||||||
|
##### Bloom Filters
|
||||||
|
|
||||||
|
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
|
||||||
|
|
||||||
|
##### Two-tier Topology
|
||||||
|
|
||||||
|
This helps with scalability
|
||||||
|
|
||||||
|
##### Web UI, REST Interface, etc.
|
||||||
|
|
||||||
|
Basically any non-gui non-cli user interface
|
||||||
|
|
||||||
|
##### Metadata editing and search
|
||||||
|
|
||||||
|
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
|
||||||
|
* Automatic adjustment of number of I2P tunnels
|
@@ -2,7 +2,7 @@ subprojects {
|
|||||||
apply plugin: 'groovy'
|
apply plugin: 'groovy'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'net.i2p:i2p:0.9.40'
|
compile 'net.i2p:i2p:0.9.42'
|
||||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
cli/build.gradle
Normal file
22
cli/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin : 'application'
|
||||||
|
mainClassName = 'com.muwire.cli.Cli'
|
||||||
|
apply plugin : 'com.github.johnrengelman.shadow'
|
||||||
|
|
||||||
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(":core")
|
||||||
|
}
|
||||||
|
|
144
cli/src/main/groovy/com/muwire/cli/Cli.groovy
Normal file
144
cli/src/main/groovy/com/muwire/cli/Cli.groovy
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package com.muwire.cli
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.UILoadedEvent
|
||||||
|
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||||
|
import com.muwire.core.connection.ConnectionEvent
|
||||||
|
import com.muwire.core.connection.DisconnectionEvent
|
||||||
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
|
import com.muwire.core.files.FileHashedEvent
|
||||||
|
import com.muwire.core.files.FileLoadedEvent
|
||||||
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
import com.muwire.core.upload.UploadEvent
|
||||||
|
import com.muwire.core.upload.UploadFinishedEvent
|
||||||
|
|
||||||
|
class Cli {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
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")
|
||||||
|
if (!propsFile.exists()) {
|
||||||
|
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def props = new Properties()
|
||||||
|
propsFile.withInputStream { props.load(it) }
|
||||||
|
props = new MuWireSettings(props)
|
||||||
|
|
||||||
|
Core core
|
||||||
|
try {
|
||||||
|
core = new Core(props, home, "0.4.12")
|
||||||
|
} catch (Exception bad) {
|
||||||
|
bad.printStackTrace(System.out)
|
||||||
|
println "Failed to initialize core, exiting"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def filesList
|
||||||
|
if (args.length == 0) {
|
||||||
|
println "Enter a file containing list of files to share"
|
||||||
|
def reader = new BufferedReader(new InputStreamReader(System.in))
|
||||||
|
filesList = reader.readLine()
|
||||||
|
} else
|
||||||
|
filesList = args[0]
|
||||||
|
|
||||||
|
Thread.sleep(1000)
|
||||||
|
println "loading shared files from $filesList"
|
||||||
|
|
||||||
|
// listener for shared files
|
||||||
|
def sharedListener = new SharedListener()
|
||||||
|
core.eventBus.register(FileHashedEvent.class, sharedListener)
|
||||||
|
core.eventBus.register(FileLoadedEvent.class, sharedListener)
|
||||||
|
|
||||||
|
// for connections
|
||||||
|
def connectionsListener = new ConnectionListener()
|
||||||
|
core.eventBus.register(ConnectionEvent.class, connectionsListener)
|
||||||
|
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
|
||||||
|
|
||||||
|
// for uploads
|
||||||
|
def uploadsListener = new UploadsListener()
|
||||||
|
core.eventBus.register(UploadEvent.class, uploadsListener)
|
||||||
|
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
|
||||||
|
|
||||||
|
Timer timer = new Timer("status-printer", true)
|
||||||
|
timer.schedule({
|
||||||
|
println String.valueOf(new Date()) + " Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
|
||||||
|
} as TimerTask, 60000, 60000)
|
||||||
|
|
||||||
|
def latch = new CountDownLatch(1)
|
||||||
|
def fileLoader = new Object() {
|
||||||
|
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
||||||
|
core.startServices()
|
||||||
|
|
||||||
|
core.eventBus.publish(new UILoadedEvent())
|
||||||
|
println "waiting for files to load"
|
||||||
|
latch.await()
|
||||||
|
// now we begin
|
||||||
|
println "MuWire is ready"
|
||||||
|
|
||||||
|
filesList = new File(filesList)
|
||||||
|
filesList.withReader {
|
||||||
|
def toShare = it.readLine()
|
||||||
|
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
|
||||||
|
}
|
||||||
|
Runtime.getRuntime().addShutdownHook({
|
||||||
|
println "shutting down.."
|
||||||
|
core.shutdown()
|
||||||
|
println "shutdown."
|
||||||
|
})
|
||||||
|
Thread.sleep(Integer.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ConnectionListener {
|
||||||
|
volatile int connections
|
||||||
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
|
||||||
|
connections++
|
||||||
|
}
|
||||||
|
public void onDisconnectionEvent(DisconnectionEvent e) {
|
||||||
|
connections--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class UploadsListener {
|
||||||
|
volatile int uploads
|
||||||
|
public void onUploadEvent(UploadEvent e) {
|
||||||
|
uploads++
|
||||||
|
println String.valueOf(new Date()) + " Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||||
|
}
|
||||||
|
public void onUploadFinishedEvent(UploadFinishedEvent e) {
|
||||||
|
uploads--
|
||||||
|
println String.valueOf(new Date()) + " Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SharedListener {
|
||||||
|
volatile int shared
|
||||||
|
void onFileHashedEvent(FileHashedEvent e) {
|
||||||
|
if (e.error != null)
|
||||||
|
println "ERROR $e.error"
|
||||||
|
else {
|
||||||
|
println "Shared file : $e.sharedFile.file"
|
||||||
|
shared++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||||
|
shared++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
cli/src/main/groovy/com/muwire/cli/CliDownloader.groovy
Normal file
166
cli/src/main/groovy/com/muwire/cli/CliDownloader.groovy
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package com.muwire.cli
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
import com.muwire.core.Core
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||||
|
import com.muwire.core.connection.ConnectionEvent
|
||||||
|
import com.muwire.core.download.DownloadStartedEvent
|
||||||
|
import com.muwire.core.download.Downloader
|
||||||
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
|
import com.muwire.core.search.QueryEvent
|
||||||
|
import com.muwire.core.search.SearchEvent
|
||||||
|
import com.muwire.core.search.UIResultEvent
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class CliDownloader {
|
||||||
|
|
||||||
|
private static final List<Downloader> downloaders = Collections.synchronizedList(new ArrayList<>())
|
||||||
|
private static final Map<UUID,ResultsHolder> resultsListeners = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
|
public static void main(String []args) {
|
||||||
|
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")
|
||||||
|
if (!propsFile.exists()) {
|
||||||
|
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def props = new Properties()
|
||||||
|
propsFile.withInputStream { props.load(it) }
|
||||||
|
props = new MuWireSettings(props)
|
||||||
|
|
||||||
|
def filesList
|
||||||
|
int connections
|
||||||
|
int resultWait
|
||||||
|
if (args.length != 3) {
|
||||||
|
println "Enter a file containing list of hashes of files to download, " +
|
||||||
|
"how many connections you want before searching" +
|
||||||
|
"and how long to wait for results to arrive"
|
||||||
|
System.exit(1)
|
||||||
|
} else {
|
||||||
|
filesList = args[0]
|
||||||
|
connections = Integer.parseInt(args[1])
|
||||||
|
resultWait = Integer.parseInt(args[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
Core core
|
||||||
|
try {
|
||||||
|
core = new Core(props, home, "0.4.12")
|
||||||
|
} catch (Exception bad) {
|
||||||
|
bad.printStackTrace(System.out)
|
||||||
|
println "Failed to initialize core, exiting"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def latch = new CountDownLatch(connections)
|
||||||
|
def connectionListener = new ConnectionWaiter(latch : latch)
|
||||||
|
core.eventBus.register(ConnectionEvent.class, connectionListener)
|
||||||
|
|
||||||
|
core.startServices()
|
||||||
|
println "starting to wait until there are $connections connections"
|
||||||
|
latch.await()
|
||||||
|
|
||||||
|
println "connected, searching for files"
|
||||||
|
|
||||||
|
def file = new File(filesList)
|
||||||
|
file.eachLine {
|
||||||
|
String[] split = it.split(",")
|
||||||
|
UUID uuid = UUID.randomUUID()
|
||||||
|
core.eventBus.register(UIResultEvent.class, new ResultsListener(fileName : split[1]))
|
||||||
|
def hash = Base64.decode(split[0])
|
||||||
|
def searchEvent = new SearchEvent(searchHash : hash, uuid : uuid)
|
||||||
|
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop:true,
|
||||||
|
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
|
||||||
|
}
|
||||||
|
|
||||||
|
println "waiting for results to arrive"
|
||||||
|
Thread.sleep(resultWait * 1000)
|
||||||
|
|
||||||
|
core.eventBus.register(DownloadStartedEvent.class, new DownloadListener())
|
||||||
|
resultsListeners.each { uuid, resultsListener ->
|
||||||
|
println "starting download of $resultsListener.fileName from ${resultsListener.getResults().size()} hosts"
|
||||||
|
File target = new File(resultsListener.fileName)
|
||||||
|
|
||||||
|
core.eventBus.publish(new UIDownloadEvent(target : target, result : resultsListener.getResults()))
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(1000)
|
||||||
|
|
||||||
|
Timer timer = new Timer("stats-printer")
|
||||||
|
timer.schedule({
|
||||||
|
println "==== STATUS UPDATE ==="
|
||||||
|
downloaders.each {
|
||||||
|
int donePieces = it.donePieces()
|
||||||
|
int totalPieces = it.nPieces
|
||||||
|
int sources = it.activeWorkers.size()
|
||||||
|
def root = Base64.encode(it.infoHash.getRoot())
|
||||||
|
def state = it.getCurrentState()
|
||||||
|
println "file $it.file hash: $root progress: $donePieces/$totalPieces sources: $sources status: $state}"
|
||||||
|
it.resume()
|
||||||
|
}
|
||||||
|
println "==== END ==="
|
||||||
|
} as TimerTask, 60000, 60000)
|
||||||
|
|
||||||
|
println "waiting for downloads to finish"
|
||||||
|
while(true) {
|
||||||
|
boolean allFinished = true
|
||||||
|
for (Downloader d : downloaders) {
|
||||||
|
allFinished &= d.getCurrentState() == Downloader.DownloadState.FINISHED
|
||||||
|
}
|
||||||
|
if (allFinished)
|
||||||
|
break
|
||||||
|
Thread.sleep(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
println "all downloads finished"
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ResultsHolder {
|
||||||
|
final List<UIResultEvent> results = Collections.synchronizedList(new ArrayList<>())
|
||||||
|
String fileName
|
||||||
|
void add(UIResultEvent e) {
|
||||||
|
results.add(e)
|
||||||
|
}
|
||||||
|
List getResults() {
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ResultsListener {
|
||||||
|
UUID uuid
|
||||||
|
String fileName
|
||||||
|
public onUIResultEvent(UIResultEvent e) {
|
||||||
|
println "got a result for $fileName from ${e.sender.getHumanReadableName()}"
|
||||||
|
ResultsHolder listener = resultsListeners.get(e.uuid)
|
||||||
|
if (listener == null) {
|
||||||
|
listener = new ResultsHolder(fileName : fileName)
|
||||||
|
resultsListeners.put(e.uuid, listener)
|
||||||
|
}
|
||||||
|
listener.add(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ConnectionWaiter {
|
||||||
|
CountDownLatch latch
|
||||||
|
public void onConnectionEvent(ConnectionEvent e) {
|
||||||
|
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class DownloadListener {
|
||||||
|
public void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||||
|
downloaders.add(e.downloader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
cli/src/main/groovy/com/muwire/cli/FileList.groovy
Normal file
23
cli/src/main/groovy/com/muwire/cli/FileList.groovy
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.muwire.cli
|
||||||
|
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class FileList {
|
||||||
|
public static void main(String [] args) {
|
||||||
|
if (args.length < 1) {
|
||||||
|
println "pass files.json as argument"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
|
File filesJson = new File(args[0])
|
||||||
|
filesJson.eachLine {
|
||||||
|
def json = slurper.parseText(it)
|
||||||
|
String name = DataUtil.readi18nString(Base64.decode(json.file))
|
||||||
|
println "$name,$json.length,$json.pieceSize,$json.infoHash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,8 +2,9 @@ apply plugin : 'application'
|
|||||||
mainClassName = 'com.muwire.core.Core'
|
mainClassName = 'com.muwire.core.Core'
|
||||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'net.i2p.client:mstreaming:0.9.40'
|
compile 'net.i2p:router:0.9.42'
|
||||||
compile 'net.i2p.client:streaming:0.9.40'
|
compile 'net.i2p.client:mstreaming:0.9.42'
|
||||||
|
compile 'net.i2p.client:streaming:0.9.42'
|
||||||
|
|
||||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
|
@@ -9,7 +9,5 @@ class Constants {
|
|||||||
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
||||||
public static final int MAX_HEADERS = 16
|
public static final int MAX_HEADERS = 16
|
||||||
|
|
||||||
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
|
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
|
||||||
|
|
||||||
public static final String SPLIT_PATTERN = "[\\.,_-]"
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core
|
package com.muwire.core
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
import com.muwire.core.connection.ConnectionAcceptor
|
import com.muwire.core.connection.ConnectionAcceptor
|
||||||
import com.muwire.core.connection.ConnectionEstablisher
|
import com.muwire.core.connection.ConnectionEstablisher
|
||||||
@@ -12,9 +13,14 @@ import com.muwire.core.connection.I2PConnector
|
|||||||
import com.muwire.core.connection.LeafConnectionManager
|
import com.muwire.core.connection.LeafConnectionManager
|
||||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||||
import com.muwire.core.download.DownloadManager
|
import com.muwire.core.download.DownloadManager
|
||||||
|
import com.muwire.core.download.SourceDiscoveredEvent
|
||||||
|
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||||
import com.muwire.core.download.UIDownloadEvent
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
|
import com.muwire.core.download.UIDownloadPausedEvent
|
||||||
|
import com.muwire.core.download.UIDownloadResumedEvent
|
||||||
import com.muwire.core.files.FileDownloadedEvent
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
import com.muwire.core.files.FileHashedEvent
|
import com.muwire.core.files.FileHashedEvent
|
||||||
|
import com.muwire.core.files.FileHashingEvent
|
||||||
import com.muwire.core.files.FileHasher
|
import com.muwire.core.files.FileHasher
|
||||||
import com.muwire.core.files.FileLoadedEvent
|
import com.muwire.core.files.FileLoadedEvent
|
||||||
import com.muwire.core.files.FileManager
|
import com.muwire.core.files.FileManager
|
||||||
@@ -22,18 +28,28 @@ import com.muwire.core.files.FileSharedEvent
|
|||||||
import com.muwire.core.files.FileUnsharedEvent
|
import com.muwire.core.files.FileUnsharedEvent
|
||||||
import com.muwire.core.files.HasherService
|
import com.muwire.core.files.HasherService
|
||||||
import com.muwire.core.files.PersisterService
|
import com.muwire.core.files.PersisterService
|
||||||
|
import com.muwire.core.files.AllFilesLoadedEvent
|
||||||
|
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||||
|
import com.muwire.core.files.DirectoryWatcher
|
||||||
import com.muwire.core.hostcache.CacheClient
|
import com.muwire.core.hostcache.CacheClient
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||||
|
import com.muwire.core.mesh.MeshManager
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
import com.muwire.core.search.ResultsEvent
|
import com.muwire.core.search.ResultsEvent
|
||||||
import com.muwire.core.search.ResultsSender
|
import com.muwire.core.search.ResultsSender
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
import com.muwire.core.search.SearchManager
|
import com.muwire.core.search.SearchManager
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
|
import com.muwire.core.trust.TrustSubscriber
|
||||||
|
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||||
|
import com.muwire.core.update.UpdateClient
|
||||||
import com.muwire.core.upload.UploadManager
|
import com.muwire.core.upload.UploadManager
|
||||||
import com.muwire.core.util.MuWireLogManager
|
import com.muwire.core.util.MuWireLogManager
|
||||||
|
import com.muwire.core.content.ContentControlEvent
|
||||||
|
import com.muwire.core.content.ContentManager
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.I2PAppContext
|
import net.i2p.I2PAppContext
|
||||||
@@ -42,6 +58,7 @@ import net.i2p.client.I2PSession
|
|||||||
import net.i2p.client.streaming.I2PSocketManager
|
import net.i2p.client.streaming.I2PSocketManager
|
||||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||||
import net.i2p.client.streaming.I2PSocketOptions
|
import net.i2p.client.streaming.I2PSocketOptions
|
||||||
|
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||||
import net.i2p.crypto.DSAEngine
|
import net.i2p.crypto.DSAEngine
|
||||||
import net.i2p.crypto.SigType
|
import net.i2p.crypto.SigType
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
@@ -49,27 +66,89 @@ import net.i2p.data.PrivateKey
|
|||||||
import net.i2p.data.Signature
|
import net.i2p.data.Signature
|
||||||
import net.i2p.data.SigningPrivateKey
|
import net.i2p.data.SigningPrivateKey
|
||||||
|
|
||||||
|
import net.i2p.router.Router
|
||||||
|
import net.i2p.router.RouterContext
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class Core {
|
public class Core {
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Persona me
|
final Persona me
|
||||||
final File home
|
final File home
|
||||||
|
final Properties i2pOptions
|
||||||
|
final MuWireSettings muOptions
|
||||||
|
|
||||||
private final TrustService trustService
|
private final TrustService trustService
|
||||||
|
private final TrustSubscriber trustSubscriber
|
||||||
private final PersisterService persisterService
|
private final PersisterService persisterService
|
||||||
private final HostCache hostCache
|
private final HostCache hostCache
|
||||||
private final ConnectionManager connectionManager
|
private final ConnectionManager connectionManager
|
||||||
private final CacheClient cacheClient
|
private final CacheClient cacheClient
|
||||||
|
private final UpdateClient updateClient
|
||||||
private final ConnectionAcceptor connectionAcceptor
|
private final ConnectionAcceptor connectionAcceptor
|
||||||
private final ConnectionEstablisher connectionEstablisher
|
private final ConnectionEstablisher connectionEstablisher
|
||||||
private final HasherService hasherService
|
private final HasherService hasherService
|
||||||
|
private final DownloadManager downloadManager
|
||||||
|
private final DirectoryWatcher directoryWatcher
|
||||||
|
final FileManager fileManager
|
||||||
|
final UploadManager uploadManager
|
||||||
|
final ContentManager contentManager
|
||||||
|
|
||||||
public Core(MuWireSettings props, File home) {
|
private final Router router
|
||||||
|
|
||||||
|
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||||
|
|
||||||
|
public Core(MuWireSettings props, File home, String myVersion) {
|
||||||
this.home = home
|
this.home = home
|
||||||
|
this.muOptions = props
|
||||||
|
|
||||||
|
i2pOptions = new Properties()
|
||||||
|
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"
|
||||||
|
Random r = new Random()
|
||||||
|
int port = r.nextInt(60000) + 4000
|
||||||
|
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||||
|
i2pOptions["i2np.udp.port"] = String.valueOf(port)
|
||||||
|
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.embeddedRouter) {
|
||||||
log.info "Initializing I2P context"
|
log.info "Initializing I2P context"
|
||||||
I2PAppContext.getGlobalContext().logManager()
|
I2PAppContext.getGlobalContext().logManager()
|
||||||
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
|
||||||
|
router = null
|
||||||
|
} else {
|
||||||
|
log.info("launching embedded router")
|
||||||
|
Properties routerProps = new Properties()
|
||||||
|
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
|
||||||
|
routerProps.setProperty("router.excludePeerCaps", "KLM")
|
||||||
|
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||||
|
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
||||||
|
routerProps.setProperty("i2cp.disableInterface", "true")
|
||||||
|
routerProps.setProperty("i2np.ntcp.port", i2pOptions["i2np.ntcp.port"])
|
||||||
|
routerProps.setProperty("i2np.udp.port", i2pOptions["i2np.udp.port"])
|
||||||
|
routerProps.setProperty("i2np.udp.internalPort", i2pOptions["i2np.udp.port"])
|
||||||
|
router = new Router(routerProps)
|
||||||
|
router.getContext().setLogManager(new MuWireLogManager())
|
||||||
|
router.runRouter()
|
||||||
|
while(!router.isRunning())
|
||||||
|
Thread.sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
log.info("initializing I2P socket manager")
|
log.info("initializing I2P socket manager")
|
||||||
def i2pClient = new I2PClientFactory().createClient()
|
def i2pClient = new I2PClientFactory().createClient()
|
||||||
@@ -81,15 +160,16 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def sysProps = System.getProperties().clone()
|
|
||||||
sysProps["inbound.nickname"] = "MuWire"
|
// options like tunnel length and quantity
|
||||||
I2PSession i2pSession
|
I2PSession i2pSession
|
||||||
I2PSocketManager socketManager
|
I2PSocketManager socketManager
|
||||||
keyDat.withInputStream {
|
keyDat.withInputStream {
|
||||||
socketManager = new I2PSocketManagerFactory().createManager(it, sysProps)
|
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||||
}
|
}
|
||||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||||
|
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||||
i2pSession = socketManager.getSession()
|
i2pSession = socketManager.getSession()
|
||||||
|
|
||||||
def destination = new Destination()
|
def destination = new Destination()
|
||||||
@@ -127,15 +207,21 @@ public class Core {
|
|||||||
|
|
||||||
|
|
||||||
log.info "initializing file manager"
|
log.info "initializing file manager"
|
||||||
FileManager fileManager = new FileManager(eventBus)
|
fileManager = new FileManager(eventBus, props)
|
||||||
eventBus.register(FileHashedEvent.class, fileManager)
|
eventBus.register(FileHashedEvent.class, fileManager)
|
||||||
eventBus.register(FileLoadedEvent.class, fileManager)
|
eventBus.register(FileLoadedEvent.class, fileManager)
|
||||||
eventBus.register(FileDownloadedEvent.class, fileManager)
|
eventBus.register(FileDownloadedEvent.class, fileManager)
|
||||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||||
eventBus.register(SearchEvent.class, fileManager)
|
eventBus.register(SearchEvent.class, fileManager)
|
||||||
|
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
|
||||||
|
|
||||||
|
log.info("initializing mesh manager")
|
||||||
|
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
||||||
|
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
||||||
|
|
||||||
log.info "initializing persistence service"
|
log.info "initializing persistence service"
|
||||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 5000, fileManager)
|
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||||
|
eventBus.register(UILoadedEvent.class, persisterService)
|
||||||
|
|
||||||
log.info("initializing host cache")
|
log.info("initializing host cache")
|
||||||
File hostStorage = new File(home, "hosts.json")
|
File hostStorage = new File(home, "hosts.json")
|
||||||
@@ -145,7 +231,8 @@ public class Core {
|
|||||||
|
|
||||||
log.info("initializing connection manager")
|
log.info("initializing connection manager")
|
||||||
connectionManager = props.isLeaf() ?
|
connectionManager = props.isLeaf() ?
|
||||||
new LeafConnectionManager(eventBus, me, 3, hostCache) : new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService)
|
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||||
|
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
||||||
eventBus.register(TrustEvent.class, connectionManager)
|
eventBus.register(TrustEvent.class, connectionManager)
|
||||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||||
@@ -154,6 +241,11 @@ public class Core {
|
|||||||
log.info("initializing cache client")
|
log.info("initializing cache client")
|
||||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||||
|
|
||||||
|
log.info("initializing update client")
|
||||||
|
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
|
||||||
|
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||||
|
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||||
|
|
||||||
log.info("initializing connector")
|
log.info("initializing connector")
|
||||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||||
|
|
||||||
@@ -166,41 +258,83 @@ public class Core {
|
|||||||
eventBus.register(ResultsEvent.class, searchManager)
|
eventBus.register(ResultsEvent.class, searchManager)
|
||||||
|
|
||||||
log.info("initializing download manager")
|
log.info("initializing download manager")
|
||||||
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
|
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||||
|
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||||
|
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||||
|
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
|
||||||
|
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||||
|
|
||||||
log.info("initializing upload manager")
|
log.info("initializing upload manager")
|
||||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
|
||||||
|
|
||||||
|
log.info("initializing connection establisher")
|
||||||
|
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||||
|
|
||||||
log.info("initializing acceptor")
|
log.info("initializing acceptor")
|
||||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager)
|
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||||
|
|
||||||
|
log.info("initializing directory watcher")
|
||||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||||
|
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||||
|
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||||
|
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||||
|
|
||||||
log.info("initializing hasher service")
|
log.info("initializing hasher service")
|
||||||
hasherService = new HasherService(new FileHasher(), eventBus)
|
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||||
eventBus.register(FileSharedEvent.class, hasherService)
|
eventBus.register(FileSharedEvent.class, hasherService)
|
||||||
|
|
||||||
|
log.info("initializing trust subscriber")
|
||||||
|
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
|
||||||
|
eventBus.register(UILoadedEvent.class, trustSubscriber)
|
||||||
|
eventBus.register(TrustSubscriptionEvent.class, trustSubscriber)
|
||||||
|
|
||||||
|
log.info("initializing content manager")
|
||||||
|
contentManager = new ContentManager()
|
||||||
|
eventBus.register(ContentControlEvent.class, contentManager)
|
||||||
|
eventBus.register(QueryEvent.class, contentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startServices() {
|
public void startServices() {
|
||||||
hasherService.start()
|
hasherService.start()
|
||||||
trustService.start()
|
trustService.start()
|
||||||
trustService.waitForLoad()
|
trustService.waitForLoad()
|
||||||
persisterService.start()
|
|
||||||
hostCache.start()
|
hostCache.start()
|
||||||
connectionManager.start()
|
connectionManager.start()
|
||||||
cacheClient.start()
|
cacheClient.start()
|
||||||
connectionAcceptor.start()
|
connectionAcceptor.start()
|
||||||
connectionEstablisher.start()
|
connectionEstablisher.start()
|
||||||
hostCache.waitForLoad()
|
hostCache.waitForLoad()
|
||||||
|
updateClient.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
if (!shutdown.compareAndSet(false, true)) {
|
||||||
|
log.info("already shutting down")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.info("shutting down trust subscriber")
|
||||||
|
trustSubscriber.stop()
|
||||||
|
log.info("shutting down download manageer")
|
||||||
|
downloadManager.shutdown()
|
||||||
|
log.info("shutting down connection acceeptor")
|
||||||
|
connectionAcceptor.stop()
|
||||||
|
log.info("shutting down connection establisher")
|
||||||
|
connectionEstablisher.stop()
|
||||||
|
log.info("shutting down directory watcher")
|
||||||
|
directoryWatcher.stop()
|
||||||
|
log.info("shutting down cache client")
|
||||||
|
cacheClient.stop()
|
||||||
|
log.info("shutting down connection manager")
|
||||||
connectionManager.shutdown()
|
connectionManager.shutdown()
|
||||||
|
if (router != null) {
|
||||||
|
log.info("shutting down embedded router")
|
||||||
|
router.shutdown(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static main(args) {
|
static main(args) {
|
||||||
@@ -227,7 +361,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home)
|
Core core = new Core(props, home, "0.4.12")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -3,6 +3,7 @@ package com.muwire.core
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
|
|
||||||
@@ -23,14 +24,18 @@ class EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void publishInternal(Event e) {
|
private void publishInternal(Event e) {
|
||||||
log.fine "publishing event $e of type ${e.getClass().getSimpleName()}"
|
log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
|
||||||
def currentHandlers
|
def currentHandlers
|
||||||
final def clazz = e.getClass()
|
final def clazz = e.getClass()
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
currentHandlers = handlers.getOrDefault(clazz, [])
|
currentHandlers = handlers.getOrDefault(clazz, [])
|
||||||
}
|
}
|
||||||
currentHandlers.each {
|
currentHandlers.each {
|
||||||
|
try {
|
||||||
it."on${clazz.getSimpleName()}"(e)
|
it."on${clazz.getSimpleName()}"(e)
|
||||||
|
} catch (Exception bad) {
|
||||||
|
log.log(Level.SEVERE, "exception dispatching event",bad)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,4 +48,9 @@ class EventBus {
|
|||||||
}
|
}
|
||||||
currentHandlers.add handler
|
currentHandlers.add handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void unregister(Class<? extends Event> eventType, def handler) {
|
||||||
|
log.info("Unregistering $handler for type $eventType")
|
||||||
|
handlers[eventType]?.remove(handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,35 @@
|
|||||||
package com.muwire.core
|
package com.muwire.core
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
import com.muwire.core.hostcache.CrawlerResponse
|
import com.muwire.core.hostcache.CrawlerResponse
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class MuWireSettings {
|
class MuWireSettings {
|
||||||
|
|
||||||
final boolean isLeaf
|
final boolean isLeaf
|
||||||
boolean allowUntrusted
|
boolean allowUntrusted
|
||||||
|
boolean allowTrustLists
|
||||||
|
int trustListInterval
|
||||||
|
Set<Persona> trustSubscriptions
|
||||||
int downloadRetryInterval
|
int downloadRetryInterval
|
||||||
|
int updateCheckInterval
|
||||||
|
boolean autoDownloadUpdate
|
||||||
|
String updateType
|
||||||
String nickname
|
String nickname
|
||||||
File downloadLocation
|
File downloadLocation
|
||||||
String sharedFiles
|
|
||||||
CrawlerResponse crawlerResponse
|
CrawlerResponse crawlerResponse
|
||||||
|
boolean shareDownloadedFiles
|
||||||
|
Set<String> watchedDirectories
|
||||||
|
float downloadSequentialRatio
|
||||||
|
int hostClearInterval
|
||||||
|
int meshExpiration
|
||||||
|
boolean embeddedRouter
|
||||||
|
int inBw, outBw
|
||||||
|
Set<String> watchedKeywords
|
||||||
|
Set<String> watchedRegexes
|
||||||
|
|
||||||
MuWireSettings() {
|
MuWireSettings() {
|
||||||
this(new Properties())
|
this(new Properties())
|
||||||
@@ -18,28 +37,92 @@ class MuWireSettings {
|
|||||||
|
|
||||||
MuWireSettings(Properties props) {
|
MuWireSettings(Properties props) {
|
||||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||||
allowUntrusted = Boolean.valueOf(props.get("allowUntrusted","true"))
|
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||||
|
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
||||||
|
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
||||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||||
nickname = props.getProperty("nickname","MuWireUser")
|
nickname = props.getProperty("nickname","MuWireUser")
|
||||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||||
System.getProperty("user.home")))
|
System.getProperty("user.home")))
|
||||||
sharedFiles = props.getProperty("sharedFiles")
|
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
|
||||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||||
|
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||||
|
updateType = props.getProperty("updateType","jar")
|
||||||
|
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||||
|
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
|
||||||
|
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
|
||||||
|
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||||
|
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","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")
|
||||||
|
|
||||||
|
trustSubscriptions = new HashSet<>()
|
||||||
|
if (props.containsKey("trustSubscriptions")) {
|
||||||
|
props.getProperty("trustSubscriptions").split(",").each {
|
||||||
|
trustSubscriptions.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
Properties props = new Properties()
|
Properties props = new Properties()
|
||||||
props.setProperty("leaf", isLeaf.toString())
|
props.setProperty("leaf", isLeaf.toString())
|
||||||
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
||||||
|
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
|
||||||
|
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
|
||||||
props.setProperty("crawlerResponse", crawlerResponse.toString())
|
props.setProperty("crawlerResponse", crawlerResponse.toString())
|
||||||
props.setProperty("nickname", nickname)
|
props.setProperty("nickname", nickname)
|
||||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||||
props.setProperty("downloadRetryInterval", "15")
|
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||||
if (sharedFiles != null)
|
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||||
props.setProperty("sharedFiles", sharedFiles)
|
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||||
|
props.setProperty("updateType",String.valueOf(updateType))
|
||||||
|
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||||
|
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||||
|
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||||
|
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||||
|
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||||
|
props.setProperty("inBw", String.valueOf(inBw))
|
||||||
|
props.setProperty("outBw", String.valueOf(outBw))
|
||||||
|
|
||||||
|
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||||
|
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||||
|
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||||
|
|
||||||
|
if (!trustSubscriptions.isEmpty()) {
|
||||||
|
String encoded = trustSubscriptions.stream().
|
||||||
|
map({it.toBase64()}).
|
||||||
|
collect(Collectors.joining(","))
|
||||||
|
props.setProperty("trustSubscriptions", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
props.store(out, "")
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
boolean isLeaf() {
|
boolean isLeaf() {
|
||||||
isLeaf
|
isLeaf
|
||||||
}
|
}
|
||||||
|
@@ -82,4 +82,13 @@ public class Persona {
|
|||||||
Persona other = (Persona)o
|
Persona other = (Persona)o
|
||||||
name.equals(other.name) && destination.equals(other.destination)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core
|
||||||
|
|
||||||
|
class RouterDisconnectedEvent extends Event {
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core
|
||||||
|
|
||||||
|
class UILoadedEvent extends Event {
|
||||||
|
}
|
@@ -6,6 +6,8 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -14,31 +16,39 @@ import com.muwire.core.trust.TrustLevel
|
|||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
abstract class Connection implements Closeable {
|
abstract class Connection implements Closeable {
|
||||||
|
|
||||||
|
private static final int SEARCHES = 10
|
||||||
|
private static final long INTERVAL = 1000
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
final Endpoint endpoint
|
final Endpoint endpoint
|
||||||
final boolean incoming
|
final boolean incoming
|
||||||
final HostCache hostCache
|
final HostCache hostCache
|
||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
|
final MuWireSettings settings
|
||||||
|
|
||||||
private final AtomicBoolean running = new AtomicBoolean()
|
private final AtomicBoolean running = new AtomicBoolean()
|
||||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||||
private final Thread reader, writer
|
private final Thread reader, writer
|
||||||
|
private final LinkedList<Long> searchTimestamps = new LinkedList<>()
|
||||||
|
|
||||||
protected final String name
|
protected final String name
|
||||||
|
|
||||||
long lastPingSentTime, lastPongReceivedTime
|
long lastPingSentTime, lastPongReceivedTime
|
||||||
|
|
||||||
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming, HostCache hostCache, TrustService trustService) {
|
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
||||||
|
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.incoming = incoming
|
this.incoming = incoming
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
this.hostCache = hostCache
|
this.hostCache = hostCache
|
||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
|
this.settings = settings
|
||||||
|
|
||||||
this.name = endpoint.destination.toBase32().substring(0,8)
|
this.name = endpoint.destination.toBase32().substring(0,8)
|
||||||
|
|
||||||
@@ -70,9 +80,9 @@ abstract class Connection implements Closeable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.info("closing $name")
|
log.info("closing $name")
|
||||||
endpoint.close()
|
|
||||||
reader.interrupt()
|
reader.interrupt()
|
||||||
writer.interrupt()
|
writer.interrupt()
|
||||||
|
endpoint.close()
|
||||||
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +130,13 @@ abstract class Connection implements Closeable {
|
|||||||
query.version = 1
|
query.version = 1
|
||||||
query.uuid = e.searchEvent.getUuid()
|
query.uuid = e.searchEvent.getUuid()
|
||||||
query.firstHop = e.firstHop
|
query.firstHop = e.firstHop
|
||||||
// TODO: first hop figure out
|
|
||||||
query.keywords = e.searchEvent.getSearchTerms()
|
query.keywords = e.searchEvent.getSearchTerms()
|
||||||
query.replyTo = e.getReceivedOn().toBase64()
|
query.oobInfohash = e.searchEvent.oobInfohash
|
||||||
|
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()
|
||||||
messages.put(query)
|
messages.put(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,23 +160,63 @@ abstract class Connection implements Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean throttleSearch() {
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
if (searchTimestamps.size() < SEARCHES) {
|
||||||
|
searchTimestamps.addLast(now)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Long oldest = searchTimestamps.getFirst()
|
||||||
|
if (now - oldest.longValue() < INTERVAL)
|
||||||
|
return true
|
||||||
|
searchTimestamps.addLast(now)
|
||||||
|
searchTimestamps.removeFirst()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
protected void handleSearch(def search) {
|
protected void handleSearch(def search) {
|
||||||
|
if (throttleSearch()) {
|
||||||
|
log.info("dropping excessive search")
|
||||||
|
return
|
||||||
|
}
|
||||||
UUID uuid = UUID.fromString(search.uuid)
|
UUID uuid = UUID.fromString(search.uuid)
|
||||||
if (search.infohash != null)
|
byte [] infohash = null
|
||||||
|
if (search.infohash != null) {
|
||||||
search.keywords = null
|
search.keywords = null
|
||||||
|
infohash = Base64.decode(search.infohash)
|
||||||
|
}
|
||||||
|
|
||||||
Destination replyTo = new Destination(search.replyTo)
|
Destination replyTo = new Destination(search.replyTo)
|
||||||
if (trustService.getLevel(replyTo) == TrustLevel.DISTRUSTED) {
|
TrustLevel trustLevel = trustService.getLevel(replyTo)
|
||||||
|
if (trustLevel == TrustLevel.DISTRUSTED) {
|
||||||
log.info "dropping search from distrusted peer"
|
log.info "dropping search from distrusted peer"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: add option to respond only to trusted peers
|
if (trustLevel == TrustLevel.NEUTRAL && !settings.allowUntrusted()) {
|
||||||
|
log.info("dropping search from neutral peer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Persona originator = null
|
||||||
|
if (search.originator != null) {
|
||||||
|
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
|
||||||
|
if (originator.destination != replyTo) {
|
||||||
|
log.info("originator doesn't match destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean oob = false
|
||||||
|
if (search.oobInfohash != null)
|
||||||
|
oob = search.oobInfohash
|
||||||
|
|
||||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||||
searchHash : search.infohash,
|
searchHash : infohash,
|
||||||
uuid : uuid)
|
uuid : uuid,
|
||||||
|
oobInfohash : oob)
|
||||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||||
replyTo : replyTo,
|
replyTo : replyTo,
|
||||||
|
originator : originator,
|
||||||
receivedOn : endpoint.destination,
|
receivedOn : endpoint.destination,
|
||||||
firstHop : search.firstHop )
|
firstHop : search.firstHop )
|
||||||
eventBus.publish(event)
|
eventBus.publish(event)
|
||||||
|
@@ -14,9 +14,12 @@ import com.muwire.core.hostcache.HostCache
|
|||||||
import com.muwire.core.trust.TrustLevel
|
import com.muwire.core.trust.TrustLevel
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
import com.muwire.core.upload.UploadManager
|
import com.muwire.core.upload.UploadManager
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
import com.muwire.core.search.InvalidSearchResultException
|
import com.muwire.core.search.InvalidSearchResultException
|
||||||
import com.muwire.core.search.ResultsParser
|
import com.muwire.core.search.ResultsParser
|
||||||
import com.muwire.core.search.SearchManager
|
import com.muwire.core.search.SearchManager
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.search.UnexpectedResultsException
|
import com.muwire.core.search.UnexpectedResultsException
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
@@ -34,13 +37,17 @@ class ConnectionAcceptor {
|
|||||||
final TrustService trustService
|
final TrustService trustService
|
||||||
final SearchManager searchManager
|
final SearchManager searchManager
|
||||||
final UploadManager uploadManager
|
final UploadManager uploadManager
|
||||||
|
final ConnectionEstablisher establisher
|
||||||
|
|
||||||
final ExecutorService acceptorThread
|
final ExecutorService acceptorThread
|
||||||
final ExecutorService handshakerThreads
|
final ExecutorService handshakerThreads
|
||||||
|
|
||||||
|
private volatile shutdown
|
||||||
|
|
||||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager) {
|
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||||
|
ConnectionEstablisher establisher) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.manager = manager
|
this.manager = manager
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
@@ -49,6 +56,7 @@ class ConnectionAcceptor {
|
|||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
this.searchManager = searchManager
|
this.searchManager = searchManager
|
||||||
this.uploadManager = uploadManager
|
this.uploadManager = uploadManager
|
||||||
|
this.establisher = establisher
|
||||||
|
|
||||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||||
def rv = new Thread(r)
|
def rv = new Thread(r)
|
||||||
@@ -70,11 +78,13 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
|
shutdown = true
|
||||||
acceptorThread.shutdownNow()
|
acceptorThread.shutdownNow()
|
||||||
handshakerThreads.shutdownNow()
|
handshakerThreads.shutdownNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acceptLoop() {
|
private void acceptLoop() {
|
||||||
|
try {
|
||||||
while(true) {
|
while(true) {
|
||||||
def incoming = acceptor.accept()
|
def incoming = acceptor.accept()
|
||||||
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
log.info("accepted connection from ${incoming.destination.toBase32()}")
|
||||||
@@ -90,6 +100,11 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "exception in accept loop",e)
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processIncoming(Endpoint e) {
|
private void processIncoming(Endpoint e) {
|
||||||
@@ -105,9 +120,15 @@ class ConnectionAcceptor {
|
|||||||
case (byte)'G':
|
case (byte)'G':
|
||||||
processGET(e)
|
processGET(e)
|
||||||
break
|
break
|
||||||
|
case (byte)'H':
|
||||||
|
processHashList(e)
|
||||||
|
break
|
||||||
case (byte)'P':
|
case (byte)'P':
|
||||||
processPOST(e)
|
processPOST(e)
|
||||||
break
|
break
|
||||||
|
case (byte)'T':
|
||||||
|
processTRUST(e)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
throw new Exception("Invalid read $read")
|
throw new Exception("Invalid read $read")
|
||||||
}
|
}
|
||||||
@@ -140,7 +161,9 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleIncoming(Endpoint e, boolean leaf) {
|
private void handleIncoming(Endpoint e, boolean leaf) {
|
||||||
boolean accept = !manager.isConnected(e.destination) && (leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
boolean accept = !manager.isConnected(e.destination) &&
|
||||||
|
!establisher.isInProgress(e.destination) &&
|
||||||
|
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
|
||||||
if (accept) {
|
if (accept) {
|
||||||
log.info("accepting connection, leaf:$leaf")
|
log.info("accepting connection, leaf:$leaf")
|
||||||
e.outputStream.write("OK".bytes)
|
e.outputStream.write("OK".bytes)
|
||||||
@@ -173,7 +196,16 @@ class ConnectionAcceptor {
|
|||||||
dis.readFully(et)
|
dis.readFully(et)
|
||||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||||
throw new IOException("Invalid GET connection")
|
throw new IOException("Invalid GET connection")
|
||||||
uploadManager.processEndpoint(e)
|
uploadManager.processGET(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processHashList(Endpoint e) {
|
||||||
|
byte[] ashList = new byte[8]
|
||||||
|
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
dis.readFully(ashList)
|
||||||
|
if (ashList != "ASHLIST ".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
throw new IOException("Invalid HASHLIST connection")
|
||||||
|
uploadManager.processHashList(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processPOST(final Endpoint e) throws IOException {
|
private void processPOST(final Endpoint e) throws IOException {
|
||||||
@@ -199,13 +231,15 @@ class ConnectionAcceptor {
|
|||||||
if (sender.destination != e.getDestination())
|
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()
|
int nResults = dis.readUnsignedShort()
|
||||||
|
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||||
for (int i = 0; i < nResults; i++) {
|
for (int i = 0; i < nResults; i++) {
|
||||||
int jsonSize = dis.readUnsignedShort()
|
int jsonSize = dis.readUnsignedShort()
|
||||||
byte [] payload = new byte[jsonSize]
|
byte [] payload = new byte[jsonSize]
|
||||||
dis.readFully(payload)
|
dis.readFully(payload)
|
||||||
def json = slurper.parse(payload)
|
def json = slurper.parse(payload)
|
||||||
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
|
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||||
}
|
}
|
||||||
|
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||||
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
||||||
log.log(Level.WARNING, "failed to process POST", bad)
|
log.log(Level.WARNING, "failed to process POST", bad)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -213,4 +247,43 @@ class ConnectionAcceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,8 @@ class ConnectionEstablisher {
|
|||||||
|
|
||||||
final Set inProgress = new ConcurrentHashSet()
|
final Set inProgress = new ConcurrentHashSet()
|
||||||
|
|
||||||
|
ConnectionEstablisher(){}
|
||||||
|
|
||||||
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
|
||||||
ConnectionManager connectionManager, HostCache hostCache) {
|
ConnectionManager connectionManager, HostCache hostCache) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
@@ -176,4 +178,8 @@ class ConnectionEstablisher {
|
|||||||
e.close()
|
e.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isInProgress(Destination d) {
|
||||||
|
inProgress.contains(d)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.connection
|
package com.muwire.core.connection
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -19,13 +20,15 @@ abstract class ConnectionManager {
|
|||||||
|
|
||||||
protected final HostCache hostCache
|
protected final HostCache hostCache
|
||||||
protected final Persona me
|
protected final Persona me
|
||||||
|
protected final MuWireSettings settings
|
||||||
|
|
||||||
ConnectionManager() {}
|
ConnectionManager() {}
|
||||||
|
|
||||||
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache) {
|
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.me = me
|
this.me = me
|
||||||
this.hostCache = hostCache
|
this.hostCache = hostCache
|
||||||
|
this.settings = settings
|
||||||
this.timer = new Timer("connections-pinger",true)
|
this.timer = new Timer("connections-pinger",true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.connection
|
package com.muwire.core.connection
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
@@ -24,7 +25,7 @@ class Endpoint implements Closeable {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (!closed.compareAndSet(false, true)) {
|
if (!closed.compareAndSet(false, true)) {
|
||||||
log.warning("Close loop detected for ${destination.toBase32()}", new Exception())
|
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
@@ -34,7 +35,7 @@ class Endpoint implements Closeable {
|
|||||||
try {outputStream.close()} catch (Exception ignore) {}
|
try {outputStream.close()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
if (toClose != null) {
|
if (toClose != null) {
|
||||||
try {toClose.close()} catch (Exception ignore) {}
|
try {toClose.reset()} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
|
|
||||||
@@ -16,8 +17,9 @@ import net.i2p.data.Destination
|
|||||||
*/
|
*/
|
||||||
class LeafConnection extends Connection {
|
class LeafConnection extends Connection {
|
||||||
|
|
||||||
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
|
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
|
||||||
super(eventBus, endpoint, true, hostCache, trustService);
|
TrustService trustService, MuWireSettings settings) {
|
||||||
|
super(eventBus, endpoint, true, hostCache, trustService, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.connection
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -17,8 +18,9 @@ class LeafConnectionManager extends ConnectionManager {
|
|||||||
|
|
||||||
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
|
||||||
|
|
||||||
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections, HostCache hostCache) {
|
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
|
||||||
super(eventBus, me, hostCache)
|
HostCache hostCache, MuWireSettings settings) {
|
||||||
|
super(eventBus, me, hostCache, settings)
|
||||||
this.maxConnections = maxConnections
|
this.maxConnections = maxConnections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
@@ -29,8 +30,9 @@ class PeerConnection extends Connection {
|
|||||||
private final JsonSlurper slurper = new JsonSlurper()
|
private final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
public PeerConnection(EventBus eventBus, Endpoint endpoint,
|
||||||
boolean incoming, HostCache hostCache, TrustService trustService) {
|
boolean incoming, HostCache hostCache, TrustService trustService,
|
||||||
super(eventBus, endpoint, incoming, hostCache, trustService)
|
MuWireSettings settings) {
|
||||||
|
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
|
||||||
this.dis = new DataInputStream(endpoint.inputStream)
|
this.dis = new DataInputStream(endpoint.inputStream)
|
||||||
this.dos = new DataOutputStream(endpoint.outputStream)
|
this.dos = new DataOutputStream(endpoint.outputStream)
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import java.util.Collection
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.hostcache.HostCache
|
import com.muwire.core.hostcache.HostCache
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
@@ -24,8 +25,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
UltrapeerConnectionManager() {}
|
UltrapeerConnectionManager() {}
|
||||||
|
|
||||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||||
HostCache hostCache, TrustService trustService) {
|
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||||
super(eventBus, me, hostCache)
|
super(eventBus, me, hostCache, settings)
|
||||||
this.maxPeers = maxPeers
|
this.maxPeers = maxPeers
|
||||||
this.maxLeafs = maxLeafs
|
this.maxLeafs = maxLeafs
|
||||||
this.trustService = trustService
|
this.trustService = trustService
|
||||||
@@ -85,8 +86,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
return
|
return
|
||||||
|
|
||||||
Connection c = e.leaf ?
|
Connection c = e.leaf ?
|
||||||
new LeafConnection(eventBus, e.endpoint, hostCache, trustService) :
|
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
|
||||||
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService)
|
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
|
||||||
def map = e.leaf ? leafConnections : peerConnections
|
def map = e.leaf ? leafConnections : peerConnections
|
||||||
map.put(e.endpoint.destination, c)
|
map.put(e.endpoint.destination, c)
|
||||||
c.start()
|
c.start()
|
||||||
@@ -103,8 +104,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
peerConnections.each {k,v -> v.close() }
|
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
leafConnections.each {k,v -> v.close() }
|
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||||
peerConnections.clear()
|
peerConnections.clear()
|
||||||
leafConnections.clear()
|
leafConnections.clear()
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
package com.muwire.core.content
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class ContentControlEvent extends Event {
|
||||||
|
String term
|
||||||
|
boolean regex
|
||||||
|
boolean add
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
package com.muwire.core.content
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
import com.muwire.core.search.QueryEvent
|
||||||
|
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
|
class ContentManager {
|
||||||
|
|
||||||
|
Set<Matcher> matchers = new ConcurrentHashSet()
|
||||||
|
|
||||||
|
void onContentControlEvent(ContentControlEvent e) {
|
||||||
|
Matcher m
|
||||||
|
if (e.regex)
|
||||||
|
m = new RegexMatcher(e.term)
|
||||||
|
else
|
||||||
|
m = new KeywordMatcher(e.term)
|
||||||
|
if (e.add)
|
||||||
|
matchers.add(m)
|
||||||
|
else
|
||||||
|
matchers.remove(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onQueryEvent(QueryEvent e) {
|
||||||
|
if (e.searchEvent.searchTerms == null)
|
||||||
|
return
|
||||||
|
matchers.each { it.process(e) }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
package com.muwire.core.content
|
||||||
|
|
||||||
|
class KeywordMatcher extends Matcher {
|
||||||
|
private final String keyword
|
||||||
|
KeywordMatcher(String keyword) {
|
||||||
|
this.keyword = keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean match(List<String> searchTerms) {
|
||||||
|
boolean found = false
|
||||||
|
searchTerms.each {
|
||||||
|
if (keyword == it)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTerm() {
|
||||||
|
keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
keyword.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof KeywordMatcher))
|
||||||
|
return false
|
||||||
|
KeywordMatcher other = (KeywordMatcher) o
|
||||||
|
keyword.equals(other.keyword)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
package com.muwire.core.content
|
||||||
|
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
class Match {
|
||||||
|
Persona persona
|
||||||
|
String [] keywords
|
||||||
|
long timestamp
|
||||||
|
}
|
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.muwire.core.content
|
||||||
|
|
||||||
|
import com.muwire.core.search.QueryEvent
|
||||||
|
|
||||||
|
abstract class Matcher {
|
||||||
|
final List<Match> matches = Collections.synchronizedList(new ArrayList<>())
|
||||||
|
final Set<UUID> uuids = new HashSet<>()
|
||||||
|
|
||||||
|
protected abstract boolean match(List<String> searchTerms);
|
||||||
|
|
||||||
|
public abstract String getTerm();
|
||||||
|
|
||||||
|
public void process(QueryEvent qe) {
|
||||||
|
def terms = qe.searchEvent.searchTerms
|
||||||
|
if (match(terms) && uuids.add(qe.searchEvent.uuid)) {
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
matches << new Match(persona : qe.originator, keywords : terms, timestamp : now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
package com.muwire.core.content
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
class RegexMatcher extends Matcher {
|
||||||
|
private final Pattern pattern
|
||||||
|
RegexMatcher(String pattern) {
|
||||||
|
this.pattern = Pattern.compile(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean match(List<String> keywords) {
|
||||||
|
String combined = keywords.join(" ")
|
||||||
|
return pattern.matcher(combined).find()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTerm() {
|
||||||
|
pattern.pattern()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
pattern.pattern().hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof RegexMatcher))
|
||||||
|
return false
|
||||||
|
RegexMatcher other = (RegexMatcher) o
|
||||||
|
pattern.pattern() == other.pattern.pattern()
|
||||||
|
}
|
||||||
|
}
|
@@ -1,31 +1,54 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
import com.muwire.core.connection.I2PConnector
|
import com.muwire.core.connection.I2PConnector
|
||||||
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
|
import com.muwire.core.files.FileHasher
|
||||||
|
import com.muwire.core.mesh.Mesh
|
||||||
|
import com.muwire.core.mesh.MeshManager
|
||||||
|
import com.muwire.core.trust.TrustLevel
|
||||||
|
import com.muwire.core.trust.TrustService
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import groovy.json.JsonBuilder
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.UILoadedEvent
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
public class DownloadManager {
|
public class DownloadManager {
|
||||||
|
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
|
private final TrustService trustService
|
||||||
|
private final MeshManager meshManager
|
||||||
|
private final MuWireSettings muSettings
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
private final Executor executor
|
private final Executor executor
|
||||||
private final File incompletes
|
private final File incompletes, home
|
||||||
private final String meB64
|
private final Persona me
|
||||||
|
|
||||||
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
|
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
|
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
|
||||||
|
I2PConnector connector, File home, Persona me) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
this.trustService = trustService
|
||||||
|
this.meshManager = meshManager
|
||||||
|
this.muSettings = muSettings
|
||||||
this.connector = connector
|
this.connector = connector
|
||||||
this.incompletes = incompletes
|
this.incompletes = new File(home,"incompletes")
|
||||||
|
this.home = home
|
||||||
def baos = new ByteArrayOutputStream()
|
this.me = me
|
||||||
me.write(baos)
|
|
||||||
this.meB64 = Base64.encode(baos.toByteArray())
|
|
||||||
|
|
||||||
incompletes.mkdir()
|
incompletes.mkdir()
|
||||||
|
|
||||||
@@ -39,14 +62,140 @@ public class DownloadManager {
|
|||||||
|
|
||||||
|
|
||||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
public void onUIDownloadEvent(UIDownloadEvent e) {
|
||||||
def downloader = new Downloader(this, meB64, e.target, e.result.size,
|
|
||||||
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
|
def size = e.result[0].size
|
||||||
incompletes)
|
def infohash = e.result[0].infohash
|
||||||
|
def pieceSize = e.result[0].pieceSize
|
||||||
|
|
||||||
|
Set<Destination> destinations = new HashSet<>()
|
||||||
|
e.result.each {
|
||||||
|
destinations.add(it.sender.destination)
|
||||||
|
}
|
||||||
|
destinations.addAll(e.sources)
|
||||||
|
destinations.remove(me.destination)
|
||||||
|
|
||||||
|
Pieces pieces = getPieces(infohash, size, pieceSize)
|
||||||
|
|
||||||
|
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||||
|
infohash, pieceSize, connector, destinations,
|
||||||
|
incompletes, pieces)
|
||||||
|
downloaders.put(infohash, downloader)
|
||||||
|
persistDownloaders()
|
||||||
executor.execute({downloader.download()} as Runnable)
|
executor.execute({downloader.download()} as Runnable)
|
||||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||||
|
downloaders.remove(e.downloader.infoHash)
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
void resume(Downloader downloader) {
|
void resume(Downloader downloader) {
|
||||||
executor.execute({downloader.download() as Runnable})
|
executor.execute({downloader.download() as Runnable})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onUILoadedEvent(UILoadedEvent e) {
|
||||||
|
File downloadsFile = new File(home, "downloads.json")
|
||||||
|
if (!downloadsFile.exists())
|
||||||
|
return
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
|
downloadsFile.eachLine {
|
||||||
|
def json = slurper.parseText(it)
|
||||||
|
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||||
|
def destinations = new HashSet<>()
|
||||||
|
json.destinations.each { destination ->
|
||||||
|
destinations.add new Destination(destination)
|
||||||
|
}
|
||||||
|
InfoHash infoHash
|
||||||
|
if (json.hashList != null) {
|
||||||
|
byte[] hashList = Base64.decode(json.hashList)
|
||||||
|
infoHash = InfoHash.fromHashList(hashList)
|
||||||
|
} else {
|
||||||
|
byte [] root = Base64.decode(json.hashRoot)
|
||||||
|
infoHash = new InfoHash(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
|
||||||
|
int pieceSize = 0x1 << pieceSizePow2
|
||||||
|
int nPieces = (int)(length / pieceSize)
|
||||||
|
if (length % pieceSize != 0)
|
||||||
|
nPieces++
|
||||||
|
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
|
||||||
|
mesh.pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||||
|
Downloader downloader = downloaders.get(e.infoHash)
|
||||||
|
if (downloader == null)
|
||||||
|
return
|
||||||
|
boolean ok = false
|
||||||
|
switch(trustService.getLevel(e.source.destination)) {
|
||||||
|
case TrustLevel.TRUSTED: ok = true; break
|
||||||
|
case TrustLevel.NEUTRAL: ok = muSettings.allowUntrusted; break
|
||||||
|
case TrustLevel.DISTRUSTED: ok = false; break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
downloader.addSource(e.source.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
|
downloaders.remove(e.downloader.infoHash)
|
||||||
|
persistDownloaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistDownloaders() {
|
||||||
|
File downloadsFile = new File(home,"downloads.json")
|
||||||
|
downloadsFile.withPrintWriter { writer ->
|
||||||
|
downloaders.values().each { downloader ->
|
||||||
|
if (!downloader.cancelled) {
|
||||||
|
def json = [:]
|
||||||
|
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
|
||||||
|
json.length = downloader.length
|
||||||
|
json.pieceSizePow2 = downloader.pieceSizePow2
|
||||||
|
def destinations = []
|
||||||
|
downloader.destinations.each {
|
||||||
|
destinations << it.toBase64()
|
||||||
|
}
|
||||||
|
json.destinations = destinations
|
||||||
|
|
||||||
|
InfoHash infoHash = downloader.getInfoHash()
|
||||||
|
if (infoHash.hashList != null)
|
||||||
|
json.hashList = Base64.encode(infoHash.hashList)
|
||||||
|
else
|
||||||
|
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||||
|
|
||||||
|
json.paused = downloader.paused
|
||||||
|
writer.println(JsonOutput.toJson(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
downloaders.values().each { it.stop() }
|
||||||
|
Downloader.executorService.shutdownNow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,23 +3,30 @@ package com.muwire.core.download;
|
|||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import static com.muwire.core.util.DataUtil.readTillRN
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.MappedByteBuffer
|
||||||
import java.nio.channels.FileChannel
|
import java.nio.channels.FileChannel
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class DownloadSession {
|
class DownloadSession {
|
||||||
|
|
||||||
|
private final EventBus eventBus
|
||||||
private final String meB64
|
private final String meB64
|
||||||
private final Pieces pieces
|
private final Pieces pieces
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
@@ -27,12 +34,17 @@ class DownloadSession {
|
|||||||
private final File file
|
private final File file
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
private final long fileLength
|
private final long fileLength
|
||||||
|
private final Set<Integer> available
|
||||||
private final MessageDigest digest
|
private final MessageDigest digest
|
||||||
|
|
||||||
private ByteBuffer mapped
|
private long lastSpeedRead = System.currentTimeMillis()
|
||||||
|
private long dataSinceLastRead
|
||||||
|
|
||||||
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
private MappedByteBuffer mapped
|
||||||
int pieceSize, long fileLength) {
|
|
||||||
|
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||||
|
int pieceSize, long fileLength, Set<Integer> available) {
|
||||||
|
this.eventBus = eventBus
|
||||||
this.meB64 = meB64
|
this.meB64 = meB64
|
||||||
this.pieces = pieces
|
this.pieces = pieces
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
@@ -40,6 +52,7 @@ class DownloadSession {
|
|||||||
this.file = file
|
this.file = file
|
||||||
this.pieceSize = pieceSize
|
this.pieceSize = pieceSize
|
||||||
this.fileLength = fileLength
|
this.fileLength = fileLength
|
||||||
|
this.available = available
|
||||||
try {
|
try {
|
||||||
digest = MessageDigest.getInstance("SHA-256")
|
digest = MessageDigest.getInstance("SHA-256")
|
||||||
} catch (NoSuchAlgorithmException impossible) {
|
} catch (NoSuchAlgorithmException impossible) {
|
||||||
@@ -48,70 +61,123 @@ class DownloadSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void request() throws IOException {
|
/**
|
||||||
|
* @return if the request will proceed. The only time it may not
|
||||||
|
* is if all the pieces have been claimed by other sessions.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public boolean request() throws IOException {
|
||||||
OutputStream os = endpoint.getOutputStream()
|
OutputStream os = endpoint.getOutputStream()
|
||||||
InputStream is = endpoint.getInputStream()
|
InputStream is = endpoint.getInputStream()
|
||||||
|
|
||||||
int piece = pieces.getRandomPiece()
|
int[] pieceAndPosition
|
||||||
long start = piece * pieceSize
|
if (available.isEmpty())
|
||||||
long end = Math.min(fileLength, start + pieceSize) - 1
|
pieceAndPosition = pieces.claim()
|
||||||
long length = end - start + 1
|
else
|
||||||
|
pieceAndPosition = pieces.claim(new HashSet<>(available))
|
||||||
|
if (pieceAndPosition == null)
|
||||||
|
return false
|
||||||
|
int piece = pieceAndPosition[0]
|
||||||
|
int position = pieceAndPosition[1]
|
||||||
|
boolean steal = pieceAndPosition[2] == 1
|
||||||
|
boolean unclaim = true
|
||||||
|
|
||||||
|
log.info("will download piece $piece from position $position steal $steal")
|
||||||
|
|
||||||
|
long pieceStart = piece * ((long)pieceSize)
|
||||||
|
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
|
||||||
|
long start = pieceStart + position
|
||||||
String root = Base64.encode(infoHash.getRoot())
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
|
|
||||||
FileChannel channel
|
|
||||||
try {
|
try {
|
||||||
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
|
||||||
|
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.flush()
|
os.flush()
|
||||||
String code = readTillRN(is)
|
String codeString = readTillRN(is)
|
||||||
if (code.startsWith("404 ")) {
|
int space = codeString.indexOf(' ')
|
||||||
|
if (space > 0)
|
||||||
|
codeString = codeString.substring(0, space)
|
||||||
|
|
||||||
|
int code = Integer.parseInt(codeString.trim())
|
||||||
|
|
||||||
|
if (code == 404) {
|
||||||
log.warning("file not found")
|
log.warning("file not found")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code.startsWith("416 ")) {
|
if (!(code == 200 || code == 416)) {
|
||||||
log.warning("range $start-$end cannot be satisfied")
|
|
||||||
return // leave endpoint open
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!code.startsWith("200 ")) {
|
|
||||||
log.warning("unknown code $code")
|
log.warning("unknown code $code")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse all headers
|
// parse all headers
|
||||||
Set<String> headers = new HashSet<>()
|
Map<String,String> headers = new HashMap<>()
|
||||||
String header
|
String header
|
||||||
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||||
headers.add(header)
|
int colon = header.indexOf(':')
|
||||||
|
if (colon == -1 || colon == header.length() - 1)
|
||||||
long receivedStart = -1
|
throw new IOException("invalid header $header")
|
||||||
long receivedEnd = -1
|
String key = header.substring(0, colon)
|
||||||
for (String receivedHeader : headers) {
|
String value = header.substring(colon + 1)
|
||||||
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
|
headers[key] = value.trim()
|
||||||
if (group.size() != 1) {
|
|
||||||
log.info("ignoring header $receivedHeader")
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedStart = Long.parseLong(group[0][1])
|
// prase X-Alt if present
|
||||||
receivedEnd = Long.parseLong(group[0][2])
|
if (headers.containsKey("X-Alt")) {
|
||||||
|
headers["X-Alt"].split(",").each {
|
||||||
|
if (it.length() > 0) {
|
||||||
|
byte [] raw = Base64.decode(it)
|
||||||
|
Persona source = new Persona(new ByteArrayInputStream(raw))
|
||||||
|
eventBus.publish(new SourceDiscoveredEvent(infoHash : infoHash, source : source))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse X-Have if present
|
||||||
|
if (headers.containsKey("X-Have")) {
|
||||||
|
DataUtil.decodeXHave(headers["X-Have"]).each {
|
||||||
|
available.add(it)
|
||||||
|
}
|
||||||
|
if (!available.contains(piece))
|
||||||
|
return true // try again next time
|
||||||
|
} else {
|
||||||
|
if (code != 200)
|
||||||
|
throw new IOException("Code $code but no X-Have")
|
||||||
|
available.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code != 200)
|
||||||
|
return true
|
||||||
|
|
||||||
|
String range = headers["Content-Range"]
|
||||||
|
if (range == null)
|
||||||
|
throw new IOException("Code 200 but no Content-Range")
|
||||||
|
|
||||||
|
def group = (range =~ /^(\d+)-(\d+)$/)
|
||||||
|
if (group.size() != 1)
|
||||||
|
throw new IOException("invalid Content-Range header $range")
|
||||||
|
|
||||||
|
long receivedStart = Long.parseLong(group[0][1])
|
||||||
|
long receivedEnd = Long.parseLong(group[0][2])
|
||||||
|
|
||||||
if (receivedStart != start || receivedEnd != end) {
|
if (receivedStart != start || receivedEnd != end) {
|
||||||
log.warning("We don't support mismatching ranges yet")
|
log.warning("We don't support mismatching ranges yet")
|
||||||
endpoint.close()
|
endpoint.close()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the download
|
// start the download
|
||||||
|
FileChannel channel
|
||||||
|
try {
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
||||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
StandardOpenOption.SPARSE, StandardOpenOption.CREATE))
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
mapped = channel.map(FileChannel.MapMode.READ_WRITE, pieceStart, end - pieceStart + 1)
|
||||||
|
mapped.position(position)
|
||||||
|
|
||||||
byte[] tmp = new byte[0x1 << 13]
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
while(mapped.hasRemaining()) {
|
while(mapped.hasRemaining()) {
|
||||||
@@ -122,6 +188,8 @@ class DownloadSession {
|
|||||||
throw new IOException()
|
throw new IOException()
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
mapped.put(tmp, 0, read)
|
mapped.put(tmp, 0, read)
|
||||||
|
dataSinceLastRead += read
|
||||||
|
pieces.markPartial(piece, mapped.position())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +198,21 @@ class DownloadSession {
|
|||||||
byte [] hash = digest.digest()
|
byte [] hash = digest.digest()
|
||||||
byte [] expected = new byte[32]
|
byte [] expected = new byte[32]
|
||||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||||
if (hash != expected)
|
if (hash != expected) {
|
||||||
throw new BadHashException()
|
pieces.markPartial(piece, 0)
|
||||||
|
throw new BadHashException("bad hash on piece $piece")
|
||||||
pieces.markDownloaded(piece)
|
}
|
||||||
} finally {
|
} finally {
|
||||||
try { channel?.close() } catch (IOException ignore) {}
|
try { channel?.close() } catch (IOException ignore) {}
|
||||||
|
DataUtil.tryUnmap(mapped)
|
||||||
}
|
}
|
||||||
|
pieces.markDownloaded(piece)
|
||||||
|
unclaim = false
|
||||||
|
} finally {
|
||||||
|
if (unclaim && !steal)
|
||||||
|
pieces.unclaim(piece)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int positionInPiece() {
|
synchronized int positionInPiece() {
|
||||||
@@ -144,4 +220,13 @@ class DownloadSession {
|
|||||||
return 0
|
return 0
|
||||||
mapped.position()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,100 +1,133 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.DownloadedFile
|
||||||
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.connection.I2PConnector
|
import com.muwire.core.connection.I2PConnector
|
||||||
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class Downloader {
|
public class Downloader {
|
||||||
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
|
||||||
|
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||||
|
|
||||||
|
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||||
|
Thread rv = new Thread(r)
|
||||||
|
rv.setName("download worker")
|
||||||
|
rv.setDaemon(true)
|
||||||
|
rv
|
||||||
|
})
|
||||||
|
|
||||||
|
private final EventBus eventBus
|
||||||
private final DownloadManager downloadManager
|
private final DownloadManager downloadManager
|
||||||
private final String meB64
|
private final Persona me
|
||||||
private final File file
|
private final File file
|
||||||
private final Pieces pieces
|
private final Pieces pieces
|
||||||
private final long length
|
private final long length
|
||||||
private final InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
private final I2PConnector connector
|
private final I2PConnector connector
|
||||||
private final Destination destination
|
private final Set<Destination> destinations
|
||||||
private final int nPieces
|
private final int nPieces
|
||||||
private final File piecesFile
|
private final File piecesFile
|
||||||
|
private final File incompleteFile
|
||||||
|
final int pieceSizePow2
|
||||||
|
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||||
|
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
|
||||||
|
|
||||||
private Endpoint endpoint
|
|
||||||
private volatile DownloadSession currentSession
|
|
||||||
private volatile DownloadState currentState
|
|
||||||
private volatile boolean cancelled
|
|
||||||
private volatile Thread downloadThread
|
|
||||||
|
|
||||||
public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash,
|
private volatile boolean cancelled, paused
|
||||||
int pieceSizePow2, I2PConnector connector, Destination destination,
|
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||||
File incompletes) {
|
private boolean piecesFileClosed
|
||||||
this.meB64 = meB64
|
|
||||||
|
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,
|
||||||
|
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||||
|
File incompletes, Pieces pieces) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.me = me
|
||||||
this.downloadManager = downloadManager
|
this.downloadManager = downloadManager
|
||||||
this.file = file
|
this.file = file
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
this.length = length
|
this.length = length
|
||||||
this.connector = connector
|
this.connector = connector
|
||||||
this.destination = destination
|
this.destinations = destinations
|
||||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||||
|
this.incompleteFile = new File(incompletes, file.getName()+".part")
|
||||||
|
this.pieceSizePow2 = pieceSizePow2
|
||||||
this.pieceSize = 1 << pieceSizePow2
|
this.pieceSize = 1 << pieceSizePow2
|
||||||
|
this.pieces = pieces
|
||||||
|
this.nPieces = pieces.nPieces
|
||||||
|
|
||||||
int nPieces
|
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
|
||||||
if (length % pieceSize == 0)
|
// it's easily adjustable by resizing the size of speedArr
|
||||||
nPieces = length / pieceSize
|
this.speedArr = [ 0, 0, 0, 0, 0 ]
|
||||||
else
|
}
|
||||||
nPieces = length / pieceSize + 1
|
|
||||||
this.nPieces = nPieces
|
|
||||||
|
|
||||||
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
public synchronized InfoHash getInfoHash() {
|
||||||
currentState = DownloadState.CONNECTING
|
infoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||||
|
this.infoHash = infoHash
|
||||||
}
|
}
|
||||||
|
|
||||||
void download() {
|
void download() {
|
||||||
readPieces()
|
readPieces()
|
||||||
downloadThread = Thread.currentThread()
|
destinations.each {
|
||||||
Endpoint endpoint = null
|
if (it != me.destination) {
|
||||||
try {
|
def worker = new DownloadWorker(it)
|
||||||
endpoint = connector.connect(destination)
|
activeWorkers.put(it, worker)
|
||||||
currentState = DownloadState.DOWNLOADING
|
executorService.submit(worker)
|
||||||
while(!pieces.isComplete()) {
|
|
||||||
currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
|
|
||||||
currentSession.request()
|
|
||||||
writePieces()
|
|
||||||
}
|
}
|
||||||
currentState = DownloadState.FINISHED
|
|
||||||
piecesFile.delete()
|
|
||||||
} catch (Exception bad) {
|
|
||||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
|
||||||
if (cancelled)
|
|
||||||
currentState = DownloadState.CANCELLED
|
|
||||||
else if (currentState != DownloadState.FINISHED)
|
|
||||||
currentState = DownloadState.FAILED
|
|
||||||
} finally {
|
|
||||||
endpoint?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void readPieces() {
|
void readPieces() {
|
||||||
if (!piecesFile.exists())
|
if (!piecesFile.exists())
|
||||||
return
|
return
|
||||||
piecesFile.withReader {
|
piecesFile.eachLine {
|
||||||
int piece = Integer.parseInt(it.readLine())
|
String [] split = it.split(",")
|
||||||
|
int piece = Integer.parseInt(split[0])
|
||||||
|
if (split.length == 1)
|
||||||
pieces.markDownloaded(piece)
|
pieces.markDownloaded(piece)
|
||||||
|
else {
|
||||||
|
int position = Integer.parseInt(split[1])
|
||||||
|
pieces.markPartial(piece, position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void writePieces() {
|
void writePieces() {
|
||||||
|
synchronized(piecesFile) {
|
||||||
|
if (piecesFileClosed)
|
||||||
|
return
|
||||||
piecesFile.withPrintWriter { writer ->
|
piecesFile.withPrintWriter { writer ->
|
||||||
pieces.getDownloaded().each { piece ->
|
pieces.write(writer)
|
||||||
writer.println(piece)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,22 +136,212 @@ public class Downloader {
|
|||||||
pieces.donePieces()
|
pieces.donePieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
public int positionInPiece() {
|
|
||||||
if (currentSession == null)
|
public int speed() {
|
||||||
return 0
|
int currSpeed = 0
|
||||||
currentSession.positionInPiece()
|
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.DOWNLOADING)
|
||||||
|
currSpeed += it.speed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize to speedArr.size
|
||||||
|
currSpeed /= speedArr.size()
|
||||||
|
|
||||||
|
// compute new speedAvg and update speedArr
|
||||||
|
if ( speedArr[speedPos] > speedAvg ) {
|
||||||
|
speedAvg = 0
|
||||||
|
} else {
|
||||||
|
speedAvg -= speedArr[speedPos]
|
||||||
|
}
|
||||||
|
speedAvg += currSpeed
|
||||||
|
speedArr[speedPos] = currSpeed
|
||||||
|
// this might be necessary due to rounding errors
|
||||||
|
if (speedAvg < 0)
|
||||||
|
speedAvg = 0
|
||||||
|
|
||||||
|
// rolling index over the speedArr
|
||||||
|
speedPos++
|
||||||
|
if (speedPos >= speedArr.size())
|
||||||
|
speedPos=0
|
||||||
|
|
||||||
|
speedAvg
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadState getCurrentState() {
|
public DownloadState getCurrentState() {
|
||||||
currentState
|
if (cancelled)
|
||||||
|
return DownloadState.CANCELLED
|
||||||
|
if (paused)
|
||||||
|
return DownloadState.PAUSED
|
||||||
|
|
||||||
|
boolean allFinished = true
|
||||||
|
activeWorkers.values().each {
|
||||||
|
allFinished &= it.currentState == WorkerState.FINISHED
|
||||||
|
}
|
||||||
|
if (allFinished) {
|
||||||
|
if (pieces.isComplete())
|
||||||
|
return DownloadState.FINISHED
|
||||||
|
return DownloadState.FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
// if at least one is downloading...
|
||||||
|
boolean oneDownloading = false
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.DOWNLOADING) {
|
||||||
|
oneDownloading = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oneDownloading)
|
||||||
|
return DownloadState.DOWNLOADING
|
||||||
|
|
||||||
|
// at least one is requesting hashlist
|
||||||
|
boolean oneHashlist = false
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState == WorkerState.HASHLIST) {
|
||||||
|
oneHashlist = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oneHashlist)
|
||||||
|
return DownloadState.HASHLIST
|
||||||
|
|
||||||
|
return DownloadState.CONNECTING
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
downloadThread?.interrupt()
|
stop()
|
||||||
|
synchronized(piecesFile) {
|
||||||
|
piecesFileClosed = true
|
||||||
|
piecesFile.delete()
|
||||||
|
}
|
||||||
|
incompleteFile.delete()
|
||||||
|
pieces.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
paused = true
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
activeWorkers.values().each {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int activeWorkers() {
|
||||||
|
int active = 0
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.currentState != WorkerState.FINISHED)
|
||||||
|
active++
|
||||||
|
}
|
||||||
|
active
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resume() {
|
public void resume() {
|
||||||
downloadManager.resume(this)
|
paused = false
|
||||||
|
readPieces()
|
||||||
|
destinations.each { destination ->
|
||||||
|
def worker = activeWorkers.get(destination)
|
||||||
|
if (worker != null) {
|
||||||
|
if (worker.currentState == WorkerState.FINISHED) {
|
||||||
|
def newWorker = new DownloadWorker(destination)
|
||||||
|
activeWorkers.put(destination, newWorker)
|
||||||
|
executorService.submit(newWorker)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
worker = new DownloadWorker(destination)
|
||||||
|
activeWorkers.put(destination, worker)
|
||||||
|
executorService.submit(worker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSource(Destination d) {
|
||||||
|
if (activeWorkers.containsKey(d))
|
||||||
|
return
|
||||||
|
DownloadWorker newWorker = new DownloadWorker(d)
|
||||||
|
activeWorkers.put(d, newWorker)
|
||||||
|
executorService.submit(newWorker)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadWorker implements Runnable {
|
||||||
|
private final Destination destination
|
||||||
|
private volatile WorkerState currentState
|
||||||
|
private volatile Thread downloadThread
|
||||||
|
private Endpoint endpoint
|
||||||
|
private volatile DownloadSession currentSession
|
||||||
|
private final Set<Integer> available = new HashSet<>()
|
||||||
|
|
||||||
|
DownloadWorker(Destination destination) {
|
||||||
|
this.destination = destination
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
downloadThread = Thread.currentThread()
|
||||||
|
currentState = WorkerState.CONNECTING
|
||||||
|
Endpoint endpoint = null
|
||||||
|
try {
|
||||||
|
endpoint = connector.connect(destination)
|
||||||
|
while(getInfoHash().hashList == null) {
|
||||||
|
currentState = WorkerState.HASHLIST
|
||||||
|
HashListSession session = new HashListSession(me.toBase64(), infoHash, endpoint)
|
||||||
|
InfoHash received = session.request()
|
||||||
|
setInfoHash(received)
|
||||||
|
}
|
||||||
|
currentState = WorkerState.DOWNLOADING
|
||||||
|
boolean requestPerformed
|
||||||
|
while(!pieces.isComplete()) {
|
||||||
|
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||||
|
endpoint, incompleteFile, pieceSize, length, available)
|
||||||
|
requestPerformed = currentSession.request()
|
||||||
|
if (!requestPerformed)
|
||||||
|
break
|
||||||
|
successfulDestinations.add(endpoint.destination)
|
||||||
|
writePieces()
|
||||||
|
}
|
||||||
|
} catch (Exception bad) {
|
||||||
|
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||||
|
} finally {
|
||||||
|
writePieces()
|
||||||
|
currentState = WorkerState.FINISHED
|
||||||
|
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||||
|
synchronized(piecesFile) {
|
||||||
|
piecesFileClosed = true
|
||||||
|
piecesFile.delete()
|
||||||
|
}
|
||||||
|
activeWorkers.values().each {
|
||||||
|
if (it.destination != destination)
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
incompleteFile.delete()
|
||||||
|
}
|
||||||
|
eventBus.publish(
|
||||||
|
new FileDownloadedEvent(
|
||||||
|
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||||
|
downloader : Downloader.this))
|
||||||
|
|
||||||
|
}
|
||||||
|
endpoint?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int speed() {
|
||||||
|
if (currentSession == null)
|
||||||
|
return 0
|
||||||
|
currentSession.speed()
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
downloadThread?.interrupt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,82 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|
||||||
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
|
||||||
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class HashListSession {
|
||||||
|
private final String meB64
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final Endpoint endpoint
|
||||||
|
|
||||||
|
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
||||||
|
this.meB64 = meB64
|
||||||
|
this.infoHash = infoHash
|
||||||
|
this.endpoint = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash request() throws IOException {
|
||||||
|
InputStream is = endpoint.getInputStream()
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
|
||||||
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
|
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
|
||||||
|
String code = readTillRN(is)
|
||||||
|
if (!code.startsWith("200"))
|
||||||
|
throw new IOException("unknown code $code")
|
||||||
|
|
||||||
|
// parse all headers
|
||||||
|
Set<String> headers = new HashSet<>()
|
||||||
|
String header
|
||||||
|
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
||||||
|
headers.add(header)
|
||||||
|
|
||||||
|
long receivedStart = -1
|
||||||
|
long receivedEnd = -1
|
||||||
|
for (String receivedHeader : headers) {
|
||||||
|
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
|
||||||
|
if (group.size() != 1) {
|
||||||
|
log.info("ignoring header $receivedHeader")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedStart = Long.parseLong(group[0][1])
|
||||||
|
receivedEnd = Long.parseLong(group[0][2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receivedStart != 0)
|
||||||
|
throw new IOException("hashlist started at $receivedStart")
|
||||||
|
|
||||||
|
byte[] hashList = new byte[receivedEnd]
|
||||||
|
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
||||||
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
|
while(hashListBuf.hasRemaining()) {
|
||||||
|
if (hashListBuf.remaining() > tmp.length)
|
||||||
|
tmp = new byte[hashListBuf.remaining()]
|
||||||
|
int read = is.read(tmp)
|
||||||
|
if (read == -1)
|
||||||
|
throw new IOException()
|
||||||
|
hashListBuf.put(tmp, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash received = InfoHash.fromHashList(hashList)
|
||||||
|
if (received.getRoot() != infoHash.getRoot())
|
||||||
|
throw new IOException("fetched list doesn't match root")
|
||||||
|
received
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,11 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
class Pieces {
|
class Pieces {
|
||||||
private final BitSet bitSet
|
private final BitSet done, claimed
|
||||||
private final int nPieces
|
private final int nPieces
|
||||||
private final float ratio
|
private final float ratio
|
||||||
private final Random random = new Random()
|
private final Random random = new Random()
|
||||||
|
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||||
|
|
||||||
Pieces(int nPieces) {
|
Pieces(int nPieces) {
|
||||||
this(nPieces, 1.0f)
|
this(nPieces, 1.0f)
|
||||||
@@ -13,43 +14,103 @@ class Pieces {
|
|||||||
Pieces(int nPieces, float ratio) {
|
Pieces(int nPieces, float ratio) {
|
||||||
this.nPieces = nPieces
|
this.nPieces = nPieces
|
||||||
this.ratio = ratio
|
this.ratio = ratio
|
||||||
bitSet = new BitSet(nPieces)
|
done = new BitSet(nPieces)
|
||||||
|
claimed = new BitSet(nPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int getRandomPiece() {
|
synchronized int[] claim() {
|
||||||
int cardinality = bitSet.cardinality()
|
int claimedCardinality = claimed.cardinality()
|
||||||
if (cardinality == nPieces)
|
if (claimedCardinality == nPieces) {
|
||||||
return -1
|
// steal
|
||||||
|
int downloadedCardinality = done.cardinality()
|
||||||
|
if (downloadedCardinality == nPieces)
|
||||||
|
return null
|
||||||
|
int rv = done.nextClearBit(0)
|
||||||
|
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||||
|
}
|
||||||
|
|
||||||
// if fuller than ratio just do sequential
|
// if fuller than ratio just do sequential
|
||||||
if ( (1.0f * cardinality) / nPieces > ratio) {
|
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||||
return bitSet.nextClearBit(0)
|
int rv = claimed.nextClearBit(0)
|
||||||
|
claimed.set(rv)
|
||||||
|
return [rv, partials.getOrDefault(rv, 0), 0]
|
||||||
}
|
}
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
int start = random.nextInt(nPieces)
|
int start = random.nextInt(nPieces)
|
||||||
while(bitSet.get(start) && ++start < nPieces);
|
if (claimed.get(start))
|
||||||
return start
|
continue
|
||||||
|
claimed.set(start)
|
||||||
|
return [start, partials.getOrDefault(start,0), 0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getDownloaded() {
|
synchronized int[] claim(Set<Integer> available) {
|
||||||
|
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1))
|
||||||
|
available.remove(i)
|
||||||
|
if (available.isEmpty())
|
||||||
|
return null
|
||||||
|
Set<Integer> availableCopy = new HashSet<>(available)
|
||||||
|
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
|
||||||
|
availableCopy.remove(i)
|
||||||
|
if (availableCopy.isEmpty()) {
|
||||||
|
// steal
|
||||||
|
int rv = available.first()
|
||||||
|
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||||
|
}
|
||||||
|
List<Integer> toList = availableCopy.toList()
|
||||||
|
Collections.shuffle(toList)
|
||||||
|
int rv = toList[0]
|
||||||
|
claimed.set(rv)
|
||||||
|
[rv, partials.getOrDefault(rv, 0), 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized def getDownloaded() {
|
||||||
def rv = []
|
def rv = []
|
||||||
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
|
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||||
rv << i
|
rv << i
|
||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void markDownloaded(int piece) {
|
synchronized void markDownloaded(int piece) {
|
||||||
bitSet.set(piece)
|
done.set(piece)
|
||||||
|
claimed.set(piece)
|
||||||
|
partials.remove(piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void markPartial(int piece, int position) {
|
||||||
|
partials.put(piece, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void unclaim(int piece) {
|
||||||
|
claimed.clear(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized boolean isComplete() {
|
synchronized boolean isComplete() {
|
||||||
bitSet.cardinality() == nPieces
|
done.cardinality() == nPieces
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int donePieces() {
|
synchronized int donePieces() {
|
||||||
bitSet.cardinality()
|
done.cardinality()
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized boolean isDownloaded(int piece) {
|
||||||
|
done.get(piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void clearAll() {
|
||||||
|
done.clear()
|
||||||
|
claimed.clear()
|
||||||
|
partials.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void write(PrintWriter writer) {
|
||||||
|
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||||
|
writer.println(i)
|
||||||
|
}
|
||||||
|
partials.each { piece, position ->
|
||||||
|
writer.println("$piece,$position")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
class SourceDiscoveredEvent extends Event {
|
||||||
|
InfoHash infoHash
|
||||||
|
Persona source
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadCancelledEvent extends Event {
|
||||||
|
Downloader downloader
|
||||||
|
}
|
@@ -3,8 +3,11 @@ package com.muwire.core.download
|
|||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
import com.muwire.core.search.UIResultEvent
|
import com.muwire.core.search.UIResultEvent
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class UIDownloadEvent extends Event {
|
class UIDownloadEvent extends Event {
|
||||||
|
|
||||||
UIResultEvent result
|
UIResultEvent[] result
|
||||||
|
Set<Destination> sources
|
||||||
File target
|
File target
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadPausedEvent extends Event {
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIDownloadResumedEvent extends Event {
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class AllFilesLoadedEvent extends Event {
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class DirectoryUnsharedEvent extends Event {
|
||||||
|
File directory
|
||||||
|
}
|
@@ -0,0 +1,148 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import static java.nio.file.StandardWatchEventKinds.*
|
||||||
|
|
||||||
|
import java.nio.file.ClosedWatchServiceException
|
||||||
|
import java.nio.file.WatchEvent
|
||||||
|
import java.nio.file.WatchKey
|
||||||
|
import java.nio.file.WatchService
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.util.SystemVersion
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class DirectoryWatcher {
|
||||||
|
|
||||||
|
private static final long WAIT_TIME = 1000
|
||||||
|
|
||||||
|
private static final WatchEvent.Kind[] kinds
|
||||||
|
static {
|
||||||
|
if (SystemVersion.isMac())
|
||||||
|
kinds = [ENTRY_MODIFY, ENTRY_DELETE]
|
||||||
|
else
|
||||||
|
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
|
||||||
|
}
|
||||||
|
|
||||||
|
private final EventBus eventBus
|
||||||
|
private final FileManager fileManager
|
||||||
|
private final Thread watcherThread, publisherThread
|
||||||
|
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
|
||||||
|
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
|
||||||
|
private WatchService watchService
|
||||||
|
private volatile boolean shutdown
|
||||||
|
|
||||||
|
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||||
|
watcherThread.setDaemon(true)
|
||||||
|
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
|
||||||
|
publisherThread.setDaemon(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||||
|
watchService = FileSystems.getDefault().newWatchService()
|
||||||
|
watcherThread.start()
|
||||||
|
publisherThread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
shutdown = true
|
||||||
|
watcherThread?.interrupt()
|
||||||
|
publisherThread?.interrupt()
|
||||||
|
watchService?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileSharedEvent(FileSharedEvent e) {
|
||||||
|
if (!e.file.isDirectory())
|
||||||
|
return
|
||||||
|
Path path = e.file.getCanonicalFile().toPath()
|
||||||
|
WatchKey wk = path.register(watchService, kinds)
|
||||||
|
watchedDirectories.put(e.file, wk)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||||
|
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||||
|
wk?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void watch() {
|
||||||
|
try {
|
||||||
|
while(!shutdown) {
|
||||||
|
WatchKey key = watchService.take()
|
||||||
|
key.pollEvents().each {
|
||||||
|
switch(it.kind()) {
|
||||||
|
case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break
|
||||||
|
case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break
|
||||||
|
case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key.reset()
|
||||||
|
}
|
||||||
|
} catch (InterruptedException|ClosedWatchServiceException e) {
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void processCreated(Path parent, Path path) {
|
||||||
|
File f= join(parent, path)
|
||||||
|
log.fine("created entry $f")
|
||||||
|
if (f.isDirectory())
|
||||||
|
f.toPath().register(watchService, kinds)
|
||||||
|
else
|
||||||
|
waitingFiles.put(f, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processModified(Path parent, Path path) {
|
||||||
|
File f = join(parent, path)
|
||||||
|
log.fine("modified entry $f")
|
||||||
|
waitingFiles.put(f, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processDeleted(Path parent, Path path) {
|
||||||
|
File f = join(parent, path)
|
||||||
|
log.fine("deleted entry $f")
|
||||||
|
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||||
|
if (sf != null)
|
||||||
|
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File join(Path parent, Path path) {
|
||||||
|
File parentFile = parent.toFile().getCanonicalFile()
|
||||||
|
new File(parentFile, path.toFile().getName()).getCanonicalFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish() {
|
||||||
|
try {
|
||||||
|
while(!shutdown) {
|
||||||
|
Thread.sleep(WAIT_TIME)
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
def published = []
|
||||||
|
waitingFiles.each { file, timestamp ->
|
||||||
|
if (now - timestamp > WAIT_TIME) {
|
||||||
|
log.fine("publishing file $file")
|
||||||
|
eventBus.publish new FileSharedEvent(file : file)
|
||||||
|
published << file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
published.each {
|
||||||
|
waitingFiles.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,10 +2,11 @@ package com.muwire.core.files
|
|||||||
|
|
||||||
import com.muwire.core.DownloadedFile
|
import com.muwire.core.DownloadedFile
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.download.Downloader
|
||||||
|
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class FileDownloadedEvent extends Event {
|
class FileDownloadedEvent extends Event {
|
||||||
|
Downloader downloader
|
||||||
DownloadedFile downloadedFile
|
DownloadedFile downloadedFile
|
||||||
}
|
}
|
||||||
|
@@ -7,4 +7,10 @@ class FileHashedEvent extends Event {
|
|||||||
|
|
||||||
SharedFile sharedFile
|
SharedFile sharedFile
|
||||||
String error
|
String error
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
import java.nio.MappedByteBuffer
|
import java.nio.MappedByteBuffer
|
||||||
import java.nio.channels.FileChannel
|
import java.nio.channels.FileChannel
|
||||||
import java.nio.channels.FileChannel.MapMode
|
import java.nio.channels.FileChannel.MapMode
|
||||||
@@ -15,14 +19,16 @@ class FileHasher {
|
|||||||
/**
|
/**
|
||||||
* @param size of the file to be shared
|
* @param size of the file to be shared
|
||||||
* @return the size of each piece in power of 2
|
* @return the size of each piece in power of 2
|
||||||
|
* piece size is minimum 128 KBytees and maximum 16 MBytes in power of 2 steps (2^17 - 2^24)
|
||||||
|
* there can be up to 8192 pieces maximum per file
|
||||||
*/
|
*/
|
||||||
static int getPieceSize(long size) {
|
static int getPieceSize(long size) {
|
||||||
if (size <= 0x1 << 25)
|
if (size <= 0x1 << 30)
|
||||||
return 18
|
return 17
|
||||||
|
|
||||||
for (int i = 26; i <= 37; i++) {
|
for (int i = 31; i <= 37; i++) {
|
||||||
if (size <= 0x1L << i) {
|
if (size <= 0x1L << i) {
|
||||||
return i-7
|
return i-13
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +58,12 @@ class FileHasher {
|
|||||||
try {
|
try {
|
||||||
MappedByteBuffer buf
|
MappedByteBuffer buf
|
||||||
for (int i = 0; i < numPieces - 1; i++) {
|
for (int i = 0; i < numPieces - 1; i++) {
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
|
DataUtil.tryUnmap(buf)
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
}
|
}
|
||||||
def lastPieceLength = length - (numPieces - 1) * size
|
def lastPieceLength = length - (numPieces - 1) * ((long)size)
|
||||||
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
|
||||||
digest.update buf
|
digest.update buf
|
||||||
output.write(digest.digest(), 0, 32)
|
output.write(digest.digest(), 0, 32)
|
||||||
@@ -67,4 +74,18 @@ class FileHasher {
|
|||||||
byte [] hashList = output.toByteArray()
|
byte [] hashList = output.toByteArray()
|
||||||
InfoHash.fromHashList(hashList)
|
InfoHash.fromHashList(hashList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length != 1) {
|
||||||
|
println "This utility computes an infohash of a file"
|
||||||
|
println "Pass absolute path to a file as an argument"
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def file = new File(args[0])
|
||||||
|
file = file.getAbsoluteFile()
|
||||||
|
def hasher = new FileHasher()
|
||||||
|
def infohash = hasher.hashFile(file)
|
||||||
|
println Base64.encode(infohash.getRoot())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.SharedFile
|
||||||
|
|
||||||
|
class FileHashingEvent extends Event {
|
||||||
|
|
||||||
|
File hashingFile
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + " hashingFile " + hashingFile.getAbsolutePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,7 +2,9 @@ package com.muwire.core.files
|
|||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
import com.muwire.core.UILoadedEvent
|
||||||
import com.muwire.core.search.ResultsEvent
|
import com.muwire.core.search.ResultsEvent
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
import com.muwire.core.search.SearchIndex
|
import com.muwire.core.search.SearchIndex
|
||||||
@@ -14,12 +16,14 @@ class FileManager {
|
|||||||
|
|
||||||
|
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
|
final MuWireSettings settings
|
||||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||||
final Map<File, SharedFile> fileToSharedFile = 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>> nameToFiles = new HashMap<>()
|
||||||
final SearchIndex index = new SearchIndex()
|
final SearchIndex index = new SearchIndex()
|
||||||
|
|
||||||
FileManager(EventBus eventBus) {
|
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||||
|
this.settings = settings
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +37,10 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
|
if (settings.shareDownloadedFiles) {
|
||||||
addToIndex(e.downloadedFile)
|
addToIndex(e.downloadedFile)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addToIndex(SharedFile sf) {
|
private void addToIndex(SharedFile sf) {
|
||||||
log.info("Adding shared file " + sf.getFile())
|
log.info("Adding shared file " + sf.getFile())
|
||||||
@@ -100,20 +106,45 @@ class FileManager {
|
|||||||
if (e.searchHash != null) {
|
if (e.searchHash != null) {
|
||||||
Set<SharedFile> found
|
Set<SharedFile> found
|
||||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||||
|
found = filter(found, e.oobInfohash)
|
||||||
if (found != null && !found.isEmpty())
|
if (found != null && !found.isEmpty())
|
||||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
|
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
} else {
|
} else {
|
||||||
def names = index.search e.searchTerms
|
def names = index.search e.searchTerms
|
||||||
Set<File> files = new HashSet<>()
|
Set<File> files = new HashSet<>()
|
||||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||||
|
files = filter(sharedFiles, e.oobInfohash)
|
||||||
if (!sharedFiles.isEmpty())
|
if (!sharedFiles.isEmpty())
|
||||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
|
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (re != null)
|
if (re != null)
|
||||||
eventBus.publish(re)
|
eventBus.publish(re)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
|
||||||
|
if (!oob)
|
||||||
|
return files
|
||||||
|
Set<SharedFile> rv = new HashSet<>()
|
||||||
|
files.each {
|
||||||
|
if (it.getPieceSize() != 0)
|
||||||
|
rv.add(it)
|
||||||
|
}
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||||
|
e.directory.listFiles().each {
|
||||||
|
if (it.isDirectory())
|
||||||
|
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||||
|
else {
|
||||||
|
SharedFile sf = fileToSharedFile.get(it)
|
||||||
|
if (sf != null)
|
||||||
|
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,4 +5,9 @@ import com.muwire.core.Event
|
|||||||
class FileSharedEvent extends Event {
|
class FileSharedEvent extends Event {
|
||||||
|
|
||||||
File file
|
File file
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return super.toString() + " file: "+file.getAbsolutePath()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,13 @@ class HasherService {
|
|||||||
|
|
||||||
final FileHasher hasher
|
final FileHasher hasher
|
||||||
final EventBus eventBus
|
final EventBus eventBus
|
||||||
|
final FileManager fileManager
|
||||||
Executor executor
|
Executor executor
|
||||||
|
|
||||||
HasherService(FileHasher hasher, EventBus eventBus) {
|
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||||
this.hasher = hasher
|
this.hasher = hasher
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
|
this.fileManager = fileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
@@ -22,21 +24,24 @@ class HasherService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onFileSharedEvent(FileSharedEvent evt) {
|
void onFileSharedEvent(FileSharedEvent evt) {
|
||||||
|
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile()))
|
||||||
|
return
|
||||||
executor.execute( { -> process(evt.file) } as Runnable)
|
executor.execute( { -> process(evt.file) } as Runnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void process(File f) {
|
private void process(File f) {
|
||||||
f = f.getCanonicalFile()
|
f = f.getCanonicalFile()
|
||||||
if (f.isDirectory()) {
|
if (f.isDirectory()) {
|
||||||
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
|
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||||
} else {
|
} else {
|
||||||
if (f.length() == 0) {
|
if (f.length() == 0) {
|
||||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||||
} else if (f.length() > FileHasher.MAX_SIZE) {
|
} else if (f.length() > FileHasher.MAX_SIZE) {
|
||||||
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
||||||
} else {
|
} else {
|
||||||
|
eventBus.publish new FileHashingEvent(hashingFile: f)
|
||||||
def hash = hasher.hashFile f
|
def hash = hasher.hashFile f
|
||||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash))
|
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
package com.muwire.core.files
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
import java.nio.file.CopyOption
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
@@ -8,6 +11,7 @@ import com.muwire.core.EventBus
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Service
|
import com.muwire.core.Service
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
|
import com.muwire.core.UILoadedEvent
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
@@ -33,14 +37,14 @@ class PersisterService extends Service {
|
|||||||
timer = new Timer("file persister", true)
|
timer = new Timer("file persister", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
|
||||||
timer.schedule({load()} as TimerTask, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onUILoadedEvent(UILoadedEvent e) {
|
||||||
|
timer.schedule({load()} as TimerTask, 1)
|
||||||
|
}
|
||||||
|
|
||||||
void load() {
|
void load() {
|
||||||
if (location.exists() && location.isFile()) {
|
if (location.exists() && location.isFile()) {
|
||||||
def slurper = new JsonSlurper()
|
def slurper = new JsonSlurper()
|
||||||
@@ -55,9 +59,12 @@ class PersisterService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||||
log.log(Level.WARNING, "couldn't load files",e)
|
log.log(Level.WARNING, "couldn't load files",e)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
listener.publish(new AllFilesLoadedEvent())
|
||||||
}
|
}
|
||||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||||
loaded = true
|
loaded = true
|
||||||
@@ -94,36 +101,46 @@ class PersisterService extends Service {
|
|||||||
if (!Arrays.equals(root, ih.getRoot()))
|
if (!Arrays.equals(root, ih.getRoot()))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
int pieceSize = 0
|
||||||
|
if (json.pieceSize != null)
|
||||||
|
pieceSize = json.pieceSize
|
||||||
|
|
||||||
if (json.sources != null) {
|
if (json.sources != null) {
|
||||||
List sources = (List)json.sources
|
List sources = (List)json.sources
|
||||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||||
DownloadedFile df = new DownloadedFile(file, ih, sourceSet)
|
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||||
return new FileLoadedEvent(loadedFile : df)
|
return new FileLoadedEvent(loadedFile : df)
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedFile sf = new SharedFile(file, ih)
|
|
||||||
|
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||||
return new FileLoadedEvent(loadedFile: sf)
|
return new FileLoadedEvent(loadedFile: sf)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistFiles() {
|
private void persistFiles() {
|
||||||
location.delete()
|
|
||||||
def sharedFiles = fileManager.getSharedFiles()
|
def sharedFiles = fileManager.getSharedFiles()
|
||||||
location.withPrintWriter { writer ->
|
|
||||||
|
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||||
|
tmp.deleteOnExit()
|
||||||
|
tmp.withPrintWriter { writer ->
|
||||||
sharedFiles.each { k, v ->
|
sharedFiles.each { k, v ->
|
||||||
def json = toJson(k,v)
|
def json = toJson(k,v)
|
||||||
json = JsonOutput.toJson(json)
|
json = JsonOutput.toJson(json)
|
||||||
writer.println json
|
writer.println json
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
tmp.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
private def toJson(File f, SharedFile sf) {
|
private def toJson(File f, SharedFile sf) {
|
||||||
def json = [:]
|
def json = [:]
|
||||||
json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
|
json.file = Base64.encode DataUtil.encodei18nString(f.toString())
|
||||||
json.length = f.length()
|
json.length = sf.getCachedLength()
|
||||||
InfoHash ih = sf.getInfoHash()
|
InfoHash ih = sf.getInfoHash()
|
||||||
json.infoHash = Base64.encode ih.getRoot()
|
json.infoHash = Base64.encode ih.getRoot()
|
||||||
|
json.pieceSize = sf.getPieceSize()
|
||||||
byte [] tmp = new byte [32]
|
byte [] tmp = new byte [32]
|
||||||
json.hashList = []
|
json.hashList = []
|
||||||
for (int i = 0;i < ih.getHashList().length / 32; i++) {
|
for (int i = 0;i < ih.getHashList().length / 32; i++) {
|
||||||
|
@@ -65,7 +65,7 @@ class CacheClient {
|
|||||||
options.setSendLeaseSet(true)
|
options.setSendLeaseSet(true)
|
||||||
CacheServers.getCacheServers().each {
|
CacheServers.getCacheServers().each {
|
||||||
log.info "Querying hostcache ${it.toBase32()}"
|
log.info "Querying hostcache ${it.toBase32()}"
|
||||||
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 0, 0, options)
|
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class CacheClient {
|
|||||||
pong.pongs.asList().each {
|
pong.pongs.asList().each {
|
||||||
Destination dest = new Destination(it)
|
Destination dest = new Destination(it)
|
||||||
if (!session.getMyDestination().equals(dest))
|
if (!session.getMyDestination().equals(dest))
|
||||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
eventBus.publish(new HostDiscoveredEvent(destination: dest, fromHostcache : true))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,12 @@ class CacheServers {
|
|||||||
|
|
||||||
private static final int TO_GIVE = 3
|
private static final int TO_GIVE = 3
|
||||||
private static Set<Destination> CACHES = [
|
private static Set<Destination> CACHES = [
|
||||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA")
|
// zlatinb
|
||||||
|
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
|
||||||
|
// sNL
|
||||||
|
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
|
||||||
|
// dark_trion
|
||||||
|
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
|
||||||
]
|
]
|
||||||
|
|
||||||
static List<Destination> getCacheServers() {
|
static List<Destination> getCacheServers() {
|
||||||
|
@@ -7,20 +7,25 @@ class Host {
|
|||||||
private static final int MAX_FAILURES = 3
|
private static final int MAX_FAILURES = 3
|
||||||
|
|
||||||
final Destination destination
|
final Destination destination
|
||||||
|
private final int clearInterval
|
||||||
int failures,successes
|
int failures,successes
|
||||||
|
long lastAttempt
|
||||||
|
|
||||||
public Host(Destination destination) {
|
public Host(Destination destination, int clearInterval) {
|
||||||
this.destination = destination
|
this.destination = destination
|
||||||
|
this.clearInterval = clearInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void onConnect() {
|
synchronized void onConnect() {
|
||||||
failures = 0
|
failures = 0
|
||||||
successes++
|
successes++
|
||||||
|
lastAttempt = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void onFailure() {
|
synchronized void onFailure() {
|
||||||
failures++
|
failures++
|
||||||
successes = 0
|
successes = 0
|
||||||
|
lastAttempt = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized boolean isFailed() {
|
synchronized boolean isFailed() {
|
||||||
@@ -30,4 +35,12 @@ class Host {
|
|||||||
synchronized boolean hasSucceeded() {
|
synchronized boolean hasSucceeded() {
|
||||||
successes > 0
|
successes > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void clearFailures() {
|
||||||
|
failures = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void canTryAgain() {
|
||||||
|
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,21 +46,25 @@ class HostCache extends Service {
|
|||||||
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
|
||||||
if (myself == e.destination)
|
if (myself == e.destination)
|
||||||
return
|
return
|
||||||
if (hosts.containsKey(e.destination))
|
if (hosts.containsKey(e.destination)) {
|
||||||
|
if (!e.fromHostcache)
|
||||||
return
|
return
|
||||||
Host host = new Host(e.destination)
|
hosts.get(e.destination).clearFailures()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Host host = new Host(e.destination, settings.hostClearInterval)
|
||||||
if (allowHost(host)) {
|
if (allowHost(host)) {
|
||||||
hosts.put(e.destination, host)
|
hosts.put(e.destination, host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onConnectionEvent(ConnectionEvent e) {
|
void onConnectionEvent(ConnectionEvent e) {
|
||||||
if (e.incoming || e.leaf)
|
if (e.leaf)
|
||||||
return
|
return
|
||||||
Destination dest = e.endpoint.destination
|
Destination dest = e.endpoint.destination
|
||||||
Host host = hosts.get(dest)
|
Host host = hosts.get(dest)
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
host = new Host(dest)
|
host = new Host(dest, settings.hostClearInterval)
|
||||||
hosts.put(dest, host)
|
hosts.put(dest, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +106,11 @@ class HostCache extends Service {
|
|||||||
storage.eachLine {
|
storage.eachLine {
|
||||||
def entry = slurper.parseText(it)
|
def entry = slurper.parseText(it)
|
||||||
Destination dest = new Destination(entry.destination)
|
Destination dest = new Destination(entry.destination)
|
||||||
Host host = new Host(dest)
|
Host host = new Host(dest, settings.hostClearInterval)
|
||||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||||
|
if (entry.lastAttempt != null)
|
||||||
|
host.lastAttempt = entry.lastAttempt
|
||||||
if (allowHost(host))
|
if (allowHost(host))
|
||||||
hosts.put(dest, host)
|
hosts.put(dest, host)
|
||||||
}
|
}
|
||||||
@@ -114,7 +120,7 @@ class HostCache extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean allowHost(Host host) {
|
private boolean allowHost(Host host) {
|
||||||
if (host.isFailed())
|
if (host.isFailed() && !host.canTryAgain())
|
||||||
return false
|
return false
|
||||||
if (host.destination == myself)
|
if (host.destination == myself)
|
||||||
return false
|
return false
|
||||||
@@ -139,6 +145,7 @@ class HostCache extends Service {
|
|||||||
map.destination = dest.toBase64()
|
map.destination = dest.toBase64()
|
||||||
map.failures = host.failures
|
map.failures = host.failures
|
||||||
map.successes = host.successes
|
map.successes = host.successes
|
||||||
|
map.lastAttempt = host.lastAttempt
|
||||||
def json = JsonOutput.toJson(map)
|
def json = JsonOutput.toJson(map)
|
||||||
writer.println json
|
writer.println json
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,10 @@ import net.i2p.data.Destination
|
|||||||
class HostDiscoveredEvent extends Event {
|
class HostDiscoveredEvent extends Event {
|
||||||
|
|
||||||
Destination destination
|
Destination destination
|
||||||
|
boolean fromHostcache
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()}"
|
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
core/src/main/groovy/com/muwire/core/mesh/Mesh.groovy
Normal file
28
core/src/main/groovy/com/muwire/core/mesh/Mesh.groovy
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.muwire.core.mesh
|
||||||
|
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.download.Pieces
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
|
class Mesh {
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final Set<Persona> sources = new ConcurrentHashSet<>()
|
||||||
|
private final Pieces pieces
|
||||||
|
|
||||||
|
Mesh(InfoHash infoHash, Pieces pieces) {
|
||||||
|
this.infoHash = infoHash
|
||||||
|
this.pieces = pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Persona> getRandom(int n, Persona exclude) {
|
||||||
|
List<Persona> tmp = new ArrayList<>(sources)
|
||||||
|
tmp.remove(exclude)
|
||||||
|
Collections.shuffle(tmp)
|
||||||
|
if (tmp.size() < n)
|
||||||
|
return tmp
|
||||||
|
tmp[0..n-1]
|
||||||
|
}
|
||||||
|
}
|
102
core/src/main/groovy/com/muwire/core/mesh/MeshManager.groovy
Normal file
102
core/src/main/groovy/com/muwire/core/mesh/MeshManager.groovy
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package com.muwire.core.mesh
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import com.muwire.core.Constants
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.download.Pieces
|
||||||
|
import com.muwire.core.download.SourceDiscoveredEvent
|
||||||
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class MeshManager {
|
||||||
|
|
||||||
|
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
|
||||||
|
private final FileManager fileManager
|
||||||
|
private final File home
|
||||||
|
private final MuWireSettings settings
|
||||||
|
|
||||||
|
MeshManager(FileManager fileManager, File home, MuWireSettings settings) {
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.home = home
|
||||||
|
this.settings = settings
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh get(InfoHash infoHash) {
|
||||||
|
meshes.get(infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh getOrCreate(InfoHash infoHash, int nPieces) {
|
||||||
|
synchronized(meshes) {
|
||||||
|
if (meshes.containsKey(infoHash))
|
||||||
|
return meshes.get(infoHash)
|
||||||
|
Pieces pieces = new Pieces(nPieces, settings.downloadSequentialRatio)
|
||||||
|
if (fileManager.rootToFiles.containsKey(infoHash)) {
|
||||||
|
for (int i = 0; i < nPieces; i++)
|
||||||
|
pieces.markDownloaded(i)
|
||||||
|
}
|
||||||
|
Mesh rv = new Mesh(infoHash, pieces)
|
||||||
|
meshes.put(infoHash, rv)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||||
|
Mesh mesh = meshes.get(e.infoHash)
|
||||||
|
if (mesh == null)
|
||||||
|
return
|
||||||
|
mesh.sources.add(e.source)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save() {
|
||||||
|
File meshFile = new File(home, "mesh.json")
|
||||||
|
synchronized(meshes) {
|
||||||
|
meshFile.withPrintWriter { writer ->
|
||||||
|
meshes.values().each { mesh ->
|
||||||
|
def json = [:]
|
||||||
|
json.timestamp = System.currentTimeMillis()
|
||||||
|
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
|
||||||
|
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||||
|
json.nPieces = mesh.pieces.nPieces
|
||||||
|
json.xHave = DataUtil.encodeXHave(mesh.pieces.downloaded, mesh.pieces.nPieces)
|
||||||
|
writer.println(JsonOutput.toJson(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
File meshFile = new File(home, "mesh.json")
|
||||||
|
if (!meshFile.exists())
|
||||||
|
return
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
JsonSlurper slurper = new JsonSlurper()
|
||||||
|
meshFile.eachLine {
|
||||||
|
def json = slurper.parseText(it)
|
||||||
|
if (now - json.timestamp > settings.meshExpiration * 60 * 1000)
|
||||||
|
return
|
||||||
|
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
|
||||||
|
Pieces pieces = new Pieces(json.nPieces, settings.downloadSequentialRatio)
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh(infoHash, pieces)
|
||||||
|
json.sources.each { source ->
|
||||||
|
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
|
||||||
|
mesh.sources.add(persona)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.xHave != null)
|
||||||
|
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
|
||||||
|
|
||||||
|
if (!mesh.sources.isEmpty())
|
||||||
|
meshes.put(infoHash, mesh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.search
|
package com.muwire.core.search
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
@@ -9,6 +10,11 @@ class QueryEvent extends Event {
|
|||||||
SearchEvent searchEvent
|
SearchEvent searchEvent
|
||||||
boolean firstHop
|
boolean firstHop
|
||||||
Destination replyTo
|
Destination replyTo
|
||||||
|
Persona originator
|
||||||
Destination receivedOn
|
Destination receivedOn
|
||||||
|
|
||||||
|
String toString() {
|
||||||
|
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||||
|
"originator: ${originator.getHumanReadableName()} receivedOn: ${receivedOn.toBase32()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import com.muwire.core.SharedFile
|
|||||||
|
|
||||||
class ResultsEvent extends Event {
|
class ResultsEvent extends Event {
|
||||||
|
|
||||||
|
SearchEvent searchEvent
|
||||||
SharedFile[] results
|
SharedFile[] results
|
||||||
UUID uuid
|
UUID uuid
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package com.muwire.core.search
|
package com.muwire.core.search
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
import javax.naming.directory.InvalidSearchControlsException
|
import javax.naming.directory.InvalidSearchControlsException
|
||||||
|
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
@@ -7,13 +9,25 @@ import com.muwire.core.Persona
|
|||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class ResultsParser {
|
class ResultsParser {
|
||||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||||
if (json.type != "Result")
|
if (json.type != "Result")
|
||||||
throw new InvalidSearchResultException("not a result json")
|
throw new InvalidSearchResultException("not a result json")
|
||||||
if (json.version != 1)
|
switch(json.version) {
|
||||||
|
case 1:
|
||||||
|
return parseV1(p, uuid, json)
|
||||||
|
case 2:
|
||||||
|
return parseV2(p, uuid, json)
|
||||||
|
default:
|
||||||
throw new InvalidSearchResultException("unknown version $json.version")
|
throw new InvalidSearchResultException("unknown version $json.version")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseV1(Persona p, UUID uuid, def json) {
|
||||||
if (json.name == null)
|
if (json.name == null)
|
||||||
throw new InvalidSearchResultException("name missing")
|
throw new InvalidSearchResultException("name missing")
|
||||||
if (json.size == null)
|
if (json.size == null)
|
||||||
@@ -47,9 +61,45 @@ class ResultsParser {
|
|||||||
size : size,
|
size : size,
|
||||||
infohash : parsedIH,
|
infohash : parsedIH,
|
||||||
pieceSize : pieceSize,
|
pieceSize : pieceSize,
|
||||||
|
sources : Collections.emptySet(),
|
||||||
uuid : uuid)
|
uuid : uuid)
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||||
|
if (json.name == null)
|
||||||
|
throw new InvalidSearchResultException("name missing")
|
||||||
|
if (json.size == null)
|
||||||
|
throw new InvalidSearchResultException("length missing")
|
||||||
|
if (json.infohash == null)
|
||||||
|
throw new InvalidSearchResultException("infohash missing")
|
||||||
|
if (json.pieceSize == null)
|
||||||
|
throw new InvalidSearchResultException("pieceSize missing")
|
||||||
|
if (json.hashList != null)
|
||||||
|
throw new InvalidSearchResultException("V2 result with hashlist")
|
||||||
|
try {
|
||||||
|
String name = DataUtil.readi18nString(Base64.decode(json.name))
|
||||||
|
long size = json.size
|
||||||
|
byte [] infoHash = Base64.decode(json.infohash)
|
||||||
|
if (infoHash.length != InfoHash.SIZE)
|
||||||
|
throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
|
||||||
|
int pieceSize = json.pieceSize
|
||||||
|
|
||||||
|
Set<Destination> sources = Collections.emptySet()
|
||||||
|
if (json.sources != null)
|
||||||
|
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
|
||||||
|
|
||||||
|
return new UIResultEvent( sender : p,
|
||||||
|
name : name,
|
||||||
|
size : size,
|
||||||
|
infohash : new InfoHash(infoHash),
|
||||||
|
pieceSize : pieceSize,
|
||||||
|
sources : sources,
|
||||||
|
uuid: uuid)
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,10 @@ import java.util.concurrent.Executor
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.ThreadFactory
|
import java.util.concurrent.ThreadFactory
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.logging.Level
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import com.muwire.core.DownloadedFile
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
|
||||||
@@ -46,22 +49,30 @@ class ResultsSender {
|
|||||||
this.me = me
|
this.me = me
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendResults(UUID uuid, SharedFile[] results, Destination target) {
|
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
|
||||||
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()}")
|
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
|
||||||
if (target.equals(me.destination)) {
|
if (target.equals(me.destination)) {
|
||||||
results.each {
|
results.each {
|
||||||
long length = it.getFile().length()
|
long length = it.getFile().length()
|
||||||
|
int pieceSize = it.getPieceSize()
|
||||||
|
if (pieceSize == 0)
|
||||||
|
pieceSize = FileHasher.getPieceSize(length)
|
||||||
|
Set<Destination> suggested = Collections.emptySet()
|
||||||
|
if (it instanceof DownloadedFile)
|
||||||
|
suggested = it.sources
|
||||||
def uiResultEvent = new UIResultEvent( sender : me,
|
def uiResultEvent = new UIResultEvent( sender : me,
|
||||||
name : it.getFile().getName(),
|
name : it.getFile().getName(),
|
||||||
size : length,
|
size : length,
|
||||||
infohash : it.getInfoHash(),
|
infohash : it.getInfoHash(),
|
||||||
pieceSize : FileHasher.getPieceSize(length),
|
pieceSize : pieceSize,
|
||||||
uuid : uuid
|
uuid : uuid,
|
||||||
|
sources : suggested
|
||||||
)
|
)
|
||||||
eventBus.publish(uiResultEvent)
|
eventBus.publish(uiResultEvent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
executor.execute(new ResultSendJob(uuid : uuid, results : results, target: target))
|
executor.execute(new ResultSendJob(uuid : uuid, results : results,
|
||||||
|
target: target, oobInfohash : oobInfohash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +80,11 @@ class ResultsSender {
|
|||||||
UUID uuid
|
UUID uuid
|
||||||
SharedFile [] results
|
SharedFile [] results
|
||||||
Destination target
|
Destination target
|
||||||
|
boolean oobInfohash
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
try {
|
||||||
byte [] tmp = new byte[InfoHash.SIZE]
|
byte [] tmp = new byte[InfoHash.SIZE]
|
||||||
JsonOutput jsonOutput = new JsonOutput()
|
JsonOutput jsonOutput = new JsonOutput()
|
||||||
Endpoint endpoint = null;
|
Endpoint endpoint = null;
|
||||||
@@ -91,11 +104,12 @@ class ResultsSender {
|
|||||||
String encodedName = Base64.encode(baos.toByteArray())
|
String encodedName = Base64.encode(baos.toByteArray())
|
||||||
def obj = [:]
|
def obj = [:]
|
||||||
obj.type = "Result"
|
obj.type = "Result"
|
||||||
obj.version = 1
|
obj.version = oobInfohash ? 2 : 1
|
||||||
obj.name = encodedName
|
obj.name = encodedName
|
||||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||||
obj.size = it.getFile().length()
|
obj.size = it.getFile().length()
|
||||||
obj.pieceSize = FileHasher.getPieceSize(it.getFile().length())
|
obj.pieceSize = it.getPieceSize()
|
||||||
|
if (!oobInfohash) {
|
||||||
byte [] hashList = it.getInfoHash().getHashList()
|
byte [] hashList = it.getInfoHash().getHashList()
|
||||||
def hashListB64 = []
|
def hashListB64 = []
|
||||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||||
@@ -103,6 +117,10 @@ class ResultsSender {
|
|||||||
hashListB64 << Base64.encode(tmp)
|
hashListB64 << Base64.encode(tmp)
|
||||||
}
|
}
|
||||||
obj.hashList = hashListB64
|
obj.hashList = hashListB64
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it instanceof DownloadedFile)
|
||||||
|
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||||
|
|
||||||
def json = jsonOutput.toJson(obj)
|
def json = jsonOutput.toJson(obj)
|
||||||
os.writeShort((short)json.length())
|
os.writeShort((short)json.length())
|
||||||
@@ -112,6 +130,9 @@ class ResultsSender {
|
|||||||
} finally {
|
} finally {
|
||||||
endpoint?.close()
|
endpoint?.close()
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "problem sending results",e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,19 @@
|
|||||||
package com.muwire.core.search
|
package com.muwire.core.search
|
||||||
|
|
||||||
import com.muwire.core.Event
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
|
||||||
class SearchEvent extends Event {
|
class SearchEvent extends Event {
|
||||||
|
|
||||||
List<String> searchTerms
|
List<String> searchTerms
|
||||||
byte [] searchHash
|
byte [] searchHash
|
||||||
UUID uuid
|
UUID uuid
|
||||||
|
boolean oobInfohash
|
||||||
|
|
||||||
|
String toString() {
|
||||||
|
def infoHash = null
|
||||||
|
if (searchHash != null)
|
||||||
|
infoHash = new InfoHash(searchHash)
|
||||||
|
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,10 @@ class SearchIndex {
|
|||||||
|
|
||||||
private static String[] split(String source) {
|
private static String[] split(String source) {
|
||||||
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
||||||
source.split(" ")
|
String [] split = source.split(" ")
|
||||||
|
def rv = []
|
||||||
|
split.each { if (it.length() > 0) rv << it }
|
||||||
|
rv.toArray(new String[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] search(List<String> terms) {
|
String[] search(List<String> terms) {
|
||||||
@@ -42,7 +45,7 @@ class SearchIndex {
|
|||||||
terms.each {
|
terms.each {
|
||||||
Set<String> forWord = keywords.getOrDefault(it,[])
|
Set<String> forWord = keywords.getOrDefault(it,[])
|
||||||
if (rv == null) {
|
if (rv == null) {
|
||||||
rv = forWord
|
rv = new HashSet<>(forWord)
|
||||||
} else {
|
} else {
|
||||||
rv.retainAll(forWord)
|
rv.retainAll(forWord)
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ public class SearchManager {
|
|||||||
log.info("No results for search uuid $event.uuid")
|
log.info("No results for search uuid $event.uuid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultsSender.sendResults(event.uuid, event.results, target)
|
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasLocalSearch(UUID uuid) {
|
boolean hasLocalSearch(UUID uuid) {
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
package com.muwire.core.search
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIResultBatchEvent extends Event {
|
||||||
|
UUID uuid
|
||||||
|
UIResultEvent[] results
|
||||||
|
}
|
@@ -4,11 +4,19 @@ import com.muwire.core.Event
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class UIResultEvent extends Event {
|
class UIResultEvent extends Event {
|
||||||
Persona sender
|
Persona sender
|
||||||
|
Set<Destination> sources
|
||||||
UUID uuid
|
UUID uuid
|
||||||
String name
|
String name
|
||||||
long size
|
long size
|
||||||
InfoHash infohash
|
InfoHash infohash
|
||||||
int pieceSize
|
int pieceSize
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
package com.muwire.core.trust
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
|
class RemoteTrustList {
|
||||||
|
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||||
|
|
||||||
|
private final Persona persona
|
||||||
|
private final Set<Persona> good, bad
|
||||||
|
volatile long timestamp
|
||||||
|
volatile boolean forceUpdate
|
||||||
|
Status status = Status.NEW
|
||||||
|
|
||||||
|
RemoteTrustList(Persona persona) {
|
||||||
|
this.persona = persona
|
||||||
|
good = new ConcurrentHashSet<>()
|
||||||
|
bad = new ConcurrentHashSet<>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof RemoteTrustList))
|
||||||
|
return false
|
||||||
|
RemoteTrustList other = (RemoteTrustList)o
|
||||||
|
persona == other.persona
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,161 @@
|
|||||||
|
package com.muwire.core.trust
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.UILoadedEvent
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.connection.I2PConnector
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class TrustSubscriber {
|
||||||
|
private final EventBus eventBus
|
||||||
|
private final I2PConnector i2pConnector
|
||||||
|
private final MuWireSettings settings
|
||||||
|
|
||||||
|
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
|
||||||
|
|
||||||
|
private final Object waitLock = new Object()
|
||||||
|
private volatile boolean shutdown
|
||||||
|
private volatile Thread thread
|
||||||
|
private final ExecutorService updateThreads = Executors.newCachedThreadPool()
|
||||||
|
|
||||||
|
TrustSubscriber(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.i2pConnector = i2pConnector
|
||||||
|
this.settings = settings
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUILoadedEvent(UILoadedEvent e) {
|
||||||
|
thread = new Thread({checkLoop()} as Runnable, "trust-subscriber")
|
||||||
|
thread.setDaemon(true)
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
shutdown = true
|
||||||
|
thread?.interrupt()
|
||||||
|
updateThreads.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
|
||||||
|
if (!e.subscribe) {
|
||||||
|
remoteTrustLists.remove(e.persona.destination)
|
||||||
|
} else {
|
||||||
|
RemoteTrustList trustList = remoteTrustLists.putIfAbsent(e.persona.destination, new RemoteTrustList(e.persona))
|
||||||
|
trustList?.forceUpdate = true
|
||||||
|
synchronized(waitLock) {
|
||||||
|
waitLock.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLoop() {
|
||||||
|
try {
|
||||||
|
while(!shutdown) {
|
||||||
|
synchronized(waitLock) {
|
||||||
|
waitLock.wait(60 * 1000)
|
||||||
|
}
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
remoteTrustLists.values().each { trustList ->
|
||||||
|
if (trustList.status == RemoteTrustList.Status.UPDATING)
|
||||||
|
return
|
||||||
|
if (!trustList.forceUpdate &&
|
||||||
|
now - trustList.timestamp < settings.trustListInterval * 60 * 60 * 1000)
|
||||||
|
return
|
||||||
|
trustList.forceUpdate = false
|
||||||
|
updateThreads.submit(new UpdateJob(trustList))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
if (!shutdown)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UpdateJob implements Runnable {
|
||||||
|
|
||||||
|
private final RemoteTrustList trustList
|
||||||
|
|
||||||
|
UpdateJob(RemoteTrustList trustList) {
|
||||||
|
this.trustList = trustList
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
trustList.status = RemoteTrustList.Status.UPDATING
|
||||||
|
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
|
||||||
|
if (check(trustList, System.currentTimeMillis()))
|
||||||
|
trustList.status = RemoteTrustList.Status.UPDATED
|
||||||
|
else
|
||||||
|
trustList.status = RemoteTrustList.Status.UPDATE_FAILED
|
||||||
|
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean check(RemoteTrustList trustList, long now) {
|
||||||
|
log.info("fetching trust list from ${trustList.persona.getHumanReadableName()}")
|
||||||
|
Endpoint endpoint = null
|
||||||
|
try {
|
||||||
|
endpoint = i2pConnector.connect(trustList.persona.destination)
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
InputStream is = endpoint.getInputStream()
|
||||||
|
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
|
||||||
|
String codeString = DataUtil.readTillRN(is)
|
||||||
|
int space = codeString.indexOf(' ')
|
||||||
|
if (space > 0)
|
||||||
|
codeString = codeString.substring(0,space)
|
||||||
|
int code = Integer.parseInt(codeString.trim())
|
||||||
|
|
||||||
|
if (code != 200) {
|
||||||
|
log.info("couldn't fetch trust list, code $code")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// swallow any headers
|
||||||
|
String header
|
||||||
|
while (( header = DataUtil.readTillRN(is)) != "");
|
||||||
|
|
||||||
|
DataInputStream dis = new DataInputStream(is)
|
||||||
|
|
||||||
|
Set<Persona> good = new HashSet<>()
|
||||||
|
int nGood = dis.readUnsignedShort()
|
||||||
|
for (int i = 0; i < nGood; i++) {
|
||||||
|
Persona p = new Persona(dis)
|
||||||
|
good.add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Persona> bad = new HashSet<>()
|
||||||
|
int nBad = dis.readUnsignedShort()
|
||||||
|
for (int i = 0; i < nBad; i++) {
|
||||||
|
Persona p = new Persona(dis)
|
||||||
|
bad.add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
trustList.timestamp = now
|
||||||
|
trustList.good.clear()
|
||||||
|
trustList.good.addAll(good)
|
||||||
|
trustList.bad.clear()
|
||||||
|
trustList.bad.addAll(bad)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING,"exception fetching trust list from ${trustList.persona.getHumanReadableName()}",e)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
endpoint?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
package com.muwire.core.trust
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
|
||||||
|
class TrustSubscriptionEvent extends Event {
|
||||||
|
Persona persona
|
||||||
|
boolean subscribe
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.trust
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class TrustSubscriptionUpdatedEvent extends Event {
|
||||||
|
RemoteTrustList trustList
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
|
||||||
|
class UpdateAvailableEvent extends Event {
|
||||||
|
String version
|
||||||
|
String signer
|
||||||
|
String infoHash
|
||||||
|
}
|
191
core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy
Normal file
191
core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.download.UIDownloadEvent
|
||||||
|
import com.muwire.core.files.FileDownloadedEvent
|
||||||
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.search.QueryEvent
|
||||||
|
import com.muwire.core.search.SearchEvent
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
|
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import net.i2p.client.I2PSession
|
||||||
|
import net.i2p.client.I2PSessionMuxedListener
|
||||||
|
import net.i2p.client.SendMessageOptions
|
||||||
|
import net.i2p.client.datagram.I2PDatagramDissector
|
||||||
|
import net.i2p.client.datagram.I2PDatagramMaker
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.util.VersionComparator
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class UpdateClient {
|
||||||
|
final EventBus eventBus
|
||||||
|
final I2PSession session
|
||||||
|
final String myVersion
|
||||||
|
final MuWireSettings settings
|
||||||
|
final FileManager fileManager
|
||||||
|
final Persona me
|
||||||
|
|
||||||
|
private final Timer timer
|
||||||
|
|
||||||
|
private long lastUpdateCheckTime
|
||||||
|
|
||||||
|
private volatile InfoHash updateInfoHash
|
||||||
|
private volatile String version, signer
|
||||||
|
private volatile boolean updateDownloading
|
||||||
|
|
||||||
|
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
|
||||||
|
this.eventBus = eventBus
|
||||||
|
this.session = session
|
||||||
|
this.myVersion = myVersion
|
||||||
|
this.settings = settings
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.me = me
|
||||||
|
timer = new Timer("update-client",true)
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||||
|
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
timer.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUIResultBatchEvent(UIResultBatchEvent results) {
|
||||||
|
if (results.results[0].infohash != updateInfoHash)
|
||||||
|
return
|
||||||
|
if (updateDownloading)
|
||||||
|
return
|
||||||
|
updateDownloading = true
|
||||||
|
def file = new File(settings.downloadLocation, results.results[0].name)
|
||||||
|
def downloadEvent = new UIDownloadEvent(result: results.results[0], sources : results.results[0].sources, target : file)
|
||||||
|
eventBus.publish(downloadEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||||
|
if (e.downloadedFile.infoHash != updateInfoHash)
|
||||||
|
return
|
||||||
|
updateDownloading = false
|
||||||
|
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer))
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkUpdate() {
|
||||||
|
final long now = System.currentTimeMillis()
|
||||||
|
if (lastUpdateCheckTime > 0) {
|
||||||
|
if (now - lastUpdateCheckTime < settings.updateCheckInterval * 60 * 60 * 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastUpdateCheckTime = now
|
||||||
|
|
||||||
|
log.info("checking for update")
|
||||||
|
|
||||||
|
def ping = [version : 1, myVersion : myVersion]
|
||||||
|
ping = JsonOutput.toJson(ping)
|
||||||
|
def maker = new I2PDatagramMaker(session)
|
||||||
|
ping = maker.makeI2PDatagram(ping.bytes)
|
||||||
|
def options = new SendMessageOptions()
|
||||||
|
options.setSendLeaseSet(true)
|
||||||
|
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Listener implements I2PSessionMuxedListener {
|
||||||
|
|
||||||
|
final JsonSlurper slurper = new JsonSlurper()
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||||
|
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||||
|
log.warning "Received unexpected protocol $proto"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
def payload = session.receiveMessage(msgId)
|
||||||
|
def dissector = new I2PDatagramDissector()
|
||||||
|
try {
|
||||||
|
dissector.loadI2PDatagram(payload)
|
||||||
|
def sender = dissector.getSender()
|
||||||
|
if (sender != UpdateServers.UPDATE_SERVER) {
|
||||||
|
log.warning("received something not from update server " + sender.toBase32())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Received something from update server")
|
||||||
|
|
||||||
|
payload = dissector.getPayload()
|
||||||
|
payload = slurper.parse(payload)
|
||||||
|
|
||||||
|
if (payload.version == null) {
|
||||||
|
log.warning("version missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.signer == null) {
|
||||||
|
log.warning("signer missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
|
||||||
|
log.info("no new version available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
String infoHash
|
||||||
|
if (settings.updateType == "jar") {
|
||||||
|
infoHash = payload.infoHash
|
||||||
|
} else
|
||||||
|
infoHash = payload[settings.updateType]
|
||||||
|
|
||||||
|
|
||||||
|
if (!settings.autoDownloadUpdate) {
|
||||||
|
log.info("new version $payload.version available, publishing event")
|
||||||
|
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash))
|
||||||
|
} else {
|
||||||
|
log.info("new version $payload.version available")
|
||||||
|
updateInfoHash = new InfoHash(Base64.decode(infoHash))
|
||||||
|
if (fileManager.rootToFiles.containsKey(updateInfoHash))
|
||||||
|
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer))
|
||||||
|
else {
|
||||||
|
updateDownloading = false
|
||||||
|
version = payload.version
|
||||||
|
signer = payload.signer
|
||||||
|
log.info("starting search for new version hash $payload.infoHash")
|
||||||
|
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
|
||||||
|
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
|
||||||
|
receivedOn : me.destination, originator : me)
|
||||||
|
eventBus.publish(queryEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING,"Invalid datagram",e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportAbuse(I2PSession session, int severity) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnected(I2PSession session) {
|
||||||
|
log.severe("I2P session disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||||
|
log.log(Level.SEVERE, message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UpdateDownloadedEvent extends Event {
|
||||||
|
String version
|
||||||
|
String signer
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.muwire.core.update
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
class UpdateServers {
|
||||||
|
static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==")
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class ContentRequest extends Request {
|
||||||
|
Range range
|
||||||
|
int have
|
||||||
|
}
|
@@ -0,0 +1,126 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.mesh.Mesh
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
|
class ContentUploader extends Uploader {
|
||||||
|
|
||||||
|
private final File file
|
||||||
|
private final ContentRequest request
|
||||||
|
private final Mesh mesh
|
||||||
|
private final int pieceSize
|
||||||
|
|
||||||
|
ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
|
||||||
|
super(endpoint)
|
||||||
|
this.file = file
|
||||||
|
this.request = request
|
||||||
|
this.mesh = mesh
|
||||||
|
this.pieceSize = pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
Range range = request.getRange()
|
||||||
|
boolean satisfiable = true
|
||||||
|
final long length = file.length()
|
||||||
|
if (range.start >= length || range.end >= length)
|
||||||
|
satisfiable = false
|
||||||
|
if (satisfiable) {
|
||||||
|
int startPiece = range.start / (0x1 << pieceSize)
|
||||||
|
int endPiece = range.end / (0x1 << pieceSize)
|
||||||
|
for (int i = startPiece; i <= endPiece; i++)
|
||||||
|
satisfiable &= mesh.pieces.isDownloaded(i)
|
||||||
|
}
|
||||||
|
if (!satisfiable) {
|
||||||
|
os.write("416 Range Not Satisfiable\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
writeMesh(request.downloader)
|
||||||
|
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("Content-Range: $range.start-$range.end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
writeMesh(request.downloader)
|
||||||
|
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
FileChannel channel = null
|
||||||
|
try {
|
||||||
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
||||||
|
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||||
|
byte [] tmp = new byte[0x1 << 13]
|
||||||
|
while(mapped.hasRemaining()) {
|
||||||
|
int start = mapped.position()
|
||||||
|
synchronized(this) {
|
||||||
|
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||||
|
}
|
||||||
|
int read = mapped.position() - start
|
||||||
|
endpoint.getOutputStream().write(tmp, 0, read)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {channel?.close() } catch (IOException ignored) {}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
|
synchronized(this) {
|
||||||
|
DataUtil.tryUnmap(mapped)
|
||||||
|
mapped = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeMesh(Persona toExclude) {
|
||||||
|
String xHave = DataUtil.encodeXHave(mesh.pieces.getDownloaded(), mesh.pieces.nPieces)
|
||||||
|
endpoint.getOutputStream().write("X-Have: $xHave\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
Set<Persona> sources = mesh.getRandom(9, toExclude)
|
||||||
|
if (!sources.isEmpty()) {
|
||||||
|
String xAlts = sources.stream().map({ it.toBase64() }).collect(Collectors.joining(","))
|
||||||
|
endpoint.getOutputStream().write("X-Alt: $xAlts\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getProgress() {
|
||||||
|
if (mapped == null)
|
||||||
|
return 0
|
||||||
|
int position = mapped.position()
|
||||||
|
int total = request.getRange().end - request.getRange().start
|
||||||
|
(int)(position * 100.0 / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloader() {
|
||||||
|
request.downloader.getHumanReadableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDonePieces() {
|
||||||
|
return request.have;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTotalPieces() {
|
||||||
|
return mesh.pieces.nPieces;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTotalSize() {
|
||||||
|
return file.length();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
class HashListRequest extends Request {
|
||||||
|
}
|
@@ -0,0 +1,68 @@
|
|||||||
|
package com.muwire.core.upload
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class HashListUploader extends Uploader {
|
||||||
|
private final InfoHash infoHash
|
||||||
|
private final HashListRequest request
|
||||||
|
|
||||||
|
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
|
||||||
|
super(endpoint)
|
||||||
|
this.infoHash = infoHash
|
||||||
|
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
||||||
|
this.request = request
|
||||||
|
}
|
||||||
|
|
||||||
|
void respond() {
|
||||||
|
OutputStream os = endpoint.getOutputStream()
|
||||||
|
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
|
||||||
|
byte[]tmp = new byte[0x1 << 13]
|
||||||
|
while(mapped.hasRemaining()) {
|
||||||
|
int start = mapped.position()
|
||||||
|
synchronized(this) {
|
||||||
|
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||||
|
}
|
||||||
|
int read = mapped.position() - start
|
||||||
|
endpoint.getOutputStream().write(tmp, 0, read)
|
||||||
|
}
|
||||||
|
endpoint.getOutputStream().flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Hash list for " + Base64.encode(infoHash.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getProgress() {
|
||||||
|
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloader() {
|
||||||
|
request.downloader.getHumanReadableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDonePieces() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTotalPieces() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTotalSize() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
|
|||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.Persona
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
@@ -16,11 +17,60 @@ class Request {
|
|||||||
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
||||||
|
|
||||||
InfoHash infoHash
|
InfoHash infoHash
|
||||||
Range range
|
|
||||||
Persona downloader
|
Persona downloader
|
||||||
Map<String, String> headers
|
Map<String, String> headers
|
||||||
|
|
||||||
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
|
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||||
|
|
||||||
|
Map<String, String> headers = parseHeaders(is)
|
||||||
|
|
||||||
|
if (!headers.containsKey("Range"))
|
||||||
|
throw new IOException("Range header not found")
|
||||||
|
|
||||||
|
String range = headers.get("Range").trim()
|
||||||
|
String[] split = range.split("-")
|
||||||
|
if (split.length != 2)
|
||||||
|
throw new IOException("Invalid range header $range")
|
||||||
|
long start
|
||||||
|
long end
|
||||||
|
try {
|
||||||
|
start = Long.parseLong(split[0])
|
||||||
|
end = Long.parseLong(split[1])
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
throw new IOException(nfe)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start < 0 || end < start)
|
||||||
|
throw new IOException("Invalid range $start - $end")
|
||||||
|
|
||||||
|
Persona downloader = null
|
||||||
|
if (headers.containsKey("X-Persona")) {
|
||||||
|
def encoded = headers["X-Persona"].trim()
|
||||||
|
def decoded = Base64.decode(encoded)
|
||||||
|
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
int have = 0
|
||||||
|
if (headers.containsKey("X-Have")) {
|
||||||
|
def encoded = headers["X-Have"].trim()
|
||||||
|
have = DataUtil.decodeXHave(encoded).size()
|
||||||
|
}
|
||||||
|
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||||
|
headers : headers, downloader : downloader, have : have)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||||
|
Map<String,String> headers = parseHeaders(is)
|
||||||
|
Persona downloader = null
|
||||||
|
if (headers.containsKey("X-Persona")) {
|
||||||
|
def encoded = headers["X-Persona"].trim()
|
||||||
|
def decoded = Base64.decode(encoded)
|
||||||
|
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||||
|
}
|
||||||
|
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseHeaders(InputStream is) {
|
||||||
Map<String,String> headers = new HashMap<>()
|
Map<String,String> headers = new HashMap<>()
|
||||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||||
while(headers.size() < Constants.MAX_HEADERS) {
|
while(headers.size() < Constants.MAX_HEADERS) {
|
||||||
@@ -67,33 +117,7 @@ class Request {
|
|||||||
String value = header.substring(keyIdx + 1)
|
String value = header.substring(keyIdx + 1)
|
||||||
headers.put(key, value)
|
headers.put(key, value)
|
||||||
}
|
}
|
||||||
|
headers
|
||||||
if (!headers.containsKey("Range"))
|
|
||||||
throw new IOException("Range header not found")
|
|
||||||
|
|
||||||
String range = headers.get("Range").trim()
|
|
||||||
String[] split = range.split("-")
|
|
||||||
if (split.length != 2)
|
|
||||||
throw new IOException("Invalid range header $range")
|
|
||||||
long start
|
|
||||||
long end
|
|
||||||
try {
|
|
||||||
start = Long.parseLong(split[0])
|
|
||||||
end = Long.parseLong(split[1])
|
|
||||||
} catch (NumberFormatException nfe) {
|
|
||||||
throw new IOException(nfe)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start < 0 || end < start)
|
|
||||||
throw new IOException("Invalid range $start - $end")
|
|
||||||
|
|
||||||
Persona downloader = null
|
|
||||||
if (headers.containsKey("X-Persona")) {
|
|
||||||
def encoded = headers["X-Persona"].trim()
|
|
||||||
def decoded = Base64.decode(encoded)
|
|
||||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
|
||||||
}
|
|
||||||
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,12 @@ import com.muwire.core.EventBus
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
import com.muwire.core.download.DownloadManager
|
||||||
|
import com.muwire.core.download.Downloader
|
||||||
|
import com.muwire.core.download.SourceDiscoveredEvent
|
||||||
import com.muwire.core.files.FileManager
|
import com.muwire.core.files.FileManager
|
||||||
|
import com.muwire.core.mesh.Mesh
|
||||||
|
import com.muwire.core.mesh.MeshManager
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
@@ -15,15 +20,20 @@ import net.i2p.data.Base64
|
|||||||
public class UploadManager {
|
public class UploadManager {
|
||||||
private final EventBus eventBus
|
private final EventBus eventBus
|
||||||
private final FileManager fileManager
|
private final FileManager fileManager
|
||||||
|
private final MeshManager meshManager
|
||||||
|
private final DownloadManager downloadManager
|
||||||
|
|
||||||
public UploadManager() {}
|
public UploadManager() {}
|
||||||
|
|
||||||
public UploadManager(EventBus eventBus, FileManager fileManager) {
|
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||||
|
MeshManager meshManager, DownloadManager downloadManager) {
|
||||||
this.eventBus = eventBus
|
this.eventBus = eventBus
|
||||||
this.fileManager = fileManager
|
this.fileManager = fileManager
|
||||||
|
this.meshManager = meshManager
|
||||||
|
this.downloadManager = downloadManager
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processEndpoint(Endpoint e) throws IOException {
|
public void processGET(Endpoint e) throws IOException {
|
||||||
byte [] infoHashStringBytes = new byte[44]
|
byte [] infoHashStringBytes = new byte[44]
|
||||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
boolean first = true
|
boolean first = true
|
||||||
@@ -44,8 +54,10 @@ public class UploadManager {
|
|||||||
log.info("Responding to upload request for root $infoHashString")
|
log.info("Responding to upload request for root $infoHashString")
|
||||||
|
|
||||||
byte [] infoHashRoot = Base64.decode(infoHashString)
|
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
InfoHash infoHash = new InfoHash(infoHashRoot)
|
||||||
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
Downloader downloader = downloadManager.downloaders.get(infoHash)
|
||||||
|
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||||
log.info "file not found"
|
log.info "file not found"
|
||||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
e.getOutputStream().flush()
|
e.getOutputStream().flush()
|
||||||
@@ -61,8 +73,31 @@ public class UploadManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
|
ContentRequest request = Request.parseContentRequest(infoHash, e.getInputStream())
|
||||||
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.have > 0)
|
||||||
|
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||||
|
|
||||||
|
Mesh mesh
|
||||||
|
File file
|
||||||
|
int pieceSize
|
||||||
|
if (downloader != null) {
|
||||||
|
mesh = meshManager.get(infoHash)
|
||||||
|
file = downloader.incompleteFile
|
||||||
|
pieceSize = downloader.pieceSizePow2
|
||||||
|
} else {
|
||||||
|
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||||
|
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||||
|
file = sharedFile.file
|
||||||
|
pieceSize = sharedFile.pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
Uploader uploader = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
try {
|
try {
|
||||||
uploader.respond()
|
uploader.respond()
|
||||||
@@ -70,7 +105,131 @@ public class UploadManager {
|
|||||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processHashList(Endpoint e) {
|
||||||
|
byte [] infoHashStringBytes = new byte[44]
|
||||||
|
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||||
|
dis.readFully(infoHashStringBytes)
|
||||||
|
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||||
|
log.info("Responding to hashlist request for root $infoHashString")
|
||||||
|
|
||||||
|
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
InfoHash infoHash = new InfoHash(infoHashRoot)
|
||||||
|
Downloader downloader = downloadManager.downloaders.get(infoHash)
|
||||||
|
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||||
|
log.info "file not found"
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
byte [] rn = new byte[2]
|
||||||
|
dis.readFully(rn)
|
||||||
|
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("Malformed HASHLIST header")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = Request.parseHashListRequest(infoHash, e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoHash fullInfoHash
|
||||||
|
if (downloader == null) {
|
||||||
|
fullInfoHash = sharedFiles.iterator().next().infoHash
|
||||||
|
} else {
|
||||||
|
byte [] hashList = downloader.getInfoHash().getHashList()
|
||||||
|
if (hashList != null && hashList.length > 0)
|
||||||
|
fullInfoHash = downloader.getInfoHash()
|
||||||
|
else {
|
||||||
|
log.info("infohash not found in downloader")
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
|
||||||
|
// proceed with content
|
||||||
|
while(true) {
|
||||||
|
byte[] get = new byte[4]
|
||||||
|
dis.readFully(get)
|
||||||
|
if (get != "GET ".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("received a method other than GET on subsequent call")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dis.readFully(infoHashStringBytes)
|
||||||
|
infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||||
|
log.info("Responding to upload request for root $infoHashString")
|
||||||
|
|
||||||
|
infoHashRoot = Base64.decode(infoHashString)
|
||||||
|
infoHash = new InfoHash(infoHashRoot)
|
||||||
|
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||||
|
downloader = downloadManager.downloaders.get(infoHash)
|
||||||
|
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
|
||||||
|
log.info "file not found"
|
||||||
|
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
|
e.getOutputStream().flush()
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rn = new byte[2]
|
||||||
|
dis.readFully(rn)
|
||||||
|
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||||
|
log.warning("Malformed GET header")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||||
|
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||||
|
log.info("Downloader persona doesn't match their destination")
|
||||||
|
e.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.have > 0)
|
||||||
|
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
|
||||||
|
|
||||||
|
Mesh mesh
|
||||||
|
File file
|
||||||
|
int pieceSize
|
||||||
|
if (downloader != null) {
|
||||||
|
mesh = meshManager.get(infoHash)
|
||||||
|
file = downloader.incompleteFile
|
||||||
|
pieceSize = downloader.pieceSizePow2
|
||||||
|
} else {
|
||||||
|
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||||
|
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||||
|
file = sharedFile.file
|
||||||
|
pieceSize = sharedFile.pieceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
uploader = new ContentUploader(file, request, e, mesh, pieceSize)
|
||||||
|
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||||
|
try {
|
||||||
|
uploader.respond()
|
||||||
|
} finally {
|
||||||
|
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,52 +8,34 @@ import java.nio.file.StandardOpenOption
|
|||||||
|
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
class Uploader {
|
abstract class Uploader {
|
||||||
private final File file
|
protected final Endpoint endpoint
|
||||||
private final Request request
|
protected ByteBuffer mapped
|
||||||
private final Endpoint endpoint
|
|
||||||
private ByteBuffer mapped
|
|
||||||
|
|
||||||
Uploader(File file, Request request, Endpoint endpoint) {
|
Uploader(Endpoint endpoint) {
|
||||||
this.file = file
|
|
||||||
this.request = request
|
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
void respond() {
|
abstract void respond()
|
||||||
OutputStream os = endpoint.getOutputStream()
|
|
||||||
Range range = request.getRange()
|
|
||||||
if (range.start >= file.length() || range.end >= file.length()) {
|
|
||||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
os.flush()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
|
||||||
|
|
||||||
FileChannel channel
|
|
||||||
try {
|
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
|
||||||
byte [] tmp = new byte[0x1 << 13]
|
|
||||||
while(mapped.hasRemaining()) {
|
|
||||||
int start = mapped.position()
|
|
||||||
synchronized(this) {
|
|
||||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
|
||||||
}
|
|
||||||
int read = mapped.position() - start
|
|
||||||
endpoint.getOutputStream().write(tmp, 0, read)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {channel?.close() } catch (IOException ignored) {}
|
|
||||||
endpoint.getOutputStream().flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized int getPosition() {
|
public synchronized int getPosition() {
|
||||||
if (mapped == null)
|
if (mapped == null)
|
||||||
return -1
|
return -1
|
||||||
mapped.position()
|
mapped.position()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an integer between 0 and 100
|
||||||
|
*/
|
||||||
|
abstract int getProgress();
|
||||||
|
|
||||||
|
abstract String getDownloader();
|
||||||
|
|
||||||
|
abstract int getDonePieces();
|
||||||
|
|
||||||
|
abstract int getTotalPieces();
|
||||||
|
|
||||||
|
abstract long getTotalSize();
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
package com.muwire.core.util
|
package com.muwire.core.util
|
||||||
|
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class DataUtil {
|
class DataUtil {
|
||||||
|
|
||||||
private final static int MAX_SHORT = (0x1 << 16) - 1
|
private final static int MAX_SHORT = (0x1 << 16) - 1
|
||||||
@@ -79,4 +84,73 @@ class DataUtil {
|
|||||||
}
|
}
|
||||||
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||||
|
int bytes = totalPieces / 8
|
||||||
|
if (totalPieces % 8 != 0)
|
||||||
|
bytes++
|
||||||
|
byte[] raw = new byte[bytes]
|
||||||
|
pieces.each {
|
||||||
|
int byteIdx = it / 8
|
||||||
|
int offset = it % 8
|
||||||
|
int mask = 0x80 >>> offset
|
||||||
|
raw[byteIdx] |= mask
|
||||||
|
}
|
||||||
|
Base64.encode(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Integer> decodeXHave(String xHave) {
|
||||||
|
byte [] availablePieces = Base64.decode(xHave)
|
||||||
|
List<Integer> available = new ArrayList<>()
|
||||||
|
availablePieces.eachWithIndex {b, i ->
|
||||||
|
for (int j = 0; j < 8 ; j++) {
|
||||||
|
byte mask = 0x80 >>> j
|
||||||
|
if ((b & mask) == mask) {
|
||||||
|
available.add(i * 8 + j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
available
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Exception findRoot(Exception e) {
|
||||||
|
while(e.getCause() != null)
|
||||||
|
e = e.getCause()
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void tryUnmap(ByteBuffer cb) {
|
||||||
|
if (cb==null || !cb.isDirect()) return;
|
||||||
|
// we could use this type cast and call functions without reflection code,
|
||||||
|
// but static import from sun.* package is risky for non-SUN virtual machine.
|
||||||
|
//try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }
|
||||||
|
|
||||||
|
// JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
|
||||||
|
boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");
|
||||||
|
try {
|
||||||
|
if (isOldJDK) {
|
||||||
|
Method cleaner = cb.getClass().getMethod("cleaner");
|
||||||
|
cleaner.setAccessible(true);
|
||||||
|
Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
|
||||||
|
clean.setAccessible(true);
|
||||||
|
clean.invoke(cleaner.invoke(cb));
|
||||||
|
} else {
|
||||||
|
Class unsafeClass;
|
||||||
|
try {
|
||||||
|
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||||
|
} catch(Exception ex) {
|
||||||
|
// jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
|
||||||
|
// but that method should be added if sun.misc.Unsafe is removed.
|
||||||
|
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
|
||||||
|
}
|
||||||
|
Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
|
||||||
|
clean.setAccessible(true);
|
||||||
|
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
|
||||||
|
theUnsafeField.setAccessible(true);
|
||||||
|
Object theUnsafe = theUnsafeField.get(null);
|
||||||
|
clean.invoke(theUnsafe, cb);
|
||||||
|
}
|
||||||
|
} catch(Exception ex) { }
|
||||||
|
cb = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -55,21 +55,21 @@ class JULLog extends Log {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldDebug() {
|
public boolean shouldDebug() {
|
||||||
level.intValue().intValue() >= Level.FINE.intValue()
|
level.intValue().intValue() <= Level.FINE.intValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldInfo() {
|
public boolean shouldInfo() {
|
||||||
level.intValue().intValue() >= Level.INFO.intValue()
|
level.intValue().intValue() <= Level.INFO.intValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldWarn() {
|
public boolean shouldWarn() {
|
||||||
level.intValue().intValue() >= Level.WARNING.intValue()
|
level.intValue().intValue() <= Level.WARNING.intValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldError() {
|
public boolean shouldError() {
|
||||||
level.intValue().intValue() >= Level.SEVERE.intValue()
|
level.intValue().intValue() <= Level.SEVERE.intValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core;
|
package com.muwire.core;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import net.i2p.data.Destination;
|
import net.i2p.data.Destination;
|
||||||
@@ -9,8 +10,9 @@ public class DownloadedFile extends SharedFile {
|
|||||||
|
|
||||||
private final Set<Destination> sources;
|
private final Set<Destination> sources;
|
||||||
|
|
||||||
public DownloadedFile(File file, InfoHash infoHash, Set<Destination> sources) {
|
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
|
||||||
super(file, infoHash);
|
throws IOException {
|
||||||
|
super(file, infoHash, pieceSize);
|
||||||
this.sources = sources;
|
this.sources = sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import net.i2p.data.Base32;
|
import net.i2p.data.Base32;
|
||||||
|
import net.i2p.data.Base64;
|
||||||
|
|
||||||
public class InfoHash {
|
public class InfoHash {
|
||||||
|
|
||||||
@@ -76,14 +77,16 @@ public class InfoHash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
String rv = "InfoHash[root:"+Base32.encode(root) + " hashList:";
|
String rv = "InfoHash[root:"+Base64.encode(root) + " hashList:";
|
||||||
List<String> b32HashList = new ArrayList<>(hashList.length / SIZE);
|
List<String> b64HashList = new ArrayList<>();
|
||||||
|
if (hashList != null) {
|
||||||
byte [] tmp = new byte[SIZE];
|
byte [] tmp = new byte[SIZE];
|
||||||
for (int i = 0; i < hashList.length / SIZE; i++) {
|
for (int i = 0; i < hashList.length / SIZE; i++) {
|
||||||
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
|
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
|
||||||
b32HashList.add(Base32.encode(tmp));
|
b64HashList.add(Base64.encode(tmp));
|
||||||
}
|
}
|
||||||
rv += b32HashList.toString();
|
}
|
||||||
|
rv += b64HashList.toString();
|
||||||
rv += "]";
|
rv += "]";
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,23 @@
|
|||||||
package com.muwire.core;
|
package com.muwire.core;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class SharedFile {
|
public class SharedFile {
|
||||||
|
|
||||||
private final File file;
|
private final File file;
|
||||||
private final InfoHash infoHash;
|
private final InfoHash infoHash;
|
||||||
|
private final int pieceSize;
|
||||||
|
|
||||||
public SharedFile(File file, InfoHash infoHash) {
|
private final String cachedPath;
|
||||||
|
private final long cachedLength;
|
||||||
|
|
||||||
|
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.infoHash = infoHash;
|
this.infoHash = infoHash;
|
||||||
|
this.pieceSize = pieceSize;
|
||||||
|
this.cachedPath = file.getAbsolutePath();
|
||||||
|
this.cachedLength = file.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
public File getFile() {
|
||||||
@@ -20,4 +28,37 @@ public class SharedFile {
|
|||||||
return infoHash;
|
return infoHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getPieceSize() {
|
||||||
|
return pieceSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNPieces() {
|
||||||
|
long length = file.length();
|
||||||
|
int rawPieceSize = 0x1 << pieceSize;
|
||||||
|
int rv = (int) (length / rawPieceSize);
|
||||||
|
if (length % rawPieceSize != 0)
|
||||||
|
rv++;
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCachedPath() {
|
||||||
|
return cachedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCachedLength() {
|
||||||
|
return cachedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return file.hashCode() ^ infoHash.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof SharedFile))
|
||||||
|
return false;
|
||||||
|
SharedFile other = (SharedFile)o;
|
||||||
|
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
11
core/src/test/groovy/com/muwire/core/Personas.groovy
Normal file
11
core/src/test/groovy/com/muwire/core/Personas.groovy
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.muwire.core
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
|
class Personas {
|
||||||
|
private final String encoded1 = "AQADemFiO~pgSoEo8wQfwncYMvBQWkvPY9I7DYUllHp289UE~zBaLdbl~wbliktAUsW-S70f3UeYgHq34~c7zVuUQjgHZ506iG9hX8B9S3a9gQ3CSG0GuDpeNyiXmZkpHp5m8vT9PZ1zMWzxvzZY~fP9yKFKgO4yrso5I9~DGOPeyJZJ4BFsTJDERv41aZqjFLYUBDmeHGgg9RjYy~93h-nQMVYj9JSO3AgowW-ix49rtiKYIXHMa2PxWHUXkUHWJZtIZntNIDEFeMnPdzLxjAl8so2G6pDcTMZPLLwyb73Ee5ZVfxUynPqyp~fIGVP8Rl4rlaGFli2~ATGBz3XY54aObC~0p7us2JnWaTC~oQT5DVDM7gaOO885o-m8BB8b0duzMBelbdnMZFQJ5jIHVKxkC6Niw4fxTOoXTyOqQmVhtK-9xcwxMuN5DF9IewkR5bhpq5rgnfBP5zvyBaAHMq-d3TCOjTsZ-d3liB98xX5p8G5zmS7gfKArQtM5~CcK~AlX-lGLBQAEAAcAAN5MW1Tq983szfZgY1l8tQFqy8I9tdMf7vc1Ktj~TCIvXYw6AYMbMGy3S67FSPLZVmfHEMQKj2KLAdaRKQkHPAY"
|
||||||
|
private final String encoded2 = "AQAHemxhdGluYiN~3G-hPoBfJ04mhcC52lC6TYSwWxH-WNWno9Y35JS-WrXlnPsodZtwy96ttEaiKTg-hkRqMsaYKpWar1FwayR6qlo0pZCo5pQOLfR7GIM3~wde0JIBEp8BUpgzF1-QXLhuRG1t7tBbenW2tSgp5jQH61RI-c9flyUlOvf6nrhQMZ3aoviZ4aZW23Fx-ajYQBDk7PIxuyn8qYNwWy3kWOhGan05c54NnumS3XCzQWFDDPlADmco1WROeY9qrwwtmLM8lzDCEtJQXJlk~K5yLbyB63hmAeTK7J4iS6f9nnWv7TbB5r-Z3kC6D9TLYrQbu3h4AAxrqso45P8yHQtKUA4QJicS-6NJoBOnlCCU887wx2k9YSxxwNydlIxb1mZsX65Ke4uY0HDFokZHTzUcxvfLB6G~5JkSPDCyZz~2fREgW2-VXu7gokEdEugkuZRrsiQzyfAOOkv53ti5MzTbMOXinBskSb1vZyN2-XcZNaDJvEqUNj~qpfhe-ov2F7FuwQUABAAHAAAfqq-MneIqWBQY92-sy9Z0s~iQsq6lUFa~sYMdY-5o-94fF8a140dm-emF3rO8vuidUIPNaS-37Rl05mAKUCcB"
|
||||||
|
|
||||||
|
Persona persona1 = new Persona(new ByteArrayInputStream(Base64.decode(encoded1)))
|
||||||
|
Persona persona2 = new Persona(new ByteArrayInputStream(Base64.decode(encoded2)))
|
||||||
|
}
|
@@ -44,6 +44,9 @@ class ConnectionAcceptorTest {
|
|||||||
def uploadManagerMock
|
def uploadManagerMock
|
||||||
UploadManager uploadManager
|
UploadManager uploadManager
|
||||||
|
|
||||||
|
def connectionEstablisherMock
|
||||||
|
ConnectionEstablisher connectionEstablisher
|
||||||
|
|
||||||
ConnectionAcceptor acceptor
|
ConnectionAcceptor acceptor
|
||||||
List<ConnectionEvent> connectionEvents
|
List<ConnectionEvent> connectionEvents
|
||||||
InputStream inputStream
|
InputStream inputStream
|
||||||
@@ -57,6 +60,7 @@ class ConnectionAcceptorTest {
|
|||||||
trustServiceMock = new MockFor(TrustService.class)
|
trustServiceMock = new MockFor(TrustService.class)
|
||||||
searchManagerMock = new MockFor(SearchManager.class)
|
searchManagerMock = new MockFor(SearchManager.class)
|
||||||
uploadManagerMock = new MockFor(UploadManager.class)
|
uploadManagerMock = new MockFor(UploadManager.class)
|
||||||
|
connectionEstablisherMock = new MockFor(ConnectionEstablisher.class)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -68,6 +72,7 @@ class ConnectionAcceptorTest {
|
|||||||
trustServiceMock.verify trustService
|
trustServiceMock.verify trustService
|
||||||
searchManagerMock.verify searchManager
|
searchManagerMock.verify searchManager
|
||||||
uploadManagerMock.verify uploadManager
|
uploadManagerMock.verify uploadManager
|
||||||
|
connectionEstablisherMock.verify connectionEstablisher
|
||||||
Thread.sleep(100)
|
Thread.sleep(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +92,10 @@ class ConnectionAcceptorTest {
|
|||||||
trustService = trustServiceMock.proxyInstance()
|
trustService = trustServiceMock.proxyInstance()
|
||||||
searchManager = searchManagerMock.proxyInstance()
|
searchManager = searchManagerMock.proxyInstance()
|
||||||
uploadManager = uploadManagerMock.proxyInstance()
|
uploadManager = uploadManagerMock.proxyInstance()
|
||||||
|
connectionEstablisher = connectionEstablisherMock.proxyInstance()
|
||||||
|
|
||||||
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor, hostCache, trustService, searchManager, uploadManager)
|
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
|
||||||
|
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||||
acceptor.start()
|
acceptor.start()
|
||||||
Thread.sleep(100)
|
Thread.sleep(100)
|
||||||
}
|
}
|
||||||
@@ -108,6 +115,7 @@ class ConnectionAcceptorTest {
|
|||||||
new Endpoint(destinations.dest1, is, os, null)
|
new Endpoint(destinations.dest1, is, os, null)
|
||||||
}
|
}
|
||||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||||
|
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||||
connectionManagerMock.demand.isConnected { dest ->
|
connectionManagerMock.demand.isConnected { dest ->
|
||||||
assert dest == destinations.dest1
|
assert dest == destinations.dest1
|
||||||
false
|
false
|
||||||
@@ -150,6 +158,7 @@ class ConnectionAcceptorTest {
|
|||||||
new Endpoint(destinations.dest1, is, os, null)
|
new Endpoint(destinations.dest1, is, os, null)
|
||||||
}
|
}
|
||||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||||
|
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||||
connectionManagerMock.demand.isConnected { dest ->
|
connectionManagerMock.demand.isConnected { dest ->
|
||||||
assert dest == destinations.dest1
|
assert dest == destinations.dest1
|
||||||
false
|
false
|
||||||
@@ -264,6 +273,7 @@ class ConnectionAcceptorTest {
|
|||||||
new Endpoint(destinations.dest1, is, os, null)
|
new Endpoint(destinations.dest1, is, os, null)
|
||||||
}
|
}
|
||||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||||
|
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||||
connectionManagerMock.demand.isConnected { dest ->
|
connectionManagerMock.demand.isConnected { dest ->
|
||||||
assert dest == destinations.dest1
|
assert dest == destinations.dest1
|
||||||
false
|
false
|
||||||
@@ -310,6 +320,7 @@ class ConnectionAcceptorTest {
|
|||||||
new Endpoint(destinations.dest1, is, os, null)
|
new Endpoint(destinations.dest1, is, os, null)
|
||||||
}
|
}
|
||||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||||
|
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||||
connectionManagerMock.demand.isConnected { dest ->
|
connectionManagerMock.demand.isConnected { dest ->
|
||||||
assert dest == destinations.dest1
|
assert dest == destinations.dest1
|
||||||
false
|
false
|
||||||
@@ -356,6 +367,7 @@ class ConnectionAcceptorTest {
|
|||||||
new Endpoint(destinations.dest1, is, os, null)
|
new Endpoint(destinations.dest1, is, os, null)
|
||||||
}
|
}
|
||||||
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
|
||||||
|
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
|
||||||
connectionManagerMock.demand.isConnected { dest ->
|
connectionManagerMock.demand.isConnected { dest ->
|
||||||
assert dest == destinations.dest1
|
assert dest == destinations.dest1
|
||||||
false
|
false
|
||||||
|
@@ -1,17 +1,26 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
|
import static org.junit.Assert.fail
|
||||||
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.Personas
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
import com.muwire.core.files.FileHasher
|
import com.muwire.core.files.FileHasher
|
||||||
import static com.muwire.core.util.DataUtil.readTillRN
|
import static com.muwire.core.util.DataUtil.readTillRN
|
||||||
|
import static com.muwire.core.util.DataUtil.encodeXHave
|
||||||
|
|
||||||
import net.i2p.data.Base64
|
import net.i2p.data.Base64
|
||||||
|
import net.i2p.util.ConcurrentHashSet
|
||||||
|
|
||||||
class DownloadSessionTest {
|
class DownloadSessionTest {
|
||||||
|
|
||||||
|
private EventBus eventBus
|
||||||
private File source, target
|
private File source, target
|
||||||
private InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private Endpoint endpoint
|
private Endpoint endpoint
|
||||||
@@ -24,7 +33,17 @@ class DownloadSessionTest {
|
|||||||
private InputStream fromDownloader, fromUploader
|
private InputStream fromDownloader, fromUploader
|
||||||
private OutputStream toDownloader, toUploader
|
private OutputStream toDownloader, toUploader
|
||||||
|
|
||||||
private void initSession(int size) {
|
private volatile boolean performed
|
||||||
|
private Set<Integer> available = new ConcurrentHashSet<>()
|
||||||
|
private volatile IOException thrown
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
eventBus = new EventBus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSession(int size, def claimedPieces = []) {
|
||||||
Random r = new Random()
|
Random r = new Random()
|
||||||
byte [] content = new byte[size]
|
byte [] content = new byte[size]
|
||||||
r.nextBytes(content)
|
r.nextBytes(content)
|
||||||
@@ -48,6 +67,7 @@ class DownloadSessionTest {
|
|||||||
else
|
else
|
||||||
nPieces = size / pieceSize + 1
|
nPieces = size / pieceSize + 1
|
||||||
pieces = new Pieces(nPieces)
|
pieces = new Pieces(nPieces)
|
||||||
|
claimedPieces.each {pieces.claimed.set(it)}
|
||||||
|
|
||||||
fromDownloader = new PipedInputStream()
|
fromDownloader = new PipedInputStream()
|
||||||
fromUploader = new PipedInputStream()
|
fromUploader = new PipedInputStream()
|
||||||
@@ -55,12 +75,20 @@ class DownloadSessionTest {
|
|||||||
toUploader = new PipedOutputStream(fromDownloader)
|
toUploader = new PipedOutputStream(fromDownloader)
|
||||||
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
||||||
|
|
||||||
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
|
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
|
||||||
downloadThread = new Thread( { session.request() } as Runnable)
|
downloadThread = new Thread( { perform() } as Runnable)
|
||||||
downloadThread.setDaemon(true)
|
downloadThread.setDaemon(true)
|
||||||
downloadThread.start()
|
downloadThread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void perform() {
|
||||||
|
try {
|
||||||
|
performed = session.request()
|
||||||
|
} catch (IOException e) {
|
||||||
|
thrown = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void teardown() {
|
public void teardown() {
|
||||||
source?.delete()
|
source?.delete()
|
||||||
@@ -75,6 +103,7 @@ class DownloadSessionTest {
|
|||||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
assert "Range: 0-19" == readTillRN(fromDownloader)
|
assert "Range: 0-19" == readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
|
readTillRN(fromDownloader)
|
||||||
assert "" == readTillRN(fromDownloader)
|
assert "" == readTillRN(fromDownloader)
|
||||||
|
|
||||||
toDownloader.write("200 OK\r\n".bytes)
|
toDownloader.write("200 OK\r\n".bytes)
|
||||||
@@ -86,6 +115,9 @@ class DownloadSessionTest {
|
|||||||
|
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
assert target.bytes == source.bytes
|
assert target.bytes == source.bytes
|
||||||
|
assert performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -97,6 +129,7 @@ class DownloadSessionTest {
|
|||||||
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
|
readTillRN(fromDownloader)
|
||||||
assert "" == readTillRN(fromDownloader)
|
assert "" == readTillRN(fromDownloader)
|
||||||
|
|
||||||
toDownloader.write("200 OK\r\n".bytes)
|
toDownloader.write("200 OK\r\n".bytes)
|
||||||
@@ -107,6 +140,9 @@ class DownloadSessionTest {
|
|||||||
Thread.sleep(150)
|
Thread.sleep(150)
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
assert target.bytes == source.bytes
|
assert target.bytes == source.bytes
|
||||||
|
assert performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -124,6 +160,7 @@ class DownloadSessionTest {
|
|||||||
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
|
||||||
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
(start == (1 << pieceSize) && end == (1 << pieceSize))
|
||||||
|
|
||||||
|
readTillRN(fromDownloader)
|
||||||
readTillRN(fromDownloader)
|
readTillRN(fromDownloader)
|
||||||
assert "" == readTillRN(fromDownloader)
|
assert "" == readTillRN(fromDownloader)
|
||||||
|
|
||||||
@@ -137,5 +174,160 @@ class DownloadSessionTest {
|
|||||||
Thread.sleep(150)
|
Thread.sleep(150)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
assert 1 == pieces.donePieces()
|
assert 1 == pieces.donePieces()
|
||||||
|
assert performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSmallFileClaimed() {
|
||||||
|
initSession(20, [0])
|
||||||
|
long now = System.currentTimeMillis()
|
||||||
|
downloadThread.join(100)
|
||||||
|
assert 100 >= (System.currentTimeMillis() - now)
|
||||||
|
assert !performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown == null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClaimedPiecesAvoided() {
|
||||||
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
|
int size = (1 << pieceSize) * 10
|
||||||
|
initSession(size, [1,2,3,4,5,6,7,8,9])
|
||||||
|
assert !pieces.claimed.get(0)
|
||||||
|
|
||||||
|
assert "GET $rootBase64" == readTillRN(fromDownloader)
|
||||||
|
String range = readTillRN(fromDownloader)
|
||||||
|
def matcher = (range =~ /^Range: (\d+)-(\d+)$/)
|
||||||
|
int start = Integer.parseInt(matcher[0][1])
|
||||||
|
int end = Integer.parseInt(matcher[0][2])
|
||||||
|
|
||||||
|
assert pieces.claimed.get(0)
|
||||||
|
assert start == 0 && end == (1 << pieceSize) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test416NoHave() {
|
||||||
|
initSession(20)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert !performed
|
||||||
|
assert available.isEmpty()
|
||||||
|
assert thrown != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test416Have() {
|
||||||
|
initSession(20)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it\r\n".bytes)
|
||||||
|
toDownloader.write("X-Have: ${encodeXHave([0], 1)}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert performed
|
||||||
|
assert available.contains(0)
|
||||||
|
assert thrown == null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test416Have2Pieces() {
|
||||||
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
|
int size = (1 << pieceSize) + 1
|
||||||
|
initSession(size)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it\r\n".bytes)
|
||||||
|
toDownloader.write("X-Have: ${encodeXHave([1], 2)}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert performed
|
||||||
|
assert available.contains(1)
|
||||||
|
assert thrown == null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test200TwoPieces1Available() {
|
||||||
|
int pieceSize = FileHasher.getPieceSize(1)
|
||||||
|
int size = (1 << pieceSize) * 9 + 1
|
||||||
|
initSession(size)
|
||||||
|
|
||||||
|
Set<String> headers = readAllHeaders(fromDownloader)
|
||||||
|
def matcher = null
|
||||||
|
headers.each {
|
||||||
|
if (it.startsWith("Range"))
|
||||||
|
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
|
||||||
|
}
|
||||||
|
assert matcher.groupCount() > 0
|
||||||
|
int start = Integer.parseInt(matcher[0][1])
|
||||||
|
int end = Integer.parseInt(matcher[0][2])
|
||||||
|
|
||||||
|
if (start == 0)
|
||||||
|
fail("inconlcusive")
|
||||||
|
|
||||||
|
toDownloader.write("416 don't have it \r\n".bytes)
|
||||||
|
toDownloader.write("X-Have: ${encodeXHave([0],2)}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
downloadThread.join()
|
||||||
|
|
||||||
|
assert performed
|
||||||
|
performed = false
|
||||||
|
assert available.contains(0)
|
||||||
|
assert thrown == null
|
||||||
|
|
||||||
|
// request same session
|
||||||
|
downloadThread = new Thread( { perform() } as Runnable)
|
||||||
|
downloadThread.setDaemon(true)
|
||||||
|
downloadThread.start()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
|
||||||
|
headers = readAllHeaders(fromDownloader)
|
||||||
|
matcher = null
|
||||||
|
headers.each {
|
||||||
|
if (it.startsWith("Range"))
|
||||||
|
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
|
||||||
|
}
|
||||||
|
assert matcher.groupCount() > 0
|
||||||
|
start = Integer.parseInt(matcher[0][1])
|
||||||
|
end = Integer.parseInt(matcher[0][2])
|
||||||
|
assert start == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testXAlt() throws Exception {
|
||||||
|
Personas personas = new Personas()
|
||||||
|
def sources = []
|
||||||
|
def listener = new Object() {
|
||||||
|
public void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
|
||||||
|
sources << e.source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventBus.register(SourceDiscoveredEvent.class, listener)
|
||||||
|
|
||||||
|
initSession(20)
|
||||||
|
readAllHeaders(fromDownloader)
|
||||||
|
toDownloader.write("416 don't have it\r\n".bytes)
|
||||||
|
toDownloader.write("X-Alt: ${personas.persona1.toBase64()},${personas.persona2.toBase64()}\r\n\r\n".bytes)
|
||||||
|
toDownloader.flush()
|
||||||
|
|
||||||
|
Thread.sleep(150)
|
||||||
|
assert sources.contains(personas.persona1)
|
||||||
|
assert sources.contains(personas.persona2)
|
||||||
|
assert 2 == sources.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> readAllHeaders(InputStream is) {
|
||||||
|
Set<String> rv = new HashSet<>()
|
||||||
|
String header
|
||||||
|
while((header = readTillRN(is)) != "")
|
||||||
|
rv.add(header)
|
||||||
|
rv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ class PiecesTest {
|
|||||||
public void testSinglePiece() {
|
public void testSinglePiece() {
|
||||||
pieces = new Pieces(1)
|
pieces = new Pieces(1)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
assert pieces.getRandomPiece() == 0
|
assert pieces.claim() == 0
|
||||||
pieces.markDownloaded(0)
|
pieces.markDownloaded(0)
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,28 @@ class PiecesTest {
|
|||||||
public void testTwoPieces() {
|
public void testTwoPieces() {
|
||||||
pieces = new Pieces(2)
|
pieces = new Pieces(2)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
int piece = pieces.getRandomPiece()
|
int piece = pieces.claim()
|
||||||
assert piece == 0 || piece == 1
|
assert piece == 0 || piece == 1
|
||||||
pieces.markDownloaded(piece)
|
pieces.markDownloaded(piece)
|
||||||
assert !pieces.isComplete()
|
assert !pieces.isComplete()
|
||||||
int piece2 = pieces.getRandomPiece()
|
int piece2 = pieces.claim()
|
||||||
assert piece != piece2
|
assert piece != piece2
|
||||||
pieces.markDownloaded(piece2)
|
pieces.markDownloaded(piece2)
|
||||||
assert pieces.isComplete()
|
assert pieces.isComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClaimAvailable() {
|
||||||
|
pieces = new Pieces(2)
|
||||||
|
int claimed = pieces.claim([0].toSet())
|
||||||
|
assert claimed == 0
|
||||||
|
assert -1 == pieces.claim([0].toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClaimNoneAvailable() {
|
||||||
|
pieces = new Pieces(20)
|
||||||
|
int claimed = pieces.claim()
|
||||||
|
assert -1 == pieces.claim([claimed].toSet())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,9 +24,9 @@ class FileHasherTest extends GroovyTestCase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPieceSize() {
|
void testPieceSize() {
|
||||||
assert 18 == FileHasher.getPieceSize(1000000)
|
assert 17 == FileHasher.getPieceSize(1000000)
|
||||||
assert 20 == FileHasher.getPieceSize(100000000)
|
assert 17 == FileHasher.getPieceSize(100000000)
|
||||||
assert 30 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
|
||||||
shouldFail IllegalArgumentException, {
|
shouldFail IllegalArgumentException, {
|
||||||
FileHasher.getPieceSize(Long.MAX_VALUE)
|
FileHasher.getPieceSize(Long.MAX_VALUE)
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ class FileHasherTest extends GroovyTestCase {
|
|||||||
fos.write b
|
fos.write b
|
||||||
fos.close()
|
fos.close()
|
||||||
def ih = hasher.hashFile tmp
|
def ih = hasher.hashFile tmp
|
||||||
assert ih.getHashList().length == 32
|
assert ih.getHashList().length == 64
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -58,7 +58,7 @@ class FileHasherTest extends GroovyTestCase {
|
|||||||
fos.write b
|
fos.write b
|
||||||
fos.close()
|
fos.close()
|
||||||
def ih = hasher.hashFile tmp
|
def ih = hasher.hashFile tmp
|
||||||
assert ih.getHashList().length == 64
|
assert ih.getHashList().length == 96
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -68,7 +68,7 @@ class FileHasherTest extends GroovyTestCase {
|
|||||||
fos.write b
|
fos.write b
|
||||||
fos.close()
|
fos.close()
|
||||||
def ih = hasher.hashFile tmp
|
def ih = hasher.hashFile tmp
|
||||||
assert ih.getHashList().length == 64
|
assert ih.getHashList().length == 128
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -78,6 +78,6 @@ class FileHasherTest extends GroovyTestCase {
|
|||||||
fos.write b
|
fos.write b
|
||||||
fos.close()
|
fos.close()
|
||||||
def ih = hasher.hashFile tmp
|
def ih = hasher.hashFile tmp
|
||||||
assert ih.getHashList().length == 32 * 3
|
assert ih.getHashList().length == 160
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import org.junit.Test
|
|||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.search.ResultsEvent
|
import com.muwire.core.search.ResultsEvent
|
||||||
import com.muwire.core.search.SearchEvent
|
import com.muwire.core.search.SearchEvent
|
||||||
@@ -26,7 +27,7 @@ class FileManagerTest {
|
|||||||
void before() {
|
void before() {
|
||||||
eventBus = new EventBus()
|
eventBus = new EventBus()
|
||||||
eventBus.register(ResultsEvent.class, listener)
|
eventBus.register(ResultsEvent.class, listener)
|
||||||
manager = new FileManager(eventBus)
|
manager = new FileManager(eventBus, new MuWireSettings())
|
||||||
results = null
|
results = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class FileManagerTest {
|
|||||||
void testHash1Result() {
|
void testHash1Result() {
|
||||||
File f = new File("a b.c")
|
File f = new File("a b.c")
|
||||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf = new SharedFile(f,ih)
|
SharedFile sf = new SharedFile(f,ih, 0)
|
||||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||||
manager.onFileHashedEvent(fhe)
|
manager.onFileHashedEvent(fhe)
|
||||||
|
|
||||||
@@ -53,8 +54,8 @@ class FileManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testHash2Results() {
|
void testHash2Results() {
|
||||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
|
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
|
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ class FileManagerTest {
|
|||||||
void testHash0Results() {
|
void testHash0Results() {
|
||||||
File f = new File("a b.c")
|
File f = new File("a b.c")
|
||||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf = new SharedFile(f,ih)
|
SharedFile sf = new SharedFile(f,ih, 0)
|
||||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||||
manager.onFileHashedEvent(fhe)
|
manager.onFileHashedEvent(fhe)
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ class FileManagerTest {
|
|||||||
void testKeyword1Result() {
|
void testKeyword1Result() {
|
||||||
File f = new File("a b.c")
|
File f = new File("a b.c")
|
||||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf = new SharedFile(f,ih)
|
SharedFile sf = new SharedFile(f,ih,0)
|
||||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||||
manager.onFileHashedEvent(fhe)
|
manager.onFileHashedEvent(fhe)
|
||||||
|
|
||||||
@@ -107,12 +108,12 @@ class FileManagerTest {
|
|||||||
void testKeyword2Results() {
|
void testKeyword2Results() {
|
||||||
File f1 = new File("a b.c")
|
File f1 = new File("a b.c")
|
||||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf1 = new SharedFile(f1, ih1)
|
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||||
|
|
||||||
File f2 = new File("c d.e")
|
File f2 = new File("c d.e")
|
||||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||||
SharedFile sf2 = new SharedFile(f2, ih2)
|
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||||
|
|
||||||
UUID uuid = UUID.randomUUID()
|
UUID uuid = UUID.randomUUID()
|
||||||
@@ -130,7 +131,7 @@ class FileManagerTest {
|
|||||||
void testKeyword0Results() {
|
void testKeyword0Results() {
|
||||||
File f = new File("a b.c")
|
File f = new File("a b.c")
|
||||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf = new SharedFile(f,ih)
|
SharedFile sf = new SharedFile(f,ih,0)
|
||||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||||
manager.onFileHashedEvent(fhe)
|
manager.onFileHashedEvent(fhe)
|
||||||
|
|
||||||
@@ -143,8 +144,8 @@ class FileManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testRemoveFileExistingHash() {
|
void testRemoveFileExistingHash() {
|
||||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
|
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
|
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||||
|
|
||||||
@@ -161,12 +162,12 @@ class FileManagerTest {
|
|||||||
void testRemoveFile() {
|
void testRemoveFile() {
|
||||||
File f1 = new File("a b.c")
|
File f1 = new File("a b.c")
|
||||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||||
SharedFile sf1 = new SharedFile(f1, ih1)
|
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||||
|
|
||||||
File f2 = new File("c d.e")
|
File f2 = new File("c d.e")
|
||||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||||
SharedFile sf2 = new SharedFile(f2, ih2)
|
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||||
|
|
||||||
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
|
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
|
||||||
|
@@ -8,6 +8,7 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
|
|
||||||
class HasherServiceTest {
|
class HasherServiceTest {
|
||||||
|
|
||||||
@@ -24,8 +25,9 @@ class HasherServiceTest {
|
|||||||
void before() {
|
void before() {
|
||||||
eventBus = new EventBus()
|
eventBus = new EventBus()
|
||||||
hasher = new FileHasher()
|
hasher = new FileHasher()
|
||||||
service = new HasherService(hasher, eventBus)
|
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
||||||
eventBus.register(FileHashedEvent.class, listener)
|
eventBus.register(FileHashedEvent.class, listener)
|
||||||
|
eventBus.register(FileSharedEvent.class, service)
|
||||||
service.start()
|
service.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -78,7 +78,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.write json
|
persisted.write json
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 1
|
assert listener.publishedFiles.size() == 1
|
||||||
@@ -99,7 +99,7 @@ class PersisterServiceLoadingTest {
|
|||||||
FileHasher fh = new FileHasher()
|
FileHasher fh = new FileHasher()
|
||||||
InfoHash ih1 = fh.hashFile(sharedFile1)
|
InfoHash ih1 = fh.hashFile(sharedFile1)
|
||||||
|
|
||||||
assert ih1.getHashList().length == 2 * 32
|
assert ih1.getHashList().length == 96
|
||||||
|
|
||||||
def json = [:]
|
def json = [:]
|
||||||
json.file = getSharedFileJsonName(sharedFile1)
|
json.file = getSharedFileJsonName(sharedFile1)
|
||||||
@@ -111,7 +111,9 @@ class PersisterServiceLoadingTest {
|
|||||||
String hash1 = Base64.encode(tmp)
|
String hash1 = Base64.encode(tmp)
|
||||||
System.arraycopy(ih1.getHashList(), 32, tmp, 0, 32)
|
System.arraycopy(ih1.getHashList(), 32, tmp, 0, 32)
|
||||||
String hash2 = Base64.encode(tmp)
|
String hash2 = Base64.encode(tmp)
|
||||||
json.hashList = [hash1, hash2]
|
System.arraycopy(ih1.getHashList(), 64, tmp, 0, 32)
|
||||||
|
String hash3 = Base64.encode(tmp)
|
||||||
|
json.hashList = [hash1, hash2, hash3]
|
||||||
|
|
||||||
json = JsonOutput.toJson(json)
|
json = JsonOutput.toJson(json)
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.write json
|
persisted.write json
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 1
|
assert listener.publishedFiles.size() == 1
|
||||||
@@ -161,7 +163,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.append "$json2\n"
|
persisted.append "$json2\n"
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 2
|
assert listener.publishedFiles.size() == 2
|
||||||
@@ -193,7 +195,7 @@ class PersisterServiceLoadingTest {
|
|||||||
persisted.write json1
|
persisted.write json1
|
||||||
|
|
||||||
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
|
|
||||||
assert listener.publishedFiles.size() == 1
|
assert listener.publishedFiles.size() == 1
|
||||||
|
@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
|
|||||||
import com.muwire.core.DownloadedFile
|
import com.muwire.core.DownloadedFile
|
||||||
import com.muwire.core.EventBus
|
import com.muwire.core.EventBus
|
||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
|
import com.muwire.core.MuWireSettings
|
||||||
import com.muwire.core.SharedFile
|
import com.muwire.core.SharedFile
|
||||||
import com.muwire.core.util.DataUtil
|
import com.muwire.core.util.DataUtil
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class PersisterServiceSavingTest {
|
|||||||
f = new File("build.gradle")
|
f = new File("build.gradle")
|
||||||
f = f.getCanonicalFile()
|
f = f.getCanonicalFile()
|
||||||
ih = fh.hashFile(f)
|
ih = fh.hashFile(f)
|
||||||
fileSource = new FileManager(eventBus) {
|
fileSource = new FileManager(eventBus, new MuWireSettings()) {
|
||||||
Map<File, SharedFile> getSharedFiles() {
|
Map<File, SharedFile> getSharedFiles() {
|
||||||
Map<File, SharedFile> rv = new HashMap<>()
|
Map<File, SharedFile> rv = new HashMap<>()
|
||||||
rv.put(f, sf)
|
rv.put(f, sf)
|
||||||
@@ -54,10 +55,10 @@ class PersisterServiceSavingTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSavingSharedFile() {
|
void testSavingSharedFile() {
|
||||||
sf = new SharedFile(f, ih)
|
sf = new SharedFile(f, ih, 0)
|
||||||
|
|
||||||
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(1500)
|
Thread.sleep(1500)
|
||||||
|
|
||||||
JsonSlurper jsonSlurper = new JsonSlurper()
|
JsonSlurper jsonSlurper = new JsonSlurper()
|
||||||
@@ -73,10 +74,10 @@ class PersisterServiceSavingTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSavingDownloadedFile() {
|
void testSavingDownloadedFile() {
|
||||||
Destinations dests = new Destinations()
|
Destinations dests = new Destinations()
|
||||||
sf = new DownloadedFile(f, ih, new HashSet([dests.dest1, dests.dest2]))
|
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
|
||||||
|
|
||||||
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
ps = new PersisterService(persisted, eventBus, 100, fileSource)
|
||||||
ps.start()
|
ps.onUILoadedEvent(null)
|
||||||
Thread.sleep(1500)
|
Thread.sleep(1500)
|
||||||
|
|
||||||
JsonSlurper jsonSlurper = new JsonSlurper()
|
JsonSlurper jsonSlurper = new JsonSlurper()
|
||||||
|
@@ -30,6 +30,17 @@ class SearchIndexTest {
|
|||||||
assert found.size() == 2
|
assert found.size() == 2
|
||||||
assert found.contains("a b.c")
|
assert found.contains("a b.c")
|
||||||
assert found.contains("c d.e")
|
assert found.contains("c d.e")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDrillDownDoesNotModifyIndex() {
|
||||||
|
initIndex(["a b.c", "c d.e"])
|
||||||
|
index.search(["c","e"])
|
||||||
|
def found = index.search(["c"])
|
||||||
|
assert found.size() == 2
|
||||||
|
assert found.contains("a b.c")
|
||||||
|
assert found.contains("c d.e")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -72,4 +83,11 @@ class SearchIndexTest {
|
|||||||
assert found.size() == 1
|
assert found.size() == 1
|
||||||
assert found.contains("b c.d")
|
assert found.contains("b c.d")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDuplicateTerm() {
|
||||||
|
initIndex(["MuWire-0.3.3.jar"])
|
||||||
|
def found = index.search(["muwire", "0", "3", "jar"])
|
||||||
|
assert found.size() == 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,14 +5,17 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import com.muwire.core.Destinations
|
import com.muwire.core.Destinations
|
||||||
|
import com.muwire.core.Persona
|
||||||
|
import com.muwire.core.Personas
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
|
|
||||||
class TrustServiceTest {
|
class TrustServiceTest {
|
||||||
|
|
||||||
TrustService service
|
TrustService service
|
||||||
File persistGood, persistBad
|
File persistGood, persistBad
|
||||||
Destinations dests = new Destinations()
|
Personas personas = new Personas()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
void before() {
|
void before() {
|
||||||
@@ -33,51 +36,50 @@ class TrustServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmpty() {
|
void testEmpty() {
|
||||||
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest1)
|
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona1.destination)
|
||||||
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest2)
|
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona2.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testOnEvent() {
|
void testOnEvent() {
|
||||||
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
|
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
|
||||||
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
|
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
|
||||||
|
|
||||||
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
|
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
|
||||||
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
|
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPersist() {
|
void testPersist() {
|
||||||
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
|
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
|
||||||
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
|
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
|
||||||
|
|
||||||
Thread.sleep(250)
|
Thread.sleep(250)
|
||||||
def trusted = new HashSet<>()
|
def trusted = new HashSet<>()
|
||||||
persistGood.eachLine {
|
persistGood.eachLine {
|
||||||
trusted.add(new Destination(it))
|
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||||
}
|
}
|
||||||
def distrusted = new HashSet<>()
|
def distrusted = new HashSet<>()
|
||||||
persistBad.eachLine {
|
persistBad.eachLine {
|
||||||
distrusted.add(new Destination(it))
|
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert trusted.size() == 1
|
assert trusted.size() == 1
|
||||||
assert trusted.contains(dests.dest1)
|
assert trusted.contains(personas.persona1)
|
||||||
assert distrusted.size() == 1
|
assert distrusted.size() == 1
|
||||||
assert distrusted.contains(dests.dest2)
|
assert distrusted.contains(personas.persona2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoad() {
|
void testLoad() {
|
||||||
service.stop()
|
service.stop()
|
||||||
persistGood.append("${dests.dest1.toBase64()}\n")
|
persistGood.append("${personas.persona1.toBase64()}\n")
|
||||||
persistBad.append("${dests.dest2.toBase64()}\n")
|
persistBad.append("${personas.persona2.toBase64()}\n")
|
||||||
service = new TrustService(persistGood, persistBad, 100)
|
service = new TrustService(persistGood, persistBad, 100)
|
||||||
service.start()
|
service.start()
|
||||||
Thread.sleep(10)
|
Thread.sleep(50)
|
||||||
|
|
||||||
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
|
|
||||||
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
|
|
||||||
|
|
||||||
|
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
|
||||||
|
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,18 +9,18 @@ import com.muwire.core.InfoHash
|
|||||||
|
|
||||||
class RequestParsingTest {
|
class RequestParsingTest {
|
||||||
|
|
||||||
Request request
|
ContentRequest request
|
||||||
|
|
||||||
private void fromString(String requestString) {
|
private void fromString(String requestString) {
|
||||||
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
||||||
request = Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
|
request = Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void failed(String requestString) {
|
private static void failed(String requestString) {
|
||||||
try {
|
try {
|
||||||
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
|
||||||
Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
|
Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
|
||||||
assert false
|
assert false
|
||||||
} catch (IOException expected) {}
|
} catch (IOException expected) {}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ class UploaderTest {
|
|||||||
InputStream is
|
InputStream is
|
||||||
OutputStream os
|
OutputStream os
|
||||||
|
|
||||||
Request request
|
ContentRequest request
|
||||||
Uploader uploader
|
Uploader uploader
|
||||||
|
|
||||||
byte[] inFile
|
byte[] inFile
|
||||||
@@ -52,7 +52,7 @@ class UploaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startUpload() {
|
private void startUpload() {
|
||||||
uploader = new Uploader(file, request, endpoint)
|
uploader = new ContentUploader(file, request, endpoint)
|
||||||
uploadThread = new Thread(uploader.respond() as Runnable)
|
uploadThread = new Thread(uploader.respond() as Runnable)
|
||||||
uploadThread.setDaemon(true)
|
uploadThread.setDaemon(true)
|
||||||
uploadThread.start()
|
uploadThread.start()
|
||||||
@@ -77,7 +77,7 @@ class UploaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testSmallFile() {
|
public void testSmallFile() {
|
||||||
fillFile(20)
|
fillFile(20)
|
||||||
request = new Request(range : new Range(0,19))
|
request = new ContentRequest(range : new Range(0,19))
|
||||||
startUpload()
|
startUpload()
|
||||||
assert "200 OK" == readUntilRN()
|
assert "200 OK" == readUntilRN()
|
||||||
assert "Content-Range: 0-19" == readUntilRN()
|
assert "Content-Range: 0-19" == readUntilRN()
|
||||||
@@ -92,7 +92,7 @@ class UploaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testRequestMiddle() {
|
public void testRequestMiddle() {
|
||||||
fillFile(20)
|
fillFile(20)
|
||||||
request = new Request(range : new Range(5,15))
|
request = new ContentRequest(range : new Range(5,15))
|
||||||
startUpload()
|
startUpload()
|
||||||
assert "200 OK" == readUntilRN()
|
assert "200 OK" == readUntilRN()
|
||||||
assert "Content-Range: 5-15" == readUntilRN()
|
assert "Content-Range: 5-15" == readUntilRN()
|
||||||
@@ -108,7 +108,7 @@ class UploaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testOutOfRange() {
|
public void testOutOfRange() {
|
||||||
fillFile(20)
|
fillFile(20)
|
||||||
request = new Request(range : new Range(0,20))
|
request = new ContentRequest(range : new Range(0,20))
|
||||||
startUpload()
|
startUpload()
|
||||||
assert "416 Range Not Satisfiable" == readUntilRN()
|
assert "416 Range Not Satisfiable" == readUntilRN()
|
||||||
assert "" == readUntilRN()
|
assert "" == readUntilRN()
|
||||||
@@ -118,7 +118,7 @@ class UploaderTest {
|
|||||||
public void testLargeFile() {
|
public void testLargeFile() {
|
||||||
final int length = 0x1 << 14
|
final int length = 0x1 << 14
|
||||||
fillFile(length)
|
fillFile(length)
|
||||||
request = new Request(range : new Range(0, length - 1))
|
request = new ContentRequest(range : new Range(0, length - 1))
|
||||||
startUpload()
|
startUpload()
|
||||||
readUntilRN()
|
readUntilRN()
|
||||||
readUntilRN()
|
readUntilRN()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user