forked from I2P_Developers/i2p.i2p
Compare commits
1995 Commits
i2p_post_g
...
i2p_0_6_1_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
230d4cd23f | ||
|
|
e9b6fcc0a4 | ||
|
|
8fcb871409 | ||
|
|
83bef43fd5 | ||
|
|
b4fc6ca31b | ||
|
|
ab3f1b708d | ||
|
|
c76402a160 | ||
|
|
a50c73aa5e | ||
|
|
5aa66795d2 | ||
|
|
ac3c2d2b15 | ||
|
|
072a45e5ce | ||
|
|
1ab14e52d2 | ||
|
|
9a820961a2 | ||
|
|
764149aef3 | ||
|
|
1b3ad31bff | ||
|
|
15e6c27c04 | ||
|
|
8b707e569f | ||
|
|
e4c4b24c61 | ||
|
|
031636e607 | ||
|
|
b5c0d77c69 | ||
|
|
d489caa88c | ||
|
|
2a24029acf | ||
|
|
c5aab8c750 | ||
|
|
343748111a | ||
|
|
c5ddfabfe9 | ||
|
|
1ef33906ed | ||
|
|
f3849a22ad | ||
|
|
b03ff21d3b | ||
|
|
52094b10c9 | ||
|
|
fc927efaa3 | ||
|
|
65dc803fb7 | ||
|
|
349adf6690 | ||
|
|
2c843fd818 | ||
|
|
863b511cde | ||
|
|
c417e7c237 | ||
|
|
1822c0d7d8 | ||
|
|
94c1c32b51 | ||
|
|
deb35f4af4 | ||
|
|
883150f943 | ||
|
|
717d1b97b2 | ||
|
|
e62135eacc | ||
|
|
2c6d953359 | ||
|
|
2b79e2df3f | ||
|
|
fab6e421b8 | ||
|
|
589cbd675a | ||
|
|
c486f5980a | ||
|
|
eee21aa301 | ||
|
|
a2854cf6f6 | ||
|
|
62b7cf64da | ||
|
|
7b2a435aad | ||
|
|
3d8d21e543 | ||
|
|
8b7958cff2 | ||
|
|
7bb792836d | ||
|
|
03f509ca54 | ||
|
|
5f05631936 | ||
|
|
5cfedd4c8b | ||
|
|
269fec64a5 | ||
|
|
f63c6f4717 | ||
|
|
b4c495531a | ||
|
|
9990126e3e | ||
|
|
ac8436a8eb | ||
|
|
dee79dfb1c | ||
|
|
9b4e6f475d | ||
|
|
7672ba23d1 | ||
|
|
4b77ddedcc | ||
|
|
222af6c090 | ||
|
|
8e879cb646 | ||
|
|
65975df1be | ||
|
|
c94de2fbb5 | ||
|
|
5aa335740a | ||
|
|
1202751359 | ||
|
|
34fcf53d87 | ||
|
|
9ddc632b9f | ||
|
|
941b65eb32 | ||
|
|
8c9167464b | ||
|
|
5b94965983 | ||
|
|
3226ea5bf6 | ||
|
|
84a24784e4 | ||
|
|
71d3fa6b8c | ||
|
|
9e00dbaafd | ||
|
|
2e9e0c64d4 | ||
|
|
fde3f1ce7d | ||
|
|
321c560648 | ||
|
|
d2ddca7d64 | ||
|
|
fb17e70f12 | ||
|
|
79f934fe17 | ||
|
|
3d76df6af3 | ||
|
|
d5c36f7f4e | ||
|
|
41ac628744 | ||
|
|
41e5e1a094 | ||
|
|
74edc3fa7d | ||
|
|
3a26218b5a | ||
|
|
687abd9427 | ||
|
|
113fbc1df3 | ||
|
|
1374ea0ea1 | ||
|
|
424e55d3b2 | ||
|
|
fde4f579f4 | ||
|
|
2d651a41f0 | ||
|
|
1eebd5463f | ||
|
|
f22601b477 | ||
|
|
ab8e11657f | ||
|
|
17eb7fa983 | ||
|
|
d1134f9704 | ||
|
|
13fe45b489 | ||
|
|
cd235e5902 | ||
|
|
b727d868fb | ||
|
|
cd2609b34a | ||
|
|
a12ede096a | ||
|
|
eb3442823e | ||
|
|
211f37c207 | ||
|
|
d60d0923c0 | ||
|
|
0448203ede | ||
|
|
00c97dbf92 | ||
|
|
d07342e3e3 | ||
|
|
205d8de281 | ||
|
|
a638301b5c | ||
|
|
4f51ad492b | ||
|
|
c3a9f72d41 | ||
|
|
79476d3609 | ||
|
|
97a206fcda | ||
|
|
f783e65a4c | ||
|
|
dbd1f65864 | ||
|
|
5c78d8108f | ||
|
|
1b273bdf43 | ||
|
|
934f4082f1 | ||
|
|
002aed145f | ||
|
|
1ca27ffd39 | ||
|
|
66e6dbec33 | ||
|
|
894caaa63c | ||
|
|
97dae94b46 | ||
|
|
c00488afeb | ||
|
|
23723b56ca | ||
|
|
76f89ac93c | ||
|
|
0f8611e465 | ||
|
|
8e87ae08fb | ||
|
|
5b1a6391f3 | ||
|
|
728f177473 | ||
|
|
1d0d0d9c69 | ||
|
|
9b7e5d1817 | ||
|
|
dc0485b526 | ||
|
|
784d465d17 | ||
|
|
327089c9d1 | ||
|
|
148dd99c86 | ||
|
|
98277d3b64 | ||
|
|
702e5a5eab | ||
|
|
54c91731c0 | ||
|
|
3bfc109476 | ||
|
|
3989638f2d | ||
|
|
4a65fd4f46 | ||
|
|
d525c49d45 | ||
|
|
c287bace0f | ||
|
|
ee0951b5b2 | ||
|
|
1eb3ae5e1b | ||
|
|
7d234b1978 | ||
|
|
6f424fa751 | ||
|
|
7726bd1a5c | ||
|
|
2a922098d6 | ||
|
|
3ec92c8b62 | ||
|
|
b37bb9372e | ||
|
|
369b6930e5 | ||
|
|
5033a22a9b | ||
|
|
6e5114a4c2 | ||
|
|
77c818a0b1 | ||
|
|
7ac673cea4 | ||
|
|
e5fa7e0ae4 | ||
|
|
ab4f3008cb | ||
|
|
f738a02760 | ||
|
|
7d64ecb628 | ||
|
|
7beacff028 | ||
|
|
952bcc696a | ||
|
|
19bba048f2 | ||
|
|
5966fcf5ff | ||
|
|
fbd7feee61 | ||
|
|
5faca98176 | ||
|
|
99951bf815 | ||
|
|
024b1a04e2 | ||
|
|
a4cc18df76 | ||
|
|
fef578973a | ||
|
|
35b75a7300 | ||
|
|
1c6c397913 | ||
|
|
c96965d364 | ||
|
|
12900ca709 | ||
|
|
f5b829a124 | ||
|
|
f62a6d3ce6 | ||
|
|
3d18bf870b | ||
|
|
d8071296eb | ||
|
|
c66e3256aa | ||
|
|
686742a67b | ||
|
|
cdf94295f3 | ||
|
|
fbf1705c4e | ||
|
|
453ecc4208 | ||
|
|
d1f2b447ac | ||
|
|
70c4560f02 | ||
|
|
9089fdd2d5 | ||
|
|
ef82cc4f20 | ||
|
|
f2c2a5b386 | ||
|
|
fc858bc950 | ||
|
|
dbb4b3d0c2 | ||
|
|
2b841ad667 | ||
|
|
5e094b43b3 | ||
|
|
33d57dd545 | ||
|
|
61f75b5f09 | ||
|
|
3f65e53592 | ||
|
|
99ae3ee459 | ||
|
|
f7236d7d58 | ||
|
|
5182008b38 | ||
|
|
9d030327e6 | ||
|
|
a15c90d2cc | ||
|
|
84c2a713e1 | ||
|
|
7db9ce6e5b | ||
|
|
da42f5717b | ||
|
|
5241953e5d | ||
|
|
b1a5f61ba2 | ||
|
|
024a5a1ad4 | ||
|
|
b031de5404 | ||
|
|
dceac73951 | ||
|
|
8f95143488 | ||
|
|
a8f3043aae | ||
|
|
6c91b2d4a9 | ||
|
|
ddd438de35 | ||
|
|
16fd46db2b | ||
|
|
1159c155a4 | ||
|
|
30b4e2aa2a | ||
|
|
ae46fa2e6d | ||
|
|
9fc34895c9 | ||
|
|
08be4c7b3d | ||
|
|
7443457af4 | ||
|
|
979a4cfb69 | ||
|
|
ed285871bf | ||
|
|
8c70b8b32a | ||
|
|
14134694d7 | ||
|
|
807d2d3509 | ||
|
|
b222cd43f4 | ||
|
|
7f6aa327f2 | ||
|
|
49564a3878 | ||
|
|
12ddaff0ce | ||
|
|
a8ea239dcc | ||
|
|
ca391097a9 | ||
|
|
4297edc88f | ||
|
|
6de4673e9e | ||
|
|
f6979c811f | ||
|
|
bd86483204 | ||
|
|
53cf03cec6 | ||
|
|
e284a8878b | ||
|
|
14cd469c6d | ||
|
|
9050d7c218 | ||
|
|
0ad18cd0ba | ||
|
|
ca0af146b7 | ||
|
|
a2d2b031f4 | ||
|
|
2f36912ac0 | ||
|
|
53e32c8e64 | ||
|
|
10dde610dc | ||
|
|
f7c2ae9a3b | ||
|
|
ac3b88b9e9 | ||
|
|
60124cdcdc | ||
|
|
e7d2281772 | ||
|
|
52ace2d695 | ||
|
|
b5a25801b4 | ||
|
|
3fc0558810 | ||
|
|
bd9c6ff463 | ||
|
|
a0c822af96 | ||
|
|
4de302101d | ||
|
|
84383c3dab | ||
|
|
a94abb13a4 | ||
|
|
ad59ab691b | ||
|
|
ee9ac31c8b | ||
|
|
788998307a | ||
|
|
2f8a2879bb | ||
|
|
c7b9525d2c | ||
|
|
3fbc6f41af | ||
|
|
6534c84578 | ||
|
|
3816c79193 | ||
|
|
8458e4e0af | ||
|
|
0b9e4967a0 | ||
|
|
05e2da7c22 | ||
|
|
13bda1f6d7 | ||
|
|
ea22c73a73 | ||
|
|
aa5f1cb18d | ||
|
|
ab9c6d59cf | ||
|
|
76655d01d0 | ||
|
|
138f7d3b8d | ||
|
|
df4b998a6a | ||
|
|
2d70103f88 | ||
|
|
731e26e7d6 | ||
|
|
f9d3b157f0 | ||
|
|
12775c416d | ||
|
|
2ca4e63216 | ||
|
|
918d8f851f | ||
|
|
20ea680ff0 | ||
|
|
cabb607211 | ||
|
|
00a4761b5e | ||
|
|
3516701272 | ||
|
|
c4d785667a | ||
|
|
123e0ba589 | ||
|
|
197237aa32 | ||
|
|
f30dc2b480 | ||
|
|
d4ff34eacb | ||
|
|
978769a05d | ||
|
|
993c70f600 | ||
|
|
5dfa9ad7f6 | ||
|
|
f282fe3854 | ||
|
|
9297564555 | ||
|
|
e7ad516685 | ||
|
|
ad574c8504 | ||
|
|
38617fe0a7 | ||
|
|
cdee5b2c31 | ||
|
|
7f6e65c76f | ||
|
|
4dd628dbc8 | ||
|
|
3b5b48ad8a | ||
|
|
c4cac3f3f1 | ||
|
|
4a49e98c31 | ||
|
|
0c0e269e72 | ||
|
|
70b6f97abe | ||
|
|
0013677b83 | ||
|
|
a98ceda64d | ||
|
|
91ea1d0395 | ||
|
|
4aa65c3bb3 | ||
|
|
0a1f59940a | ||
|
|
f540dc798b | ||
|
|
30f6f26a68 | ||
|
|
ea3bf3ffc8 | ||
|
|
831d5ac70c | ||
|
|
1962867ad9 | ||
|
|
6019a03029 | ||
|
|
df5736f571 | ||
|
|
9a73c6defe | ||
|
|
9dfa87ba47 | ||
|
|
934a269753 | ||
|
|
1c0dfc242b | ||
|
|
3bc3e5d47e | ||
|
|
55869af2cc | ||
|
|
9f336dd05b | ||
|
|
411ca5e6c3 | ||
|
|
c528e4db03 | ||
|
|
848ead7683 | ||
|
|
1b8419b9b5 | ||
|
|
900420719e | ||
|
|
ef7d1ba964 | ||
|
|
ab1654c784 | ||
|
|
24bad8e4bb | ||
|
|
f6d8200bc8 | ||
|
|
aef33548b3 | ||
|
|
56ecdcce82 | ||
|
|
b9b59ff95f | ||
|
|
aa9dd3e5c6 | ||
|
|
30bd659149 | ||
|
|
3286ca49c8 | ||
|
|
7700d12178 | ||
|
|
557b7e3f2e | ||
|
|
3e1e9146e1 | ||
|
|
40d8d1aac1 | ||
|
|
1457b8efba | ||
|
|
3821e80ac8 | ||
|
|
d40bb459ea | ||
|
|
edf04f07c9 | ||
|
|
2bdea23986 | ||
|
|
6be0c4b694 | ||
|
|
2a272f465c | ||
|
|
a8ecd32b45 | ||
|
|
20c42a175d | ||
|
|
d6c3ffde87 | ||
|
|
177e0ae6a3 | ||
|
|
dab1b4d256 | ||
|
|
3aba12631b | ||
|
|
cfee6430d4 | ||
|
|
6b96df1cec | ||
|
|
deecfa5047 | ||
|
|
6ca3f01038 | ||
|
|
d89f589f2b | ||
|
|
8c1895e04f | ||
|
|
c3d0132a98 | ||
|
|
d955279d17 | ||
|
|
76266dce0d | ||
|
|
5694206b35 | ||
|
|
4293a18726 | ||
|
|
9865af4174 | ||
|
|
c8c109093d | ||
|
|
b5784d6025 | ||
|
|
31bdb8909a | ||
|
|
ee921c22ae | ||
|
|
172ffd0434 | ||
|
|
d9b4406c09 | ||
|
|
8ac0e85df4 | ||
|
|
249ccd5e3c | ||
|
|
727d76d43e | ||
|
|
44770b7c07 | ||
|
|
b5d571c75f | ||
|
|
da56d83716 | ||
|
|
f777e213ce | ||
|
|
79906f5a7d | ||
|
|
54074e76b5 | ||
|
|
c2ea8db683 | ||
|
|
744671a518 | ||
|
|
7f5b127bbc | ||
|
|
89eff0c628 | ||
|
|
177aeebb1c | ||
|
|
e0e6bde4a5 | ||
|
|
e6b145716f | ||
|
|
f958342704 | ||
|
|
5a1f738505 | ||
|
|
8147cdf40c | ||
|
|
6afc64ac39 | ||
|
|
61b8e3598b | ||
|
|
3bb445ff40 | ||
|
|
59a8037599 | ||
|
|
09cb5fad59 | ||
|
|
ee8e45ecf7 | ||
|
|
339868838d | ||
|
|
c5579fa349 | ||
|
|
d4a859547c | ||
|
|
779aa240d2 | ||
|
|
9aaad00383 | ||
|
|
6422f7ef78 | ||
|
|
3e51584b3c | ||
|
|
4ff8a53084 | ||
|
|
ccb73437c4 | ||
|
|
b43114f61b | ||
|
|
9bd87ab511 | ||
|
|
b6ea55f7ef | ||
|
|
5f18cec97d | ||
|
|
3ba921ec0e | ||
|
|
e313da254c | ||
|
|
8660cf0d74 | ||
|
|
e0bfdff152 | ||
|
|
c27aed3603 | ||
|
|
cdc6002f0e | ||
|
|
4cf3d9c1a2 | ||
|
|
0473e08e21 | ||
|
|
346faa3de2 | ||
|
|
5ec6dca64d | ||
|
|
1a6b49cfb8 | ||
|
|
c7b75df390 | ||
|
|
f97c09291b | ||
|
|
8f2a5b403c | ||
|
|
ea41a90eae | ||
|
|
b1dd29e64d | ||
|
|
46e47c47ac | ||
|
|
b7bf431f0d | ||
|
|
7f432122d9 | ||
|
|
e7be8c6097 | ||
|
|
adf56a16e1 | ||
|
|
11204b8a2b | ||
|
|
cade27dceb | ||
|
|
5597d28e59 | ||
|
|
0502fec432 | ||
|
|
a6714fc2de | ||
|
|
1219dadbd5 | ||
|
|
77b995f5ed | ||
|
|
2f53b9ff68 | ||
|
|
d84d045849 | ||
|
|
d8e72dfe48 | ||
|
|
88b9f7a74c | ||
|
|
6a19501214 | ||
|
|
ba30b56c5f | ||
|
|
a375e4b2ce | ||
|
|
44fd71e17f | ||
|
|
b41c378de9 | ||
|
|
4ce6b308b3 | ||
|
|
72c6e7d1c5 | ||
|
|
7ca3f22e77 | ||
|
|
59790dafef | ||
|
|
7227cae6ef | ||
|
|
03bba51c1e | ||
|
|
0637050cbc | ||
|
|
7f58a68c5a | ||
|
|
8120b0397c | ||
|
|
fbe42b7dce | ||
|
|
def24e34ad | ||
|
|
593253e6a3 | ||
|
|
56dd4cb8b5 | ||
|
|
10c6f67500 | ||
|
|
5c1f968afa | ||
|
|
aaaf437d62 | ||
|
|
a8a866b5f6 | ||
|
|
aeb8f02269 | ||
|
|
45767360ab | ||
|
|
3563aa2e4d | ||
|
|
843d5b625a | ||
|
|
0f8ede85ca | ||
|
|
9267d7cae2 | ||
|
|
dade5a981b | ||
|
|
f873cba27e | ||
|
|
108dec53a5 | ||
|
|
e9592ed400 | ||
|
|
4c230522a2 | ||
|
|
16bd19c6dc | ||
|
|
b4b6d49d34 | ||
|
|
9d5f16a889 | ||
|
|
51c492b842 | ||
|
|
d3380228ac | ||
|
|
ad47bf5da3 | ||
|
|
76e8631e31 | ||
|
|
f688b9112d | ||
|
|
18d3f5d25d | ||
|
|
440cf2c983 | ||
|
|
adeb09576a | ||
|
|
fd52bcf8cd | ||
|
|
c2696bba00 | ||
|
|
fef9d57483 | ||
|
|
c250692ef0 | ||
|
|
2a6024e196 | ||
|
|
835662b3c9 | ||
|
|
6b5b880ab6 | ||
|
|
3de23d4206 | ||
|
|
ea82f2a8cc | ||
|
|
b5ad7642bc | ||
|
|
0fbe84e9f0 | ||
|
|
8063889d23 | ||
|
|
6e1ac8e173 | ||
|
|
1b0bb5ea19 | ||
|
|
4ce51261f1 | ||
|
|
6e34d9b73e | ||
|
|
6e01637400 | ||
|
|
9a96798f9f | ||
|
|
c9db6f87d1 | ||
|
|
567ce84e1e | ||
|
|
cde7ac7e52 | ||
|
|
b2f0d17e94 | ||
|
|
dae6be14b7 | ||
|
|
20cec857d2 | ||
|
|
739f694cfe | ||
|
|
84779002fb | ||
|
|
df926fb60d | ||
|
|
a2c7c5a516 | ||
|
|
1861379d43 | ||
|
|
408a344aae | ||
|
|
e9c1ed70d0 | ||
|
|
916dcca2b0 | ||
|
|
31e81bab17 | ||
|
|
6a5170c341 | ||
|
|
42bff8093c | ||
|
|
d1df94f284 | ||
|
|
9cf1744291 | ||
|
|
f0545c8c9a | ||
|
|
ef9ed87d30 | ||
|
|
58ffd92a34 | ||
|
|
418facc7e0 | ||
|
|
7f3c953e14 | ||
|
|
addab1fa2a | ||
|
|
39343ce957 | ||
|
|
7389cec78f | ||
|
|
9e5fe7d2b6 | ||
|
|
a7dfaee5ac | ||
|
|
7beb92b1cc | ||
|
|
5b56d22da9 | ||
|
|
e6b343070a | ||
|
|
8496b88518 | ||
|
|
aa542b7876 | ||
|
|
3f7d46378b | ||
|
|
b36def1f72 | ||
|
|
5a6a3a5e8d | ||
|
|
c3bd26d9b4 | ||
|
|
967e106ee7 | ||
|
|
7c73e59482 | ||
|
|
03dfa913d1 | ||
|
|
348e845793 | ||
|
|
80827c3aad | ||
|
|
3b4cf0a024 | ||
|
|
941252fd80 | ||
|
|
bc626ece2d | ||
|
|
400feb3ba7 | ||
|
|
756a4e3995 | ||
|
|
578301240e | ||
|
|
9b8f91c7f9 | ||
|
|
c7c389d4fb | ||
|
|
68f7adfa0b | ||
|
|
c4ac5170c7 | ||
|
|
32e0c8ac71 | ||
|
|
c9c1eae32f | ||
|
|
33366cc291 | ||
|
|
083ac1f125 | ||
|
|
80c6290b89 | ||
|
|
6492ad165a | ||
|
|
f0d1b1a40e | ||
|
|
17f044e6cd | ||
|
|
63f3a9cd7b | ||
|
|
b8ddbf13b4 | ||
|
|
be9bdbfe0f | ||
|
|
bc74bf1402 | ||
|
|
5c2a57f95a | ||
|
|
9cd8cc692e | ||
|
|
ebac4df2d3 | ||
|
|
0626f714c6 | ||
|
|
21842291e9 | ||
|
|
d461c295f6 | ||
|
|
85b3450525 | ||
|
|
75d7c81b7c | ||
|
|
1433e20f73 | ||
|
|
e614a2f726 | ||
|
|
32be7f1fd8 | ||
|
|
66e1d95a2a | ||
|
|
ff03be217e | ||
|
|
a52f8b89dc | ||
|
|
21c7c043b3 | ||
|
|
45e6608ad3 | ||
|
|
28978e3680 | ||
|
|
904f755c8c | ||
|
|
a2c309ddd3 | ||
|
|
677eeac8f7 | ||
|
|
b232cc0f24 | ||
|
|
18bbae1d1e | ||
|
|
08ee62b52c | ||
|
|
5b83aed719 | ||
|
|
b5875ca07b | ||
|
|
3f9bf28382 | ||
|
|
a2bd71c75b | ||
|
|
89509490c5 | ||
|
|
a997a46040 | ||
|
|
538dd07e7b | ||
|
|
046778404e | ||
|
|
766f83d653 | ||
|
|
b20aee6753 | ||
|
|
f9aa3aef18 | ||
|
|
d74aa6e53d | ||
|
|
ea6fbc7835 | ||
|
|
536e604b8e | ||
|
|
49d6f5018f | ||
|
|
4a830e422a | ||
|
|
df6c52fe75 | ||
|
|
01979c08b3 | ||
|
|
7928ef83cc | ||
|
|
10afe0a060 | ||
|
|
ef230cfa3d | ||
|
|
2d15a42137 | ||
|
|
57d6a2f645 | ||
|
|
469a0852d7 | ||
|
|
7983bb1490 | ||
|
|
2e7eac02ed | ||
|
|
238389fc7f | ||
|
|
4cec9da0a6 | ||
|
|
00f27d4400 | ||
|
|
f61618e4a4 | ||
|
|
265d5e306e | ||
|
|
10ed058c2e | ||
|
|
8a21f0efec | ||
|
|
b8291ac5a4 | ||
|
|
c17433cb93 | ||
|
|
35fe7f8203 | ||
|
|
21f13dba43 | ||
|
|
0db239a3fe | ||
|
|
4745d61f9b | ||
|
|
b9a4c3ba52 | ||
|
|
cbf6a70a1a | ||
|
|
7d4e093b58 | ||
|
|
d27feabcb3 | ||
|
|
0d9efa17de | ||
|
|
b125b04c8d | ||
|
|
0539f1d794 | ||
|
|
a4b6709f02 | ||
|
|
f2db143a6f | ||
|
|
b615f54d41 | ||
|
|
db2328e03e | ||
|
|
e2071935ad | ||
|
|
1c40ff773f | ||
|
|
37a3645663 | ||
|
|
eb8accd1e0 | ||
|
|
3af97894b4 | ||
|
|
15a0dcf4d8 | ||
|
|
aa3a44c42a | ||
|
|
40f4b47b87 | ||
|
|
dca09d96b3 | ||
|
|
dd10747460 | ||
|
|
77176162af | ||
|
|
8b9ee4dfd7 | ||
|
|
6e8e77b9ec | ||
|
|
7ef9ce8cc6 | ||
|
|
9646ac2911 | ||
|
|
566a713baa | ||
|
|
36f7e98e90 | ||
|
|
4da755816a | ||
|
|
3ef0258faf | ||
|
|
293ceaee93 | ||
|
|
7b58d0fa0f | ||
|
|
bd68c1e056 | ||
|
|
200162d973 | ||
|
|
4b37a53f1c | ||
|
|
2d41de7ae0 | ||
|
|
bc5bc62c18 | ||
|
|
a0d680024e | ||
|
|
45013feea7 | ||
|
|
2abbe992dd | ||
|
|
d7081b3eeb | ||
|
|
a2f5289bd9 | ||
|
|
b366a4b942 | ||
|
|
27e92653fe | ||
|
|
80120b7b7d | ||
|
|
af8a618826 | ||
|
|
af0e554562 | ||
|
|
382cbb18db | ||
|
|
252b523155 | ||
|
|
4303b3b716 | ||
|
|
87715dc21a | ||
|
|
8552494fc1 | ||
|
|
1c2290b613 | ||
|
|
5f6060b801 | ||
|
|
b39958604d | ||
|
|
22ca1491bc | ||
|
|
690d7e30cf | ||
|
|
4fac2f1094 | ||
|
|
eb0935d577 | ||
|
|
425fedf55b | ||
|
|
a33de09ae6 | ||
|
|
5018e56103 | ||
|
|
de2c975ac2 | ||
|
|
d86e2c0f59 | ||
|
|
14023163b3 | ||
|
|
d85dc8213e | ||
|
|
f6a34055ac | ||
|
|
3beb0d9c12 | ||
|
|
60968fe6f1 | ||
|
|
517c3101c7 | ||
|
|
998f03ba68 | ||
|
|
f3b0e0cfc7 | ||
|
|
a65e6c888c | ||
|
|
cd939d3379 | ||
|
|
29e5aeff5c | ||
|
|
0e5cf81fca | ||
|
|
61f217c610 | ||
|
|
ccb1f491c7 | ||
|
|
49fdac9b4e | ||
|
|
6b6a9490f6 | ||
|
|
2c783e9876 | ||
|
|
ecd971c0e5 | ||
|
|
c48875a6fb | ||
|
|
a245ccb8b7 | ||
|
|
75a18debcb | ||
|
|
1a15d3bb55 | ||
|
|
ffdcae47e3 | ||
|
|
34a2bc8590 | ||
|
|
8ae4d00ccb | ||
|
|
9ed6d5e7fb | ||
|
|
c9243b241c | ||
|
|
9c364a64e3 | ||
|
|
b34306205c | ||
|
|
77f778dbf9 | ||
|
|
23fa4e4161 | ||
|
|
5b6fd0b829 | ||
|
|
8fa8d7739f | ||
|
|
dc552c7a29 | ||
|
|
cf84f453d3 | ||
|
|
daf32a24bc | ||
|
|
34ecfd9857 | ||
|
|
4838564460 | ||
|
|
3dd2f67ff3 | ||
|
|
0ccec3dde0 | ||
|
|
ad77879caa | ||
|
|
27999983cc | ||
|
|
48b039940d | ||
|
|
84dc7d9d82 | ||
|
|
70d6332bad | ||
|
|
aec0b0c86a | ||
|
|
099f6a88c2 | ||
|
|
00a5d42d3d | ||
|
|
28f4a2cb67 | ||
|
|
1ac18ba10e | ||
|
|
1503ee2dfa | ||
|
|
484b528d4f | ||
|
|
758293dc02 | ||
|
|
6cb316b33e | ||
|
|
1d31831e7d | ||
|
|
ee32b07995 | ||
|
|
81f04ca692 | ||
|
|
1756997608 | ||
|
|
ec11ea4ca7 | ||
|
|
a1ebf85e1b | ||
|
|
4b2a734cda | ||
|
|
97ae8f78a0 | ||
|
|
834665c3ba | ||
|
|
d969dd2d8d | ||
|
|
3cb727561c | ||
|
|
cbc89376d3 | ||
|
|
66aa29e3d4 | ||
|
|
5c72aca5ee | ||
|
|
8824815d6d | ||
|
|
ad72e5cbdf | ||
|
|
b2f183fc17 | ||
|
|
9e16bc203a | ||
|
|
83c6eac017 | ||
|
|
d5b277a536 | ||
|
|
77ce6c33e3 | ||
|
|
60f8d349cf | ||
|
|
f539c3df70 | ||
|
|
fe1cf1758c | ||
|
|
8c71c26487 | ||
|
|
2ce39d1fd4 | ||
|
|
88a994b712 | ||
|
|
24c8cc1a0c | ||
|
|
caf684394c | ||
|
|
4b74510450 | ||
|
|
0ddcfc423e | ||
|
|
b4ac56e204 | ||
|
|
3b19ac3942 | ||
|
|
af52cad4ea | ||
|
|
d88396c1e2 | ||
|
|
4c5f7b9451 | ||
|
|
e601cedbb8 | ||
|
|
fa12dc867f | ||
|
|
acfb6c4578 | ||
|
|
e52d637092 | ||
|
|
2fba055696 | ||
|
|
88bb176f3b | ||
|
|
499eeb275b | ||
|
|
61a8d679bb | ||
|
|
2bbde91625 | ||
|
|
9ce098ee06 | ||
|
|
927ae57d24 | ||
|
|
d65c2d3539 | ||
|
|
2d9d8f32dc | ||
|
|
1a30cd5f4a | ||
|
|
f54687f398 | ||
|
|
9f4b4c5de1 | ||
|
|
33bfa94229 | ||
|
|
61e5f190a6 | ||
|
|
a4946272d0 | ||
|
|
8abd99d134 | ||
|
|
97e8ab7c5b | ||
|
|
cb930a7ab5 | ||
|
|
610f1f7dd4 | ||
|
|
516d0b4db8 | ||
|
|
df61ae5c6f | ||
|
|
9f6584b55e | ||
|
|
e4b41f5bb0 | ||
|
|
8d0cea93e9 | ||
|
|
d294d07919 | ||
|
|
153eea2bd5 | ||
|
|
571e3c5c13 | ||
|
|
a2d268f3d6 | ||
|
|
02d456d7a0 | ||
|
|
b3626ad86f | ||
|
|
9b6eab451f | ||
|
|
72be9b5f04 | ||
|
|
35e94a7f65 | ||
|
|
8e02586cc9 | ||
|
|
0b5a640896 | ||
|
|
64b5089909 | ||
|
|
e0e09bfa45 | ||
|
|
8c7f9f2c65 | ||
|
|
aff5cea949 | ||
|
|
8bd99f699f | ||
|
|
b0513fff8a | ||
|
|
f10db9d91a | ||
|
|
9b5fb17068 | ||
|
|
608d713dca | ||
|
|
6d5fc8ca21 | ||
|
|
8c3145b70f | ||
|
|
12a6f3e938 | ||
|
|
2c59435762 | ||
|
|
7336bf5c55 | ||
|
|
2b21b97277 | ||
|
|
603bc99a2f | ||
|
|
426ede1c99 | ||
|
|
21506c1d1b | ||
|
|
0b48b18e7e | ||
|
|
c8f6d9c7a1 | ||
|
|
ed8eced9dd | ||
|
|
4a029b7853 | ||
|
|
6bd9e58ece | ||
|
|
cd075fc8a6 | ||
|
|
e733427920 | ||
|
|
107da0ae22 | ||
|
|
3629d7a32c | ||
|
|
71e1152cde | ||
|
|
d01ab7fd23 | ||
|
|
f46d0a720c | ||
|
|
d943b4993a | ||
|
|
4a4f57d6ac | ||
|
|
085da16268 | ||
|
|
306f6b0037 | ||
|
|
3780d290fa | ||
|
|
ad7dc66f90 | ||
|
|
258244fed8 | ||
|
|
5f7982540f | ||
|
|
b1c0de4b77 | ||
|
|
45b3fecfff | ||
|
|
9774ded4dd | ||
|
|
7ec027854e | ||
|
|
b457001b42 | ||
|
|
6fc6866eb4 | ||
|
|
299e5528bc | ||
|
|
881524a5e4 | ||
|
|
ffc405138d | ||
|
|
f6ff74af16 | ||
|
|
73a12d47de | ||
|
|
83165df7e5 | ||
|
|
30074be5a5 | ||
|
|
16715aa309 | ||
|
|
53f3802a81 | ||
|
|
07626b5cc2 | ||
|
|
9ea603caf2 | ||
|
|
18ab9b80d2 | ||
|
|
71c1cb4e12 | ||
|
|
0c049f39d9 | ||
|
|
096b807c37 | ||
|
|
9018af4765 | ||
|
|
323f28e306 | ||
|
|
e9dbd00f42 | ||
|
|
98b5252a2d | ||
|
|
8abf42023a | ||
|
|
b792238f3f | ||
|
|
2486e5e75f | ||
|
|
5f113f1610 | ||
|
|
592e9dc3ff | ||
|
|
314316cee0 | ||
|
|
9cf663063e | ||
|
|
9ea9210a4b | ||
|
|
2bf1a94608 | ||
|
|
7a0236ad29 | ||
|
|
4341a0c198 | ||
|
|
8071612c12 | ||
|
|
ea9cc3da04 | ||
|
|
0df588fffb | ||
|
|
a622311dbc | ||
|
|
1c95ac2470 | ||
|
|
6ef22166f9 | ||
|
|
1107e50108 | ||
|
|
c19355a7b2 | ||
|
|
65d415fade | ||
|
|
b37313d3f9 | ||
|
|
58fcbad20a | ||
|
|
b571f331ec | ||
|
|
2547d4b3e7 | ||
|
|
892786bf0c | ||
|
|
0c51f2b583 | ||
|
|
d5607ca195 | ||
|
|
48cdf17a4f | ||
|
|
669a8fae15 | ||
|
|
d592936873 | ||
|
|
87898dd2f1 | ||
|
|
15c227f568 | ||
|
|
8de41acfe1 | ||
|
|
9680effb9f | ||
|
|
40df846e3f | ||
|
|
eee94fbf84 | ||
|
|
813679ba25 | ||
|
|
2b9e16c9c9 | ||
|
|
f9bb7f7cff | ||
|
|
41e9569094 | ||
|
|
336ee07191 | ||
|
|
81e0a145f1 | ||
|
|
a95a968fa8 | ||
|
|
6c08941d8b | ||
|
|
e13a5b3865 | ||
|
|
9011d5604a | ||
|
|
78aa4ca137 | ||
|
|
93111842df | ||
|
|
88693f8adc | ||
|
|
f904b012e9 | ||
|
|
cebe0a151f | ||
|
|
8fffad0891 | ||
|
|
fb1263dad7 | ||
|
|
28c5d6c10d | ||
|
|
e7a6f6836e | ||
|
|
f8ffe016d1 | ||
|
|
ec322f0966 | ||
|
|
0674709fc6 | ||
|
|
d91ac7ef21 | ||
|
|
2f0c3c7baf | ||
|
|
be68407707 | ||
|
|
f799a25aeb | ||
|
|
8329d045f1 | ||
|
|
503b289240 | ||
|
|
35e3bbb862 | ||
|
|
8dc261da79 | ||
|
|
65676f8988 | ||
|
|
730da3aa27 | ||
|
|
ff8674bca9 | ||
|
|
c7cfef3b61 | ||
|
|
32188b1cc0 | ||
|
|
37479d8c0d | ||
|
|
f5c7d6576d | ||
|
|
38c422bbc0 | ||
|
|
39d4e5ea81 | ||
|
|
4191ad1cbf | ||
|
|
29287da37c | ||
|
|
98c780415b | ||
|
|
756af9c699 | ||
|
|
7f9076bb1d | ||
|
|
2404f1ab9a | ||
|
|
64bcfd09ec | ||
|
|
6251d22c6e | ||
|
|
de1b4937a1 | ||
|
|
d092dd79ba | ||
|
|
a3ba968386 | ||
|
|
5ca2b97128 | ||
|
|
c9daad1cfd | ||
|
|
0526d5b53a | ||
|
|
34163fb8e4 | ||
|
|
98d2d661a8 | ||
|
|
d9f0a0fd74 | ||
|
|
d20d043e0f | ||
|
|
ce186e1872 | ||
|
|
a14da92e1d | ||
|
|
2b54d850ea | ||
|
|
a63c1b19fc | ||
|
|
34f74cd6ef | ||
|
|
c0b8e62135 | ||
|
|
ea24166b8e | ||
|
|
178b229d66 | ||
|
|
276493da65 | ||
|
|
e85dadfef2 | ||
|
|
1c70efb350 | ||
|
|
6804a0c564 | ||
|
|
6eb7ecc2d4 | ||
|
|
f4956b06b6 | ||
|
|
9a2f7c2660 | ||
|
|
b6017c558a | ||
|
|
62ed6c6a58 | ||
|
|
24966c812f | ||
|
|
ea8dc2e0af | ||
|
|
010b285e67 | ||
|
|
774231f347 | ||
|
|
ff1dfd8f25 | ||
|
|
2741ac195d | ||
|
|
cf780e296e | ||
|
|
0361246db0 | ||
|
|
63355ecd5b | ||
|
|
0f54ba59fb | ||
|
|
b67b243ebd | ||
|
|
4c29c20613 | ||
|
|
4c2619d948 | ||
|
|
ea5662a4a2 | ||
|
|
7c1ce777a1 | ||
|
|
3bb85f2d61 | ||
|
|
93e36b3113 | ||
|
|
932fb670e3 | ||
|
|
54dce61a95 | ||
|
|
e686c0e0a2 | ||
|
|
05acf32f39 | ||
|
|
67064012c9 | ||
|
|
10e93c3b1b | ||
|
|
5b2ec1cbb5 | ||
|
|
972f701c5c | ||
|
|
51285efbc3 | ||
|
|
e2635705f9 | ||
|
|
7762107543 | ||
|
|
9123ad89c8 | ||
|
|
39f3d6cc80 | ||
|
|
af5665f67c | ||
|
|
c2175cc692 | ||
|
|
1ef371a467 | ||
|
|
58461ff5bb | ||
|
|
665959da90 | ||
|
|
8e63974f94 | ||
|
|
eae86f54ba | ||
|
|
30128a122d | ||
|
|
56e22a39ac | ||
|
|
6ceb330baa | ||
|
|
f30509c7ba | ||
|
|
9489136bd6 | ||
|
|
05cd3d736b | ||
|
|
29b17772e5 | ||
|
|
6151d63eac | ||
|
|
e57aa68854 | ||
|
|
73fa6d9bd0 | ||
|
|
da3c4b87c1 | ||
|
|
0eedc1b128 | ||
|
|
db339d40de | ||
|
|
f72aa7884d | ||
|
|
1434f1bb40 | ||
|
|
6bc92b26a7 | ||
|
|
63937d0fba | ||
|
|
7b86edaf7f | ||
|
|
49d4e565c6 | ||
|
|
44c54ecc16 | ||
|
|
6e543f825d | ||
|
|
591800a28a | ||
|
|
3340d74e3f | ||
|
|
446d863106 | ||
|
|
8b2d27a916 | ||
|
|
9b31f2257d | ||
|
|
252ec98e24 | ||
|
|
77dde5711b | ||
|
|
94e891880b | ||
|
|
c414b3fad2 | ||
|
|
c0b63ee7a8 | ||
|
|
6fbbfbaa43 | ||
|
|
4d663e4500 | ||
|
|
88ba2436c9 | ||
|
|
bfda22ad57 | ||
|
|
b888f17672 | ||
|
|
8aa07e6f12 | ||
|
|
79e973af65 | ||
|
|
83c6fd43e5 | ||
|
|
1323d89912 | ||
|
|
6b89996b0b | ||
|
|
8a9a60410c | ||
|
|
09e8678369 | ||
|
|
2ff7efadc2 | ||
|
|
5f3fcd2f37 | ||
|
|
7dffae4620 | ||
|
|
ce2e7305d4 | ||
|
|
4783b09f03 | ||
|
|
15c089ca9c | ||
|
|
e93a2ddd93 | ||
|
|
57b9c40609 | ||
|
|
caaadd63ec | ||
|
|
ca1b707fab | ||
|
|
a0499451a5 | ||
|
|
bbdf1a0b30 | ||
|
|
bcd5230854 | ||
|
|
5ec3d2a587 | ||
|
|
9cb2be6ec4 | ||
|
|
4e90ca0b26 | ||
|
|
8c4c72c8b5 | ||
|
|
8690d4d7a9 | ||
|
|
d5d9c9b483 | ||
|
|
0de8129457 | ||
|
|
13f70ad42c | ||
|
|
49d7b568df | ||
|
|
93afcd5c0c | ||
|
|
07ef3582f7 | ||
|
|
53c7ff14df | ||
|
|
6b993688fc | ||
|
|
b9e667e155 | ||
|
|
3cd26781b0 | ||
|
|
2d20ac6f29 | ||
|
|
944d467654 | ||
|
|
f68271c3d7 | ||
|
|
4eb5070753 | ||
|
|
f57adc9cc4 | ||
|
|
3e0b7bfeff | ||
|
|
faa78c67d8 | ||
|
|
a5ed02eb1c | ||
|
|
bfe11110aa | ||
|
|
d8b1d2382d | ||
|
|
7881a13610 | ||
|
|
aaa328950e | ||
|
|
d8eb1a0a4f | ||
|
|
e3379b31cb | ||
|
|
f36ce3d245 | ||
|
|
c73f3385c0 | ||
|
|
2eb8b84bbd | ||
|
|
b31378ad1a | ||
|
|
53213bb553 | ||
|
|
36b446c012 | ||
|
|
ca70fc8dc8 | ||
|
|
18ff889b56 | ||
|
|
fab3c0df3e | ||
|
|
7e7f97d72a | ||
|
|
3a1fcf2865 | ||
|
|
fd85416088 | ||
|
|
18a6a9e965 | ||
|
|
eed8d9c61b | ||
|
|
cbe12adbe6 | ||
|
|
84f8931ddd | ||
|
|
db135e502c | ||
|
|
6f205f8adf | ||
|
|
e81c1df19f | ||
|
|
71577c9b0e | ||
|
|
c88c245094 | ||
|
|
30ce04bc84 | ||
|
|
205d8f7db2 | ||
|
|
d70c22d73f | ||
|
|
920161bc07 | ||
|
|
cdafefebd3 | ||
|
|
f220300212 | ||
|
|
a2b86acc22 | ||
|
|
8e53028d78 | ||
|
|
852dfa4abf | ||
|
|
5d6845a58a | ||
|
|
be846e69c5 | ||
|
|
eef8c06b39 | ||
|
|
54aa0fdb11 | ||
|
|
3c62a5d2b4 | ||
|
|
0fe70b660a | ||
|
|
4f787ddb03 | ||
|
|
be33752eb3 | ||
|
|
a88dbbe5ba | ||
|
|
9b4144b815 | ||
|
|
bce5b44275 | ||
|
|
9f7320fa67 | ||
|
|
e9310ee8dd | ||
|
|
be93db51f7 | ||
|
|
8e3e8ada32 | ||
|
|
4564a6e8fd | ||
|
|
240190fa8f | ||
|
|
4ca7c0d978 | ||
|
|
190d0f9304 | ||
|
|
b8d2a363fb | ||
|
|
2382785240 | ||
|
|
476994595c | ||
|
|
287969f169 | ||
|
|
ca93c52161 | ||
|
|
b126b19e03 | ||
|
|
2c907060d4 | ||
|
|
e28c0d0b4a | ||
|
|
918df735ed | ||
|
|
294936d137 | ||
|
|
115da03a23 | ||
|
|
cc085755aa | ||
|
|
cb5e3efd8a | ||
|
|
274fd0b528 | ||
|
|
1431d1fecd | ||
|
|
dc3d6bfc43 | ||
|
|
c9d4745a59 | ||
|
|
921aef7f2c | ||
|
|
3c772f1974 | ||
|
|
bee9c7ee17 | ||
|
|
75ca438f2f | ||
|
|
9ea6eed22f | ||
|
|
56f13c53ce | ||
|
|
fbc63c957a | ||
|
|
6052a9382b | ||
|
|
f7f05cfc8b | ||
|
|
f4754d7481 | ||
|
|
7dc8d0cfec | ||
|
|
846c393168 | ||
|
|
78b7f228f5 | ||
|
|
84e03f8b16 | ||
|
|
288580aed7 | ||
|
|
de63bbcc86 | ||
|
|
80b8c284b4 | ||
|
|
ffff6d701f | ||
|
|
0b084ece08 | ||
|
|
28855d3fd1 | ||
|
|
f7d356dc95 | ||
|
|
104b332906 | ||
|
|
8b30852639 | ||
|
|
bdaa14c257 | ||
|
|
0234fb62fb | ||
|
|
687ca781ab | ||
|
|
3053c797e8 | ||
|
|
62d6709949 | ||
|
|
410abaf92c | ||
|
|
5e07c478f5 | ||
|
|
3eda53a97f | ||
|
|
4e25382901 | ||
|
|
fccb172e20 | ||
|
|
5a761242f5 | ||
|
|
aaaf1e14a5 | ||
|
|
3dcb9f6424 | ||
|
|
04621ff64a | ||
|
|
5053808058 | ||
|
|
9912c673bf | ||
|
|
4636f7be7b | ||
|
|
0ffc0a1959 | ||
|
|
e86032b129 | ||
|
|
87941a0975 | ||
|
|
3d6a40a683 | ||
|
|
9753470dcb | ||
|
|
a45e1b4781 | ||
|
|
54f52d37ca | ||
|
|
6e295a7afb | ||
|
|
692cd7adae | ||
|
|
7794547d30 | ||
|
|
35eaaee627 | ||
|
|
8029901ed7 | ||
|
|
342c55043d | ||
|
|
3cf363667c | ||
|
|
2f8993995b | ||
|
|
8e9c541eba | ||
|
|
7ed310ffd2 | ||
|
|
bc1b020e95 | ||
|
|
5fdff16b1e | ||
|
|
43e22a9028 | ||
|
|
e102bf9eed | ||
|
|
bf3ee5c158 | ||
|
|
2e99e3d9c5 | ||
|
|
3d7029493a | ||
|
|
a6ad2bbc5b | ||
|
|
4dc17773c4 | ||
|
|
e5d66f46c6 | ||
|
|
d2fc24e792 | ||
|
|
ec52c81f46 | ||
|
|
0eb0c4cc83 | ||
|
|
b54e6bc933 | ||
|
|
83f891138d | ||
|
|
c621940b0f | ||
|
|
a27b0a0a1e | ||
|
|
23a52dbc9a | ||
|
|
f8a57c7885 | ||
|
|
a295d0ad1e | ||
|
|
190a2147cc | ||
|
|
49573b9e72 | ||
|
|
e60b30ed44 | ||
|
|
5c10ddf54c | ||
|
|
600ece819f | ||
|
|
6bc7a3d8aa | ||
|
|
0af07e5352 | ||
|
|
8732f54c64 | ||
|
|
437d5d76e9 | ||
|
|
7378be05d3 | ||
|
|
75febe4b75 | ||
|
|
f6d8d93a1b | ||
|
|
130310fddd | ||
|
|
8bd312046d | ||
|
|
9cc96f45d0 | ||
|
|
c18fc1984d | ||
|
|
3b651076d1 | ||
|
|
352396bdc2 | ||
|
|
3c9b0273d4 | ||
|
|
5122f9989c | ||
|
|
8ebd22da96 | ||
|
|
c2d55013a6 | ||
|
|
25eda1378e | ||
|
|
dfac7bde9c | ||
|
|
348168d6c0 | ||
|
|
a1c772c8d8 | ||
|
|
f1ce1b5361 | ||
|
|
ebdc7d70a1 | ||
|
|
c5947c23bb | ||
|
|
eeb1852d95 | ||
|
|
2f28a635a9 | ||
|
|
d524c77560 | ||
|
|
0025d94aa4 | ||
|
|
bb5ae2922d | ||
|
|
fbe9fe1ba8 | ||
|
|
007194d674 | ||
|
|
cdd74505d7 | ||
|
|
0aa023189d | ||
|
|
79aa10dfcb | ||
|
|
9ecfda0110 | ||
|
|
b89e26c460 | ||
|
|
97e5952544 | ||
|
|
8627328047 | ||
|
|
ec0c912c6f | ||
|
|
953de3f1f2 | ||
|
|
e1264de514 | ||
|
|
5abd2b400c | ||
|
|
2c2a103676 | ||
|
|
44af799b66 | ||
|
|
ec22ba3248 | ||
|
|
7fcc05c037 | ||
|
|
edf17d0a46 | ||
|
|
9cccd0bfc9 | ||
|
|
e57c010e3d | ||
|
|
4dfcf1c1c8 | ||
|
|
8d7786e97d | ||
|
|
2cb519cd06 | ||
|
|
bc46ad4331 | ||
|
|
be08e8f23b | ||
|
|
f937809903 | ||
|
|
c0f32c942d | ||
|
|
39c5c830bb | ||
|
|
83c8953d1b | ||
|
|
4b100a5a64 | ||
|
|
b7e50e0b3a | ||
|
|
6933052de7 | ||
|
|
22d945f7b7 | ||
|
|
b81c5628ce | ||
|
|
cdb4576bd7 | ||
|
|
4859cd7dcf | ||
|
|
3f70593ca8 | ||
|
|
676288e6c0 | ||
|
|
1aa3e0cc5a | ||
|
|
b0f8064d0d | ||
|
|
e5e85732d4 | ||
|
|
f97c1ef0d9 | ||
|
|
83cf815160 | ||
|
|
fea62a529b | ||
|
|
2cff5ae2bb | ||
|
|
8aa29f5340 | ||
|
|
8051bfef1d | ||
|
|
85bc79ab1b | ||
|
|
97e5588184 | ||
|
|
e622fdc885 | ||
|
|
4f81e1debe | ||
|
|
9ccfd852d8 | ||
|
|
9df57a47d5 | ||
|
|
f2cadb7278 | ||
|
|
3f6e7cb84c | ||
|
|
4ed4ce8240 | ||
|
|
4373956a3f | ||
|
|
36fb99a00d | ||
|
|
8f5d325c4d | ||
|
|
8d8b6da0bf | ||
|
|
3a61d260d7 | ||
|
|
5bc433d1c8 | ||
|
|
a0e4bbac6f | ||
|
|
d44d8cc53d | ||
|
|
1305969247 | ||
|
|
94becebafa | ||
|
|
8add433966 | ||
|
|
c5b289fb1f | ||
|
|
f85ce180ed | ||
|
|
8101fa1c92 | ||
|
|
8a091e0205 | ||
|
|
edc3a54ad3 | ||
|
|
a3cd7d1068 | ||
|
|
337441b8de | ||
|
|
bd78a66bd4 | ||
|
|
96f9618081 | ||
|
|
cf7be2d601 | ||
|
|
598732915e | ||
|
|
99c18396ab | ||
|
|
6d5dd81066 | ||
|
|
7cd9451a22 | ||
|
|
29b5a7c5c2 | ||
|
|
393a04165e | ||
|
|
e97e834a5b | ||
|
|
bec685682b | ||
|
|
34f119ca23 | ||
|
|
09ed1b1f9e | ||
|
|
fcb109f46d | ||
|
|
f30823e4ac | ||
|
|
c04885449d | ||
|
|
8c31e47eeb | ||
|
|
d8ee5c180b | ||
|
|
823f4a26b3 | ||
|
|
a05e8a446d | ||
|
|
21126f766c | ||
|
|
8f46ead756 | ||
|
|
75652fc2c4 | ||
|
|
7cdc46f007 | ||
|
|
ed9f9625ae | ||
|
|
7b60d3dab9 | ||
|
|
a6993fa489 | ||
|
|
bc2774bde4 | ||
|
|
d10dc1e8d3 | ||
|
|
48556de92b | ||
|
|
7f6b477d2e | ||
|
|
11d8c67d12 | ||
|
|
fd2a4029e7 | ||
|
|
4467928845 | ||
|
|
cc85a00bfd | ||
|
|
15d58ecdcd | ||
|
|
08d93b9a78 | ||
|
|
a75a999e3b | ||
|
|
684ef709f5 | ||
|
|
2e98dd09e7 | ||
|
|
6635425bbc | ||
|
|
5d4bdc5697 | ||
|
|
0fdb286005 | ||
|
|
59a8493aa7 | ||
|
|
25378e894b | ||
|
|
3b9fea20b6 | ||
|
|
c6bb8f09ca | ||
|
|
4a9bd84bf0 | ||
|
|
c02522b0fe | ||
|
|
c2a71ef756 | ||
|
|
e669110cf4 | ||
|
|
f4cf31c13d | ||
|
|
7b23a5dcce | ||
|
|
b2fda0c79d | ||
|
|
5af96f5ccb | ||
|
|
ca445ac178 | ||
|
|
16fb31b6eb | ||
|
|
a1ff325b7b | ||
|
|
5eaec4c841 | ||
|
|
ffcc34c4f9 | ||
|
|
2dbe33e769 | ||
|
|
60c7db0733 | ||
|
|
0ed95bbdf1 | ||
|
|
c901bcf9b7 | ||
|
|
0ccf915a18 | ||
|
|
52b1c0a926 | ||
|
|
399865e6c8 | ||
|
|
54aeab1524 | ||
|
|
91f83277e2 | ||
|
|
c937cb2f07 | ||
|
|
ebd150e473 | ||
|
|
9218f7b82c | ||
|
|
edaf7aee5d | ||
|
|
43c18d0f4d | ||
|
|
65d85f7479 | ||
|
|
476e23db5b | ||
|
|
abaa5d87f6 | ||
|
|
ce3e7e623c | ||
|
|
3fd35a9c18 | ||
|
|
f170ae741e | ||
|
|
03562b037d | ||
|
|
472312709a | ||
|
|
b68463249e | ||
|
|
740a2da702 | ||
|
|
85c8e56417 | ||
|
|
481ef56e74 | ||
|
|
008795770f | ||
|
|
834fb7e317 | ||
|
|
da4827f287 | ||
|
|
9f4439583d | ||
|
|
69981e4d78 | ||
|
|
a857c6a88f | ||
|
|
e8d19439f8 | ||
|
|
56216250a7 | ||
|
|
bea331db26 | ||
|
|
bc4e833a47 | ||
|
|
83f399fffc | ||
|
|
5214436d18 | ||
|
|
8603250d73 | ||
|
|
9a8a099701 | ||
|
|
a5a0c8c837 | ||
|
|
604bcd5874 | ||
|
|
d29f9409bf | ||
|
|
b5a0f5910d | ||
|
|
ccb2600e67 | ||
|
|
f06e21ff5a | ||
|
|
bb0817a2ec | ||
|
|
6911f865ca | ||
|
|
fe28b2732c | ||
|
|
e8e8c37496 | ||
|
|
ef0f1ca1e7 | ||
|
|
31ca34b954 | ||
|
|
c4e6a2f0a8 | ||
|
|
b56845e200 | ||
|
|
d7a1fee781 | ||
|
|
b1f802c42d | ||
|
|
5f022e6e1f | ||
|
|
392cbb817e | ||
|
|
130399a1e7 | ||
|
|
37d5531737 | ||
|
|
f0b6cbaf89 | ||
|
|
707b173e77 | ||
|
|
4381bb5026 | ||
|
|
5850ad1217 | ||
|
|
806d598a04 | ||
|
|
e737e5c950 | ||
|
|
bbcde2f52b | ||
|
|
f6ef77429c | ||
|
|
44491c1514 | ||
|
|
71a6cf4ee6 | ||
|
|
744ce6966f | ||
|
|
d25cec02c2 | ||
|
|
f02bf37fd3 | ||
|
|
304b9d41d7 | ||
|
|
2d6af89f60 | ||
|
|
d6425973e2 | ||
|
|
d5ad56c4de | ||
|
|
4f1f2cc99e | ||
|
|
da439dd127 | ||
|
|
1375d01bdf | ||
|
|
7b9db07f13 | ||
|
|
f2f26136c1 | ||
|
|
0f60ac5acf | ||
|
|
c28f19fe8a | ||
|
|
09a6dbc755 | ||
|
|
3bc0e0fc8a | ||
|
|
eb0e187a54 | ||
|
|
a788d30f34 | ||
|
|
591dfc961e | ||
|
|
809e12b034 | ||
|
|
1669d174e1 | ||
|
|
3cfd28de43 | ||
|
|
4888207eca | ||
|
|
294cb96107 | ||
|
|
b648fa2b70 | ||
|
|
ab99122211 | ||
|
|
dd014fee88 | ||
|
|
c81f864de3 | ||
|
|
90fe7dceec | ||
|
|
3a568096f2 | ||
|
|
94e694fc61 | ||
|
|
bdfa6e4af5 | ||
|
|
8e64ffb4f6 | ||
|
|
6c162643cb | ||
|
|
ff7742bca3 | ||
|
|
9685884279 | ||
|
|
a8b0d7f999 | ||
|
|
dd84233085 | ||
|
|
fe3eac07f4 | ||
|
|
0948686989 | ||
|
|
4c82970319 | ||
|
|
fe2fede8ed | ||
|
|
b23b1e5f1f | ||
|
|
dca66c8de8 | ||
|
|
49090014cc | ||
|
|
fa4f100705 | ||
|
|
bbf68cd9a8 | ||
|
|
bbc5f6588a | ||
|
|
b3632a6a35 | ||
|
|
3943c51bb4 | ||
|
|
4c19ddde69 | ||
|
|
d8f0f1a1d3 | ||
|
|
121c0d89f2 | ||
|
|
badfb9088e | ||
|
|
ff392fee14 | ||
|
|
a13693161a | ||
|
|
4b8ac81669 | ||
|
|
219a704ee0 | ||
|
|
3996cd1f08 | ||
|
|
c636b0a0ec | ||
|
|
aae9f671f0 | ||
|
|
aec6e901ee | ||
|
|
f49be25288 | ||
|
|
f9a96126e1 | ||
|
|
8c9f58b939 | ||
|
|
8a7e787f42 | ||
|
|
148dcc084d | ||
|
|
15b1cbd762 | ||
|
|
e9b7ca3697 | ||
|
|
1b03e9a3ee | ||
|
|
2b951e3f61 | ||
|
|
f51e064cf6 | ||
|
|
9640e93895 | ||
|
|
d5bd22040c | ||
|
|
dcdcb7521a | ||
|
|
4058c63884 | ||
|
|
dd34548cc6 | ||
|
|
a6b5211fa7 | ||
|
|
4e89b9c363 | ||
|
|
f3e267d2d0 | ||
|
|
2fd87dc1f1 | ||
|
|
40b6b77cfa | ||
|
|
d0c61dbf4d | ||
|
|
1cd5a3fcf7 | ||
|
|
af81cf2c50 | ||
|
|
04373c5d1b | ||
|
|
e9cdb0f78d | ||
|
|
021b933ad3 | ||
|
|
9fd067c9da | ||
|
|
d3f3f3bdf7 | ||
|
|
72727dacd8 | ||
|
|
caeb2bc4e3 | ||
|
|
13974b601f | ||
|
|
cbc6aea8b4 | ||
|
|
290a2f7ccd | ||
|
|
349e80f206 | ||
|
|
5c1e001a73 | ||
|
|
f312318fab | ||
|
|
dc04b7cf09 | ||
|
|
77a8a46d8e | ||
|
|
95c7cd55c2 | ||
|
|
5f0ef5e0e8 | ||
|
|
1f26c603e0 | ||
|
|
7e2227ad42 | ||
|
|
9b4899da07 | ||
|
|
83c88ac0c6 | ||
|
|
44623065b4 | ||
|
|
47c7c8177d | ||
|
|
bde7a5ff59 | ||
|
|
a8ad8644c8 | ||
|
|
4e91bb88a5 | ||
|
|
f60a90e2da | ||
|
|
784dc0f6a7 | ||
|
|
e80e627fba | ||
|
|
d5987c51c9 | ||
|
|
5ced441b17 | ||
|
|
57801202fd | ||
|
|
a019399c3c | ||
|
|
e6f610a86c | ||
|
|
7ef528bbde | ||
|
|
a351a29bf3 | ||
|
|
983d258bce | ||
|
|
f6d38dd5e0 | ||
|
|
d51245aada | ||
|
|
56cf51f0f9 | ||
|
|
eb40fb9c5d | ||
|
|
085da0cea7 | ||
|
|
5539b19938 | ||
|
|
94feb762ca | ||
|
|
40b59d5a5a | ||
|
|
9ffd147470 | ||
|
|
3fea4ad2ba | ||
|
|
1ab5536879 | ||
|
|
9690a89a6d | ||
|
|
8f895f4349 | ||
|
|
980c0aa1d7 | ||
|
|
52fd6ca513 | ||
|
|
eb5dd2ff2e | ||
|
|
7ca35452eb | ||
|
|
dd781e256c | ||
|
|
551a7ab82f | ||
|
|
2901287d9e | ||
|
|
2b714967aa | ||
|
|
e8734ef1e7 | ||
|
|
14b9f9509f | ||
|
|
b1f973d304 | ||
|
|
2f17bfd71c | ||
|
|
b6670ee23a | ||
|
|
f1036df1f6 | ||
|
|
5c3e815757 | ||
|
|
55e780d885 | ||
|
|
d502df7d56 | ||
|
|
beb6cc8c0f | ||
|
|
c99db5e75c | ||
|
|
65cd70a85b | ||
|
|
5166eab5ee | ||
|
|
232f6f158d | ||
|
|
2a07ceba62 | ||
|
|
3e4b8c7dd4 | ||
|
|
26138e213f | ||
|
|
d82796e3ad | ||
|
|
cdcb81c867 | ||
|
|
5669e8f060 | ||
|
|
d84a40b4dc | ||
|
|
591be43763 | ||
|
|
97d0686354 | ||
|
|
e2da05b197 | ||
|
|
4f0052043d | ||
|
|
cfc1d1a2db | ||
|
|
9957e6ef17 | ||
|
|
6a02c8383c | ||
|
|
bc0a4ee68d | ||
|
|
1ca615da77 | ||
|
|
7da0cee29a | ||
|
|
f25bccd19f | ||
|
|
4e5a2e012c | ||
|
|
c9ee2a92a3 | ||
|
|
95a7938328 | ||
|
|
baedcdb2c1 | ||
|
|
bc06b3671a | ||
|
|
a9172811ca | ||
|
|
91b1fd6d07 | ||
|
|
bab7b8b9ed | ||
|
|
1b7fb96ca8 | ||
|
|
6d84b8c02f | ||
|
|
3e3749f011 | ||
|
|
52384fb3a5 | ||
|
|
e401670087 | ||
|
|
ae0b4c59cc | ||
|
|
0a8dc8afcc | ||
|
|
d59b94df66 | ||
|
|
e28502454b | ||
|
|
bbf73f0937 | ||
|
|
592519c45c | ||
|
|
cc904ba9dc | ||
|
|
1679ba6719 | ||
|
|
07fadd4a6c | ||
|
|
57e1ff39e0 | ||
|
|
51e259c198 | ||
|
|
de334b003d | ||
|
|
a61ff12390 | ||
|
|
f4697be159 | ||
|
|
3835fe3960 | ||
|
|
76c374ef06 | ||
|
|
57d24bd948 | ||
|
|
deff14dfd8 | ||
|
|
0a479be370 | ||
|
|
ba6a2e3fd2 | ||
|
|
c3a395a41e | ||
|
|
a3136a19e9 | ||
|
|
b631568003 | ||
|
|
1d0c03eca4 | ||
|
|
eb30525a26 | ||
|
|
3e66ea3f56 | ||
|
|
9f1189e606 | ||
|
|
698927bed4 | ||
|
|
8fd02ee8dd | ||
|
|
f3154e8f5e | ||
|
|
878af163a9 | ||
|
|
da8341d014 | ||
|
|
95c33e88ed | ||
|
|
fed8369a5f | ||
|
|
d85806e3d5 | ||
|
|
097660ce53 | ||
|
|
b08cfbbb35 | ||
|
|
97ee3c47a0 | ||
|
|
05918de6ab | ||
|
|
8d7abd8298 | ||
|
|
727f4c3bb5 | ||
|
|
7372ad0cc4 | ||
|
|
ca6884dbca | ||
|
|
1ebb0ac5fb | ||
|
|
bf0e53f13b | ||
|
|
8888a960c0 | ||
|
|
04be41aac5 | ||
|
|
fd1313d49f | ||
|
|
3599dba5c3 | ||
|
|
67edc437d2 | ||
|
|
7a39d9240c | ||
|
|
6d2d9aed7e | ||
|
|
3c2e5f22b6 | ||
|
|
ddb6348bfd | ||
|
|
d70c5df5a0 | ||
|
|
b2799d198c | ||
|
|
f2fa2038b1 | ||
|
|
bfd59e64ea | ||
|
|
60e05e270a | ||
|
|
242b9a6af9 | ||
|
|
e7e8ad9bdc | ||
|
|
0e5d164a8a | ||
|
|
7293a8d3c0 | ||
|
|
097a4647a8 | ||
|
|
0942a7f3ff | ||
|
|
2df4370477 | ||
|
|
7243963106 | ||
|
|
9f17654052 | ||
|
|
1a65d7061d | ||
|
|
292363eb65 | ||
|
|
07e79ce61a | ||
|
|
1cf7dac82b | ||
|
|
6003b2902f | ||
|
|
ff0023a889 | ||
|
|
8c6bf5a1cc | ||
|
|
61c97ab940 | ||
|
|
b0a1b3b5ca | ||
|
|
4c7af01edc | ||
|
|
0d431213cd | ||
|
|
ad9dd9a2e2 | ||
|
|
c7895ed905 | ||
|
|
57d7979d51 | ||
|
|
61f6871cd1 | ||
|
|
406048f7b9 | ||
|
|
6dd5b0fe45 | ||
|
|
fd4bc5e3cf | ||
|
|
3bab2d8957 | ||
|
|
af2f5cd2e1 | ||
|
|
d4bb32da82 | ||
|
|
08aca6ca61 | ||
|
|
697b3c6772 | ||
|
|
878525ced8 | ||
|
|
418531736b | ||
|
|
6c175440c6 | ||
|
|
723a2f2008 | ||
|
|
ea9b9fbf17 | ||
|
|
303e257841 | ||
|
|
07b6a8ba92 | ||
|
|
e216e18368 | ||
|
|
e57c5b4bc2 | ||
|
|
f772d6ddeb | ||
|
|
cd37c301d9 | ||
|
|
f5fa26639e | ||
|
|
45ec73c115 | ||
|
|
13952ebd8b | ||
|
|
2df0007a10 | ||
|
|
89bc5db3e1 | ||
|
|
4021deec7f | ||
|
|
a3977f37f7 | ||
|
|
766c12242e | ||
|
|
a82b951aff | ||
|
|
997a94eecc | ||
|
|
635535aac2 | ||
|
|
25314fd91a | ||
|
|
9a06a5758d | ||
|
|
e5a2a9644f | ||
|
|
e0e7211852 | ||
|
|
cdaeb4d176 | ||
|
|
07aa2e280d | ||
|
|
6c4bc67ff3 | ||
|
|
d9f0cc27ef | ||
|
|
59aec9d289 | ||
|
|
451f4c503d | ||
|
|
cd82089d4d | ||
|
|
3db8b63cde | ||
|
|
6edf5d1e4f | ||
|
|
a23fa6fadd | ||
|
|
51eb77e409 | ||
|
|
691326cea8 | ||
|
|
3cac1238ed | ||
|
|
b04512a4f6 | ||
|
|
3a4d0549aa | ||
|
|
d7467f5dc3 | ||
|
|
141902b86d | ||
|
|
5aa680fc93 | ||
|
|
a790117f5a | ||
|
|
2a5a52c810 | ||
|
|
2156f4c2f3 | ||
|
|
2585460286 | ||
|
|
1b4af66986 | ||
|
|
0324bac044 | ||
|
|
2bfbe1ca27 | ||
|
|
60584228d9 | ||
|
|
44e34f7b11 | ||
|
|
7912050647 | ||
|
|
2231abd407 | ||
|
|
8d17ba4d66 | ||
|
|
8244bdb440 | ||
|
|
bc3b7ffd86 | ||
|
|
e22cb62493 | ||
|
|
e923aa1f72 | ||
|
|
68a21f1fbb | ||
|
|
74209e2607 | ||
|
|
1a38271104 | ||
|
|
e5ab5d6a5a | ||
|
|
2745ff727f | ||
|
|
24ea383937 | ||
|
|
9cb11d4d5f | ||
|
|
7202ea3340 | ||
|
|
d234ea01d0 | ||
|
|
e246cd37dd | ||
|
|
0a4ddedac9 | ||
|
|
0e4b80b002 | ||
|
|
a460a0dc44 | ||
|
|
f7212112b8 | ||
|
|
86d55b32a6 | ||
|
|
4b0d1aac15 | ||
|
|
fb7c06aa01 | ||
|
|
5c41be3470 | ||
|
|
a78df1a152 | ||
|
|
34e8db0fe3 | ||
|
|
70faecb8b5 | ||
|
|
237f278479 | ||
|
|
e766a00a12 | ||
|
|
ea03637ec1 | ||
|
|
d0f6d47b14 | ||
|
|
5bf1658d9a | ||
|
|
4ce9fb5b5a | ||
|
|
f80f02da73 | ||
|
|
52ece833a7 | ||
|
|
1ad6dde146 | ||
|
|
64bcfd23bd | ||
|
|
d659447879 | ||
|
|
e73eb55d75 | ||
|
|
a52cea29f4 | ||
|
|
3d91e59386 | ||
|
|
393b1d7674 | ||
|
|
c29a6b95ae | ||
|
|
567a4e8361 | ||
|
|
4d3e4c1a15 | ||
|
|
afeecdf4af | ||
|
|
4fe7105e2f | ||
|
|
d7c3a53f2d | ||
|
|
58e7574a6a | ||
|
|
94ab703c7c | ||
|
|
90350786e6 | ||
|
|
8038e1ee7d | ||
|
|
65f1a5fed6 | ||
|
|
64b94ab124 | ||
|
|
c03cb1de5e | ||
|
|
f0004290b4 | ||
|
|
7e1b49a742 | ||
|
|
d26c56e467 | ||
|
|
a51e0c26e5 | ||
|
|
180d39534c | ||
|
|
203d0e870a | ||
|
|
bed7d09764 | ||
|
|
3c762c9a02 | ||
|
|
ba5f0fb70b | ||
|
|
ebc3e05f23 | ||
|
|
674ad899f9 | ||
|
|
d945eb6fcf | ||
|
|
fb170e3c42 | ||
|
|
3658cca3e5 | ||
|
|
5e78a41b75 | ||
|
|
c23d8efe08 | ||
|
|
0d3d4b60ce | ||
|
|
38091c3c25 | ||
|
|
f0cd04ebc3 | ||
|
|
3295c18829 | ||
|
|
ccb309fd92 | ||
|
|
8206a26267 | ||
|
|
8c7b91bd79 | ||
|
|
061460f978 | ||
|
|
a47c7b8f27 | ||
|
|
a859908e83 | ||
|
|
86759d2f9c | ||
|
|
58c145ba08 | ||
|
|
c0bb3da22f | ||
|
|
031338d84d | ||
|
|
2a619f3fba | ||
|
|
1c145cbc09 | ||
|
|
5d71fde51b | ||
|
|
eec29f5c68 | ||
|
|
8fbd7acf2a | ||
|
|
7b824e6178 | ||
|
|
24c69a26ea | ||
|
|
7b03c95cfd | ||
|
|
d2b09ecfda | ||
|
|
4cdd42f391 | ||
|
|
4d0b3b287f | ||
|
|
0d7f784773 | ||
|
|
66ad54fbf0 | ||
|
|
33782859b8 | ||
|
|
3d294d5771 | ||
|
|
c889a83707 | ||
|
|
6a55af1950 | ||
|
|
a85cf2c2af | ||
|
|
4c0e3f92d3 | ||
|
|
e3354a805e | ||
|
|
ceb9a935de | ||
|
|
d3ad111b85 | ||
|
|
de740b1d9d | ||
|
|
7c155545ae | ||
|
|
a7597b2a6d | ||
|
|
22916c1904 | ||
|
|
a72e479e50 | ||
|
|
60c1776994 | ||
|
|
fbddb24728 | ||
|
|
e716f9e63a | ||
|
|
51c49d7c1b | ||
|
|
17a1b11f66 | ||
|
|
8a8e68146f | ||
|
|
dbe5dea525 | ||
|
|
604af822aa | ||
|
|
435759c21c | ||
|
|
847f094975 | ||
|
|
b939d86975 | ||
|
|
3e5f56b19b | ||
|
|
d81f42e3e9 | ||
|
|
4fb3d34786 | ||
|
|
9a0a527b2b | ||
|
|
ba8fb23b9d | ||
|
|
99790695a2 | ||
|
|
e3a86bb150 | ||
|
|
0ac1e885bd | ||
|
|
36aca1d39b | ||
|
|
d0192b4cab | ||
|
|
a3a7a585d7 | ||
|
|
b437d858f0 | ||
|
|
06f78178da | ||
|
|
06412cc5c0 | ||
|
|
1b86b9170f | ||
|
|
6e71d34390 | ||
|
|
0e880f2479 | ||
|
|
400a35de1d | ||
|
|
c47bfce010 | ||
|
|
73db7b399e | ||
|
|
f8a47c3c6a | ||
|
|
ee119de6c4 | ||
|
|
f37c0ed612 | ||
|
|
6b1d671aed | ||
|
|
44bbcd7033 | ||
|
|
e56dcba9d3 | ||
|
|
e8a2130094 | ||
|
|
be13c14376 | ||
|
|
6f0d0bed0b |
101
Makefile.gcj
Normal file
101
Makefile.gcj
Normal file
@@ -0,0 +1,101 @@
|
||||
# Makefile for building native I2P binaries and libraries with GCJ
|
||||
#
|
||||
# WARNING: Do not use this yet, as it may explode (etc).
|
||||
#
|
||||
GCJ=gcj #/usr/local/gcc-4.0.2/bin/gcj
|
||||
EXTRA_LD_PATH= #/usr/local/gcc-4.0.2/lib
|
||||
ANT=ant #/opt/apache-ant-1.6.5/bin/ant
|
||||
ANT_TARGET=buildclean
|
||||
NATIVE_DIR=native
|
||||
|
||||
##
|
||||
# Define what jar files get into libi2p.so. The current setup is
|
||||
# *incredibly* lazy, throwing everything in the .so, rather than
|
||||
# give each .jar file its own .so.
|
||||
# i2p.jar: base SDK
|
||||
# mstreaming.jar: streaming API
|
||||
# streaming.jar: full streaming lib implementation
|
||||
# i2ptunnel.jar: I2PTunnel proxy
|
||||
# sam.jar: SAM bridge and API
|
||||
# i2psnark.jar: bittorrent client
|
||||
# router.jar: full I2P router
|
||||
# jbigi.jar: collection of native optimized GMP routines for crypto
|
||||
JAR_BASE=i2p.jar mstreaming.jar streaming.jar
|
||||
JAR_CLIENTS=i2ptunnel.jar sam.jar
|
||||
JAR_ROUTER=router.jar
|
||||
JAR_JBIGI=jbigi.jar
|
||||
JAR_XML=xml-apis.jar resolver.jar xercesImpl.jar
|
||||
JAR_CONSOLE=\
|
||||
i2psnark.jar \
|
||||
javax.servlet.jar \
|
||||
commons-el.jar \
|
||||
commons-logging.jar \
|
||||
jasper-runtime.jar \
|
||||
ant-apache-bcel.jar \
|
||||
ant.jar \
|
||||
jasper-compiler.jar \
|
||||
org.mortbay.jetty.jar \
|
||||
routerconsole.jar
|
||||
JAR_SUCKER=jdom.jar rome-0.7.jar sucker.jar
|
||||
LIBI2P_JARS=${JAR_BASE} ${JAR_CLIENTS} ${JAR_ROUTER} ${JAR_JBIGI}
|
||||
# unfortunately, its not quite ready for most end users, as the
|
||||
# ${JAR_CONSOLE} fails to compile with:
|
||||
# org/apache/commons/logging/impl/LogKitLogger.java: In class 'org.apache.commons.logging.impl.LogKitLogger':
|
||||
# .../LogKitLogger.java: In constructor '(java.lang.String)':
|
||||
# .../LogKitLogger.java:91: error: cannot find file for class org.apache.log.Hierarchy
|
||||
# .../LogKitLogger.java:91: error: cannot find file for class org.apache.log.Hierarchy
|
||||
# .../LogKitLogger.java:104: error: cannot find file for class org.apache.log.Hierarchy
|
||||
# .../LogKitLogger.java:104: confused by earlier errors, bailing out
|
||||
|
||||
#${JAR_CONSOLE}\
|
||||
#${JAR_XML} \
|
||||
#${JAR_SUCKER}
|
||||
#${JAR_CONSOLE}
|
||||
|
||||
SYSTEM_PROPS=-DloggerFilenameOverride=logs/log-router-@.txt \
|
||||
-Dorg.mortbay.http.Version.paranoid=true \
|
||||
-Dorg.mortbay.util.FileResource.checkAliases=false \
|
||||
-Dorg.mortbay.xml.XmlParser.NotValidating=true
|
||||
#SYSTEM_PROPS=-Di2p.weakPRNG=true
|
||||
OPTIMIZE=-O2
|
||||
#OPTIMIZE=-O3
|
||||
|
||||
LD_LIBRARY_PATH=${EXTRA_LD_PATH}:.
|
||||
|
||||
all: jars native
|
||||
@echo "* Build complete"
|
||||
|
||||
jars:
|
||||
@${ANT} ${ANT_TARGET}
|
||||
|
||||
clean: native_clean
|
||||
|
||||
native: native_clean native_shared
|
||||
@echo "* Native code build in ${NATIVE}"
|
||||
|
||||
native_clean:
|
||||
@rm -rf ${NATIVE_DIR}
|
||||
@mkdir ${NATIVE_DIR}
|
||||
|
||||
native_shared: libi2p.so
|
||||
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2p_dsa --main=net.i2p.crypto.DSAEngine
|
||||
@echo "* i2p_dsa is a simple test app with the DSA engine and Fortuna PRNG to make sure crypto is working"
|
||||
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/prng --main=gnu.crypto.prng.FortunaStandalone
|
||||
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2ptunnel --main=net.i2p.i2ptunnel.I2PTunnel
|
||||
@echo "* i2ptunnel is mihi's I2PTunnel CLI"
|
||||
@echo " run it as ./i2ptunnel -cli to avoid awt complaints"
|
||||
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2ptunnelctl --main=net.i2p.i2ptunnel.TunnelControllerGroup
|
||||
@echo "* i2ptunnelctl is a controller for I2PTunnel, reading i2ptunnel.config"
|
||||
@echo " and launching the appropriate proxies"
|
||||
#@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2psnark --main=org.klomp.snark.Snark
|
||||
#@echo "* i2psnark is an anonymous bittorrent client"
|
||||
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2prouter --main=net.i2p.router.Router
|
||||
@echo "* i2prouter is the main I2P router"
|
||||
@echo " it can be used, and while the router console won't load,"
|
||||
@echo " i2ptunnel will, so it will start all the proxies defined in i2ptunnel.config"
|
||||
|
||||
libi2p.so:
|
||||
@echo "* Building libi2p.so"
|
||||
@(cd build ; time ${GCJ} ${OPTIMIZE} -fPIC -fjni -shared -o ../${NATIVE_DIR}/libi2p.so ${LIBI2P_JARS} ; cd .. )
|
||||
@ls -l ${NATIVE_DIR}/libi2p.so
|
||||
@echo "* libi2p.so built"
|
||||
43
apps/addressbook/README.txt
Normal file
43
apps/addressbook/README.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
addressbook v2.0.2 - A simple name resolution mechanism for I2P
|
||||
|
||||
addressbook is a simple implementation of subscribable address books for I2P.
|
||||
Addresses are stored in userhosts.txt and a second copy of the address book is
|
||||
placed on your eepsite as hosts.txt.
|
||||
|
||||
subscriptions.txt contains a list of urls to check for new addresses.
|
||||
Since the urls are checked in order, and conflicting addresses are not added,
|
||||
addressbook.subscriptions can be considered to be ranked in order of trust.
|
||||
|
||||
The system created by addressbook is similar to the early days of DNS,
|
||||
when everyone ran a local name server. The major difference is the lack of
|
||||
authority. Name cannot be guaranteed to be globally unique, but in practise
|
||||
they probably will be, for a variety of social reasons.
|
||||
|
||||
Requirements
|
||||
************
|
||||
|
||||
i2p with a running http proxy
|
||||
|
||||
Installation and Usage
|
||||
**********************
|
||||
|
||||
1. Unzip addressbook-%ver.zip into your i2p directory.
|
||||
2. Restart your router.
|
||||
|
||||
The addressbook daemon will automatically run while the router is up.
|
||||
|
||||
Aside from the daemon itself, the other elements of the addressbook interface
|
||||
are the config.txt, myhosts.txt, and subscriptions.txt files found in the addressbook
|
||||
directory.
|
||||
|
||||
config.txt is the configuration file for addressbook.
|
||||
|
||||
myhosts.txt is the addressbook master address book. Addresses placed in this file
|
||||
take precidence over those in the router address book and in remote address books.
|
||||
If changes are made to this file, they will be reflected in the router address book
|
||||
and published address book after the next update. Do not make changes directly to the
|
||||
router address book, as they could be lost during an update.
|
||||
|
||||
subscriptions.txt is the subscription list for addressbook. Each entry is an absolute
|
||||
url to a file in hosts.txt format. Since the list is checked in order, url's should be
|
||||
listed in order of trust.
|
||||
51
apps/addressbook/build.xml
Normal file
51
apps/addressbook/build.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0"?>
|
||||
<project name="addressbook" default="war" basedir=".">
|
||||
|
||||
<property name="src" value="java/src/addressbook"/>
|
||||
<property name="build" value="build"/>
|
||||
<property name="dist" location="dist"/>
|
||||
<property name="jar" value="addressbook.jar"/>
|
||||
<property name="war" value="addressbook.war"/>
|
||||
|
||||
<target name="init">
|
||||
<mkdir dir="${build}"/>
|
||||
<mkdir dir="${dist}"/>
|
||||
</target>
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="${build}"/>
|
||||
<delete dir="${dist}"/>
|
||||
</target>
|
||||
|
||||
<target name="distclean" depends="clean" />
|
||||
|
||||
<target name="compile" depends="init">
|
||||
<javac debug="true" deprecation="on" source="1.3" target="1.3"
|
||||
srcdir="${src}" destdir="${build}">
|
||||
<classpath>
|
||||
<pathelement location="../../core/java/build/i2p.jar" />
|
||||
<pathelement location="../jetty/jettylib/javax.servlet.jar" />
|
||||
</classpath>
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
<target name="jar" depends="compile">
|
||||
<jar basedir="${build}" destfile="${dist}/${jar}">
|
||||
<manifest>
|
||||
<attribute name="Main-Class" value="addressbook.Daemon"/>
|
||||
</manifest>
|
||||
</jar>
|
||||
</target>
|
||||
|
||||
<target name="war" depends="compile">
|
||||
<mkdir dir="${dist}/tmp"/>
|
||||
<mkdir dir="${dist}/tmp/WEB-INF"/>
|
||||
<mkdir dir="${dist}/tmp/WEB-INF/classes"/>
|
||||
<copy todir="${dist}/tmp/WEB-INF/classes">
|
||||
<fileset dir="${build}"/>
|
||||
</copy>
|
||||
<war basedir="${dist}/tmp" webxml="web.xml" destfile="${dist}/${war}"/>
|
||||
<delete dir="${dist}/tmp"/>
|
||||
</target>
|
||||
|
||||
</project>
|
||||
43
apps/addressbook/config.txt
Normal file
43
apps/addressbook/config.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
# This is the configuration file for addressbook.
|
||||
#
|
||||
# Options
|
||||
# *******
|
||||
# All paths are realitive to i2p/addressbook. Default value for
|
||||
# each option is given in parentheses.
|
||||
#
|
||||
# proxy_host The hostname of your I2P http proxy.
|
||||
# (localhost)
|
||||
#
|
||||
# proxy_port The port of your I2P http proxy. (4444)
|
||||
#
|
||||
# master_addressbook The path to your master address book, used for local
|
||||
# changes only. (myhosts.txt)
|
||||
#
|
||||
# router_addressbook The path to the address book used by the router.
|
||||
# Contains the addresses from your master address book
|
||||
# and your subscribed address books. (../userhosts.txt)
|
||||
#
|
||||
# published_addressbook The path to the copy of your address book made
|
||||
# available on i2p. (../eepsite/docroot/hosts.txt)
|
||||
#
|
||||
# log The path to your addressbook log. (log.txt)
|
||||
#
|
||||
# subscriptions The path to your subscription file. (subscriptions.txt)
|
||||
#
|
||||
# etags The path to the etags header storage file. (etags)
|
||||
#
|
||||
# last_modified The path to the last-modified header storage file.
|
||||
# (last_modified)
|
||||
#
|
||||
# update_delay The time (in hours) between each update. (1)
|
||||
|
||||
proxy_host=localhost
|
||||
proxy_port=4444
|
||||
master_addressbook=myhosts.txt
|
||||
router_addressbook=../userhosts.txt
|
||||
published_addressbook=../eepsite/docroot/hosts.txt
|
||||
log=log.txt
|
||||
subscriptions=subscriptions.txt
|
||||
etags=etags
|
||||
last_modified=last_modified
|
||||
update_delay=1
|
||||
218
apps/addressbook/java/src/addressbook/AddressBook.java
Normal file
218
apps/addressbook/java/src/addressbook/AddressBook.java
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.EepGet;
|
||||
|
||||
/**
|
||||
* An address book for storing human readable names mapped to base64 i2p
|
||||
* destinations. AddressBooks can be created from local and remote files, merged
|
||||
* together, and written out to local files.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class AddressBook {
|
||||
|
||||
private String location;
|
||||
|
||||
private Map addresses;
|
||||
|
||||
private boolean modified;
|
||||
|
||||
/**
|
||||
* Construct an AddressBook from the contents of the Map addresses.
|
||||
*
|
||||
* @param addresses
|
||||
* A Map containing human readable addresses as keys, mapped to
|
||||
* base64 i2p destinations.
|
||||
*/
|
||||
public AddressBook(Map addresses) {
|
||||
this.addresses = addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AddressBook from the contents of the file at url. If the
|
||||
* remote file cannot be read, construct an empty AddressBook
|
||||
*
|
||||
* @param url
|
||||
* A URL pointing at a file with lines in the format "key=value",
|
||||
* where key is a human readable name, and value is a base64 i2p
|
||||
* destination.
|
||||
*/
|
||||
public AddressBook(String url, String proxyHost, int proxyPort) {
|
||||
this.location = url;
|
||||
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
|
||||
proxyHost, proxyPort, 0, "addressbook.tmp", url, true,
|
||||
null);
|
||||
get.fetch();
|
||||
try {
|
||||
this.addresses = ConfigParser.parse(new File("addressbook.tmp"));
|
||||
} catch (IOException exp) {
|
||||
this.addresses = new HashMap();
|
||||
}
|
||||
new File("addressbook.tmp").delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AddressBook from the Subscription subscription. If the
|
||||
* address book at subscription has not changed since the last time it was
|
||||
* read or cannot be read, return an empty AddressBook.
|
||||
*
|
||||
* @param subscription
|
||||
* A Subscription instance pointing at a remote address book.
|
||||
*/
|
||||
public AddressBook(Subscription subscription, String proxyHost, int proxyPort) {
|
||||
this.location = subscription.getLocation();
|
||||
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
|
||||
proxyHost, proxyPort, 0, "addressbook.tmp",
|
||||
subscription.getLocation(), true, subscription.getEtag());
|
||||
get.fetch();
|
||||
subscription.setEtag(get.getETag());
|
||||
try {
|
||||
this.addresses = ConfigParser.parse(new File("addressbook.tmp"));
|
||||
} catch (IOException exp) {
|
||||
this.addresses = new HashMap();
|
||||
}
|
||||
new File("addressbook.tmp").delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an AddressBook from the contents of the file at file. If the
|
||||
* file cannot be read, construct an empty AddressBook
|
||||
*
|
||||
* @param file
|
||||
* A File pointing at a file with lines in the format
|
||||
* "key=value", where key is a human readable name, and value is
|
||||
* a base64 i2p destination.
|
||||
*/
|
||||
public AddressBook(File file) {
|
||||
this.location = file.toString();
|
||||
try {
|
||||
this.addresses = ConfigParser.parse(file);
|
||||
} catch (IOException exp) {
|
||||
this.addresses = new HashMap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map containing the addresses in the AddressBook.
|
||||
*
|
||||
* @return A Map containing the addresses in the AddressBook, where the key
|
||||
* is a human readable name, and the value is a base64 i2p
|
||||
* destination.
|
||||
*/
|
||||
public Map getAddresses() {
|
||||
return this.addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location of the file this AddressBook was constructed from.
|
||||
*
|
||||
* @return A String representing either an abstract path, or a url,
|
||||
* depending on how the instance was constructed.
|
||||
*/
|
||||
public String getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the contents of the AddressBook.
|
||||
*
|
||||
* @return A String representing the contents of the AddressBook.
|
||||
*/
|
||||
public String toString() {
|
||||
return this.addresses.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this AddressBook with AddressBook other, writing messages about new
|
||||
* addresses or conflicts to log. Addresses in AddressBook other that are
|
||||
* not in this AddressBook are added to this AddressBook. In case of a
|
||||
* conflict, addresses in this AddressBook take precedence
|
||||
*
|
||||
* @param other
|
||||
* An AddressBook to merge with.
|
||||
* @param log
|
||||
* The log to write messages about new addresses or conflicts to.
|
||||
*/
|
||||
public void merge(AddressBook other, boolean overwrite, Log log) {
|
||||
Iterator otherIter = other.addresses.keySet().iterator();
|
||||
|
||||
while (otherIter.hasNext()) {
|
||||
String otherKey = (String) otherIter.next();
|
||||
String otherValue = (String) other.addresses.get(otherKey);
|
||||
|
||||
if (otherKey.endsWith(".i2p") && otherValue.length() >= 516) {
|
||||
if (this.addresses.containsKey(otherKey) && !overwrite) {
|
||||
if (!this.addresses.get(otherKey).equals(otherValue)
|
||||
&& log != null) {
|
||||
log.append("Conflict for " + otherKey + " from "
|
||||
+ other.location
|
||||
+ ". Destination in remote address book is "
|
||||
+ otherValue);
|
||||
}
|
||||
} else if (!this.addresses.containsKey(otherKey)
|
||||
|| !this.addresses.get(otherKey).equals(otherValue)) {
|
||||
this.addresses.put(otherKey, otherValue);
|
||||
this.modified = true;
|
||||
if (log != null) {
|
||||
log.append("New address " + otherKey
|
||||
+ " added to address book.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the contents of this AddressBook out to the File file. If the file
|
||||
* cannot be writen to, this method will silently fail.
|
||||
*
|
||||
* @param file
|
||||
* The file to write the contents of this AddressBook too.
|
||||
*/
|
||||
public void write(File file) {
|
||||
if (this.modified) {
|
||||
try {
|
||||
ConfigParser.write(this.addresses, file);
|
||||
} catch (IOException exp) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this AddressBook out to the file it was read from. Requires that
|
||||
* AddressBook was constructed from a file on the local filesystem. If the
|
||||
* file cannot be writen to, this method will silently fail.
|
||||
*/
|
||||
public void write() {
|
||||
this.write(new File(this.location));
|
||||
}
|
||||
}
|
||||
304
apps/addressbook/java/src/addressbook/ConfigParser.java
Normal file
304
apps/addressbook/java/src/addressbook/ConfigParser.java
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Iterator;
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Utility class providing methods to parse and write files in config file
|
||||
* format, and subscription file format.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*/
|
||||
public class ConfigParser {
|
||||
|
||||
/**
|
||||
* Strip the comments from a String. Lines that begin with '#' and ';' are
|
||||
* considered comments, as well as any part of a line after a '#'.
|
||||
*
|
||||
* @param inputLine
|
||||
* A String to strip comments from.
|
||||
* @return A String without comments, but otherwise identical to inputLine.
|
||||
*/
|
||||
public static String stripComments(String inputLine) {
|
||||
if (inputLine.startsWith(";")) {
|
||||
return "";
|
||||
}
|
||||
if (inputLine.split("#").length > 0) {
|
||||
return inputLine.split("#")[0];
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map using the contents of BufferedReader input. input must have
|
||||
* a single key, value pair on each line, in the format: key=value. Lines
|
||||
* starting with '#' or ';' are considered comments, and ignored. Lines that
|
||||
* are obviously not in the format key=value are also ignored.
|
||||
*
|
||||
* @param input
|
||||
* A BufferedReader with lines in key=value format to parse into
|
||||
* a Map.
|
||||
* @return A Map containing the key, value pairs from input.
|
||||
* @throws IOException
|
||||
* if the BufferedReader cannot be read.
|
||||
*
|
||||
*/
|
||||
public static Map parse(BufferedReader input) throws IOException {
|
||||
Map result = new HashMap();
|
||||
String inputLine;
|
||||
inputLine = input.readLine();
|
||||
while (inputLine != null) {
|
||||
inputLine = ConfigParser.stripComments(inputLine);
|
||||
String[] splitLine = inputLine.split("=");
|
||||
if (splitLine.length == 2) {
|
||||
result.put(splitLine[0].trim(), splitLine[1].trim());
|
||||
}
|
||||
inputLine = input.readLine();
|
||||
}
|
||||
input.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map using the contents of the File file. See parseBufferedReader
|
||||
* for details of the input format.
|
||||
*
|
||||
* @param file
|
||||
* A File to parse.
|
||||
* @return A Map containing the key, value pairs from file.
|
||||
* @throws IOException
|
||||
* if file cannot be read.
|
||||
*/
|
||||
public static Map parse(File file) throws IOException {
|
||||
FileInputStream fileStream = new FileInputStream(file);
|
||||
BufferedReader input = new BufferedReader(new InputStreamReader(
|
||||
fileStream));
|
||||
return ConfigParser.parse(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map using the contents of the String string. See
|
||||
* parseBufferedReader for details of the input format.
|
||||
*
|
||||
* @param string
|
||||
* A String to parse.
|
||||
* @return A Map containing the key, value pairs from string.
|
||||
* @throws IOException
|
||||
* if file cannot be read.
|
||||
*/
|
||||
public static Map parse(String string) throws IOException {
|
||||
StringReader stringReader = new StringReader(string);
|
||||
BufferedReader input = new BufferedReader(stringReader);
|
||||
return ConfigParser.parse(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Map using the contents of the File file. If file cannot be read,
|
||||
* use map instead, and write the result to where file should have been.
|
||||
*
|
||||
* @param file
|
||||
* A File to attempt to parse.
|
||||
* @param map
|
||||
* A Map to use as the default, if file fails.
|
||||
* @return A Map containing the key, value pairs from file, or if file
|
||||
* cannot be read, map.
|
||||
*/
|
||||
public static Map parse(File file, Map map) {
|
||||
Map result = new HashMap();
|
||||
try {
|
||||
result = ConfigParser.parse(file);
|
||||
} catch (IOException exp) {
|
||||
result = map;
|
||||
try {
|
||||
ConfigParser.write(result, file);
|
||||
} catch (IOException exp2) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a List where each element is a line from the BufferedReader input.
|
||||
*
|
||||
* @param input
|
||||
* A BufferedReader to parse.
|
||||
* @return A List consisting of one element for each line in input.
|
||||
* @throws IOException
|
||||
* if input cannot be read.
|
||||
*/
|
||||
public static List parseSubscriptions(BufferedReader input)
|
||||
throws IOException {
|
||||
List result = new LinkedList();
|
||||
String inputLine = input.readLine();
|
||||
while (inputLine != null) {
|
||||
inputLine = ConfigParser.stripComments(inputLine).trim();
|
||||
if (inputLine.length() > 0) {
|
||||
result.add(inputLine);
|
||||
}
|
||||
inputLine = input.readLine();
|
||||
}
|
||||
input.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a List where each element is a line from the File file.
|
||||
*
|
||||
* @param file
|
||||
* A File to parse.
|
||||
* @return A List consisting of one element for each line in file.
|
||||
* @throws IOException
|
||||
* if file cannot be read.
|
||||
*/
|
||||
public static List parseSubscriptions(File file) throws IOException {
|
||||
FileInputStream fileStream = new FileInputStream(file);
|
||||
BufferedReader input = new BufferedReader(new InputStreamReader(
|
||||
fileStream));
|
||||
return ConfigParser.parseSubscriptions(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a List where each element is a line from the String string.
|
||||
*
|
||||
* @param string
|
||||
* A String to parse.
|
||||
* @return A List consisting of one element for each line in string.
|
||||
* @throws IOException
|
||||
* if string cannot be read.
|
||||
*/
|
||||
public static List parseSubscriptions(String string) throws IOException {
|
||||
StringReader stringReader = new StringReader(string);
|
||||
BufferedReader input = new BufferedReader(stringReader);
|
||||
return ConfigParser.parseSubscriptions(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a List using the contents of the File file. If file cannot be
|
||||
* read, use list instead, and write the result to where file should have
|
||||
* been.
|
||||
*
|
||||
* @param file
|
||||
* A File to attempt to parse.
|
||||
* @param string
|
||||
* A List to use as the default, if file fails.
|
||||
* @return A List consisting of one element for each line in file, or if
|
||||
* file cannot be read, list.
|
||||
*/
|
||||
public static List parseSubscriptions(File file, List list) {
|
||||
List result = new LinkedList();
|
||||
try {
|
||||
result = ConfigParser.parseSubscriptions(file);
|
||||
} catch (IOException exp) {
|
||||
result = list;
|
||||
try {
|
||||
ConfigParser.writeSubscriptions(result, file);
|
||||
} catch (IOException exp2) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write contents of Map map to BufferedWriter output. Output is written
|
||||
* with one key, value pair on each line, in the format: key=value.
|
||||
*
|
||||
* @param map
|
||||
* A Map to write to output.
|
||||
* @param output
|
||||
* A BufferedWriter to write the Map to.
|
||||
* @throws IOException
|
||||
* if the BufferedWriter cannot be written to.
|
||||
*/
|
||||
public static void write(Map map, BufferedWriter output) throws IOException {
|
||||
Iterator keyIter = map.keySet().iterator();
|
||||
|
||||
while (keyIter.hasNext()) {
|
||||
String key = (String) keyIter.next();
|
||||
output.write(key + "=" + (String) map.get(key));
|
||||
output.newLine();
|
||||
}
|
||||
output.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write contents of Map map to the File file. Output is written
|
||||
* with one key, value pair on each line, in the format: key=value.
|
||||
*
|
||||
* @param map
|
||||
* A Map to write to file.
|
||||
* @param file
|
||||
* A File to write the Map to.
|
||||
* @throws IOException
|
||||
* if file cannot be written to.
|
||||
*/
|
||||
public static void write(Map map, File file) throws IOException {
|
||||
ConfigParser
|
||||
.write(map, new BufferedWriter(new FileWriter(file, false)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write contents of List list to BufferedReader output. Output is written
|
||||
* with each element of list on a new line.
|
||||
*
|
||||
* @param list
|
||||
* A List to write to file.
|
||||
* @param output
|
||||
* A BufferedReader to write list to.
|
||||
* @throws IOException
|
||||
* if output cannot be written to.
|
||||
*/
|
||||
public static void writeSubscriptions(List list, BufferedWriter output)
|
||||
throws IOException {
|
||||
Iterator iter = list.iterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
output.write((String) iter.next());
|
||||
output.newLine();
|
||||
}
|
||||
output.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write contents of List list to File file. Output is written with each
|
||||
* element of list on a new line.
|
||||
*
|
||||
* @param list
|
||||
* A List to write to file.
|
||||
* @param file
|
||||
* A File to write list to.
|
||||
* @throws IOException
|
||||
* if output cannot be written to.
|
||||
*/
|
||||
public static void writeSubscriptions(List list, File file)
|
||||
throws IOException {
|
||||
ConfigParser.writeSubscriptions(list, new BufferedWriter(
|
||||
new FileWriter(file, false)));
|
||||
}
|
||||
|
||||
}
|
||||
191
apps/addressbook/java/src/addressbook/Daemon.java
Normal file
191
apps/addressbook/java/src/addressbook/Daemon.java
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.LinkedList;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Main class of addressbook. Performs updates, and runs the main loop.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class Daemon {
|
||||
public static final String VERSION = "2.0.3";
|
||||
private static final Daemon _instance = new Daemon();
|
||||
|
||||
/**
|
||||
* Update the router and published address books using remote data from the
|
||||
* subscribed address books listed in subscriptions.
|
||||
*
|
||||
* @param master
|
||||
* The master AddressBook. This address book is never
|
||||
* overwritten, so it is safe for the user to write to.
|
||||
* @param router
|
||||
* The router AddressBook. This is the address book read by
|
||||
* client applications.
|
||||
* @param published
|
||||
* The published AddressBook. This address book is published on
|
||||
* the user's eepsite so that others may subscribe to it.
|
||||
* @param subscriptions
|
||||
* A SubscriptionList listing the remote address books to update
|
||||
* from.
|
||||
* @param log
|
||||
* The log to write changes and conflicts to.
|
||||
*/
|
||||
public void update(AddressBook master, AddressBook router,
|
||||
File published, SubscriptionList subscriptions, Log log) {
|
||||
router.merge(master, true, null);
|
||||
Iterator iter = subscriptions.iterator();
|
||||
while (iter.hasNext()) {
|
||||
router.merge((AddressBook) iter.next(), false, log);
|
||||
}
|
||||
router.write();
|
||||
if (published != null)
|
||||
router.write(published);
|
||||
subscriptions.write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an update, using the Map settings to provide the parameters.
|
||||
*
|
||||
* @param settings
|
||||
* A Map containg the parameters needed by update.
|
||||
* @param home
|
||||
* The directory containing addressbook's configuration files.
|
||||
*/
|
||||
public void update(Map settings, String home) {
|
||||
File masterFile = new File(home, (String) settings
|
||||
.get("master_addressbook"));
|
||||
File routerFile = new File(home, (String) settings
|
||||
.get("router_addressbook"));
|
||||
File published = null;
|
||||
if ("true".equals(settings.get("should_publish")))
|
||||
published = new File(home, (String) settings
|
||||
.get("published_addressbook"));
|
||||
File subscriptionFile = new File(home, (String) settings
|
||||
.get("subscriptions"));
|
||||
File logFile = new File(home, (String) settings.get("log"));
|
||||
File etagsFile = new File(home, (String) settings.get("etags"));
|
||||
File lastModifiedFile = new File(home, (String) settings
|
||||
.get("last_modified"));
|
||||
|
||||
AddressBook master = new AddressBook(masterFile);
|
||||
AddressBook router = new AddressBook(routerFile);
|
||||
|
||||
List defaultSubs = new LinkedList();
|
||||
defaultSubs.add("http://i2p/NF2RLVUxVulR3IqK0sGJR0dHQcGXAzwa6rEO4WAWYXOHw-DoZhKnlbf1nzHXwMEJoex5nFTyiNMqxJMWlY54cvU~UenZdkyQQeUSBZXyuSweflUXFqKN-y8xIoK2w9Ylq1k8IcrAFDsITyOzjUKoOPfVq34rKNDo7fYyis4kT5bAHy~2N1EVMs34pi2RFabATIOBk38Qhab57Umpa6yEoE~rbyR~suDRvD7gjBvBiIKFqhFueXsR2uSrPB-yzwAGofTXuklofK3DdKspciclTVzqbDjsk5UXfu2nTrC1agkhLyqlOfjhyqC~t1IXm-Vs2o7911k7KKLGjB4lmH508YJ7G9fLAUyjuB-wwwhejoWqvg7oWvqo4oIok8LG6ECR71C3dzCvIjY2QcrhoaazA9G4zcGMm6NKND-H4XY6tUWhpB~5GefB3YczOqMbHq4wi0O9MzBFrOJEOs3X4hwboKWANf7DT5PZKJZ5KorQPsYRSq0E3wSOsFCSsdVCKUGsAAAA/i2p/hosts.txt");
|
||||
|
||||
SubscriptionList subscriptions = new SubscriptionList(subscriptionFile,
|
||||
etagsFile, lastModifiedFile, defaultSubs, (String) settings
|
||||
.get("proxy_host"), Integer.parseInt((String) settings.get("proxy_port")));
|
||||
Log log = new Log(logFile);
|
||||
|
||||
update(master, router, published, subscriptions, log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the settings, set the proxy, then enter into the main loop. The main
|
||||
* loop performs an immediate update, and then an update every number of
|
||||
* hours, as configured in the settings file.
|
||||
*
|
||||
* @param args
|
||||
* Command line arguments. If there are any arguments provided,
|
||||
* the first is taken as addressbook's home directory, and the
|
||||
* others are ignored.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
_instance.run(args);
|
||||
}
|
||||
|
||||
public void run(String[] args) {
|
||||
String settingsLocation = "config.txt";
|
||||
Map settings = new HashMap();
|
||||
String home;
|
||||
if (args.length > 0) {
|
||||
home = args[0];
|
||||
} else {
|
||||
home = ".";
|
||||
}
|
||||
|
||||
Map defaultSettings = new HashMap();
|
||||
defaultSettings.put("proxy_host", "localhost");
|
||||
defaultSettings.put("proxy_port", "4444");
|
||||
defaultSettings.put("master_addressbook", "../userhosts.txt");
|
||||
defaultSettings.put("router_addressbook", "../hosts.txt");
|
||||
defaultSettings.put("published_addressbook", "../eepsite/docroot/hosts.txt");
|
||||
defaultSettings.put("should_publish", "false");
|
||||
defaultSettings.put("log", "log.txt");
|
||||
defaultSettings.put("subscriptions", "subscriptions.txt");
|
||||
defaultSettings.put("etags", "etags");
|
||||
defaultSettings.put("last_modified", "last_modified");
|
||||
defaultSettings.put("update_delay", "12");
|
||||
|
||||
File homeFile = new File(home);
|
||||
if (!homeFile.exists()) {
|
||||
boolean created = homeFile.mkdirs();
|
||||
if (created)
|
||||
System.out.println("INFO: Addressbook directory " + homeFile.getName() + " created");
|
||||
else
|
||||
System.out.println("ERROR: Addressbook directory " + homeFile.getName() + " could not be created");
|
||||
}
|
||||
|
||||
File settingsFile = new File(homeFile, settingsLocation);
|
||||
|
||||
settings = ConfigParser.parse(settingsFile, defaultSettings);
|
||||
// wait
|
||||
try {
|
||||
Thread.currentThread().sleep(5*60*1000);
|
||||
} catch (InterruptedException ie) {}
|
||||
|
||||
while (true) {
|
||||
long delay = Long.parseLong((String) settings.get("update_delay"));
|
||||
if (delay < 1) {
|
||||
delay = 1;
|
||||
}
|
||||
|
||||
update(settings, home);
|
||||
try {
|
||||
synchronized (this) {
|
||||
wait(delay * 60 * 60 * 1000);
|
||||
}
|
||||
} catch (InterruptedException exp) {
|
||||
}
|
||||
settings = ConfigParser.parse(settingsFile, defaultSettings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to get the addressbook to reread its config and
|
||||
* refetch its subscriptions.
|
||||
*/
|
||||
public static void wakeup() {
|
||||
synchronized (_instance) {
|
||||
_instance.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
53
apps/addressbook/java/src/addressbook/DaemonThread.java
Normal file
53
apps/addressbook/java/src/addressbook/DaemonThread.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
/**
|
||||
* A thread that waits five minutes, then runs the addressbook daemon.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class DaemonThread extends Thread {
|
||||
|
||||
private String[] args;
|
||||
|
||||
/**
|
||||
* Construct a DaemonThread with the command line arguments args.
|
||||
* @param args
|
||||
* A String array to pass to Daemon.main().
|
||||
*/
|
||||
public DaemonThread(String[] args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Runnable#run()
|
||||
*/
|
||||
public void run() {
|
||||
//try {
|
||||
// Thread.sleep(5 * 60 * 1000);
|
||||
//} catch (InterruptedException exp) {
|
||||
//}
|
||||
Daemon.main(this.args);
|
||||
}
|
||||
}
|
||||
76
apps/addressbook/java/src/addressbook/Log.java
Normal file
76
apps/addressbook/java/src/addressbook/Log.java
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A simple log with automatic time stamping.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class Log {
|
||||
|
||||
private File file;
|
||||
|
||||
/**
|
||||
* Construct a Log instance that writes to the File file.
|
||||
*
|
||||
* @param file
|
||||
* A File for the log to write to.
|
||||
*/
|
||||
public Log(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write entry to a new line in the log, with appropriate time stamp.
|
||||
*
|
||||
* @param entry
|
||||
* A String containing a message to append to the log.
|
||||
*/
|
||||
public void append(String entry) {
|
||||
try {
|
||||
BufferedWriter bw = new BufferedWriter(new FileWriter(this.file,
|
||||
true));
|
||||
String timestamp = new Date().toString();
|
||||
bw.write(timestamp + " -- " + entry);
|
||||
bw.newLine();
|
||||
bw.close();
|
||||
} catch (IOException exp) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the File that the Log is writing to.
|
||||
*
|
||||
* @return The File that the log is writing to.
|
||||
*/
|
||||
public File getFile() {
|
||||
return this.file;
|
||||
}
|
||||
}
|
||||
61
apps/addressbook/java/src/addressbook/Servlet.java
Normal file
61
apps/addressbook/java/src/addressbook/Servlet.java
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import javax.servlet.GenericServlet;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
/**
|
||||
* A wrapper for addressbook to allow it to be started as a web application.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class Servlet extends GenericServlet {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
|
||||
*/
|
||||
public void service(ServletRequest request, ServletResponse response) {
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
|
||||
*/
|
||||
public void init(ServletConfig config) {
|
||||
try {
|
||||
super.init(config);
|
||||
} catch (ServletException exp) {
|
||||
}
|
||||
String[] args = new String[1];
|
||||
args[0] = config.getInitParameter("home");
|
||||
DaemonThread thread = new DaemonThread(args);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
System.out.println("INFO: Starting Addressbook " + Daemon.VERSION);
|
||||
System.out.println("INFO: config root under " + args[0]);
|
||||
}
|
||||
|
||||
}
|
||||
105
apps/addressbook/java/src/addressbook/Subscription.java
Normal file
105
apps/addressbook/java/src/addressbook/Subscription.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
/**
|
||||
* A subscription to a remote address book.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class Subscription {
|
||||
|
||||
private String location;
|
||||
|
||||
private String etag;
|
||||
|
||||
private String lastModified;
|
||||
|
||||
/**
|
||||
* Construct a Subscription pointing to the address book at location, that
|
||||
* was last read at the time represented by etag and lastModified.
|
||||
*
|
||||
* @param location
|
||||
* A String representing a url to a remote address book.
|
||||
* @param etag
|
||||
* The etag header that we recieved the last time we read this
|
||||
* subscription.
|
||||
* @param lastModified
|
||||
* the last-modified header we recieved the last time we read
|
||||
* this subscription.
|
||||
*/
|
||||
public Subscription(String location, String etag, String lastModified) {
|
||||
this.location = location;
|
||||
this.etag = etag;
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location this Subscription points at.
|
||||
*
|
||||
* @return A String representing a url to a remote address book.
|
||||
*/
|
||||
public String getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the etag header that we recieved the last time we read this
|
||||
* subscription.
|
||||
*
|
||||
* @return A String containing the etag header.
|
||||
*/
|
||||
public String getEtag() {
|
||||
return this.etag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the etag header.
|
||||
*
|
||||
* @param etag
|
||||
* A String containing the etag header.
|
||||
*/
|
||||
public void setEtag(String etag) {
|
||||
this.etag = etag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last-modified header that we recieved the last time we read
|
||||
* this subscription.
|
||||
*
|
||||
* @return A String containing the last-modified header.
|
||||
*/
|
||||
public String getLastModified() {
|
||||
return this.lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last-modified header.
|
||||
*
|
||||
* @param lastModified
|
||||
* A String containing the last-modified header.
|
||||
*/
|
||||
public void setLastModified(String lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An iterator over the subscriptions in a SubscriptionList. Note that this iterator
|
||||
* returns AddressBook objects, and not Subscription objects.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*/
|
||||
public class SubscriptionIterator implements Iterator {
|
||||
|
||||
private Iterator subIterator;
|
||||
private String proxyHost;
|
||||
private int proxyPort;
|
||||
|
||||
/**
|
||||
* Construct a SubscriptionIterator using the Subscriprions in List subscriptions.
|
||||
*
|
||||
* @param subscriptions
|
||||
* List of Subscription objects that represent address books.
|
||||
*/
|
||||
public SubscriptionIterator(List subscriptions, String proxyHost, int proxyPort) {
|
||||
this.subIterator = subscriptions.iterator();
|
||||
this.proxyHost = proxyHost;
|
||||
this.proxyPort = proxyPort;
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.util.Iterator#hasNext()
|
||||
*/
|
||||
public boolean hasNext() {
|
||||
return this.subIterator.hasNext();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.util.Iterator#next()
|
||||
*/
|
||||
public Object next() {
|
||||
Subscription sub = (Subscription) this.subIterator.next();
|
||||
return new AddressBook(sub, this.proxyHost, this.proxyPort);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.util.Iterator#remove()
|
||||
*/
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
129
apps/addressbook/java/src/addressbook/SubscriptionList.java
Normal file
129
apps/addressbook/java/src/addressbook/SubscriptionList.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2004 Ragnarok
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package addressbook;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A list of Subscriptions loaded from a file.
|
||||
*
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
public class SubscriptionList {
|
||||
|
||||
private List subscriptions;
|
||||
|
||||
private File etagsFile;
|
||||
|
||||
private File lastModifiedFile;
|
||||
|
||||
private String proxyHost;
|
||||
|
||||
private int proxyPort;
|
||||
|
||||
/**
|
||||
* Construct a SubscriptionList using the urls from locationsFile and, if
|
||||
* available, the etags and last-modified headers loaded from etagsFile and
|
||||
* lastModifiedFile.
|
||||
*
|
||||
* @param locationsFile
|
||||
* A file containing one url on each line.
|
||||
* @param etagsFile
|
||||
* A file containg the etag headers used for conditional GET. The
|
||||
* file is in the format "url=etag".
|
||||
* @param lastModifiedFile
|
||||
* A file containg the last-modified headers used for conditional
|
||||
* GET. The file is in the format "url=leastmodified".
|
||||
*/
|
||||
public SubscriptionList(File locationsFile, File etagsFile,
|
||||
File lastModifiedFile, List defaultSubs, String proxyHost,
|
||||
int proxyPort) {
|
||||
this.subscriptions = new LinkedList();
|
||||
this.etagsFile = etagsFile;
|
||||
this.lastModifiedFile = lastModifiedFile;
|
||||
this.proxyHost = proxyHost;
|
||||
this.proxyPort = proxyPort;
|
||||
Map etags;
|
||||
Map lastModified;
|
||||
String location;
|
||||
List locations = ConfigParser.parseSubscriptions(locationsFile,
|
||||
defaultSubs);
|
||||
try {
|
||||
etags = ConfigParser.parse(etagsFile);
|
||||
} catch (IOException exp) {
|
||||
etags = new HashMap();
|
||||
}
|
||||
try {
|
||||
lastModified = ConfigParser.parse(lastModifiedFile);
|
||||
} catch (IOException exp) {
|
||||
lastModified = new HashMap();
|
||||
}
|
||||
Iterator iter = locations.iterator();
|
||||
while (iter.hasNext()) {
|
||||
location = (String) iter.next();
|
||||
this.subscriptions.add(new Subscription(location, (String) etags
|
||||
.get(location), (String) lastModified.get(location)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an iterator over the AddressBooks represented by the Subscriptions
|
||||
* in this SubscriptionList.
|
||||
*
|
||||
* @return A SubscriptionIterator.
|
||||
*/
|
||||
public SubscriptionIterator iterator() {
|
||||
return new SubscriptionIterator(this.subscriptions, this.proxyHost,
|
||||
this.proxyPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the etag and last-modified headers for each Subscription to files.
|
||||
*/
|
||||
public void write() {
|
||||
Iterator iter = this.subscriptions.iterator();
|
||||
Subscription sub;
|
||||
Map etags = new HashMap();
|
||||
Map lastModified = new HashMap();
|
||||
while (iter.hasNext()) {
|
||||
sub = (Subscription) iter.next();
|
||||
if (sub.getEtag() != null) {
|
||||
etags.put(sub.getLocation(), sub.getEtag());
|
||||
}
|
||||
if (sub.getLastModified() != null) {
|
||||
lastModified.put(sub.getLocation(), sub.getLastModified());
|
||||
}
|
||||
}
|
||||
try {
|
||||
ConfigParser.write(etags, this.etagsFile);
|
||||
ConfigParser.write(lastModified, this.lastModifiedFile);
|
||||
} catch (IOException exp) {
|
||||
}
|
||||
}
|
||||
}
|
||||
10
apps/addressbook/myhosts.txt
Normal file
10
apps/addressbook/myhosts.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
# addressbook master address book. Addresses placed in this file take precidence
|
||||
# over those in the router address book and in remote address books. If changes
|
||||
# are made to this file, they will be reflected in the router address book and
|
||||
# published address book after the next update.
|
||||
#
|
||||
# Do not make changes directly to the router address book, as they could be lost
|
||||
# during an update.
|
||||
#
|
||||
# This file takes addresses in the hosts.txt format, i.e.
|
||||
# example.i2p=somereallylongbase64thingAAAA
|
||||
7
apps/addressbook/subscriptions.txt
Normal file
7
apps/addressbook/subscriptions.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Subscription list for addressbook
|
||||
#
|
||||
# Each entry is an absolute url to a file in hosts.txt format.
|
||||
# Since the list is checked in order, url's should be listed in order of trust.
|
||||
#
|
||||
http://dev.i2p/i2p/hosts.txt
|
||||
http://duck.i2p/hosts.txt
|
||||
16
apps/addressbook/web.xml
Normal file
16
apps/addressbook/web.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE web-app
|
||||
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
|
||||
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">
|
||||
|
||||
<web-app>
|
||||
<servlet>
|
||||
<servlet-name>addressbook</servlet-name>
|
||||
<servlet-class>addressbook.Servlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>home</param-name>
|
||||
<param-value>./addressbook</param-value>
|
||||
</init-param>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
</web-app>
|
||||
349
apps/bogobot/Bogobot.java
Normal file
349
apps/bogobot/Bogobot.java
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* bogobot - A simple join/part stats logger bot for I2P IRC.
|
||||
*
|
||||
* Bogobot.java
|
||||
* 2004 The I2P Project
|
||||
* http://www.i2p.net
|
||||
* This code is public domain.
|
||||
*/
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.log4j.DailyRollingFileAppender;
|
||||
import org.apache.log4j.Level;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.apache.log4j.PatternLayout;
|
||||
|
||||
import org.jibble.pircbot.IrcException;
|
||||
import org.jibble.pircbot.NickAlreadyInUseException;
|
||||
import org.jibble.pircbot.PircBot;
|
||||
import org.jibble.pircbot.User;
|
||||
|
||||
/**
|
||||
* TODO 0.5 Add multi-server capability.
|
||||
*
|
||||
* @author hypercubus, oOo
|
||||
* @version 0.4
|
||||
*/
|
||||
public class Bogobot extends PircBot {
|
||||
|
||||
private static final String INTERVAL_DAILY = "daily";
|
||||
private static final String INTERVAL_MONTHLY = "monthly";
|
||||
private static final String INTERVAL_WEEKLY = "weekly";
|
||||
|
||||
private boolean _isIntentionalDisconnect = false;
|
||||
private long _lastUserlistCommandTimestamp = 0;
|
||||
private Logger _logger = Logger.getLogger(Bogobot.class);
|
||||
|
||||
private int _currentAutoRoundTripTag = 0;
|
||||
private long _lastAutoRoundTripSentTime = 0;
|
||||
private Timer _tickTimer;
|
||||
|
||||
private String _configFile;
|
||||
|
||||
private String _botPrimaryNick;
|
||||
private String _botSecondaryNick;
|
||||
private String _botNickservPassword;
|
||||
private String _botUsername;
|
||||
private String _ownerPrimaryNick;
|
||||
private String _ownerSecondaryNick;
|
||||
private String _botShutdownPassword;
|
||||
private String _ircChannel;
|
||||
private String _ircServer;
|
||||
private int _ircServerPort;
|
||||
private boolean _isLoggerEnabled;
|
||||
private String _loggedHostnamePattern;
|
||||
private boolean _isUserlistCommandEnabled;
|
||||
private String _logFilePrefix;
|
||||
private String _logFileRotationInterval;
|
||||
private long _commandAntiFloodInterval;
|
||||
private String _userlistCommandTrigger;
|
||||
private boolean _isRoundTripDelayEnabled;
|
||||
private int _roundTripDelayPeriod;
|
||||
|
||||
class BogobotTickTask extends TimerTask {
|
||||
private Bogobot _caller;
|
||||
|
||||
public BogobotTickTask(Bogobot caller) {
|
||||
_caller = caller;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
_caller.onTick();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadConfigFile(String configFileName) {
|
||||
|
||||
_configFile = configFileName;
|
||||
|
||||
Properties config = new Properties();
|
||||
FileInputStream fis = null;
|
||||
|
||||
try {
|
||||
fis = new FileInputStream(configFileName);
|
||||
config.load(fis);
|
||||
} catch (IOException ioe) {
|
||||
System.err.println("Error loading configuration file");
|
||||
System.exit(2);
|
||||
|
||||
} finally {
|
||||
if (fis != null) try {
|
||||
fis.close();
|
||||
} catch (IOException ioe) { // nop
|
||||
}
|
||||
}
|
||||
|
||||
_botPrimaryNick = config.getProperty("botPrimaryNick", "somebot");
|
||||
_botSecondaryNick = config.getProperty("botSecondaryNick", "somebot_");
|
||||
_botNickservPassword = config.getProperty("botNickservPassword", "");
|
||||
_botUsername = config.getProperty("botUsername", "somebot");
|
||||
|
||||
_ownerPrimaryNick = config.getProperty("ownerPrimaryNick", "somenick");
|
||||
_ownerSecondaryNick = config.getProperty("ownerSecondaryNick", "somenick_");
|
||||
|
||||
_botShutdownPassword = config.getProperty("botShutdownPassword", "take off eh");
|
||||
|
||||
_ircChannel = config.getProperty("ircChannel", "#i2p-chat");
|
||||
_ircServer = config.getProperty("ircServer", "irc.postman.i2p");
|
||||
_ircServerPort = Integer.parseInt(config.getProperty("ircServerPort", "6668"));
|
||||
|
||||
_isLoggerEnabled = Boolean.valueOf(config.getProperty("isLoggerEnabled", "true")).booleanValue();
|
||||
_loggedHostnamePattern = config.getProperty("loggedHostnamePattern", "");
|
||||
_logFilePrefix = config.getProperty("logFilePrefix", "irc.postman.i2p.i2p-chat");
|
||||
_logFileRotationInterval = config.getProperty("logFileRotationInterval", INTERVAL_DAILY);
|
||||
|
||||
_isRoundTripDelayEnabled = Boolean.valueOf(config.getProperty("isRoundTripDelayEnabled", "false")).booleanValue();
|
||||
_roundTripDelayPeriod = Integer.parseInt(config.getProperty("roundTripDelayPeriod", "300"));
|
||||
|
||||
_isUserlistCommandEnabled = Boolean.valueOf(config.getProperty("isUserlistCommandEnabled", "true")).booleanValue();
|
||||
_userlistCommandTrigger = config.getProperty("userlistCommandTrigger", "!who");
|
||||
_commandAntiFloodInterval = Long.parseLong(config.getProperty("commandAntiFloodInterval", "60"));
|
||||
}
|
||||
|
||||
public Bogobot(String configFileName) {
|
||||
|
||||
loadConfigFile(configFileName);
|
||||
|
||||
this.setName(_botPrimaryNick);
|
||||
this.setLogin(_botUsername);
|
||||
_tickTimer = new Timer();
|
||||
_tickTimer.scheduleAtFixedRate(new BogobotTickTask(this), 1000, 10 * 1000);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
Bogobot bogobot;
|
||||
|
||||
if (args.length > 1) {
|
||||
System.err.println("Too many arguments, the only allowed parameter is configuration file name");
|
||||
System.exit(3);
|
||||
}
|
||||
if (args.length == 1) {
|
||||
bogobot = new Bogobot(args[0]);
|
||||
} else {
|
||||
bogobot = new Bogobot("bogobot.config");
|
||||
}
|
||||
|
||||
bogobot.setVerbose(true);
|
||||
|
||||
if (bogobot._isLoggerEnabled)
|
||||
bogobot.initLogger();
|
||||
|
||||
bogobot.connectToServer();
|
||||
}
|
||||
|
||||
protected void onTick() {
|
||||
// Tick about once every ten seconds
|
||||
|
||||
if (this.isConnected() && _isRoundTripDelayEnabled) {
|
||||
if( ( (System.currentTimeMillis() - _lastAutoRoundTripSentTime) >= (_roundTripDelayPeriod * 1000) ) && (this.getOutgoingQueueSize() == 0) ) {
|
||||
// Connected, sending queue is empty and last RoundTrip is more then 5 minutes old -> Send a new one
|
||||
_currentAutoRoundTripTag ++;
|
||||
_lastAutoRoundTripSentTime = System.currentTimeMillis();
|
||||
sendNotice(this.getNick(),"ROUNDTRIP " + _currentAutoRoundTripTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onDisconnect() {
|
||||
|
||||
if (_isIntentionalDisconnect)
|
||||
System.exit(0);
|
||||
|
||||
if (_isLoggerEnabled)
|
||||
_logger.info(System.currentTimeMillis() + " quits *** " + this.getName() + " *** (Lost connection)");
|
||||
|
||||
try {
|
||||
Thread.sleep(60000);
|
||||
} catch (InterruptedException e) {
|
||||
// No worries.
|
||||
}
|
||||
connectToServer();
|
||||
}
|
||||
|
||||
protected void onJoin(String channel, String sender, String login, String hostname) {
|
||||
|
||||
if (_isLoggerEnabled) {
|
||||
if (sender.equals(this.getName())) {
|
||||
|
||||
_logger.info(System.currentTimeMillis() + " joins *** " + _botPrimaryNick + " ***");
|
||||
|
||||
} else {
|
||||
|
||||
String prependedHostname = "@" + hostname;
|
||||
if (prependedHostname.endsWith(_loggedHostnamePattern)) {
|
||||
_logger.info(System.currentTimeMillis() + " joins " + sender);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onMessage(String channel, String sender, String login, String hostname, String message) {
|
||||
message = message.replaceFirst("<.+?> ", "");
|
||||
if (_isUserlistCommandEnabled && message.equals(_userlistCommandTrigger)) {
|
||||
|
||||
if (System.currentTimeMillis() - _lastUserlistCommandTimestamp < _commandAntiFloodInterval * 1000)
|
||||
return;
|
||||
|
||||
Object[] users = getUsers(_ircChannel);
|
||||
String output = "Userlist for " + _ircChannel + ": ";
|
||||
|
||||
for (int i = 0; i < users.length; i++)
|
||||
output += "[" + ((User) users[i]).getNick() + "] ";
|
||||
|
||||
sendMessage(_ircChannel, output);
|
||||
_lastUserlistCommandTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPart(String channel, String sender, String login, String hostname) {
|
||||
|
||||
if (_isLoggerEnabled) {
|
||||
if (sender.equals(this.getName())) {
|
||||
_logger.info(System.currentTimeMillis() + " parts *** " + _botPrimaryNick + " ***");
|
||||
} else {
|
||||
String prependedHostname = "@" + hostname;
|
||||
if (prependedHostname.endsWith(_loggedHostnamePattern)) {
|
||||
_logger.info(System.currentTimeMillis() + " parts " + sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void onPrivateMessage(String sender, String login, String hostname, String message) {
|
||||
/*
|
||||
* Nobody else except the bot's owner can shut it down, unless of
|
||||
* course the owner's nick isn't registered and someone's spoofing it.
|
||||
*/
|
||||
if ((sender.equals(_ownerPrimaryNick) || sender.equals(_ownerSecondaryNick)) && message.equals(_botShutdownPassword)) {
|
||||
|
||||
if (_isLoggerEnabled)
|
||||
_logger.info(System.currentTimeMillis() + " quits *** " + this.getName() + " ***");
|
||||
|
||||
_isIntentionalDisconnect = true;
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onQuit(String sourceNick, String sourceLogin, String sourceHostname, String reason) {
|
||||
String prependedHostname = "@" + sourceHostname;
|
||||
|
||||
if (sourceNick.equals(_botPrimaryNick))
|
||||
changeNick(_botPrimaryNick);
|
||||
|
||||
if (_isLoggerEnabled) {
|
||||
if (prependedHostname.endsWith(_loggedHostnamePattern)) {
|
||||
_logger.info(System.currentTimeMillis() + " quits " + sourceNick + " " + reason);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void connectToServer() {
|
||||
|
||||
int loginAttempts = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
connect(_ircServer, _ircServerPort);
|
||||
break;
|
||||
} catch (NickAlreadyInUseException e) {
|
||||
if (loginAttempts == 1) {
|
||||
System.out.println("Sorry, the primary and secondary bot nicks are already taken. Exiting.");
|
||||
System.exit(1);
|
||||
}
|
||||
loginAttempts++;
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e1) {
|
||||
// Hmph.
|
||||
}
|
||||
|
||||
if (getName().equals(_botPrimaryNick))
|
||||
setName(_botSecondaryNick);
|
||||
else
|
||||
setName(_botPrimaryNick);
|
||||
|
||||
continue;
|
||||
} catch (IOException e) {
|
||||
System.out.println("Error during login: ");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
} catch (IrcException e) {
|
||||
System.out.println("Error during login: ");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
joinChannel(_ircChannel);
|
||||
}
|
||||
|
||||
protected void onNotice(String sourceNick, String sourceLogin, String sourceHostname, String target, String notice) {
|
||||
|
||||
if (sourceNick.equals("NickServ") && (notice.indexOf("/msg NickServ IDENTIFY") >= 0) && (_botNickservPassword != "")) {
|
||||
sendRawLineViaQueue("NICKSERV IDENTIFY " + _botNickservPassword);
|
||||
}
|
||||
|
||||
if (sourceNick.equals(getNick()) && notice.equals( "ROUNDTRIP " + _currentAutoRoundTripTag)) {
|
||||
int delay = (int)((System.currentTimeMillis() - _lastAutoRoundTripSentTime) / 100);
|
||||
// sendMessage(_ircChannel, "Round-trip delay = " + (delay / 10.0f) + " seconds");
|
||||
if (_isLoggerEnabled)
|
||||
_logger.info(System.currentTimeMillis() + " roundtrip " + delay);
|
||||
}
|
||||
}
|
||||
|
||||
private void initLogger() {
|
||||
|
||||
String logFilePath = "logs" + File.separator + _logFilePrefix;
|
||||
DailyRollingFileAppender rollingFileAppender = null;
|
||||
|
||||
if (!(new File("logs").exists()))
|
||||
(new File("logs")).mkdirs();
|
||||
|
||||
try {
|
||||
|
||||
if (_logFileRotationInterval.equals("monthly"))
|
||||
rollingFileAppender = new DailyRollingFileAppender(new PatternLayout("%m%n"), logFilePath, "'.'yyyy-MM'.log'");
|
||||
else if (_logFileRotationInterval.equals("weekly"))
|
||||
rollingFileAppender = new DailyRollingFileAppender(new PatternLayout("%m%n"), logFilePath, "'.'yyyy-ww'.log'");
|
||||
else
|
||||
rollingFileAppender = new DailyRollingFileAppender(new PatternLayout("%m%n"), logFilePath, "'.'yyyy-MM-dd'.log'");
|
||||
|
||||
rollingFileAppender.setThreshold(Level.INFO);
|
||||
_logger.addAppender(rollingFileAppender);
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Error: Couldn't create or open an existing log file. Exiting.");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
353
apps/bogobot/Bogoparser.java
Normal file
353
apps/bogobot/Bogoparser.java
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* bogoparser - A simple logfile analyzer for bogobot.
|
||||
*
|
||||
* Bogoparser.java
|
||||
* 2004 The I2P Project
|
||||
* http://www.i2p.net
|
||||
* This code is public domain.
|
||||
*/
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author hypercubus
|
||||
* @version 0.4
|
||||
*/
|
||||
public class Bogoparser {
|
||||
|
||||
private static void displayUsageAndExit() {
|
||||
System.out.println("\r\nUsage:\r\n\r\n java Bogoparser [--by-duration] <logfile>\r\n");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
Bogoparser bogoparser;
|
||||
|
||||
if (args.length < 1 || args.length > 2)
|
||||
displayUsageAndExit();
|
||||
|
||||
if (args.length == 2) {
|
||||
if (!args[0].equals("--by-duration"))
|
||||
displayUsageAndExit();
|
||||
bogoparser = new Bogoparser(args[1], true);
|
||||
}
|
||||
|
||||
if (args.length == 1)
|
||||
bogoparser = new Bogoparser(args[0], false);
|
||||
}
|
||||
|
||||
private Bogoparser(String logfile, boolean sortByDuration) {
|
||||
|
||||
ArrayList sortedSessions;
|
||||
|
||||
if (sortByDuration) {
|
||||
sortedSessions = sortSessionsByDuration(calculateSessionDurations(sortSessionsByTime(readLogfile(logfile))));
|
||||
formatAndOutputByDuration(sortedSessions);
|
||||
} else {
|
||||
sortedSessions = calculateSessionDurations(sortSessionsByQuitReason(sortSessionsByNick(sortSessionsByTime(readLogfile(logfile)))));
|
||||
formatAndOutput(sortedSessions);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList calculateSessionDurations(ArrayList sortedSessionsByQuitReasonOrDuration) {
|
||||
|
||||
ArrayList calculatedSessionDurations = new ArrayList();
|
||||
|
||||
for (int i = 0; i+1 < sortedSessionsByQuitReasonOrDuration.size(); i += 2) {
|
||||
|
||||
String joinsEntry = (String) sortedSessionsByQuitReasonOrDuration.get(i);
|
||||
String[] joinsEntryFields = joinsEntry.split(" ");
|
||||
|
||||
String quitsEntry = (String) sortedSessionsByQuitReasonOrDuration.get(i+1);
|
||||
Pattern p = Pattern.compile("^([^ ]+) [^ ]+ ([^ ]+) (.*)$");
|
||||
Matcher m = p.matcher(quitsEntry);
|
||||
|
||||
if (m.matches()) {
|
||||
|
||||
String currentJoinTime = joinsEntryFields[0];
|
||||
String currentNick = m.group(2);
|
||||
String currentQuitReason = m.group(3);
|
||||
String currentQuitTime = m.group(1);
|
||||
long joinsTimeInMilliseconds;
|
||||
long quitsTimeInMilliseconds;
|
||||
long sessionLengthInMilliseconds;
|
||||
|
||||
joinsTimeInMilliseconds = Long.parseLong(currentJoinTime);
|
||||
quitsTimeInMilliseconds = Long.parseLong(currentQuitTime);
|
||||
sessionLengthInMilliseconds = quitsTimeInMilliseconds - joinsTimeInMilliseconds;
|
||||
|
||||
String hours = "" + sessionLengthInMilliseconds/1000/60/60;
|
||||
String minutes = "" + (sessionLengthInMilliseconds/1000/60)%60;
|
||||
|
||||
if (hours.length() < 2)
|
||||
hours = "0" + hours;
|
||||
|
||||
if (hours.length() < 3)
|
||||
hours = "0" + hours;
|
||||
|
||||
if (minutes.length() < 2)
|
||||
minutes = "0" + minutes;
|
||||
|
||||
int columnPadding = 19-currentNick.length();
|
||||
String columnPaddingString = " ";
|
||||
|
||||
for (int j = 0; j < columnPadding; j++)
|
||||
columnPaddingString = columnPaddingString + " ";
|
||||
|
||||
calculatedSessionDurations.add(sessionLengthInMilliseconds + " " + currentNick + columnPaddingString + " online " + hours + " hours " + minutes + " minutes " + currentQuitReason);
|
||||
} else {
|
||||
System.out.println("\r\nError: Unexpected entry in logfile: " + quitsEntry);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
return calculatedSessionDurations;
|
||||
}
|
||||
|
||||
private void formatAndOutput(ArrayList sortedSessions) {
|
||||
|
||||
String quitReason = null;
|
||||
|
||||
for (int i = 0; i < sortedSessions.size(); i++) {
|
||||
|
||||
String entry = (String) sortedSessions.get(i);
|
||||
Pattern p = Pattern.compile("^[\\d]+ ([^ ]+ +online [\\d]+ hours [\\d]+ minutes) (.*)$");
|
||||
Matcher m = p.matcher(entry);
|
||||
|
||||
if (m.matches()) {
|
||||
|
||||
if (quitReason == null) {
|
||||
quitReason = m.group(2);
|
||||
System.out.println("\r\nQUIT: " + ((m.group(2).equals("")) ? "No Reason Given" : quitReason) + "\r\n");
|
||||
}
|
||||
|
||||
String tempQuitReason = m.group(2);
|
||||
String tempSession = m.group(1);
|
||||
|
||||
if (tempQuitReason.equals(quitReason)) {
|
||||
System.out.println(" " + tempSession);
|
||||
} else {
|
||||
quitReason = null;
|
||||
i -= 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
System.out.println("\r\nError: Unexpected entry in logfile: " + entry);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
System.out.println("\r\n");
|
||||
}
|
||||
|
||||
private void formatAndOutputByDuration(ArrayList sortedSessions) {
|
||||
System.out.println("\r\n");
|
||||
|
||||
for (int i = 0; i < sortedSessions.size(); i++) {
|
||||
String[] columns = ((String) sortedSessions.get(i)).split(" ", 2);
|
||||
System.out.println(columns[1]);
|
||||
}
|
||||
|
||||
System.out.println("\r\n");
|
||||
}
|
||||
|
||||
private ArrayList readLogfile(String logfile) {
|
||||
|
||||
ArrayList log = new ArrayList();
|
||||
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(logfile)));
|
||||
|
||||
for (String line; (line = in.readLine()) != null; )
|
||||
log.add(line);
|
||||
|
||||
in.close();
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
System.out.println("\r\nError: Can't find logfile '" + logfile + "'.\r\n");
|
||||
System.exit(1);
|
||||
|
||||
} catch (IOException e) {
|
||||
System.out.println("\r\nError: Can't read logfile '" + logfile + "'.\r\n");
|
||||
System.exit(1);
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
/*
|
||||
* Performs an odd-even transposition sort.
|
||||
*/
|
||||
private ArrayList sortSessionsByDuration(ArrayList calculatedSessionDurations) {
|
||||
|
||||
for (int i = 0; i < calculatedSessionDurations.size()/2; i++) {
|
||||
for (int j = 0; j+1 < calculatedSessionDurations.size(); j += 2) {
|
||||
|
||||
String[] currentDurationString = ((String) calculatedSessionDurations.get(j)).split(" ", 2);
|
||||
long currentDuration = Long.parseLong(currentDurationString[0]);
|
||||
String[] nextDurationString = ((String) calculatedSessionDurations.get(j+1)).split(" ", 2);
|
||||
long nextDuration = Long.parseLong(nextDurationString[0]);
|
||||
|
||||
if (currentDuration > nextDuration) {
|
||||
calculatedSessionDurations.add(j, calculatedSessionDurations.get(j+1));
|
||||
calculatedSessionDurations.remove(j+2);
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 1; j+1 < calculatedSessionDurations.size(); j += 2) {
|
||||
|
||||
String[] currentDurationString = ((String) calculatedSessionDurations.get(j)).split(" ", 2);
|
||||
long currentDuration = Long.parseLong(currentDurationString[0]);
|
||||
String[] nextDurationString = ((String) calculatedSessionDurations.get(j+1)).split(" ", 2);
|
||||
long nextDuration = Long.parseLong(nextDurationString[0]);
|
||||
|
||||
if (currentDuration > nextDuration) {
|
||||
calculatedSessionDurations.add(j, calculatedSessionDurations.get(j+1));
|
||||
calculatedSessionDurations.remove(j+2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return calculatedSessionDurations;
|
||||
}
|
||||
|
||||
private ArrayList sortSessionsByNick(ArrayList sortedSessionsByTime) {
|
||||
|
||||
ArrayList sortedSessionsByNick = new ArrayList();
|
||||
|
||||
while (sortedSessionsByTime.size() != 0) {
|
||||
|
||||
String entry = (String) sortedSessionsByTime.get(0);
|
||||
String[] entryFields = entry.split(" ");
|
||||
String currentNick = entryFields[2];
|
||||
|
||||
sortedSessionsByNick.add(entry);
|
||||
sortedSessionsByNick.add(sortedSessionsByTime.get(1));
|
||||
sortedSessionsByTime.remove(0);
|
||||
sortedSessionsByTime.remove(0);
|
||||
for (int i = 0; i+1 < sortedSessionsByTime.size(); i += 2) {
|
||||
|
||||
String nextEntry = (String) sortedSessionsByTime.get(i);
|
||||
String[] nextEntryFields = nextEntry.split(" ");
|
||||
|
||||
if (nextEntryFields[2].equals(currentNick)) {
|
||||
sortedSessionsByNick.add(nextEntry);
|
||||
sortedSessionsByNick.add(sortedSessionsByTime.get(i+1));
|
||||
sortedSessionsByTime.remove(i);
|
||||
sortedSessionsByTime.remove(i);
|
||||
i -= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedSessionsByNick;
|
||||
}
|
||||
|
||||
private ArrayList sortSessionsByQuitReason(ArrayList sortedSessionsByNick) {
|
||||
|
||||
ArrayList sortedSessionsByQuitReason = new ArrayList();
|
||||
|
||||
while (sortedSessionsByNick.size() != 0) {
|
||||
|
||||
String entry = (String) sortedSessionsByNick.get(1);
|
||||
Pattern p = Pattern.compile("^[^ ]+ [^ ]+ [^ ]+ (.*)$");
|
||||
Matcher m = p.matcher(entry);
|
||||
|
||||
if (m.matches()) {
|
||||
|
||||
String currentQuitReason = m.group(1);
|
||||
|
||||
sortedSessionsByQuitReason.add(sortedSessionsByNick.get(0));
|
||||
sortedSessionsByQuitReason.add(entry);
|
||||
sortedSessionsByNick.remove(0);
|
||||
sortedSessionsByNick.remove(0);
|
||||
for (int i = 0; i+1 < sortedSessionsByNick.size(); i += 2) {
|
||||
|
||||
String nextEntry = (String) sortedSessionsByNick.get(i+1);
|
||||
Pattern p2 = Pattern.compile("^[^ ]+ [^ ]+ [^ ]+ (.*)$");
|
||||
Matcher m2 = p2.matcher(nextEntry);
|
||||
|
||||
if (m2.matches()) {
|
||||
|
||||
String nextQuitReason = m2.group(1);
|
||||
|
||||
if (nextQuitReason.equals(currentQuitReason)) {
|
||||
sortedSessionsByQuitReason.add(sortedSessionsByNick.get(i));
|
||||
sortedSessionsByQuitReason.add(nextEntry);
|
||||
sortedSessionsByNick.remove(i);
|
||||
sortedSessionsByNick.remove(i);
|
||||
i -= 2;
|
||||
}
|
||||
} else {
|
||||
System.out.println("\r\nError: Unexpected entry in logfile: " + nextEntry);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.out.println("\r\nError: Unexpected entry in logfile: " + entry);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
return sortedSessionsByQuitReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sessions terminated with "parts" messages instead of "quits" are filtered
|
||||
* out.
|
||||
*/
|
||||
private ArrayList sortSessionsByTime(ArrayList log) {
|
||||
|
||||
ArrayList sortedSessionsByTime = new ArrayList();
|
||||
|
||||
mainLoop:
|
||||
while (log.size() > 0) {
|
||||
|
||||
String entry = (String) log.get(0);
|
||||
String[] entryFields = entry.split(" ");
|
||||
|
||||
if (entryFields[1].equals("quits") && !entryFields[1].equals("joins")) {
|
||||
/*
|
||||
* Discard entry. The specified log either doesn't contain
|
||||
* the corresponding "joins" time for this quit entry or the
|
||||
* entry is a "parts" or unknown message, and in both cases
|
||||
* the entry's data is useless.
|
||||
*/
|
||||
log.remove(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 1; i < log.size(); i++) { // Find corresponding "quits" entry.
|
||||
|
||||
String tempEntry = (String) log.get(i);
|
||||
String[] tempEntryFields = tempEntry.split(" ");
|
||||
|
||||
if (tempEntryFields[2].equals(entryFields[2])) { // Check if the nick fields for the two entries match.
|
||||
if (!tempEntryFields[1].equals("quits")) {
|
||||
if (tempEntryFields[1].equals("joins")) { // Don't discard a subsequent "joins" entry.
|
||||
log.remove(0);
|
||||
continue mainLoop;
|
||||
}
|
||||
log.remove(i);
|
||||
continue;
|
||||
}
|
||||
sortedSessionsByTime.add(entry);
|
||||
sortedSessionsByTime.add(tempEntry);
|
||||
log.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Discard "joins" entry. The specified log doesn't contain the
|
||||
* corresponding "quits" time for this entry so the entry's
|
||||
* data is useless.
|
||||
*/
|
||||
|
||||
log.remove(0);
|
||||
}
|
||||
|
||||
return sortedSessionsByTime;
|
||||
}
|
||||
}
|
||||
48
apps/bogobot/LICENSE.log4j.txt
Normal file
48
apps/bogobot/LICENSE.log4j.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* ============================================================================
|
||||
* The Apache Software License, Version 1.1
|
||||
* ============================================================================
|
||||
*
|
||||
* Copyright (C) 1999 The Apache Software Foundation. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modifica-
|
||||
* tion, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. The end-user documentation included with the redistribution, if any, must
|
||||
* include the following acknowledgment: "This product includes software
|
||||
* developed by the Apache Software Foundation (http://www.apache.org/)."
|
||||
* Alternately, this acknowledgment may appear in the software itself, if
|
||||
* and wherever such third-party acknowledgments normally appear.
|
||||
*
|
||||
* 4. The names "log4j" and "Apache Software Foundation" must not be used to
|
||||
* endorse or promote products derived from this software without prior
|
||||
* written permission. For written permission, please contact
|
||||
* apache@apache.org.
|
||||
*
|
||||
* 5. Products derived from this software may not be called "Apache", nor may
|
||||
* "Apache" appear in their name, without prior written permission of the
|
||||
* Apache Software Foundation.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU-
|
||||
* DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* on behalf of the Apache Software Foundation. For more information on the
|
||||
* Apache Software Foundation, please see <http://www.apache.org/>.
|
||||
*
|
||||
*/
|
||||
@@ -55,7 +55,7 @@ patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
@@ -225,7 +225,7 @@ impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
@@ -276,3 +276,65 @@ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
1
apps/bogobot/bogobot.bat
Normal file
1
apps/bogobot/bogobot.bat
Normal file
@@ -0,0 +1 @@
|
||||
java -cp .;log4j-1.2.8.jar;pircbot.jar Bogobot
|
||||
101
apps/bogobot/bogobot.config
Normal file
101
apps/bogobot/bogobot.config
Normal file
@@ -0,0 +1,101 @@
|
||||
#####
|
||||
# Bogobot user configuration
|
||||
#####
|
||||
|
||||
###
|
||||
# The bot's nick and backup nick. You will probably want to register these with
|
||||
# the IRC server's NickServ.(a NickServ interface is forthcoming).
|
||||
#
|
||||
botPrimaryNick=somebot
|
||||
botSecondaryNick=somebot_
|
||||
|
||||
###
|
||||
# The bot's password required by Nickserv service's identify command.
|
||||
# You have to register the nickname yourself first, the bot will not.
|
||||
#
|
||||
botNickservPassword=
|
||||
|
||||
###
|
||||
# The bot's username. Appears in the whois replies
|
||||
#
|
||||
botUsername=somebot
|
||||
|
||||
#####
|
||||
# The bot owner's nick and backup nick. One of these must match the owner's
|
||||
# currently-used nick or else remote shutdown will not be possible. You will
|
||||
# probably want to register these with the IRC server's NickServ.
|
||||
#
|
||||
ownerPrimaryNick=somenick
|
||||
ownerSecondaryNick=somenick_
|
||||
|
||||
###
|
||||
# The bot will disconnect and shut down when sent this password via private
|
||||
# message (aka query) from either of the owner nicks specified above. DO NOT USE
|
||||
# THIS DEFAULT VALUE!
|
||||
#
|
||||
botShutdownPassword=take off eh
|
||||
|
||||
###
|
||||
# The server, channel, and port the bot will connect to.
|
||||
#
|
||||
ircChannel=#i2p-chat
|
||||
ircServer=irc.duck.i2p
|
||||
ircServerPort=6668
|
||||
|
||||
###
|
||||
# Set to "true" to enable logging, else "false" (but don't use quotation marks).
|
||||
#
|
||||
isLoggerEnabled=true
|
||||
|
||||
###
|
||||
# Restrict logging of joins and parts on the user hostname.
|
||||
# Leave empty to log all of them
|
||||
# Prepend with a @ for a perfect match
|
||||
# Otherwise, specify the required end of the user hostname
|
||||
#
|
||||
loggedHostnamePattern=@free.duck.i2p
|
||||
|
||||
###
|
||||
# The prefix to be used for the filenames of logs.
|
||||
#
|
||||
logFilePrefix=irc.duck.i2p.i2p-chat
|
||||
|
||||
###
|
||||
# How often the logs should be rotated. Either "daily", "weekly", or "monthly"
|
||||
# (but don't use quotation marks).
|
||||
#
|
||||
logFileRotationInterval=daily
|
||||
|
||||
###
|
||||
# Set to "true" to enable the regular round-trip delay computation,
|
||||
# else "false" (but don't use quotation marks).
|
||||
#
|
||||
isRoundTripDelayEnabled=false
|
||||
|
||||
###
|
||||
# How often should the round-trip delay be recorded.
|
||||
# (in seconds)
|
||||
#
|
||||
roundTripDelayPeriod=300
|
||||
|
||||
###
|
||||
# Set to "true" to enable the userlist command, else "false" (but don't use
|
||||
# quotation marks).
|
||||
#
|
||||
isUserlistCommandEnabled=true
|
||||
|
||||
###
|
||||
# The userlist trigger command to listen for. It is a good idea to prefix
|
||||
# triggers with some non-alphanumeric character in order to avoid accidental
|
||||
# trigger use during normal channel conversation. In most cases you will
|
||||
# probably want to choose a unique trigger here that no other bots in the
|
||||
# channel will respond to.
|
||||
#
|
||||
userlistCommandTrigger=!who
|
||||
|
||||
###
|
||||
# The number of seconds to rest after replying to a userlist command issued by
|
||||
# a user in the channel. The bot will ignore subsequent userlist commands during
|
||||
# this period. This helps prevent flooding.
|
||||
#
|
||||
commandAntiFloodInterval=60
|
||||
2
apps/bogobot/bogobot.sh
Normal file
2
apps/bogobot/bogobot.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
java -cp .:log4j-1.2.8.jar:pircbot.jar Bogobot
|
||||
58
apps/bogobot/build-eclipse.xml
Normal file
58
apps/bogobot/build-eclipse.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- ********************************************************** -->
|
||||
<!-- bogobot - A simple join/part stats logger bot for I2P IRC. -->
|
||||
<!-- -->
|
||||
<!-- build-eclipse.xml -->
|
||||
<!-- 2004 The I2P Project -->
|
||||
<!-- http://www.i2p.net -->
|
||||
<!-- This code is public domain. -->
|
||||
<!-- -->
|
||||
<!-- authors: hypercubus, oOo -->
|
||||
<!-- version 0.4 -->
|
||||
<!-- ********************************************************** -->
|
||||
|
||||
<project basedir="." default="dist" name="Bogobot">
|
||||
|
||||
<!-- init:
|
||||
Create distribution directory if missing and initialize time stamp for
|
||||
archive naming -->
|
||||
<target name="init">
|
||||
<mkdir dir="dist" />
|
||||
<tstamp>
|
||||
<format pattern="yyyy-MM-dd" property="DSTAMP" />
|
||||
</tstamp>
|
||||
</target>
|
||||
|
||||
<!-- dist.bin:
|
||||
Create the binary distribution archive -->
|
||||
<target depends="init" description="Create the binary distribution archive" name="dist.bin">
|
||||
<zip destfile="dist/Bogobot_${DSTAMP}.zip">
|
||||
<zipfileset dir="${basedir}" includes="bogobot.bat bogobot.config Bogobot.class bogobot.sh Bogoparser.class LICENSE_log4j.txt LICENSE_pircbot.txt log4j-1.2.8.jar pircbot.jar" />
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
<!-- dist.source:
|
||||
Create the source distribution archive -->
|
||||
<target depends="init" description="Create the source distribution archive" name="dist.source">
|
||||
<zip destfile="dist/Bogobot_source_${DSTAMP}.zip">
|
||||
<zipfileset dir="${basedir}" includes="bogobot.bat bogobot.config Bogobot.java bogobot.sh Bogoparser.java build.xml build_eclipse.xml LICENSE_log4j.txt LICENSE_pircbot.txt log4j-1.2.8.jar pircbot.jar" />
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
<!-- dist:
|
||||
Create both the binary and source distribution archives -->
|
||||
<target depends="dist.bin,dist.source" description="Create both the binary and source distribution archives" name="dist">
|
||||
<echo message="Successfully created binary and source distribution archives in directory 'dist'." />
|
||||
</target>
|
||||
|
||||
<!-- clean:
|
||||
Delete all class files and temporary directories -->
|
||||
<target description="Delete all class files and temporary directories" name="clean">
|
||||
<delete>
|
||||
<fileset dir="${basedir}" includes="**/*.class" />
|
||||
</delete>
|
||||
<echo message="Clean successful." />
|
||||
</target>
|
||||
|
||||
</project>
|
||||
64
apps/bogobot/build.xml
Normal file
64
apps/bogobot/build.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- ********************************************************** -->
|
||||
<!-- bogobot - A simple join/part stats logger bot for I2P IRC. -->
|
||||
<!-- -->
|
||||
<!-- build.xml -->
|
||||
<!-- 2004 The I2P Project -->
|
||||
<!-- http://www.i2p.net -->
|
||||
<!-- This code is public domain. -->
|
||||
<!-- -->
|
||||
<!-- authors: hypercubus, oOo -->
|
||||
<!-- version 0.4 -->
|
||||
<!-- ********************************************************** -->
|
||||
|
||||
<project basedir="." default="compile" name="Bogobot">
|
||||
|
||||
<!-- init:
|
||||
Create distribution directory if missing and initialize time stamp for
|
||||
archive naming -->
|
||||
<target name="init">
|
||||
<mkdir dir="dist" />
|
||||
<tstamp>
|
||||
<format pattern="yyyy-MM-dd" property="DSTAMP" />
|
||||
</tstamp>
|
||||
</target>
|
||||
|
||||
<!-- compile:
|
||||
Compile source code -->
|
||||
<target depends="init" description="Compile source code" name="compile">
|
||||
<javac classpath="${basedir};log4j-1.2.8.jar;pircbot.jar" source="1.4" srcdir="." />
|
||||
</target>
|
||||
|
||||
<!-- dist.bin:
|
||||
Create the binary distribution archive -->
|
||||
<target depends="init,compile" description="Create the binary distribution archive" name="dist.bin">
|
||||
<zip destfile="dist/Bogobot_${DSTAMP}.zip">
|
||||
<zipfileset dir="${basedir}" includes="bogobot.bat bogobot.config Bogobot.class Bogobot$BogobotTickTask.class bogobot.sh Bogoparser.class LICENSE_log4j.txt LICENSE_pircbot.txt log4j-1.2.8.jar pircbot.jar" />
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
<!-- dist.source:
|
||||
Create the source distribution archive -->
|
||||
<target depends="init" description="Create the source distribution archive" name="dist.source">
|
||||
<zip destfile="dist/Bogobot_source_${DSTAMP}.zip">
|
||||
<zipfileset dir="${basedir}" includes="bogobot.bat bogobot.config Bogobot.java bogobot.sh Bogoparser.java build.xml build_eclipse.xml LICENSE_log4j.txt LICENSE_pircbot.txt log4j-1.2.8.jar pircbot.jar" />
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
<!-- dist:
|
||||
Create both the binary and source distribution archives -->
|
||||
<target depends="dist.bin,dist.source" description="Create both the binary and source distribution archives" name="dist">
|
||||
<echo message="Successfully created binary and source distribution archives in directory 'dist'." />
|
||||
</target>
|
||||
|
||||
<!-- clean:
|
||||
Delete all class files and temporary directories -->
|
||||
<target description="Delete all class files and temporary directories" name="clean">
|
||||
<delete>
|
||||
<fileset dir="${basedir}" includes="**/*.class" />
|
||||
</delete>
|
||||
<echo message="Clean successful." />
|
||||
</target>
|
||||
|
||||
</project>
|
||||
BIN
apps/bogobot/log4j-1.2.8.jar
Normal file
BIN
apps/bogobot/log4j-1.2.8.jar
Normal file
Binary file not shown.
BIN
apps/bogobot/pircbot.jar
Normal file
BIN
apps/bogobot/pircbot.jar
Normal file
Binary file not shown.
106
apps/fortuna/build.xml
Normal file
106
apps/fortuna/build.xml
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project basedir="." default="all" name="fortuna">
|
||||
|
||||
<property name="cvs.base.dir" value="java/gnu-crypto" />
|
||||
<property name="cvs.etc.dir" value="${cvs.base.dir}/etc" />
|
||||
<property name="cvs.lib.dir" value="${cvs.base.dir}/lib" />
|
||||
<property name="cvs.object.dir" value="${cvs.base.dir}/classes" />
|
||||
<property name="cvs.base.crypto.object.dir" value="${cvs.object.dir}/gnu/crypto" />
|
||||
<property name="cvs.cipher.object.dir" value="${cvs.base.crypto.object.dir}/cipher" />
|
||||
<property name="cvs.hash.object.dir" value="${cvs.base.crypto.object.dir}/hash" />
|
||||
<property name="cvs.prng.object.dir" value="${cvs.base.crypto.object.dir}/prng" />
|
||||
|
||||
<patternset id="fortuna.files">
|
||||
<include name="${cvs.base.crypto.object.dir}/Registry.class"/>
|
||||
<include name="${cvs.prng.object.dir}/Fortuna*.class"/>
|
||||
<include name="${cvs.prng.object.dir}/BasePRNG.class"/>
|
||||
<include name="${cvs.prng.object.dir}/RandomEventListener.class"/>
|
||||
<include name="${cvs.prng.object.dir}/IRandom.class"/>
|
||||
<include name="${cvs.cipher.object.dir}/CipherFactory.class"/>
|
||||
<include name="${cvs.cipher.object.dir}/IBlockCipher.class"/>
|
||||
<include name="${cvs.hash.object.dir}/HashFactory.class"/>
|
||||
<include name="${cvs.hash.object.dir}/IMessageDigest.class"/>
|
||||
</patternset>
|
||||
|
||||
<target name="all" depends="build,jar"
|
||||
description="Create and test the custom Fortuna library" />
|
||||
|
||||
<target name="build" depends="-init,checkout"
|
||||
description="Build the source and tests">
|
||||
<ant dir="${cvs.base.dir}" target="jar" />
|
||||
</target>
|
||||
|
||||
<target name="builddep" />
|
||||
|
||||
<target name="checkout" depends="-init" unless="cvs.source.available"
|
||||
description="Check out GNU Crypto sources from CVS HEAD">
|
||||
<cvs cvsRoot=":ext:anoncvs@savannah.gnu.org:/cvsroot/gnu-crypto"
|
||||
cvsRsh="ssh"
|
||||
dest="java"
|
||||
package="gnu-crypto" />
|
||||
</target>
|
||||
|
||||
<target name="clean"
|
||||
description="Remove generated tests and object files">
|
||||
<ant dir="${cvs.base.dir}" target="clean" />
|
||||
</target>
|
||||
|
||||
<target name="cleandep" />
|
||||
|
||||
<target name="compile" />
|
||||
|
||||
<target name="distclean" depends="clean"
|
||||
description="Remove all generated files">
|
||||
<delete dir="build" />
|
||||
<delete dir="jartemp" />
|
||||
<!--
|
||||
Annoyingly the GNU Crypto distclean task called here doesn't clean
|
||||
*all* derived files from java/gnu-crypto/lib like it should.....
|
||||
-->
|
||||
<ant dir="${cvs.base.dir}" target="distclean" />
|
||||
<!--
|
||||
.....and so we mop up the rest ourselves.
|
||||
-->
|
||||
<delete dir="${cvs.lib.dir}" />
|
||||
</target>
|
||||
|
||||
<target name="-init">
|
||||
<available property="cvs.source.available" file="${cvs.base.dir}" />
|
||||
</target>
|
||||
|
||||
<target name="jar" depends="build"
|
||||
description="Create the custom Fortuna jar library">
|
||||
<delete dir="build" />
|
||||
<delete dir="jartemp" />
|
||||
<mkdir dir="build" />
|
||||
<mkdir dir="jartemp/${cvs.object.dir}" />
|
||||
<copy todir="jartemp">
|
||||
<fileset dir=".">
|
||||
<patternset refid="fortuna.files" />
|
||||
</fileset>
|
||||
</copy>
|
||||
<jar basedir="jartemp/${cvs.object.dir}" jarfile="build/fortuna.jar">
|
||||
<manifest>
|
||||
<section name="fortuna">
|
||||
<attribute name="Implementation-Title" value="I2P Custom GNU Crypto Fortuna Library" />
|
||||
<attribute name="Implementation-Version" value="CVS HEAD" />
|
||||
<attribute name="Implementation-Vendor" value="Free Software Foundation" />
|
||||
<attribute name="Implementation-Vendor-Id" value="FSF" />
|
||||
<attribute name="Implementation-URL" value="http://www.gnu.org/software/gnu-crypto" />
|
||||
</section>
|
||||
</manifest>
|
||||
</jar>
|
||||
<delete dir="jartemp" />
|
||||
</target>
|
||||
|
||||
<target name="test" depends="jar"
|
||||
description="Perform crypto tests on custom Fortuna jar library" />
|
||||
<!--
|
||||
Add this when Fortuna tests are added to GNU Crypto, else write some
|
||||
-->
|
||||
|
||||
<target name="update" depends="checkout"
|
||||
description="Update GNU Crypto sources to latest CVS HEAD">
|
||||
<cvs command="update -d" cvsRsh="ssh" dest="java/gnu-crypto" />
|
||||
</target>
|
||||
</project>
|
||||
@@ -1,11 +0,0 @@
|
||||
$Id$
|
||||
|
||||
the i2p/apps/httptunnel module is the root of the
|
||||
HTTPTunnel application, and everything within it
|
||||
is released according to the terms of the I2P
|
||||
license policy. That means everything contained
|
||||
within the i2p/apps/httptunnel module is released
|
||||
under the GPL plus the java exception unless
|
||||
otherwise marked. Alternate licenses that may be
|
||||
used include BSD, Cryptix, and MIT, as well as
|
||||
code granted into the public domain.
|
||||
@@ -1,67 +0,0 @@
|
||||
package net.i2p.httptunnel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Listens on a port for HTTP connections.
|
||||
*/
|
||||
public class HTTPListener extends Thread {
|
||||
|
||||
private static final Log _log = new Log(HTTPListener.class);
|
||||
|
||||
private int port;
|
||||
private String listenHost;
|
||||
private SocketManagerProducer smp;
|
||||
|
||||
public HTTPListener(SocketManagerProducer smp, int port,
|
||||
String listenHost) {
|
||||
this.smp = smp;
|
||||
this.port = port;
|
||||
start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
InetAddress lh = listenHost == null
|
||||
? null
|
||||
: InetAddress.getByName(listenHost);
|
||||
ServerSocket ss = new ServerSocket(port, 0, lh);
|
||||
while(true) {
|
||||
Socket s = ss.accept();
|
||||
new HTTPSocketHandler(this, s);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error while accepting connections", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean proxyUsed=false;
|
||||
|
||||
public boolean firstProxyUse() {
|
||||
// FIXME: check a config option here
|
||||
if (true) return false;
|
||||
if (proxyUsed) {
|
||||
return false;
|
||||
} else {
|
||||
proxyUsed=true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public SocketManagerProducer getSMP() {
|
||||
return smp;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public void handleNotImplemented(OutputStream out) throws IOException {
|
||||
out.write(("HTTP/1.1 200 Document following\n\n"+
|
||||
"<h1>Feature not implemented</h1>").getBytes("ISO-8859-1"));
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package net.i2p.httptunnel;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import net.i2p.httptunnel.handler.RootHandler;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Handles a single HTTP socket connection.
|
||||
*/
|
||||
public class HTTPSocketHandler extends Thread {
|
||||
|
||||
private static final Log _log = new Log(HTTPSocketHandler.class);
|
||||
|
||||
private Socket s;
|
||||
private HTTPListener httpl;
|
||||
private RootHandler h;
|
||||
|
||||
public HTTPSocketHandler(HTTPListener httpl, Socket s) {
|
||||
this.httpl = httpl;
|
||||
this.s=s;
|
||||
h = RootHandler.getInstance();
|
||||
start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
try {
|
||||
in = new BufferedInputStream(s.getInputStream());
|
||||
out = new BufferedOutputStream(s.getOutputStream());
|
||||
Request req = new Request(in);
|
||||
h.handle(req, httpl, out);
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error while handling data", ex);
|
||||
} finally {
|
||||
try {
|
||||
if (in != null) in.close();
|
||||
if (out != null) {
|
||||
out.flush();
|
||||
out.close();
|
||||
}
|
||||
s.close();
|
||||
} catch (IOException ex) {
|
||||
_log.error("IOException in finalizer", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* HTTPTunnel
|
||||
* (c) 2003 - 2004 mihi
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*
|
||||
* In addition, as a special exception, mihi gives permission to link
|
||||
* the code of this program with the proprietary Java implementation
|
||||
* provided by Sun (or other vendors as well), and distribute linked
|
||||
* combinations including the two. You must obey the GNU General
|
||||
* Public License in all respects for all of the code used other than
|
||||
* the proprietary Java implementation. If you modify this file, you
|
||||
* may extend this exception to your version of the file, but you are
|
||||
* not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
package net.i2p.httptunnel;
|
||||
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
|
||||
/**
|
||||
* HTTPTunnel main class.
|
||||
*/
|
||||
public class HTTPTunnel {
|
||||
|
||||
/**
|
||||
* Create a HTTPTunnel instance.
|
||||
*
|
||||
* @param initialManagers a list of socket managers to use
|
||||
* @param maxManagers how many managers to have in the cache
|
||||
* @param mcDonaldsMode whether to throw away a manager after use
|
||||
* @param listenPort which port to listen on
|
||||
*/
|
||||
public HTTPTunnel(I2PSocketManager[] initialManagers, int maxManagers,
|
||||
boolean mcDonaldsMode, int listenPort) {
|
||||
this(initialManagers, maxManagers, mcDonaldsMode, listenPort,
|
||||
"127.0.0.1", 7654);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a HTTPTunnel instance.
|
||||
*
|
||||
* @param initialManagers a list of socket managers to use
|
||||
* @param maxManagers how many managers to have in the cache
|
||||
* @param mcDonaldsMode whether to throw away a manager after use
|
||||
* @param listenPort which port to listen on
|
||||
* @param i2cpAddress the I2CP address
|
||||
* @param i2cpPort the I2CP port
|
||||
*/
|
||||
public HTTPTunnel(I2PSocketManager[] initialManagers, int maxManagers,
|
||||
boolean mcDonaldsMode, int listenPort,
|
||||
String i2cpAddress, int i2cpPort) {
|
||||
SocketManagerProducer smp =
|
||||
new SocketManagerProducer(initialManagers, maxManagers,
|
||||
mcDonaldsMode, i2cpAddress, i2cpPort);
|
||||
new HTTPListener(smp, listenPort, "127.0.0.1");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String host = "127.0.0.1";
|
||||
int port = 7654, max = 1;
|
||||
boolean mc = false;
|
||||
if (args.length >1) {
|
||||
if (args.length == 4) {
|
||||
host = args[2];
|
||||
port = Integer.parseInt(args[3]);
|
||||
} else if (args.length != 2) {
|
||||
showInfo(); return;
|
||||
}
|
||||
max = Integer.parseInt(args[1]);
|
||||
} else if (args.length != 1) {
|
||||
showInfo(); return;
|
||||
}
|
||||
if (max == 0) {
|
||||
max = 1;
|
||||
} else if (max <0) {
|
||||
max = -max;
|
||||
mc = true;
|
||||
}
|
||||
new HTTPTunnel(null, max, mc, Integer.parseInt(args[0]), host, port);
|
||||
}
|
||||
|
||||
private static void showInfo() {
|
||||
System.out.println
|
||||
("Usage: java HTTPTunnel <listenPort> [<max> "+
|
||||
"[<i2cphost> <i2cpport>]]\n"+
|
||||
" <listenPort> port to listen for browsers\n"+
|
||||
" <max> max number of SocketMangers in pool, "+
|
||||
"use neg. number\n"+
|
||||
" to use each SocketManager only once "+
|
||||
"(default: 1)\n"+
|
||||
" <i2cphost> host to connect to the router "+
|
||||
"(default: 127.0.0.1)\n"+
|
||||
" <i2cpport> port to connect to the router "+
|
||||
"(default: 7654)");
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package net.i2p.httptunnel;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* A HTTP request (GET or POST). This will be passed to a hander for
|
||||
* handling it.
|
||||
*/
|
||||
public class Request {
|
||||
|
||||
private static final Log _log = new Log(Request.class);
|
||||
|
||||
// all strings are forced to be ISO-8859-1 encoding
|
||||
private String method;
|
||||
private String url;
|
||||
private String proto;
|
||||
private String params;
|
||||
private String postData;
|
||||
|
||||
public Request(InputStream in) throws IOException {
|
||||
BufferedReader br = new BufferedReader
|
||||
(new InputStreamReader(in, "ISO-8859-1"));
|
||||
String line = br.readLine();
|
||||
if (line == null) { // no data at all
|
||||
method = null;
|
||||
_log.error("Connection but no data");
|
||||
return;
|
||||
}
|
||||
int pos = line.indexOf(" ");
|
||||
if (pos == -1) {
|
||||
method = line;
|
||||
url="";
|
||||
_log.error("Malformed HTTP request: "+line);
|
||||
} else {
|
||||
method = line.substring(0,pos);
|
||||
url=line.substring(pos+1);
|
||||
}
|
||||
proto="";
|
||||
pos = url.indexOf(" ");
|
||||
if (pos != -1) {
|
||||
proto=url.substring(pos); // leading space intended
|
||||
url = url.substring(0,pos);
|
||||
}
|
||||
StringBuffer sb = new StringBuffer(512);
|
||||
while((line=br.readLine()) != null) {
|
||||
if (line.length() == 0) break;
|
||||
sb.append(line).append("\r\n");
|
||||
}
|
||||
params = sb.toString(); // no leading empty line!
|
||||
sb = new StringBuffer();
|
||||
// hack for POST requests, ripped from HttpClient
|
||||
// this won't work for large POSTDATA
|
||||
// FIXME: do this better, please.
|
||||
if (!method.equals("GET")) {
|
||||
while (br.ready()) { // empty the buffer (POST requests)
|
||||
int i=br.read();
|
||||
if (i != -1) {
|
||||
sb.append((char)i);
|
||||
}
|
||||
}
|
||||
postData = sb.toString();
|
||||
} else {
|
||||
postData="";
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] toByteArray() throws IOException {
|
||||
if (method == null) return null;
|
||||
return toISO8859_1String().getBytes("ISO-8859-1");
|
||||
|
||||
}
|
||||
|
||||
private String toISO8859_1String() throws IOException {
|
||||
if (method == null) return null;
|
||||
return method+" "+url+proto+"\r\n"+params+"\r\n"+postData;
|
||||
}
|
||||
|
||||
public String getURL() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setURL(String newURL) {
|
||||
url=newURL;
|
||||
}
|
||||
|
||||
public String getParam(String name) {
|
||||
try {
|
||||
BufferedReader br= new BufferedReader(new StringReader(params));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.startsWith(name)) {
|
||||
return line.substring(name.length());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error getting parameter", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setParam(String name, String value) {
|
||||
try {
|
||||
StringBuffer sb = new StringBuffer(params.length()+value.length());
|
||||
BufferedReader br= new BufferedReader(new StringReader(params));
|
||||
String line;
|
||||
boolean replaced = false;
|
||||
while((line=br.readLine()) != null) {
|
||||
if (line.startsWith(name)) {
|
||||
replaced=true;
|
||||
if (value == null) continue; // kill param
|
||||
line = name+value;
|
||||
}
|
||||
sb.append(line).append("\r\n");
|
||||
}
|
||||
if (!replaced && value != null) {
|
||||
sb.append(name).append(value).append("\r\n");
|
||||
}
|
||||
params=sb.toString();
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error getting parameter", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package net.i2p.httptunnel;
|
||||
|
||||
import java.util.*;
|
||||
import net.i2p.client.streaming.*;
|
||||
|
||||
/**
|
||||
* Produces SocketManagers in a thread and gives them to those who
|
||||
* need them.
|
||||
*/
|
||||
public class SocketManagerProducer extends Thread {
|
||||
|
||||
private ArrayList myManagers = new ArrayList();
|
||||
private HashMap usedManagers = new HashMap();
|
||||
private int port;
|
||||
private String host;
|
||||
private int maxManagers;
|
||||
private boolean mcDonalds;
|
||||
|
||||
public SocketManagerProducer(I2PSocketManager[] initialManagers,
|
||||
int maxManagers,
|
||||
boolean mcDonaldsMode,
|
||||
String host, int port) {
|
||||
if (maxManagers < 1) {
|
||||
throw new IllegalArgumentException("maxManagers < 1");
|
||||
}
|
||||
this.host=host;
|
||||
this.port=port;
|
||||
mcDonalds=mcDonaldsMode;
|
||||
if (initialManagers != null) {
|
||||
myManagers.addAll(Arrays.asList(initialManagers));
|
||||
}
|
||||
this.maxManagers=maxManagers;
|
||||
mcDonalds=mcDonaldsMode;
|
||||
setDaemon(true);
|
||||
start();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Thread producing new SocketManagers.
|
||||
*/
|
||||
public void run() {
|
||||
while (true) {
|
||||
synchronized(this) {
|
||||
// without mcDonalds mode, we most probably need no
|
||||
// new managers.
|
||||
while (!mcDonalds && myManagers.size() == maxManagers) {
|
||||
myWait();
|
||||
}
|
||||
}
|
||||
// produce a new manager, regardless whether it is needed
|
||||
// or not. Do not synchronized this part, since it can be
|
||||
// quite time-consuming.
|
||||
I2PSocketManager newManager =
|
||||
I2PSocketManagerFactory.createManager(host, port,
|
||||
new Properties());
|
||||
// when done, check if it is needed.
|
||||
synchronized(this) {
|
||||
while(myManagers.size() == maxManagers) {
|
||||
myWait();
|
||||
}
|
||||
myManagers.add(newManager);
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a manager for connecting to a given destination. Each
|
||||
* destination will always get the same manager.
|
||||
*
|
||||
* @param dest the destination to connect to
|
||||
* @return the SocketManager to use
|
||||
*/
|
||||
public synchronized I2PSocketManager getManager(String dest) {
|
||||
I2PSocketManager result = (I2PSocketManager) usedManagers.get(dest);
|
||||
if (result == null) {
|
||||
result = getManager();
|
||||
usedManagers.put(dest,result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a "new" SocketManager. Depending on the anonymity settings,
|
||||
* this can be a completely new one or one randomly selected from
|
||||
* a pool.
|
||||
*
|
||||
* @return the SocketManager to use
|
||||
*/
|
||||
public synchronized I2PSocketManager getManager() {
|
||||
while (myManagers.size() == 0) {
|
||||
myWait(); // no manager here, so wait until one is produced
|
||||
}
|
||||
int which = (int)(Math.random()*myManagers.size());
|
||||
I2PSocketManager result = (I2PSocketManager) myManagers.get(which);
|
||||
if (mcDonalds) {
|
||||
myManagers.remove(which);
|
||||
notifyAll();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void myWait() {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package net.i2p.httptunnel.filter;
|
||||
|
||||
import net.i2p.util.Log;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Chain multiple filters. Decorator pattern...
|
||||
*/
|
||||
public class ChainFilter implements Filter {
|
||||
|
||||
private static final Log _log = new Log(ChainFilter.class);
|
||||
|
||||
public Collection filters;
|
||||
|
||||
public ChainFilter(Collection filters) {
|
||||
this.filters=filters;
|
||||
}
|
||||
|
||||
public byte[] filter(byte[] toFilter) {
|
||||
byte[] buf = toFilter;
|
||||
for (Iterator it = filters.iterator(); it.hasNext();) {
|
||||
Filter f = (Filter) it.next();
|
||||
buf = f.filter(buf);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public byte[] finish() {
|
||||
// this is a bit complicated. Think about it...
|
||||
try {
|
||||
byte[] buf = EMPTY;
|
||||
for (Iterator it = filters.iterator(); it.hasNext();) {
|
||||
Filter f = (Filter) it.next();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
if (buf.length != 0) {
|
||||
baos.write(f.filter(buf));
|
||||
}
|
||||
baos.write(f.finish());
|
||||
buf = baos.toByteArray();
|
||||
}
|
||||
return buf;
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error chaining filters", ex);
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package net.i2p.httptunnel.filter;
|
||||
|
||||
/**
|
||||
* A generic filtering interface.
|
||||
*/
|
||||
public interface Filter {
|
||||
|
||||
/**
|
||||
* An empty byte array.
|
||||
*/
|
||||
public static final byte[] EMPTY = new byte[0];
|
||||
|
||||
/**
|
||||
* Filter some data. Not all filtered data need to be returned.
|
||||
*/
|
||||
public byte[] filter(byte[] toFilter);
|
||||
|
||||
/**
|
||||
* Data stream has finished. Return all of the rest data.
|
||||
*/
|
||||
public byte[] finish();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package net.i2p.httptunnel.filter;
|
||||
|
||||
/**
|
||||
* A filter letting everything pass as is.
|
||||
*/
|
||||
public class NullFilter implements Filter {
|
||||
|
||||
public byte[] filter(byte[] toFilter) {
|
||||
return toFilter;
|
||||
}
|
||||
|
||||
public byte[] finish() {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package net.i2p.httptunnel.handler;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.naming.NamingService;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.client.streaming.I2PSocketOptions;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.httptunnel.HTTPListener;
|
||||
import net.i2p.httptunnel.Request;
|
||||
import net.i2p.httptunnel.SocketManagerProducer;
|
||||
import net.i2p.httptunnel.filter.Filter;
|
||||
import net.i2p.httptunnel.filter.NullFilter;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Handler for browsing Eepsites.
|
||||
*/
|
||||
public class EepHandler {
|
||||
|
||||
private static final Log _log = new Log(EepHandler.class);
|
||||
|
||||
protected ErrorHandler errorHandler;
|
||||
|
||||
/* package private */ EepHandler(ErrorHandler eh) {
|
||||
errorHandler=eh;
|
||||
}
|
||||
|
||||
public void handle(Request req, HTTPListener httpl, OutputStream out,
|
||||
boolean fromProxy, String destination)
|
||||
throws IOException {
|
||||
SocketManagerProducer smp = httpl.getSMP();
|
||||
Destination dest = NamingService.getInstance().lookup(destination);
|
||||
if (dest == null) {
|
||||
errorHandler.handle(req, httpl, out,
|
||||
"Could not lookup host: "+destination);
|
||||
return;
|
||||
}
|
||||
I2PSocketManager sm = smp.getManager(destination);
|
||||
Filter f = new NullFilter(); //FIXME: use other filter
|
||||
req.setParam("Host: ", dest.toBase64());
|
||||
if (!handle(req, f, out, dest, sm)) {
|
||||
errorHandler.handle(req, httpl, out, "Unable to reach peer");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handle(Request req, Filter f, OutputStream out,
|
||||
Destination dest, I2PSocketManager sm)
|
||||
throws IOException {
|
||||
I2PSocket s = null;
|
||||
boolean written = false;
|
||||
try {
|
||||
synchronized(sm) {
|
||||
s = sm.connect(dest, new I2PSocketOptions());
|
||||
}
|
||||
InputStream in = new BufferedInputStream(s.getInputStream());
|
||||
OutputStream sout = new BufferedOutputStream(s.getOutputStream());
|
||||
sout.write(req.toByteArray());
|
||||
sout.flush();
|
||||
byte[] buffer = new byte[16384], filtered;
|
||||
int len;
|
||||
while ((len=in.read(buffer)) != -1) {
|
||||
if (len != buffer.length) {
|
||||
byte[] b2 = new byte[len];
|
||||
System.arraycopy(buffer, 0, b2, 0, len);
|
||||
filtered=f.filter(b2);
|
||||
} else {
|
||||
filtered=f.filter(buffer);
|
||||
}
|
||||
written=true;
|
||||
out.write(filtered);
|
||||
}
|
||||
filtered=f.finish();
|
||||
written=true;
|
||||
out.write(filtered);
|
||||
out.flush();
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error while handling eepsite request");
|
||||
return written;
|
||||
} catch (I2PException ex) {
|
||||
_log.error("Error while handling eepsite request");
|
||||
return written;
|
||||
} finally {
|
||||
if (s != null) s.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package net.i2p.httptunnel.handler;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.httptunnel.HTTPListener;
|
||||
import net.i2p.httptunnel.Request;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Handler for general error messages.
|
||||
*/
|
||||
public class ErrorHandler {
|
||||
|
||||
private static final Log _log = new Log(ErrorHandler.class);
|
||||
|
||||
/* package private */ ErrorHandler() {
|
||||
|
||||
}
|
||||
|
||||
public void handle(Request req, HTTPListener httpl,
|
||||
OutputStream out, String error) throws IOException {
|
||||
// FIXME: Make nicer messages for more likely errors.
|
||||
out.write(("HTTP/1.1 500 Internal Server Error\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n\r\n")
|
||||
.getBytes("ISO-8859-1"));
|
||||
out.write(("<html><head><title>"+error+"</title></head><body><h1>"+
|
||||
error+"</h1>An internal error occurred while "+
|
||||
"handling a request by HTTPTunnel:<br><b>"+error+
|
||||
"</b><h2>Complete request:</h2><b>---</b><br><i><pre>\r\n")
|
||||
.getBytes("ISO-8859-1"));
|
||||
out.write(req.toByteArray());
|
||||
out.write(("</pre></i><br><b>---</b></body></html>")
|
||||
.getBytes("ISO-8859-1"));
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package net.i2p.httptunnel.handler;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.httptunnel.HTTPListener;
|
||||
import net.i2p.httptunnel.Request;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Handler for requests that do not require any connection to anyone
|
||||
* (except errors).
|
||||
*/
|
||||
public class LocalHandler {
|
||||
|
||||
private static final Log _log = new Log(LocalHandler.class);
|
||||
|
||||
/* package private */ LocalHandler() {
|
||||
|
||||
}
|
||||
|
||||
public void handle(Request req, HTTPListener httpl, OutputStream out,
|
||||
boolean fromProxy) throws IOException {
|
||||
//FIXME: separate multiple pages, not only a start page
|
||||
//FIXME: provide some info on this page
|
||||
out.write(("HTTP/1.1 200 Document following\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n\r\n"+
|
||||
"<html><head><title>Welcome to I2P HTTPTunnel</title>"+
|
||||
"</head><body><h1>Welcome to I2P HTTPTunnel</h1>You can "+
|
||||
"browse Eepsites by adding an eepsite name to the request."+
|
||||
"</body></html>").getBytes("ISO-8859-1"));
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public void handleProxyConfWarning(Request req, HTTPListener httpl,
|
||||
OutputStream out) throws IOException {
|
||||
//FIXME
|
||||
throw new IOException("jrandom ate the deprecated method. mooo");
|
||||
//httpl.handleNotImplemented(out);
|
||||
|
||||
}
|
||||
|
||||
public void handleHTTPWarning(Request req, HTTPListener httpl,
|
||||
OutputStream out, boolean fromProxy)
|
||||
throws IOException {
|
||||
// FIXME
|
||||
throw new IOException("jrandom ate the deprecated method. mooo");
|
||||
//httpl.handleNotImplemented(out);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package net.i2p.httptunnel.handler;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.client.naming.NamingService;
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.httptunnel.HTTPListener;
|
||||
import net.i2p.httptunnel.Request;
|
||||
import net.i2p.httptunnel.SocketManagerProducer;
|
||||
import net.i2p.httptunnel.filter.Filter;
|
||||
import net.i2p.httptunnel.filter.NullFilter;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Handler for proxying "normal" HTTP requests.
|
||||
*/
|
||||
public class ProxyHandler extends EepHandler {
|
||||
|
||||
private static final Log _log = new Log(ErrorHandler.class);
|
||||
|
||||
/* package private */ ProxyHandler(ErrorHandler eh) {
|
||||
super(eh);
|
||||
}
|
||||
|
||||
public void handle(Request req, HTTPListener httpl, OutputStream out,
|
||||
boolean fromProxy) throws IOException {
|
||||
SocketManagerProducer smp = httpl.getSMP();
|
||||
Destination dest = findProxy();
|
||||
if (dest == null) {
|
||||
errorHandler.handle(req, httpl, out,
|
||||
"Could not find proxy");
|
||||
return;
|
||||
}
|
||||
// one manager for all proxy requests
|
||||
I2PSocketManager sm = smp.getManager("--proxy--");
|
||||
Filter f = new NullFilter(); //FIXME: use other filter
|
||||
if (!handle(req, f, out, dest, sm)) {
|
||||
errorHandler.handle(req, httpl, out, "Unable to reach peer");
|
||||
}
|
||||
}
|
||||
|
||||
private Destination findProxy() {
|
||||
//FIXME!
|
||||
return NamingService.getInstance().lookup("squid.i2p");
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package net.i2p.httptunnel.handler;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.httptunnel.HTTPListener;
|
||||
import net.i2p.httptunnel.Request;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Main handler for all requests. Dispatches requests to other handlers.
|
||||
*/
|
||||
public class RootHandler {
|
||||
|
||||
private static final Log _log = new Log(RootHandler.class);
|
||||
|
||||
private RootHandler() {
|
||||
errorHandler=new ErrorHandler();
|
||||
localHandler=new LocalHandler();
|
||||
proxyHandler=new ProxyHandler(errorHandler);
|
||||
eepHandler=new EepHandler(errorHandler);
|
||||
}
|
||||
|
||||
private ErrorHandler errorHandler;
|
||||
private ProxyHandler proxyHandler;
|
||||
private LocalHandler localHandler;
|
||||
private EepHandler eepHandler;
|
||||
|
||||
private static RootHandler instance;
|
||||
|
||||
public static synchronized RootHandler getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RootHandler();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void handle(Request req, HTTPListener httpl,
|
||||
OutputStream out) throws IOException {
|
||||
String url=req.getURL();
|
||||
System.out.println(url);
|
||||
boolean byProxy = false;
|
||||
int pos;
|
||||
if (url.startsWith("http://")) { // access via proxy
|
||||
byProxy=true;
|
||||
if (httpl.firstProxyUse()) {
|
||||
localHandler.handleProxyConfWarning(req,httpl,out);
|
||||
return;
|
||||
}
|
||||
url = url.substring(7);
|
||||
pos = url.indexOf("/");
|
||||
String host;
|
||||
String rest;
|
||||
if (pos == -1) {
|
||||
errorHandler.handle(req, httpl, out, "No host end in URL");
|
||||
return;
|
||||
} else {
|
||||
host = url.substring(0,pos);
|
||||
url = url.substring(pos);
|
||||
if ("i2p".equals(host) || "i2p.i2p".equals(host)) {
|
||||
// normal request; go on below...
|
||||
} else if (host.endsWith(".i2p")) {
|
||||
// "old" service request, send a redirect...
|
||||
out.write(("HTTP/1.1 302 Moved\r\nLocation: "+
|
||||
"http://i2p.i2p/"+host+url+
|
||||
"\r\n\r\n").getBytes("ISO-8859-1"));
|
||||
return;
|
||||
} else {
|
||||
// this is for proxying to the real web
|
||||
proxyHandler.handle(req, httpl, out, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (url.equals("/")) { // main page
|
||||
url="/_/local/index";
|
||||
} else if (!url.startsWith("/")) {
|
||||
errorHandler.handle(req, httpl, out,
|
||||
"No leading slash in URL: "+url);
|
||||
return;
|
||||
}
|
||||
String dest;
|
||||
url=url.substring(1);
|
||||
pos = url.indexOf("/");
|
||||
if (pos == -1) {
|
||||
dest=url;
|
||||
url="/";
|
||||
} else {
|
||||
dest = url.substring(0,pos);
|
||||
url=url.substring(pos);
|
||||
}
|
||||
req.setURL(url);
|
||||
if (dest.equals("_")) { // no eepsite
|
||||
if (url.startsWith("/local/")) { // local request
|
||||
req.setURL(url.substring(6));
|
||||
localHandler.handle(req, httpl, out, byProxy);
|
||||
} else if (url.startsWith("/http/")) { // http warning
|
||||
localHandler.handleHTTPWarning(req, httpl, out, byProxy);
|
||||
} else if (url.startsWith("/proxy/")) { // http proxying
|
||||
req.setURL("http://"+url.substring(7));
|
||||
proxyHandler.handle(req, httpl, out, byProxy);
|
||||
} else {
|
||||
errorHandler.handle(req, httpl, out,
|
||||
"No local handler for this URL: "+url);
|
||||
}
|
||||
} else {
|
||||
eepHandler.handle(req, httpl, out, byProxy, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
340
apps/i2psnark/COPYING
Normal file
340
apps/i2psnark/COPYING
Normal file
@@ -0,0 +1,340 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
24
apps/i2psnark/TODO
Normal file
24
apps/i2psnark/TODO
Normal file
@@ -0,0 +1,24 @@
|
||||
- I2PSnark:
|
||||
- add multitorrent support by checking the metainfo hash in the
|
||||
PeerAcceptor and feeding it off to the appropriate coordinator
|
||||
- add a web interface
|
||||
|
||||
- BEncode
|
||||
- Byte array length indicator can overflow.
|
||||
- Support really big BigNums (only 256 chars allowed now)
|
||||
- Better BEValue toString(). Uses stupid heuristic now for debugging.
|
||||
- Implemented bencoding.
|
||||
- Remove application level hack to calculate sha1 hash for metainfo
|
||||
(But can it be done as efficiently?)
|
||||
|
||||
- Storage
|
||||
- Check file name filter.
|
||||
|
||||
- TrackerClient
|
||||
- Support undocumented &numwant= request.
|
||||
|
||||
- PeerCoordinator
|
||||
- Disconnect from other seeds as soon as you are a seed yourself.
|
||||
|
||||
- Text UI
|
||||
- Make it completely silent.
|
||||
1
apps/i2psnark/authors.snark
Normal file
1
apps/i2psnark/authors.snark
Normal file
@@ -0,0 +1 @@
|
||||
Mark Wielaard <mark@klomp.org>
|
||||
487
apps/i2psnark/changelog.snark
Normal file
487
apps/i2psnark/changelog.snark
Normal file
@@ -0,0 +1,487 @@
|
||||
2003-06-27 14:24 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* README: Update version number and explain new features.
|
||||
|
||||
2003-06-27 13:51 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/GnomeInfoWindow.java,
|
||||
org/klomp/snark/GnomePeerList.java,
|
||||
org/klomp/snark/PeerCoordinator.java,
|
||||
org/klomp/snark/SnarkGnome.java: Add GnomeInfoWindow.
|
||||
|
||||
2003-06-27 00:37 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Implement 'info' and 'list' commands.
|
||||
|
||||
2003-06-27 00:05 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/GnomePeerList.java,
|
||||
org/klomp/snark/SnarkGnome.java: Add GnomePeerList to show state of
|
||||
connected peers.
|
||||
|
||||
2003-06-27 00:04 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: Peer.java, PeerID.java: Make Comparable.
|
||||
|
||||
2003-06-23 23:32 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerMonitorTask.java: Correctly update
|
||||
lastDownloaded and lastUploaded.
|
||||
|
||||
2003-06-23 23:20 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: When checking storage use the
|
||||
MetaInfo from the storage.
|
||||
|
||||
2003-06-23 21:47 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Storage.java: Fill piece hashes, not info hashes.
|
||||
|
||||
2003-06-23 21:42 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/MetaInfo.java: New package private
|
||||
getPieceHashes() method.
|
||||
|
||||
2003-06-22 19:49 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* README, TODO, org/klomp/snark/Snark.java: Add new command line
|
||||
switch --no-commands. Don't read interactive commands or show
|
||||
usage info.
|
||||
|
||||
2003-06-22 19:26 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/PeerCheckerTask.java,
|
||||
org/klomp/snark/PeerMonitorTask.java, org/klomp/snark/Snark.java:
|
||||
Split peer statistic reporting from PeerCheckerTask into
|
||||
PeerMonitorTask. Use new task in Snark text ui.
|
||||
|
||||
2003-06-22 18:32 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Only print peer id when debug level
|
||||
is INFO or higher.
|
||||
|
||||
2003-06-22 18:00 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/ShutdownListener.java: Add new ShutdownListener
|
||||
interface.
|
||||
|
||||
2003-06-22 17:18 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* TODO: Text UI item to not read from stdin.
|
||||
|
||||
2003-06-22 17:18 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* snark-gnome.sh: kaffe java-gnome support (but crashes hard at the
|
||||
moment).
|
||||
|
||||
2003-06-22 14:04 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/CoordinatorListener.java,
|
||||
org/klomp/snark/PeerCoordinator.java,
|
||||
org/klomp/snark/ProgressListener.java, org/klomp/snark/Snark.java,
|
||||
org/klomp/snark/SnarkGnome.java,
|
||||
org/klomp/snark/SnarkShutdown.java, org/klomp/snark/Storage.java,
|
||||
org/klomp/snark/StorageListener.java: Split ProgressListener into
|
||||
Storage, Coordinator and Shutdown listener.
|
||||
|
||||
2003-06-20 19:06 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: PeerCoordinator.java, Snark.java,
|
||||
SnarkGnome.java, Storage.java: Progress listeners for both Storage
|
||||
and PeerCoordinator.
|
||||
|
||||
2003-06-20 14:50 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/PeerCoordinator.java,
|
||||
org/klomp/snark/ProgressListener.java,
|
||||
org/klomp/snark/SnarkGnome.java: Add ProgressListener.
|
||||
|
||||
2003-06-20 13:22 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/SnarkGnome.java: Add Pieces collected field.
|
||||
|
||||
2003-06-20 12:26 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: PeerCoordinator.java, PeerListener.java,
|
||||
PeerState.java: Add PeerListener.downloaded() which gets called on
|
||||
chunk updates. Keep PeerCoordinator.downloaded up to date using
|
||||
this remove adjusting in gotPiece() except when we receive a bad
|
||||
piece.
|
||||
|
||||
2003-06-16 00:27 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, snark-gnome.sh, org/klomp/snark/Snark.java,
|
||||
org/klomp/snark/SnarkGnome.java: Start of a Gnome GUI.
|
||||
|
||||
2003-06-05 13:19 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerCoordinator.java: Don't remove a BAD piece
|
||||
from the wantedPieces list. Revert to synchronizing on
|
||||
wantedPieces for all relevant sections.
|
||||
|
||||
2003-06-03 21:09 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Only call readLine() when !quit.
|
||||
Always print exception when fatal() is called.
|
||||
|
||||
2003-06-01 23:12 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* README: Set release version to 0.4.
|
||||
|
||||
2003-06-01 22:59 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionIn.java: Handle negative length
|
||||
prefixes (terminates connection).
|
||||
|
||||
2003-06-01 21:34 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: Snark.java, SnarkShutdown.java: Implement
|
||||
correct shutdown and read commands from stdin.
|
||||
|
||||
2003-06-01 21:34 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/TrackerInfo.java: Check that interval and peers
|
||||
list actually exist.
|
||||
|
||||
2003-06-01 21:33 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Storage.java: Implement close().
|
||||
|
||||
2003-06-01 21:05 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Fix debug logging.
|
||||
|
||||
2003-06-01 20:55 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerCoordinator.java: Implement halt().
|
||||
|
||||
2003-06-01 20:55 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/ConnectionAcceptor.java: Rename stop() to halt().
|
||||
|
||||
2003-06-01 17:35 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Drop lock on this when calling
|
||||
addRequest() from havePiece().
|
||||
|
||||
2003-06-01 14:46 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* README, org/klomp/snark/ConnectionAcceptor.java,
|
||||
org/klomp/snark/HttpAcceptor.java, org/klomp/snark/Peer.java,
|
||||
org/klomp/snark/PeerCheckerTask.java,
|
||||
org/klomp/snark/PeerConnectionIn.java,
|
||||
org/klomp/snark/PeerConnectionOut.java,
|
||||
org/klomp/snark/PeerCoordinator.java,
|
||||
org/klomp/snark/PeerState.java, org/klomp/snark/Snark.java,
|
||||
org/klomp/snark/SnarkShutdown.java, org/klomp/snark/Storage.java,
|
||||
org/klomp/snark/Tracker.java, org/klomp/snark/TrackerClient.java:
|
||||
Add debug/log level.
|
||||
|
||||
2003-05-31 23:04 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java: Use
|
||||
just one lock (peers) for all synchronization (even for
|
||||
wantedPieces). Let PeerChecker handle real disconnect and keep
|
||||
count of uploaders.
|
||||
|
||||
2003-05-31 22:29 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: Peer.java, PeerConnectionIn.java: Set state to
|
||||
null on first disconnect() call. So always check whether it might
|
||||
already be null. Helps disconnect check.
|
||||
|
||||
2003-05-31 22:27 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionOut.java: Don't explicitly close
|
||||
the DataOutputStream (if another thread is using it libgcj seems to
|
||||
not like it very much).
|
||||
|
||||
2003-05-30 21:33 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionOut.java: Cancel
|
||||
(un)interested/(un)choke when (inverse) is still in send queue.
|
||||
Remove pieces from send queue when choke message is actaully send.
|
||||
|
||||
2003-05-30 19:32 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Make sure listener.wantPiece(int)
|
||||
is never called while lock on this is held.
|
||||
|
||||
2003-05-30 19:00 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionOut.java: Indentation cleanup.
|
||||
|
||||
2003-05-30 17:50 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Storage.java: Only synchronize on bitfield as
|
||||
long as necessary.
|
||||
|
||||
2003-05-30 17:43 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Tracker.java: Identing cleanup.
|
||||
|
||||
2003-05-30 16:32 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Better error message.
|
||||
|
||||
2003-05-30 15:11 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Make sure not to hold the lock on
|
||||
this when calling the listener to prevent deadlocks. Implement
|
||||
handling and sending of cancel messages.
|
||||
|
||||
2003-05-30 14:50 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerCoordinator.java: First check if we still
|
||||
want a piece before trying to add it to the Storage.
|
||||
|
||||
2003-05-30 14:49 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionOut.java: Implement
|
||||
sendCancel(Request). Add cancelRequest(int, int, int).
|
||||
|
||||
2003-05-30 14:46 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Request.java: Add hashCode() and equals(Object)
|
||||
methods.
|
||||
|
||||
2003-05-30 14:45 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Peer.java: Fix wheter -> whether javadoc
|
||||
comments. Mark state null immediatly after calling
|
||||
listener.disconnected(). Call PeerState.havePiece() not
|
||||
PeerConnectionOut.sendHave() directly.
|
||||
|
||||
2003-05-25 19:23 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* TODO: Add PeerCoordinator TODO for connecting to seeds.
|
||||
|
||||
2003-05-23 12:12 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile: Create class files with jikes again.
|
||||
|
||||
2003-05-18 22:01 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java:
|
||||
Prefer to (optimistically) unchoke first those peers that unchoked
|
||||
us. And make sure to not unchoke a peer that we just choked.
|
||||
|
||||
2003-05-18 21:48 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Peer.java: Fix isChoked() to not always return
|
||||
true.
|
||||
|
||||
2003-05-18 14:46 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: Peer.java, PeerCheckerTask.java,
|
||||
PeerCoordinator.java, PeerState.java: Remove separate Peer
|
||||
downloading/uploading states. Keep choke and interest always up to
|
||||
date. Uploading is now just when we are not choking the peer.
|
||||
Downloading is now defined as being unchoked and interesting.
|
||||
CHECK_PERIOD is now 20 seconds. MAX_CONNECTIONS is now 24.
|
||||
MAX_DOWNLOADERS doesn't exists anymore. We download whenever we can
|
||||
from peers.
|
||||
|
||||
2003-05-18 13:57 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionOut.java: Remove piece messages
|
||||
from queue when we are choking. (They will have to be rerequested
|
||||
when we unchoke the peer again.)
|
||||
|
||||
2003-05-15 00:08 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Ignore missed chunk requests,
|
||||
don't requeue them.
|
||||
|
||||
2003-05-15 00:06 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Request.java: Add sanity check
|
||||
|
||||
2003-05-10 15:47 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Add extra '(' to usage message.
|
||||
|
||||
2003-05-10 15:22 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* README: Set version to 0.3 (The Bakers Tale).
|
||||
|
||||
2003-05-10 15:17 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Mention received piece in warning
|
||||
message.
|
||||
|
||||
2003-05-10 03:20 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: PeerConnectionIn.java, PeerState.java,
|
||||
Request.java: Remove currentRequest and handle all piece messages
|
||||
from the lastRequested list.
|
||||
|
||||
2003-05-09 20:02 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Fix nothing requested warning
|
||||
message.
|
||||
|
||||
2003-05-09 19:59 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionOut.java: Piece messages are big.
|
||||
So if there are other (control) messages make sure they are send
|
||||
first. Also remove request messages from the queue if we are
|
||||
currently being choked to prevent them from being send even if we
|
||||
get unchoked a little later. (Since we will resent them anyway in
|
||||
that case.)
|
||||
|
||||
2003-05-09 18:33 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: Peer.java, PeerCheckerTask.java,
|
||||
PeerCoordinator.java, PeerID.java: New definition of PeerID.equals
|
||||
(port + address + id) and new method PeerID.sameID (only id). These
|
||||
are used to really see if we already have a connection to a certain
|
||||
peer (active setup vs passive setup).
|
||||
|
||||
2003-05-08 03:05 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Use Snark.debug() not
|
||||
System.out.println().
|
||||
|
||||
2003-05-06 20:29 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: s/noting/nothing/
|
||||
|
||||
2003-05-06 20:28 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile: s/lagacy/legacy/
|
||||
|
||||
2003-05-05 23:17 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* README: Set version to 0.2, explain new functionality and add
|
||||
examples.
|
||||
|
||||
2003-05-05 22:42 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* .cvsignore, Makefile, org/klomp/snark/StaticSnark.java: Enable
|
||||
-static binary creation.
|
||||
|
||||
2003-05-05 22:42 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Tracker.java: Disable --ip support.
|
||||
|
||||
2003-05-05 21:02 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: HttpAcceptor.java, PeerCheckerTask.java,
|
||||
PeerCoordinator.java, TrackerClient.java: Use Snark.debug() not
|
||||
System.out.println().
|
||||
|
||||
2003-05-05 21:01 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerConnectionIn.java: Be prepared to handle the
|
||||
case where currentRequest is null.
|
||||
|
||||
2003-05-05 21:00 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Improve argument parsing errors.
|
||||
|
||||
2003-05-05 21:00 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile: Use gcj -C again for creating the class files.
|
||||
|
||||
2003-05-05 09:24 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Just clear outstandingRequests,
|
||||
never make it null.
|
||||
|
||||
2003-05-05 02:55 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/TrackerClient.java: Always retry both first
|
||||
started event and every other event as long the TrackerClient is
|
||||
not stopped.
|
||||
|
||||
2003-05-05 02:54 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Remove double assignment port.
|
||||
|
||||
2003-05-05 02:54 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* TODO: Add Tracker TODO item.
|
||||
|
||||
2003-05-04 23:38 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: ConnectionAcceptor.java, MetaInfo.java,
|
||||
Snark.java, Storage.java, Tracker.java: Add info hash calcultation
|
||||
to MetaInfo. Add torrent creation to Storage. Add ip parameter
|
||||
handling to Tracker. Make ConnectionAcceptor handle
|
||||
null/non-existing HttpAcceptors. Add debug output, --ip handling
|
||||
and all the above to Snark.
|
||||
|
||||
2003-05-04 23:36 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/TrackerClient.java: Handle all failing requests
|
||||
the same (print a warning).
|
||||
|
||||
2003-05-03 15:46 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: Peer.java, PeerID.java, TrackerInfo.java: Split
|
||||
Peer and PeerID a little more.
|
||||
|
||||
2003-05-03 15:44 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/MetaInfo.java: Add reannounce() and
|
||||
getTorrentData().
|
||||
|
||||
2003-05-03 15:38 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/: PeerCheckerTask.java, PeerCoordinator.java:
|
||||
More concise verbose/debug output. Always use addUpDownloader() to
|
||||
set peers upload or download state to true.
|
||||
|
||||
2003-05-03 13:38 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/TrackerClient.java: Compile fixes.
|
||||
|
||||
2003-05-03 13:32 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/TrackerClient.java: Only generate fatal() call on
|
||||
first Tracker access. Otherwise just print a warning error message.
|
||||
|
||||
2003-05-03 03:10 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerState.java: Better handle resending
|
||||
outstanding pieces and try to recover better from unrequested
|
||||
pieces.
|
||||
|
||||
2003-05-02 21:33 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/HttpAcceptor.java,
|
||||
org/klomp/snark/MetaInfo.java, org/klomp/snark/PeerID.java,
|
||||
org/klomp/snark/Snark.java, org/klomp/snark/Tracker.java,
|
||||
org/klomp/snark/TrackerClient.java,
|
||||
org/klomp/snark/bencode/BEncoder.java: Add Tracker, PeerID and
|
||||
BEncoder.
|
||||
|
||||
2003-05-01 20:17 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* Makefile, org/klomp/snark/ConnectionAcceptor.java,
|
||||
org/klomp/snark/HttpAcceptor.java, org/klomp/snark/Peer.java,
|
||||
org/klomp/snark/PeerAcceptor.java, org/klomp/snark/Snark.java: Add
|
||||
ConnectionAcceptor that handles both PeerAcceptor and HttpAcceptor.
|
||||
|
||||
2003-05-01 18:39 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/PeerCoordinator.java: connected() synchronize on
|
||||
peers.
|
||||
|
||||
2003-04-28 02:56 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/SnarkShutdown.java: Wait some time before
|
||||
returning...
|
||||
|
||||
2003-04-28 02:56 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* TODO: More items.
|
||||
|
||||
2003-04-28 02:56 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* org/klomp/snark/Snark.java: Calculate real random ID.
|
||||
|
||||
2003-04-27 Mark Wielaard <mark@klomp.org>
|
||||
|
||||
* snark: Initial (0.1) version.
|
||||
93
apps/i2psnark/java/build.xml
Normal file
93
apps/i2psnark/java/build.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project basedir="." default="all" name="i2psnark">
|
||||
<target name="all" depends="clean, build" />
|
||||
<target name="build" depends="builddep, jar, war" />
|
||||
<target name="builddep">
|
||||
<ant dir="../../jetty/" target="build" />
|
||||
<ant dir="../../streaming/java/" target="build" />
|
||||
<!-- streaming will build ministreaming and core -->
|
||||
</target>
|
||||
<target name="compile">
|
||||
<mkdir dir="./build" />
|
||||
<mkdir dir="./build/obj" />
|
||||
<javac
|
||||
srcdir="./src"
|
||||
debug="true" deprecation="on" source="1.3" target="1.3"
|
||||
destdir="./build/obj"
|
||||
classpath="../../../core/java/build/i2p.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../ministreaming/java/build/mstreaming.jar" />
|
||||
</target>
|
||||
<target name="jar" depends="builddep, compile">
|
||||
<jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class" excludes="**/*Servlet.class">
|
||||
<manifest>
|
||||
<attribute name="Main-Class" value="org.klomp.snark.Snark" />
|
||||
<attribute name="Class-Path" value="i2p.jar mstreaming.jar streaming.jar" />
|
||||
</manifest>
|
||||
</jar>
|
||||
</target>
|
||||
<target name="war" depends="jar">
|
||||
<war destfile="../i2psnark.war" webxml="../web.xml">
|
||||
<classes dir="./build/obj" includes="**/*" />
|
||||
</war>
|
||||
</target>
|
||||
|
||||
<target name="standalone" depends="standalone_prep">
|
||||
<zip destfile="i2psnark-standalone.zip">
|
||||
<zipfileset dir="./dist/" prefix="i2psnark/" />
|
||||
</zip>
|
||||
</target>
|
||||
<target name="standalone_prep" depends="war">
|
||||
<javac debug="true" deprecation="on" source="1.3" target="1.3"
|
||||
destdir="./build" srcdir="src/" includes="org/klomp/snark/web/RunStandalone.java" >
|
||||
<classpath>
|
||||
<pathelement location="../../jetty/jettylib/commons-logging.jar" />
|
||||
<pathelement location="../../jetty/jettylib/commons-el.jar" />
|
||||
<pathelement location="../../jetty/jettylib/org.mortbay.jetty.jar" />
|
||||
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
|
||||
<pathelement location="../../../core/java/build/i2p.jar" />
|
||||
</classpath>
|
||||
</javac>
|
||||
|
||||
<jar destfile="./build/launch-i2psnark.jar" basedir="./build/" includes="org/klomp/snark/web/RunStandalone.class">
|
||||
<manifest>
|
||||
<attribute name="Main-Class" value="org.klomp.snark.web.RunStandalone" />
|
||||
<attribute name="Class-Path" value="lib/i2p.jar lib/mstreaming.jar lib/streaming.jar lib/commons-el.jar lib/commons-logging.jar lib/jasper-compiler.jar lib/jasper-runtime.jar lib/javax.servlet.jar lib/org.mortbay.jetty.jar" />
|
||||
</manifest>
|
||||
</jar>
|
||||
|
||||
<delete dir="./dist" />
|
||||
<mkdir dir="./dist" />
|
||||
<copy file="./build/launch-i2psnark.jar" tofile="./dist/launch-i2psnark.jar" />
|
||||
<mkdir dir="./dist/webapps" />
|
||||
<copy file="../i2psnark.war" tofile="./dist/webapps/i2psnark.war" />
|
||||
<mkdir dir="./dist/lib" />
|
||||
<copy file="../../../core/java/build/i2p.jar" tofile="./dist/lib/i2p.jar" />
|
||||
<copy file="../../jetty/jettylib/commons-el.jar" tofile="./dist/lib/commons-el.jar" />
|
||||
<copy file="../../jetty/jettylib/commons-logging.jar" tofile="./dist/lib/commons-logging.jar" />
|
||||
<copy file="../../jetty/jettylib/javax.servlet.jar" tofile="./dist/lib/javax.servlet.jar" />
|
||||
<copy file="../../jetty/jettylib/org.mortbay.jetty.jar" tofile="./dist/lib/org.mortbay.jetty.jar" />
|
||||
<copy file="../../jetty/jettylib/jasper-compiler.jar" tofile="./dist/lib/jasper-compiler.jar" />
|
||||
<copy file="../../jetty/jettylib/jasper-runtime.jar" tofile="./dist/lib/jasper-runtime.jar" />
|
||||
<copy file="../../ministreaming/java/build/mstreaming.jar" tofile="./dist/lib/mstreaming.jar" />
|
||||
<copy file="../../streaming/java/build/streaming.jar" tofile="./dist/lib/streaming.jar" />
|
||||
<copy file="../jetty-i2psnark.xml" tofile="./dist/jetty-i2psnark.xml" />
|
||||
<copy file="../readme-standalone.txt" tofile="./dist/readme.txt" />
|
||||
<mkdir dir="./dist/work" />
|
||||
<mkdir dir="./dist/logs" />
|
||||
|
||||
<zip destfile="i2psnark-standalone.zip">
|
||||
<zipfileset dir="./dist/" prefix="i2psnark/" />
|
||||
</zip>
|
||||
</target>
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="./build" />
|
||||
<delete file="../i2psnark.war" />
|
||||
<delete file="./i2psnark-standalone.zip" />
|
||||
</target>
|
||||
<target name="cleandep" depends="clean">
|
||||
<ant dir="../../ministreaming/java/" target="distclean" />
|
||||
</target>
|
||||
<target name="distclean" depends="clean">
|
||||
<ant dir="../../ministreaming/java/" target="distclean" />
|
||||
</target>
|
||||
</project>
|
||||
132
apps/i2psnark/java/src/org/klomp/snark/BitField.java
Normal file
132
apps/i2psnark/java/src/org/klomp/snark/BitField.java
Normal file
@@ -0,0 +1,132 @@
|
||||
/* BitField - Container of a byte array representing set and unset bits.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Container of a byte array representing set and unset bits.
|
||||
*/
|
||||
public class BitField
|
||||
{
|
||||
|
||||
private final byte[] bitfield;
|
||||
private final int size;
|
||||
|
||||
/**
|
||||
* Creates a new BitField that represents <code>size</code> unset bits.
|
||||
*/
|
||||
public BitField(int size)
|
||||
{
|
||||
this.size = size;
|
||||
int arraysize = ((size-1)/8)+1;
|
||||
bitfield = new byte[arraysize];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new BitField that represents <code>size</code> bits
|
||||
* as set by the given byte array. This will make a copy of the array.
|
||||
* Extra bytes will be ignored.
|
||||
*
|
||||
* @exception ArrayOutOfBoundsException if give byte array is not large
|
||||
* enough.
|
||||
*/
|
||||
public BitField(byte[] bitfield, int size)
|
||||
{
|
||||
this.size = size;
|
||||
int arraysize = ((size-1)/8)+1;
|
||||
this.bitfield = new byte[arraysize];
|
||||
|
||||
// XXX - More correct would be to check that unused bits are
|
||||
// cleared or clear them explicitly ourselves.
|
||||
System.arraycopy(bitfield, 0, this.bitfield, 0, arraysize);
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the actual byte array used. Changes to this array
|
||||
* effect this BitField. Note that some bits at the end of the byte
|
||||
* array are supposed to be always unset if they represent bits
|
||||
* bigger then the size of the bitfield.
|
||||
*/
|
||||
public byte[] getFieldBytes()
|
||||
{
|
||||
return bitfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size of the BitField. The returned value is one bigger
|
||||
* then the last valid bit number (since bit numbers are counted
|
||||
* from zero).
|
||||
*/
|
||||
public int size()
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given bit to true.
|
||||
*
|
||||
* @exception IndexOutOfBoundsException if bit is smaller then zero
|
||||
* bigger then size (inclusive).
|
||||
*/
|
||||
public void set(int bit)
|
||||
{
|
||||
if (bit < 0 || bit >= size)
|
||||
throw new IndexOutOfBoundsException(Integer.toString(bit));
|
||||
int index = bit/8;
|
||||
int mask = 128 >> (bit % 8);
|
||||
bitfield[index] |= mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the bit is set or false if it is not.
|
||||
*
|
||||
* @exception IndexOutOfBoundsException if bit is smaller then zero
|
||||
* bigger then size (inclusive).
|
||||
*/
|
||||
public boolean get(int bit)
|
||||
{
|
||||
if (bit < 0 || bit >= size)
|
||||
throw new IndexOutOfBoundsException(Integer.toString(bit));
|
||||
|
||||
int index = bit/8;
|
||||
int mask = 128 >> (bit % 8);
|
||||
return (bitfield[index] & mask) != 0;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
// Not very efficient
|
||||
StringBuffer sb = new StringBuffer("BitField(");
|
||||
sb.append(size).append(")[");
|
||||
for (int i = 0; i < size; i++)
|
||||
if (get(i))
|
||||
{
|
||||
sb.append(' ');
|
||||
sb.append(i);
|
||||
}
|
||||
sb.append(" ]");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
187
apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
Normal file
187
apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java
Normal file
@@ -0,0 +1,187 @@
|
||||
/* ConnectionAcceptor - Accepts connections and routes them to sub-acceptors.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.streaming.I2PServerSocket;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Accepts connections on a TCP port and routes them to sub-acceptors.
|
||||
*/
|
||||
public class ConnectionAcceptor implements Runnable
|
||||
{
|
||||
private static final ConnectionAcceptor _instance = new ConnectionAcceptor();
|
||||
public static final ConnectionAcceptor instance() { return _instance; }
|
||||
private Log _log = new Log(ConnectionAcceptor.class);
|
||||
private I2PServerSocket serverSocket;
|
||||
private PeerAcceptor peeracceptor;
|
||||
private Thread thread;
|
||||
|
||||
private boolean stop;
|
||||
private boolean socketChanged;
|
||||
|
||||
private ConnectionAcceptor() {}
|
||||
|
||||
public synchronized void startAccepting(PeerCoordinatorSet set, I2PServerSocket socket) {
|
||||
if (serverSocket != socket) {
|
||||
if ( (peeracceptor == null) || (peeracceptor.coordinators != set) )
|
||||
peeracceptor = new PeerAcceptor(set);
|
||||
serverSocket = socket;
|
||||
stop = false;
|
||||
socketChanged = true;
|
||||
if (thread == null) {
|
||||
thread = new I2PThread(this, "I2PSnark acceptor");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionAcceptor(I2PServerSocket serverSocket,
|
||||
PeerAcceptor peeracceptor)
|
||||
{
|
||||
this.serverSocket = serverSocket;
|
||||
this.peeracceptor = peeracceptor;
|
||||
|
||||
socketChanged = false;
|
||||
stop = false;
|
||||
thread = new I2PThread(this, "I2PSnark acceptor");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void halt()
|
||||
{
|
||||
if (true) throw new RuntimeException("wtf");
|
||||
stop = true;
|
||||
|
||||
I2PServerSocket ss = serverSocket;
|
||||
if (ss != null)
|
||||
try
|
||||
{
|
||||
ss.close();
|
||||
}
|
||||
catch(I2PException ioe) { }
|
||||
|
||||
Thread t = thread;
|
||||
if (t != null)
|
||||
t.interrupt();
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
serverSocket = I2PSnarkUtil.instance().getServerSocket();
|
||||
socketChanged = true;
|
||||
Thread t = thread;
|
||||
if (t != null)
|
||||
t.interrupt();
|
||||
}
|
||||
|
||||
public int getPort()
|
||||
{
|
||||
return 6881; // serverSocket.getLocalPort();
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
while(!stop)
|
||||
{
|
||||
if (socketChanged) {
|
||||
// ok, already updated
|
||||
socketChanged = false;
|
||||
}
|
||||
while ( (serverSocket == null) && (!stop)) {
|
||||
serverSocket = I2PSnarkUtil.instance().getServerSocket();
|
||||
if (serverSocket == null)
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
try
|
||||
{
|
||||
I2PSocket socket = serverSocket.accept();
|
||||
if (socket == null) {
|
||||
if (socketChanged) {
|
||||
continue;
|
||||
} else {
|
||||
I2PServerSocket ss = I2PSnarkUtil.instance().getServerSocket();
|
||||
if (ss != serverSocket) {
|
||||
serverSocket = ss;
|
||||
socketChanged = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Thread t = new I2PThread(new Handler(socket), "Connection-" + socket);
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
catch (I2PException ioe)
|
||||
{
|
||||
if (!socketChanged) {
|
||||
Snark.debug("Error while accepting: " + ioe, Snark.ERROR);
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
Snark.debug("Error while accepting: " + ioe, Snark.ERROR);
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (serverSocket != null)
|
||||
serverSocket.close();
|
||||
}
|
||||
catch (I2PException ignored) { }
|
||||
|
||||
throw new RuntimeException("wtf");
|
||||
}
|
||||
|
||||
private class Handler implements Runnable {
|
||||
private I2PSocket _socket;
|
||||
public Handler(I2PSocket socket) {
|
||||
_socket = socket;
|
||||
}
|
||||
public void run() {
|
||||
try {
|
||||
InputStream in = _socket.getInputStream();
|
||||
OutputStream out = _socket.getOutputStream();
|
||||
|
||||
if (true) {
|
||||
in = new BufferedInputStream(in);
|
||||
//out = new BufferedOutputStream(out);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64());
|
||||
peeracceptor.connection(_socket, in, out);
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Error handling connection from " + _socket.getPeerDestination().calculateHash().toBase64(), ioe);
|
||||
try { _socket.close(); } catch (IOException ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* CoordinatorListener.java - Callback when a peer changes state
|
||||
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
/**
|
||||
* Callback used when some peer changes state.
|
||||
*/
|
||||
public interface CoordinatorListener
|
||||
{
|
||||
/**
|
||||
* Called when the PeerCoordinator notices a change in the state of a peer.
|
||||
*/
|
||||
void peerChange(PeerCoordinator coordinator, Peer peer);
|
||||
}
|
||||
256
apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
Normal file
256
apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
Normal file
@@ -0,0 +1,256 @@
|
||||
package org.klomp.snark;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.*;
|
||||
import net.i2p.client.streaming.I2PServerSocket;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* I2P specific helpers for I2PSnark
|
||||
*/
|
||||
public class I2PSnarkUtil {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private static I2PSnarkUtil _instance = new I2PSnarkUtil();
|
||||
public static I2PSnarkUtil instance() { return _instance; }
|
||||
|
||||
private boolean _shouldProxy;
|
||||
private String _proxyHost;
|
||||
private int _proxyPort;
|
||||
private String _i2cpHost;
|
||||
private int _i2cpPort;
|
||||
private Map _opts;
|
||||
private I2PSocketManager _manager;
|
||||
private boolean _configured;
|
||||
private Set _shitlist;
|
||||
|
||||
private I2PSnarkUtil() {
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_log = _context.logManager().getLog(Snark.class);
|
||||
_opts = new HashMap();
|
||||
setProxy("127.0.0.1", 4444);
|
||||
setI2CPConfig("127.0.0.1", 7654, null);
|
||||
_shitlist = new HashSet(64);
|
||||
_configured = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify what HTTP proxy tracker requests should go through (specify a null
|
||||
* host for no proxying)
|
||||
*
|
||||
*/
|
||||
public void setProxy(String host, int port) {
|
||||
if ( (host != null) && (port > 0) ) {
|
||||
_shouldProxy = true;
|
||||
_proxyHost = host;
|
||||
_proxyPort = port;
|
||||
} else {
|
||||
_shouldProxy = false;
|
||||
_proxyHost = null;
|
||||
_proxyPort = -1;
|
||||
}
|
||||
_configured = true;
|
||||
}
|
||||
|
||||
public boolean configured() { return _configured; }
|
||||
|
||||
public void setI2CPConfig(String i2cpHost, int i2cpPort, Map opts) {
|
||||
_i2cpHost = i2cpHost;
|
||||
_i2cpPort = i2cpPort;
|
||||
if (opts != null)
|
||||
_opts.putAll(opts);
|
||||
_configured = true;
|
||||
}
|
||||
|
||||
public String getI2CPHost() { return _i2cpHost; }
|
||||
public int getI2CPPort() { return _i2cpPort; }
|
||||
public Map getI2CPOptions() { return _opts; }
|
||||
public String getEepProxyHost() { return _proxyHost; }
|
||||
public int getEepProxyPort() { return _proxyPort; }
|
||||
public boolean getEepProxySet() { return _shouldProxy; }
|
||||
|
||||
/**
|
||||
* Connect to the router, if we aren't already
|
||||
*/
|
||||
public boolean connect() {
|
||||
if (_manager == null) {
|
||||
Properties opts = new Properties();
|
||||
if (_opts != null) {
|
||||
for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) {
|
||||
String key = (String)iter.next();
|
||||
opts.setProperty(key, _opts.get(key).toString());
|
||||
}
|
||||
}
|
||||
if (opts.getProperty("inbound.nickname") == null)
|
||||
opts.setProperty("inbound.nickname", "I2PSnark");
|
||||
if (opts.getProperty("i2p.streaming.inactivityTimeout") == null)
|
||||
opts.setProperty("i2p.streaming.inactivityTimeout", "90000");
|
||||
if (opts.getProperty("i2p.streaming.inactivityAction") == null)
|
||||
opts.setProperty("i2p.streaming.inactivityAction", "2"); // 1 == disconnect, 2 == ping
|
||||
//if (opts.getProperty("i2p.streaming.writeTimeout") == null)
|
||||
// opts.setProperty("i2p.streaming.writeTimeout", "90000");
|
||||
//if (opts.getProperty("i2p.streaming.readTimeout") == null)
|
||||
// opts.setProperty("i2p.streaming.readTimeout", "120000");
|
||||
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
|
||||
}
|
||||
return (_manager != null);
|
||||
}
|
||||
|
||||
public boolean connected() { return _manager != null; }
|
||||
/**
|
||||
* Destroy the destination itself
|
||||
*/
|
||||
public void disconnect() {
|
||||
I2PSocketManager mgr = _manager;
|
||||
_manager = null;
|
||||
_shitlist.clear();
|
||||
mgr.destroySocketManager();
|
||||
}
|
||||
|
||||
/** connect to the given destination */
|
||||
I2PSocket connect(PeerID peer) throws IOException {
|
||||
Hash dest = peer.getAddress().calculateHash();
|
||||
synchronized (_shitlist) {
|
||||
if (_shitlist.contains(dest))
|
||||
throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted");
|
||||
}
|
||||
try {
|
||||
I2PSocket rv = _manager.connect(peer.getAddress());
|
||||
if (rv != null) synchronized (_shitlist) { _shitlist.remove(dest); }
|
||||
return rv;
|
||||
} catch (I2PException ie) {
|
||||
synchronized (_shitlist) {
|
||||
_shitlist.add(dest);
|
||||
}
|
||||
SimpleTimer.getInstance().addEvent(new Unshitlist(dest), 10*60*1000);
|
||||
throw new IOException("Unable to reach the peer " + peer + ": " + ie.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private class Unshitlist implements SimpleTimer.TimedEvent {
|
||||
private Hash _dest;
|
||||
public Unshitlist(Hash dest) { _dest = dest; }
|
||||
public void timeReached() { synchronized (_shitlist) { _shitlist.remove(_dest); } }
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch the given URL, returning the file it is stored in, or null on error
|
||||
*/
|
||||
public File get(String url) { return get(url, true); }
|
||||
public File get(String url, boolean rewrite) {
|
||||
_log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy);
|
||||
File out = null;
|
||||
try {
|
||||
out = File.createTempFile("i2psnark", "url", new File("."));
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
out.delete();
|
||||
return null;
|
||||
}
|
||||
String fetchURL = url;
|
||||
if (rewrite)
|
||||
fetchURL = rewriteAnnounce(url);
|
||||
//_log.debug("Rewritten url [" + fetchURL + "]");
|
||||
EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), fetchURL);
|
||||
if (get.fetch()) {
|
||||
_log.debug("Fetch successful [" + url + "]: size=" + out.length());
|
||||
return out;
|
||||
} else {
|
||||
_log.warn("Fetch failed [" + url + "]");
|
||||
out.delete();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public I2PServerSocket getServerSocket() {
|
||||
I2PSocketManager mgr = _manager;
|
||||
if (mgr != null)
|
||||
return mgr.getServerSocket();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
String getOurIPString() {
|
||||
I2PSession sess = _manager.getSession();
|
||||
if (sess != null) {
|
||||
Destination dest = sess.getMyDestination();
|
||||
if (dest != null)
|
||||
return dest.toBase64();
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
Destination getDestination(String ip) {
|
||||
if (ip == null) return null;
|
||||
if (ip.endsWith(".i2p")) {
|
||||
Destination dest = _context.namingService().lookup(ip);
|
||||
if (dest != null) {
|
||||
return dest;
|
||||
} else {
|
||||
try {
|
||||
return new Destination(ip.substring(0, ip.length()-4)); // sans .i2p
|
||||
} catch (DataFormatException dfe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return new Destination(ip);
|
||||
} catch (DataFormatException dfe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given http://blah.i2p/foo/announce turn it into http://i2p/blah/foo/announce
|
||||
*/
|
||||
String rewriteAnnounce(String origAnnounce) {
|
||||
int destStart = "http://".length();
|
||||
int destEnd = origAnnounce.indexOf(".i2p");
|
||||
int pathStart = origAnnounce.indexOf('/', destEnd);
|
||||
String rv = "http://i2p/" + origAnnounce.substring(destStart, destEnd) + origAnnounce.substring(pathStart);
|
||||
//_log.debug("Rewriting [" + origAnnounce + "] as [" + rv + "]");
|
||||
return rv;
|
||||
}
|
||||
|
||||
/** hook between snark's logger and an i2p log */
|
||||
void debug(String msg, int snarkDebugLevel, Throwable t) {
|
||||
if (t instanceof OutOfMemoryError) {
|
||||
try { Thread.sleep(100); } catch (InterruptedException ie) {}
|
||||
try {
|
||||
t.printStackTrace();
|
||||
} catch (Throwable tt) {}
|
||||
try {
|
||||
System.out.println("OOM thread: " + Thread.currentThread().getName());
|
||||
} catch (Throwable tt) {}
|
||||
}
|
||||
switch (snarkDebugLevel) {
|
||||
case 0:
|
||||
case 1:
|
||||
_log.error(msg, t);
|
||||
break;
|
||||
case 2:
|
||||
_log.warn(msg, t);
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
_log.info(msg, t);
|
||||
break;
|
||||
case 5:
|
||||
case 6:
|
||||
default:
|
||||
_log.debug(msg, t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
141
apps/i2psnark/java/src/org/klomp/snark/Message.java
Normal file
141
apps/i2psnark/java/src/org/klomp/snark/Message.java
Normal file
@@ -0,0 +1,141 @@
|
||||
/* Message - A protocol message which can be send through a DataOutputStream.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
// Used to queue outgoing connections
|
||||
// sendMessage() should be used to translate them to wire format.
|
||||
class Message
|
||||
{
|
||||
final static byte KEEP_ALIVE = -1;
|
||||
final static byte CHOKE = 0;
|
||||
final static byte UNCHOKE = 1;
|
||||
final static byte INTERESTED = 2;
|
||||
final static byte UNINTERESTED = 3;
|
||||
final static byte HAVE = 4;
|
||||
final static byte BITFIELD = 5;
|
||||
final static byte REQUEST = 6;
|
||||
final static byte PIECE = 7;
|
||||
final static byte CANCEL = 8;
|
||||
|
||||
// Not all fields are used for every message.
|
||||
// KEEP_ALIVE doesn't have a real wire representation
|
||||
byte type;
|
||||
|
||||
// Used for HAVE, REQUEST, PIECE and CANCEL messages.
|
||||
int piece;
|
||||
|
||||
// Used for REQUEST, PIECE and CANCEL messages.
|
||||
int begin;
|
||||
int length;
|
||||
|
||||
// Used for PIECE and BITFIELD messages
|
||||
byte[] data;
|
||||
int off;
|
||||
int len;
|
||||
|
||||
SimpleTimer.TimedEvent expireEvent;
|
||||
|
||||
/** Utility method for sending a message through a DataStream. */
|
||||
void sendMessage(DataOutputStream dos) throws IOException
|
||||
{
|
||||
// KEEP_ALIVE is special.
|
||||
if (type == KEEP_ALIVE)
|
||||
{
|
||||
dos.writeInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the total length in bytes
|
||||
|
||||
// Type is one byte.
|
||||
int datalen = 1;
|
||||
|
||||
// piece is 4 bytes.
|
||||
if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL)
|
||||
datalen += 4;
|
||||
|
||||
// begin/offset is 4 bytes
|
||||
if (type == REQUEST || type == PIECE || type == CANCEL)
|
||||
datalen += 4;
|
||||
|
||||
// length is 4 bytes
|
||||
if (type == REQUEST || type == CANCEL)
|
||||
datalen += 4;
|
||||
|
||||
// add length of data for piece or bitfield array.
|
||||
if (type == BITFIELD || type == PIECE)
|
||||
datalen += len;
|
||||
|
||||
// Send length
|
||||
dos.writeInt(datalen);
|
||||
dos.writeByte(type & 0xFF);
|
||||
|
||||
// Send additional info (piece number)
|
||||
if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL)
|
||||
dos.writeInt(piece);
|
||||
|
||||
// Send additional info (begin/offset)
|
||||
if (type == REQUEST || type == PIECE || type == CANCEL)
|
||||
dos.writeInt(begin);
|
||||
|
||||
// Send additional info (length); for PIECE this is implicit.
|
||||
if (type == REQUEST || type == CANCEL)
|
||||
dos.writeInt(length);
|
||||
|
||||
// Send actual data
|
||||
if (type == BITFIELD || type == PIECE)
|
||||
dos.write(data, off, len);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case KEEP_ALIVE:
|
||||
return "KEEP_ALIVE";
|
||||
case CHOKE:
|
||||
return "CHOKE";
|
||||
case UNCHOKE:
|
||||
return "UNCHOKE";
|
||||
case INTERESTED:
|
||||
return "INTERESTED";
|
||||
case UNINTERESTED:
|
||||
return "UNINTERESTED";
|
||||
case HAVE:
|
||||
return "HAVE(" + piece + ")";
|
||||
case BITFIELD:
|
||||
return "BITFIELD";
|
||||
case REQUEST:
|
||||
return "REQUEST(" + piece + "," + begin + "," + length + ")";
|
||||
case PIECE:
|
||||
return "PIECE(" + piece + "," + begin + "," + length + ")";
|
||||
case CANCEL:
|
||||
return "CANCEL(" + piece + "," + begin + "," + length + ")";
|
||||
default:
|
||||
return "<UNKNOWN>";
|
||||
}
|
||||
}
|
||||
}
|
||||
461
apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
Normal file
461
apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
Normal file
@@ -0,0 +1,461 @@
|
||||
/* MetaInfo - Holds all information gotten from a torrent file.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.File;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.crypto.SHA1;
|
||||
|
||||
/**
|
||||
* Note: this class is buggy, as it doesn't propogate custom meta fields into the bencoded
|
||||
* info data, and from there to the info_hash. At the moment, though, it seems to work with
|
||||
* torrents created by I2P-BT, I2PRufus and Azureus.
|
||||
*
|
||||
*/
|
||||
public class MetaInfo
|
||||
{
|
||||
private static final Log _log = new Log(MetaInfo.class);
|
||||
private final String announce;
|
||||
private final byte[] info_hash;
|
||||
private final String name;
|
||||
private final String name_utf8;
|
||||
private final List files;
|
||||
private final List files_utf8;
|
||||
private final List lengths;
|
||||
private final int piece_length;
|
||||
private final byte[] piece_hashes;
|
||||
private final long length;
|
||||
private final Map infoMap;
|
||||
|
||||
private byte[] torrentdata;
|
||||
|
||||
MetaInfo(String announce, String name, String name_utf8, List files, List lengths,
|
||||
int piece_length, byte[] piece_hashes, long length)
|
||||
{
|
||||
this.announce = announce;
|
||||
this.name = name;
|
||||
this.name_utf8 = name_utf8;
|
||||
this.files = files;
|
||||
this.files_utf8 = null;
|
||||
this.lengths = lengths;
|
||||
this.piece_length = piece_length;
|
||||
this.piece_hashes = piece_hashes;
|
||||
this.length = length;
|
||||
|
||||
this.info_hash = calculateInfoHash();
|
||||
infoMap = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MetaInfo from the given InputStream. The
|
||||
* InputStream must start with a correctly bencoded dictonary
|
||||
* describing the torrent.
|
||||
*/
|
||||
public MetaInfo(InputStream in) throws IOException
|
||||
{
|
||||
this(new BDecoder(in));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MetaInfo from the given BDecoder. The BDecoder
|
||||
* must have a complete dictionary describing the torrent.
|
||||
*/
|
||||
public MetaInfo(BDecoder be) throws IOException
|
||||
{
|
||||
// Note that evaluation order matters here...
|
||||
this(be.bdecodeMap().getMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MetaInfo from a Map of BEValues and the SHA1 over
|
||||
* the original bencoded info dictonary (this is a hack, we could
|
||||
* reconstruct the bencoded stream and recalculate the hash). Will
|
||||
* throw a InvalidBEncodingException if the given map does not
|
||||
* contain a valid announce string or info dictonary.
|
||||
*/
|
||||
public MetaInfo(Map m) throws InvalidBEncodingException
|
||||
{
|
||||
_log.debug("Creating a metaInfo: " + m, new Exception("source"));
|
||||
BEValue val = (BEValue)m.get("announce");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing announce string");
|
||||
this.announce = val.getString();
|
||||
|
||||
val = (BEValue)m.get("info");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing info map");
|
||||
Map info = val.getMap();
|
||||
infoMap = info;
|
||||
|
||||
val = (BEValue)info.get("name");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing name string");
|
||||
name = val.getString();
|
||||
|
||||
val = (BEValue)info.get("name.utf-8");
|
||||
if (val != null)
|
||||
name_utf8 = val.getString();
|
||||
else
|
||||
name_utf8 = null;
|
||||
|
||||
val = (BEValue)info.get("piece length");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing piece length number");
|
||||
piece_length = val.getInt();
|
||||
|
||||
val = (BEValue)info.get("pieces");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing piece bytes");
|
||||
piece_hashes = val.getBytes();
|
||||
|
||||
val = (BEValue)info.get("length");
|
||||
if (val != null)
|
||||
{
|
||||
// Single file case.
|
||||
length = val.getLong();
|
||||
files = null;
|
||||
files_utf8 = null;
|
||||
lengths = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multi file case.
|
||||
val = (BEValue)info.get("files");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException
|
||||
("Missing length number and/or files list");
|
||||
|
||||
List list = val.getList();
|
||||
int size = list.size();
|
||||
if (size == 0)
|
||||
throw new InvalidBEncodingException("zero size files list");
|
||||
|
||||
files = new ArrayList(size);
|
||||
files_utf8 = new ArrayList(size);
|
||||
lengths = new ArrayList(size);
|
||||
long l = 0;
|
||||
for (int i = 0; i < list.size(); i++)
|
||||
{
|
||||
Map desc = ((BEValue)list.get(i)).getMap();
|
||||
val = (BEValue)desc.get("length");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing length number");
|
||||
long len = val.getLong();
|
||||
lengths.add(new Long(len));
|
||||
l += len;
|
||||
|
||||
val = (BEValue)desc.get("path");
|
||||
if (val == null)
|
||||
throw new InvalidBEncodingException("Missing path list");
|
||||
List path_list = val.getList();
|
||||
int path_length = path_list.size();
|
||||
if (path_length == 0)
|
||||
throw new InvalidBEncodingException("zero size file path list");
|
||||
|
||||
List file = new ArrayList(path_length);
|
||||
Iterator it = path_list.iterator();
|
||||
while (it.hasNext())
|
||||
file.add(((BEValue)it.next()).getString());
|
||||
|
||||
files.add(file);
|
||||
|
||||
val = (BEValue)desc.get("path.utf-8");
|
||||
if (val != null) {
|
||||
path_list = val.getList();
|
||||
path_length = path_list.size();
|
||||
if (path_length > 0) {
|
||||
file = new ArrayList(path_length);
|
||||
it = path_list.iterator();
|
||||
while (it.hasNext())
|
||||
file.add(((BEValue)it.next()).getString());
|
||||
files_utf8.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
length = l;
|
||||
}
|
||||
|
||||
info_hash = calculateInfoHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representing the URL of the tracker for this torrent.
|
||||
*/
|
||||
public String getAnnounce()
|
||||
{
|
||||
return announce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original 20 byte SHA1 hash over the bencoded info map.
|
||||
*/
|
||||
public byte[] getInfoHash()
|
||||
{
|
||||
// XXX - Should we return a clone, just to be sure?
|
||||
return info_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the piece hashes. Only used by storage so package local.
|
||||
*/
|
||||
byte[] getPieceHashes()
|
||||
{
|
||||
return piece_hashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested name for the file or toplevel directory.
|
||||
* If it is a toplevel directory name getFiles() will return a
|
||||
* non-null List of file name hierarchy name.
|
||||
*/
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of lists of file name hierarchies or null if it is
|
||||
* a single name. It has the same size as the list returned by
|
||||
* getLengths().
|
||||
*/
|
||||
public List getFiles()
|
||||
{
|
||||
// XXX - Immutable?
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Longs indication the size of the individual
|
||||
* files, or null if it is a single file. It has the same size as
|
||||
* the list returned by getFiles().
|
||||
*/
|
||||
public List getLengths()
|
||||
{
|
||||
// XXX - Immutable?
|
||||
return lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of pieces.
|
||||
*/
|
||||
public int getPieces()
|
||||
{
|
||||
return piece_hashes.length/20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of a piece. All pieces are of equal length
|
||||
* except for the last one (<code>getPieces()-1</code>).
|
||||
*
|
||||
* @exception IndexOutOfBoundsException when piece is equal to or
|
||||
* greater then the number of pieces in the torrent.
|
||||
*/
|
||||
public int getPieceLength(int piece)
|
||||
{
|
||||
int pieces = getPieces();
|
||||
if (piece >= 0 && piece < pieces -1)
|
||||
return piece_length;
|
||||
else if (piece == pieces -1)
|
||||
return (int)(length - piece * piece_length);
|
||||
else
|
||||
throw new IndexOutOfBoundsException("no piece: " + piece);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the given piece has the same SHA1 hash as the given
|
||||
* byte array. Returns random results or IndexOutOfBoundsExceptions
|
||||
* when the piece number is unknown.
|
||||
*/
|
||||
public boolean checkPiece(int piece, byte[] bs, int off, int length)
|
||||
{
|
||||
if (true)
|
||||
return fast_checkPiece(piece, bs, off, length);
|
||||
else
|
||||
return orig_checkPiece(piece, bs, off, length);
|
||||
}
|
||||
private boolean orig_checkPiece(int piece, byte[] bs, int off, int length) {
|
||||
// Check digest
|
||||
MessageDigest sha1;
|
||||
try
|
||||
{
|
||||
sha1 = MessageDigest.getInstance("SHA");
|
||||
}
|
||||
catch (NoSuchAlgorithmException nsae)
|
||||
{
|
||||
throw new InternalError("No SHA digest available: " + nsae);
|
||||
}
|
||||
|
||||
sha1.update(bs, off, length);
|
||||
byte[] hash = sha1.digest();
|
||||
for (int i = 0; i < 20; i++)
|
||||
if (hash[i] != piece_hashes[20 * piece + i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean fast_checkPiece(int piece, byte[] bs, int off, int length) {
|
||||
SHA1 sha1 = new SHA1();
|
||||
|
||||
sha1.update(bs, off, length);
|
||||
byte[] hash = sha1.digest();
|
||||
for (int i = 0; i < 20; i++)
|
||||
if (hash[i] != piece_hashes[20 * piece + i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total length of the torrent in bytes.
|
||||
*/
|
||||
public long getTotalLength()
|
||||
{
|
||||
return length;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return "MetaInfo[info_hash='" + hexencode(info_hash)
|
||||
+ "', announce='" + announce
|
||||
+ "', name='" + name
|
||||
+ "', files=" + files
|
||||
+ ", #pieces='" + piece_hashes.length/20
|
||||
+ "', piece_length='" + piece_length
|
||||
+ "', length='" + length
|
||||
+ "']";
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a byte array as a hex encoded string.
|
||||
*/
|
||||
private static String hexencode(byte[] bs)
|
||||
{
|
||||
StringBuffer sb = new StringBuffer(bs.length*2);
|
||||
for (int i = 0; i < bs.length; i++)
|
||||
{
|
||||
int c = bs[i] & 0xFF;
|
||||
if (c < 16)
|
||||
sb.append('0');
|
||||
sb.append(Integer.toHexString(c));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this MetaInfo that shares everything except the
|
||||
* announce URL.
|
||||
*/
|
||||
public MetaInfo reannounce(String announce)
|
||||
{
|
||||
return new MetaInfo(announce, name, name_utf8, files,
|
||||
lengths, piece_length,
|
||||
piece_hashes, length);
|
||||
}
|
||||
|
||||
public byte[] getTorrentData()
|
||||
{
|
||||
if (torrentdata == null)
|
||||
{
|
||||
Map m = new HashMap();
|
||||
m.put("announce", announce);
|
||||
Map info = createInfoMap();
|
||||
m.put("info", info);
|
||||
torrentdata = BEncoder.bencode(m);
|
||||
}
|
||||
return torrentdata;
|
||||
}
|
||||
|
||||
private Map createInfoMap()
|
||||
{
|
||||
Map info = new HashMap();
|
||||
if (infoMap != null) {
|
||||
info.putAll(infoMap);
|
||||
return info;
|
||||
}
|
||||
info.put("name", name);
|
||||
if (name_utf8 != null)
|
||||
info.put("name.utf-8", name_utf8);
|
||||
info.put("piece length", new Integer(piece_length));
|
||||
info.put("pieces", piece_hashes);
|
||||
if (files == null)
|
||||
info.put("length", new Long(length));
|
||||
else
|
||||
{
|
||||
List l = new ArrayList();
|
||||
for (int i = 0; i < files.size(); i++)
|
||||
{
|
||||
Map file = new HashMap();
|
||||
file.put("path", files.get(i));
|
||||
if ( (files_utf8 != null) && (files_utf8.size() > i) )
|
||||
file.put("path.utf-8", files_utf8.get(i));
|
||||
file.put("length", lengths.get(i));
|
||||
l.add(file);
|
||||
}
|
||||
info.put("files", l);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private byte[] calculateInfoHash()
|
||||
{
|
||||
Map info = createInfoMap();
|
||||
StringBuffer buf = new StringBuffer(128);
|
||||
buf.append("info: ");
|
||||
for (Iterator iter = info.keySet().iterator(); iter.hasNext(); ) {
|
||||
String key = (String)iter.next();
|
||||
Object val = info.get(key);
|
||||
buf.append(key).append('=');
|
||||
if (val instanceof byte[])
|
||||
buf.append(Base64.encode((byte[])val, true));
|
||||
else
|
||||
buf.append(val.toString());
|
||||
}
|
||||
_log.debug(buf.toString());
|
||||
byte[] infoBytes = BEncoder.bencode(info);
|
||||
//_log.debug("info bencoded: [" + Base64.encode(infoBytes, true) + "]");
|
||||
try
|
||||
{
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA");
|
||||
byte hash[] = digest.digest(infoBytes);
|
||||
_log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]");
|
||||
return hash;
|
||||
}
|
||||
catch(NoSuchAlgorithmException nsa)
|
||||
{
|
||||
throw new InternalError(nsa.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
461
apps/i2psnark/java/src/org/klomp/snark/Peer.java
Normal file
461
apps/i2psnark/java/src/org/klomp/snark/Peer.java
Normal file
@@ -0,0 +1,461 @@
|
||||
/* Peer - All public information concerning a peer.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
public class Peer implements Comparable
|
||||
{
|
||||
private Log _log = new Log(Peer.class);
|
||||
// Identifying property, the peer id of the other side.
|
||||
private final PeerID peerID;
|
||||
|
||||
private final byte[] my_id;
|
||||
final MetaInfo metainfo;
|
||||
|
||||
// The data in/output streams set during the handshake and used by
|
||||
// the actual connections.
|
||||
private DataInputStream din;
|
||||
private DataOutputStream dout;
|
||||
|
||||
// Keeps state for in/out connections. Non-null when the handshake
|
||||
// was successful, the connection setup and runs
|
||||
PeerState state;
|
||||
|
||||
private I2PSocket sock;
|
||||
|
||||
private boolean deregister = true;
|
||||
private static long __id;
|
||||
private long _id;
|
||||
|
||||
/**
|
||||
* Creates a disconnected peer given a PeerID, your own id and the
|
||||
* relevant MetaInfo.
|
||||
*/
|
||||
public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
this.peerID = peerID;
|
||||
this.my_id = my_id;
|
||||
this.metainfo = metainfo;
|
||||
_id = ++__id;
|
||||
//_log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unconnected peer from the input and output stream got
|
||||
* from the socket. Note that the complete handshake (which can take
|
||||
* some time or block indefinitely) is done in the calling Thread to
|
||||
* get the remote peer id. To completely start the connection call
|
||||
* the connect() method.
|
||||
*
|
||||
* @exception IOException when an error occurred during the handshake.
|
||||
*/
|
||||
public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
this.my_id = my_id;
|
||||
this.metainfo = metainfo;
|
||||
this.sock = sock;
|
||||
|
||||
byte[] id = handshake(in, out);
|
||||
this.peerID = new PeerID(id, sock.getPeerDestination());
|
||||
_id = ++__id;
|
||||
_log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating " + _id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the peer.
|
||||
*/
|
||||
public PeerID getPeerID()
|
||||
{
|
||||
return peerID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the String representation of the peerID.
|
||||
*/
|
||||
public String toString()
|
||||
{
|
||||
if (peerID != null)
|
||||
return peerID.toString() + ' ' + _id;
|
||||
else
|
||||
return "[unknown id] " + _id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The hash code of a Peer is the hash code of the peerID.
|
||||
*/
|
||||
public int hashCode()
|
||||
{
|
||||
return peerID.hashCode() ^ (2 << _id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Two Peers are equal when they have the same PeerID.
|
||||
* All other properties are ignored.
|
||||
*/
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (o instanceof Peer)
|
||||
{
|
||||
Peer p = (Peer)o;
|
||||
return _id == p._id && peerID.equals(p.peerID);
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the PeerIDs.
|
||||
*/
|
||||
public int compareTo(Object o)
|
||||
{
|
||||
Peer p = (Peer)o;
|
||||
int rv = peerID.compareTo(p.peerID);
|
||||
if (rv == 0) {
|
||||
if (_id > p._id) return 1;
|
||||
else if (_id < p._id) return -1;
|
||||
else return 0;
|
||||
} else {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the connection to the other peer. This method does not
|
||||
* return until the connection is terminated.
|
||||
*
|
||||
* When the connection is correctly started the connected() method
|
||||
* of the given PeerListener is called. If the connection ends or
|
||||
* the connection could not be setup correctly the disconnected()
|
||||
* method is called.
|
||||
*
|
||||
* If the given BitField is non-null it is send to the peer as first
|
||||
* message.
|
||||
*/
|
||||
public void runConnection(PeerListener listener, BitField bitfield)
|
||||
{
|
||||
if (state != null)
|
||||
throw new IllegalStateException("Peer already started");
|
||||
|
||||
_log.debug("Running connection to " + peerID.getAddress().calculateHash().toBase64(), new Exception("connecting"));
|
||||
try
|
||||
{
|
||||
// Do we need to handshake?
|
||||
if (din == null)
|
||||
{
|
||||
sock = I2PSnarkUtil.instance().connect(peerID);
|
||||
_log.debug("Connected to " + peerID + ": " + sock);
|
||||
if ((sock == null) || (sock.isClosed())) {
|
||||
throw new IOException("Unable to reach " + peerID);
|
||||
}
|
||||
InputStream in = sock.getInputStream();
|
||||
OutputStream out = sock.getOutputStream(); //new BufferedOutputStream(sock.getOutputStream());
|
||||
if (true) {
|
||||
// buffered output streams are internally synchronized, so we can't get through to the underlying
|
||||
// I2PSocket's MessageOutputStream to close() it if we are blocking on a write(...). Oh, and the
|
||||
// buffer is unnecessary anyway, as unbuffered access lets the streaming lib do the 'right thing'.
|
||||
//out = new BufferedOutputStream(out);
|
||||
in = new BufferedInputStream(sock.getInputStream());
|
||||
}
|
||||
//BufferedInputStream bis
|
||||
// = new BufferedInputStream(sock.getInputStream());
|
||||
//BufferedOutputStream bos
|
||||
// = new BufferedOutputStream(sock.getOutputStream());
|
||||
byte [] id = handshake(in, out); //handshake(bis, bos);
|
||||
byte [] expected_id = peerID.getID();
|
||||
if (!Arrays.equals(expected_id, id))
|
||||
throw new IOException("Unexpected peerID '"
|
||||
+ PeerID.idencode(id)
|
||||
+ "' expected '"
|
||||
+ PeerID.idencode(expected_id) + "'");
|
||||
_log.debug("Handshake got matching IDs with " + toString());
|
||||
} else {
|
||||
_log.debug("Already have din [" + sock + "] with " + toString());
|
||||
}
|
||||
|
||||
PeerConnectionIn in = new PeerConnectionIn(this, din);
|
||||
PeerConnectionOut out = new PeerConnectionOut(this, dout);
|
||||
PeerState s = new PeerState(this, listener, metainfo, in, out);
|
||||
|
||||
// Send our bitmap
|
||||
if (bitfield != null)
|
||||
s.out.sendBitfield(bitfield);
|
||||
|
||||
// We are up and running!
|
||||
state = s;
|
||||
listener.connected(this);
|
||||
|
||||
_log.debug("Start running the reader with " + toString());
|
||||
// Use this thread for running the incomming connection.
|
||||
// The outgoing connection creates its own Thread.
|
||||
out.startup();
|
||||
Thread.currentThread().setName("Snark reader from " + peerID);
|
||||
s.in.run();
|
||||
}
|
||||
catch(IOException eofe)
|
||||
{
|
||||
// Ignore, probably just the other side closing the connection.
|
||||
// Or refusing the connection, timing out, etc.
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(this.toString(), eofe);
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
_log.error(this + ": " + t.getMessage(), t);
|
||||
if (t instanceof OutOfMemoryError)
|
||||
throw (OutOfMemoryError)t;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (deregister) listener.disconnected(this);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets DataIn/OutputStreams, does the handshake and returns the id
|
||||
* reported by the other side.
|
||||
*/
|
||||
private byte[] handshake(InputStream in, OutputStream out) //BufferedInputStream bis, BufferedOutputStream bos)
|
||||
throws IOException
|
||||
{
|
||||
din = new DataInputStream(in);
|
||||
dout = new DataOutputStream(out);
|
||||
|
||||
// Handshake write - header
|
||||
dout.write(19);
|
||||
dout.write("BitTorrent protocol".getBytes("UTF-8"));
|
||||
// Handshake write - zeros
|
||||
byte[] zeros = new byte[8];
|
||||
dout.write(zeros);
|
||||
// Handshake write - metainfo hash
|
||||
byte[] shared_hash = metainfo.getInfoHash();
|
||||
dout.write(shared_hash);
|
||||
// Handshake write - peer id
|
||||
dout.write(my_id);
|
||||
dout.flush();
|
||||
|
||||
_log.debug("Wrote my shared hash and ID to " + toString());
|
||||
|
||||
// Handshake read - header
|
||||
byte b = din.readByte();
|
||||
if (b != 19)
|
||||
throw new IOException("Handshake failure, expected 19, got "
|
||||
+ (b & 0xff) + " on " + sock);
|
||||
|
||||
byte[] bs = new byte[19];
|
||||
din.readFully(bs);
|
||||
String bittorrentProtocol = new String(bs, "UTF-8");
|
||||
if (!"BitTorrent protocol".equals(bittorrentProtocol))
|
||||
throw new IOException("Handshake failure, expected "
|
||||
+ "'Bittorrent protocol', got '"
|
||||
+ bittorrentProtocol + "'");
|
||||
|
||||
// Handshake read - zeros
|
||||
din.readFully(zeros);
|
||||
|
||||
// Handshake read - metainfo hash
|
||||
bs = new byte[20];
|
||||
din.readFully(bs);
|
||||
if (!Arrays.equals(shared_hash, bs))
|
||||
throw new IOException("Unexpected MetaInfo hash");
|
||||
|
||||
// Handshake read - peer id
|
||||
din.readFully(bs);
|
||||
_log.debug("Read the remote side's hash and peerID fully from " + toString());
|
||||
return bs;
|
||||
}
|
||||
|
||||
public boolean isConnected()
|
||||
{
|
||||
return state != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects this peer if it was connected. If deregister is
|
||||
* true, PeerListener.disconnected() will be called when the
|
||||
* connection is completely terminated. Otherwise the connection is
|
||||
* silently terminated.
|
||||
*/
|
||||
public void disconnect(boolean deregister)
|
||||
{
|
||||
// Both in and out connection will call this.
|
||||
this.deregister = deregister;
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
PeerState s = state;
|
||||
if (s != null)
|
||||
{
|
||||
state = null;
|
||||
|
||||
PeerConnectionIn in = s.in;
|
||||
if (in != null)
|
||||
in.disconnect();
|
||||
PeerConnectionOut out = s.out;
|
||||
if (out != null)
|
||||
out.disconnect();
|
||||
PeerListener pl = s.listener;
|
||||
if (pl != null)
|
||||
pl.disconnected(this);
|
||||
}
|
||||
I2PSocket csock = sock;
|
||||
sock = null;
|
||||
if ( (csock != null) && (!csock.isClosed()) ) {
|
||||
try {
|
||||
csock.close();
|
||||
} catch (IOException ioe) {
|
||||
_log.warn("Error disconnecting " + toString(), ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the peer we have another piece.
|
||||
*/
|
||||
public void have(int piece)
|
||||
{
|
||||
PeerState s = state;
|
||||
if (s != null)
|
||||
s.havePiece(piece);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the peer is interested in pieces we have. Returns
|
||||
* false if not connected.
|
||||
*/
|
||||
public boolean isInterested()
|
||||
{
|
||||
PeerState s = state;
|
||||
return (s != null) && s.interested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not we are interested in pieces from this peer.
|
||||
* Defaults to false. When interest is true and this peer unchokes
|
||||
* us then we start downloading from it. Has no effect when not connected.
|
||||
*/
|
||||
public void setInteresting(boolean interest)
|
||||
{
|
||||
PeerState s = state;
|
||||
if (s != null)
|
||||
s.setInteresting(interest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the peer has pieces we want from it. Returns false
|
||||
* if not connected.
|
||||
*/
|
||||
public boolean isInteresting()
|
||||
{
|
||||
PeerState s = state;
|
||||
return (s != null) && s.interesting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not we are choking the peer. Defaults to
|
||||
* true. When choke is false and the peer requests some pieces we
|
||||
* upload them, otherwise requests of this peer are ignored.
|
||||
*/
|
||||
public void setChoking(boolean choke)
|
||||
{
|
||||
PeerState s = state;
|
||||
if (s != null)
|
||||
s.setChoking(choke);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we are choking the peer. Returns true when not connected.
|
||||
*/
|
||||
public boolean isChoking()
|
||||
{
|
||||
PeerState s = state;
|
||||
return (s == null) || s.choking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the peer choked us. Returns true when not connected.
|
||||
*/
|
||||
public boolean isChoked()
|
||||
{
|
||||
PeerState s = state;
|
||||
return (s == null) || s.choked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes that have been downloaded.
|
||||
* Can be reset to zero with <code>resetCounters()</code>/
|
||||
*/
|
||||
public long getDownloaded()
|
||||
{
|
||||
PeerState s = state;
|
||||
return (s != null) ? s.downloaded : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes that have been uploaded.
|
||||
* Can be reset to zero with <code>resetCounters()</code>/
|
||||
*/
|
||||
public long getUploaded()
|
||||
{
|
||||
PeerState s = state;
|
||||
return (s != null) ? s.uploaded : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the downloaded and uploaded counters to zero.
|
||||
*/
|
||||
public void resetCounters()
|
||||
{
|
||||
PeerState s = state;
|
||||
if (s != null)
|
||||
{
|
||||
s.downloaded = 0;
|
||||
s.uploaded = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public long getInactiveTime() {
|
||||
PeerState s = state;
|
||||
if (s != null) {
|
||||
PeerConnectionOut out = s.out;
|
||||
if (out != null)
|
||||
return System.currentTimeMillis() - out.lastSent;
|
||||
else
|
||||
return -1; //"state, no out";
|
||||
} else {
|
||||
return -1; //"no state";
|
||||
}
|
||||
}
|
||||
}
|
||||
145
apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
Normal file
145
apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
Normal file
@@ -0,0 +1,145 @@
|
||||
/* PeerAcceptor - Accepts incomming connections from peers.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.Iterator;
|
||||
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Accepts incomming connections from peers. The ConnectionAcceptor
|
||||
* will call the connection() method when it detects an incomming BT
|
||||
* protocol connection. The PeerAcceptor will then create a new peer
|
||||
* if the PeerCoordinator wants more peers.
|
||||
*/
|
||||
public class PeerAcceptor
|
||||
{
|
||||
private static final Log _log = new Log(PeerAcceptor.class);
|
||||
private final PeerCoordinator coordinator;
|
||||
final PeerCoordinatorSet coordinators;
|
||||
|
||||
public PeerAcceptor(PeerCoordinator coordinator)
|
||||
{
|
||||
this.coordinator = coordinator;
|
||||
this.coordinators = null;
|
||||
}
|
||||
|
||||
public PeerAcceptor(PeerCoordinatorSet coordinators)
|
||||
{
|
||||
this.coordinators = coordinators;
|
||||
this.coordinator = null;
|
||||
}
|
||||
|
||||
public void connection(I2PSocket socket,
|
||||
InputStream in, OutputStream out)
|
||||
throws IOException
|
||||
{
|
||||
// inside this Peer constructor's handshake is where you'd deal with the other
|
||||
// side saying they want to communicate with another torrent - aka multitorrent
|
||||
// support, but because of how the protocol works, we can get away with just reading
|
||||
// ahead the first $LOOKAHEAD_SIZE bytes to figure out which infohash they want to
|
||||
// talk about, and we can just look for that in our list of active torrents.
|
||||
byte peerInfoHash[] = null;
|
||||
if (in instanceof BufferedInputStream) {
|
||||
in.mark(LOOKAHEAD_SIZE);
|
||||
peerInfoHash = readHash(in);
|
||||
in.reset();
|
||||
} else {
|
||||
// is this working right?
|
||||
try {
|
||||
peerInfoHash = readHash(in);
|
||||
_log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64()
|
||||
+ ": " + Base64.encode(peerInfoHash));
|
||||
} catch (IOException ioe) {
|
||||
_log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
|
||||
throw ioe;
|
||||
}
|
||||
in = new SequenceInputStream(new ByteArrayInputStream(peerInfoHash), in);
|
||||
}
|
||||
if (coordinator != null) {
|
||||
// single torrent capability
|
||||
MetaInfo meta = coordinator.getMetaInfo();
|
||||
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
|
||||
if (coordinator.needPeers())
|
||||
{
|
||||
Peer peer = new Peer(socket, in, out, coordinator.getID(),
|
||||
coordinator.getMetaInfo());
|
||||
coordinator.addPeer(peer);
|
||||
}
|
||||
else
|
||||
socket.close();
|
||||
} else {
|
||||
// its for another infohash, but we are only single torrent capable. b0rk.
|
||||
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash)
|
||||
+ ") while we only support (" + Base64.encode(meta.getInfoHash()) + ")");
|
||||
}
|
||||
} else {
|
||||
// multitorrent capable, so lets see what we can handle
|
||||
for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) {
|
||||
PeerCoordinator cur = (PeerCoordinator)iter.next();
|
||||
MetaInfo meta = cur.getMetaInfo();
|
||||
|
||||
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
|
||||
if (cur.needPeers())
|
||||
{
|
||||
Peer peer = new Peer(socket, in, out, cur.getID(),
|
||||
cur.getMetaInfo());
|
||||
cur.addPeer(peer);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Rejecting new peer for " + cur.snark.torrent);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// this is only reached if none of the coordinators match the infohash
|
||||
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash)
|
||||
+ ") while we don't support that hash");
|
||||
}
|
||||
}
|
||||
|
||||
private static final int LOOKAHEAD_SIZE = "19".length() +
|
||||
"BitTorrent protocol".length() +
|
||||
8 + // blank, reserved
|
||||
20; // infohash
|
||||
|
||||
/**
|
||||
* Read ahead to the infohash, throwing an exception if there isn't enough data
|
||||
*/
|
||||
private byte[] readHash(InputStream in) throws IOException {
|
||||
byte buf[] = new byte[LOOKAHEAD_SIZE];
|
||||
int read = DataHelper.read(in, buf);
|
||||
if (read != buf.length)
|
||||
throw new IOException("Unable to read the hash (read " + read + ")");
|
||||
byte rv[] = new byte[20];
|
||||
System.arraycopy(buf, buf.length-rv.length-1, rv, 0, rv.length);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
204
apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
Normal file
204
apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java
Normal file
@@ -0,0 +1,204 @@
|
||||
/* PeerCheckTasks - TimerTask that checks for good/bad up/downloaders.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* TimerTask that checks for good/bad up/downloader. Works together
|
||||
* with the PeerCoordinator to select which Peers get (un)choked.
|
||||
*/
|
||||
class PeerCheckerTask extends TimerTask
|
||||
{
|
||||
private final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000);
|
||||
|
||||
private final PeerCoordinator coordinator;
|
||||
|
||||
PeerCheckerTask(PeerCoordinator coordinator)
|
||||
{
|
||||
this.coordinator = coordinator;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
synchronized(coordinator.peers)
|
||||
{
|
||||
// Calculate total uploading and worst downloader.
|
||||
long worstdownload = Long.MAX_VALUE;
|
||||
Peer worstDownloader = null;
|
||||
|
||||
int peers = 0;
|
||||
int uploaders = 0;
|
||||
int downloaders = 0;
|
||||
int interested = 0;
|
||||
int interesting = 0;
|
||||
int choking = 0;
|
||||
int choked = 0;
|
||||
|
||||
long uploaded = 0;
|
||||
long downloaded = 0;
|
||||
|
||||
// Keep track of peers we remove now,
|
||||
// we will add them back to the end of the list.
|
||||
List removed = new ArrayList();
|
||||
|
||||
Iterator it = coordinator.peers.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Peer peer = (Peer)it.next();
|
||||
|
||||
// Remove dying peers
|
||||
if (!peer.isConnected())
|
||||
{
|
||||
it.remove();
|
||||
coordinator.removePeerFromPieces(peer);
|
||||
coordinator.peerCount = coordinator.peers.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
peers++;
|
||||
|
||||
if (!peer.isChoking())
|
||||
uploaders++;
|
||||
if (!peer.isChoked() && peer.isInteresting())
|
||||
downloaders++;
|
||||
if (peer.isInterested())
|
||||
interested++;
|
||||
if (peer.isInteresting())
|
||||
interesting++;
|
||||
if (peer.isChoking())
|
||||
choking++;
|
||||
if (peer.isChoked())
|
||||
choked++;
|
||||
|
||||
// XXX - We should calculate the up/download rate a bit
|
||||
// more intelligently
|
||||
long upload = peer.getUploaded();
|
||||
uploaded += upload;
|
||||
long download = peer.getDownloaded();
|
||||
downloaded += download;
|
||||
peer.resetCounters();
|
||||
|
||||
if (Snark.debug >= Snark.DEBUG)
|
||||
{
|
||||
Snark.debug(peer + ":", Snark.DEBUG);
|
||||
Snark.debug(" ul: " + upload/KILOPERSECOND
|
||||
+ " dl: " + download/KILOPERSECOND
|
||||
+ " i: " + peer.isInterested()
|
||||
+ " I: " + peer.isInteresting()
|
||||
+ " c: " + peer.isChoking()
|
||||
+ " C: " + peer.isChoked(),
|
||||
Snark.DEBUG);
|
||||
}
|
||||
|
||||
// If we are at our max uploaders and we have lots of other
|
||||
// interested peers try to make some room.
|
||||
// (Note use of coordinator.uploaders)
|
||||
if (coordinator.uploaders >= PeerCoordinator.MAX_UPLOADERS
|
||||
&& interested > PeerCoordinator.MAX_UPLOADERS
|
||||
&& !peer.isChoking())
|
||||
{
|
||||
// Check if it still wants pieces from us.
|
||||
if (!peer.isInterested())
|
||||
{
|
||||
if (Snark.debug >= Snark.INFO)
|
||||
Snark.debug("Choke uninterested peer: " + peer,
|
||||
Snark.INFO);
|
||||
peer.setChoking(true);
|
||||
uploaders--;
|
||||
coordinator.uploaders--;
|
||||
|
||||
// Put it at the back of the list
|
||||
it.remove();
|
||||
removed.add(peer);
|
||||
}
|
||||
else if (peer.isChoked())
|
||||
{
|
||||
// If they are choking us make someone else a downloader
|
||||
if (Snark.debug >= Snark.DEBUG)
|
||||
Snark.debug("Choke choking peer: " + peer, Snark.DEBUG);
|
||||
peer.setChoking(true);
|
||||
uploaders--;
|
||||
coordinator.uploaders--;
|
||||
|
||||
// Put it at the back of the list
|
||||
it.remove();
|
||||
removed.add(peer);
|
||||
}
|
||||
else if (peer.isInteresting()
|
||||
&& !peer.isChoked()
|
||||
&& download == 0)
|
||||
{
|
||||
// We are downloading but didn't receive anything...
|
||||
if (Snark.debug >= Snark.DEBUG)
|
||||
Snark.debug("Choke downloader that doesn't deliver:"
|
||||
+ peer, Snark.DEBUG);
|
||||
peer.setChoking(true);
|
||||
uploaders--;
|
||||
coordinator.uploaders--;
|
||||
|
||||
// Put it at the back of the list
|
||||
it.remove();
|
||||
removed.add(peer);
|
||||
}
|
||||
else if (!peer.isChoking() && download < worstdownload)
|
||||
{
|
||||
// Make sure download is good if we are uploading
|
||||
worstdownload = download;
|
||||
worstDownloader = peer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resync actual uploaders value
|
||||
// (can shift a bit by disconnecting peers)
|
||||
coordinator.uploaders = uploaders;
|
||||
|
||||
// Remove the worst downloader if needed.
|
||||
if (uploaders >= PeerCoordinator.MAX_UPLOADERS
|
||||
&& interested > PeerCoordinator.MAX_UPLOADERS
|
||||
&& worstDownloader != null)
|
||||
{
|
||||
if (Snark.debug >= Snark.DEBUG)
|
||||
Snark.debug("Choke worst downloader: " + worstDownloader,
|
||||
Snark.DEBUG);
|
||||
|
||||
worstDownloader.setChoking(true);
|
||||
coordinator.uploaders--;
|
||||
|
||||
// Put it at the back of the list
|
||||
coordinator.peers.remove(worstDownloader);
|
||||
coordinator.peerCount = coordinator.peers.size();
|
||||
removed.add(worstDownloader);
|
||||
}
|
||||
|
||||
// Optimistically unchoke a peer
|
||||
coordinator.unchokePeer();
|
||||
|
||||
// Put peers back at the end of the list that we removed earlier.
|
||||
coordinator.peers.addAll(removed);
|
||||
coordinator.peerCount = coordinator.peers.size();
|
||||
}
|
||||
if (coordinator.halted()) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
186
apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
Normal file
186
apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
Normal file
@@ -0,0 +1,186 @@
|
||||
/* PeerConnectionIn - Handles incomming messages and hands them to PeerState.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
|
||||
import net.i2p.util.Log;
|
||||
|
||||
class PeerConnectionIn implements Runnable
|
||||
{
|
||||
private Log _log = new Log(PeerConnectionIn.class);
|
||||
private final Peer peer;
|
||||
private final DataInputStream din;
|
||||
|
||||
private Thread thread;
|
||||
private boolean quit;
|
||||
|
||||
public PeerConnectionIn(Peer peer, DataInputStream din)
|
||||
{
|
||||
this.peer = peer;
|
||||
this.din = din;
|
||||
quit = false;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
if (quit == true)
|
||||
return;
|
||||
|
||||
quit = true;
|
||||
Thread t = thread;
|
||||
if (t != null)
|
||||
t.interrupt();
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
thread = Thread.currentThread();
|
||||
try
|
||||
{
|
||||
PeerState ps = peer.state;
|
||||
while (!quit && ps != null)
|
||||
{
|
||||
// Common variables used for some messages.
|
||||
int piece;
|
||||
int begin;
|
||||
int len;
|
||||
|
||||
// Wait till we hear something...
|
||||
// The length of a complete message in bytes.
|
||||
int i = din.readInt();
|
||||
if (i < 0)
|
||||
throw new IOException("Unexpected length prefix: " + i);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
ps.keepAliveMessage();
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
byte b = din.readByte();
|
||||
Message m = new Message();
|
||||
m.type = b;
|
||||
switch (b)
|
||||
{
|
||||
case 0:
|
||||
ps.chokeMessage(true);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received choke from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
case 1:
|
||||
ps.chokeMessage(false);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
case 2:
|
||||
ps.interestedMessage(true);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received interested from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
case 3:
|
||||
ps.interestedMessage(false);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
case 4:
|
||||
piece = din.readInt();
|
||||
ps.haveMessage(piece);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
case 5:
|
||||
byte[] bitmap = new byte[i-1];
|
||||
din.readFully(bitmap);
|
||||
ps.bitfieldMessage(bitmap);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received bitmap from " + peer + " on " + peer.metainfo.getName() + ": size=" + (i-1) + ": " + ps.bitfield);
|
||||
break;
|
||||
case 6:
|
||||
piece = din.readInt();
|
||||
begin = din.readInt();
|
||||
len = din.readInt();
|
||||
ps.requestMessage(piece, begin, len);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received request(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
case 7:
|
||||
piece = din.readInt();
|
||||
begin = din.readInt();
|
||||
len = i-9;
|
||||
Request req = ps.getOutstandingRequest(piece, begin, len);
|
||||
byte[] piece_bytes;
|
||||
if (req != null)
|
||||
{
|
||||
piece_bytes = req.bs;
|
||||
din.readFully(piece_bytes, begin, len);
|
||||
ps.pieceMessage(req);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
// XXX - Consume but throw away afterwards.
|
||||
piece_bytes = new byte[len];
|
||||
din.readFully(piece_bytes);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
piece = din.readInt();
|
||||
begin = din.readInt();
|
||||
len = din.readInt();
|
||||
ps.cancelMessage(piece, begin, len);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
||||
break;
|
||||
default:
|
||||
byte[] bs = new byte[i-1];
|
||||
din.readFully(bs);
|
||||
ps.unknownMessage(b, bs);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
// Ignore, probably the other side closed connection.
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("IOError talking with " + peer, ioe);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
_log.error("Error talking with " + peer, t);
|
||||
if (t instanceof OutOfMemoryError)
|
||||
throw (OutOfMemoryError)t;
|
||||
}
|
||||
finally
|
||||
{
|
||||
peer.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
399
apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
Normal file
399
apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
Normal file
@@ -0,0 +1,399 @@
|
||||
/* PeerConnectionOut - Keeps a queue of outgoing messages and delivers them.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
class PeerConnectionOut implements Runnable
|
||||
{
|
||||
private Log _log = new Log(PeerConnectionOut.class);
|
||||
private final Peer peer;
|
||||
private final DataOutputStream dout;
|
||||
|
||||
private Thread thread;
|
||||
private boolean quit;
|
||||
|
||||
// Contains Messages.
|
||||
private List sendQueue = new ArrayList();
|
||||
|
||||
private static long __id = 0;
|
||||
private long _id;
|
||||
|
||||
long lastSent;
|
||||
|
||||
public PeerConnectionOut(Peer peer, DataOutputStream dout)
|
||||
{
|
||||
this.peer = peer;
|
||||
this.dout = dout;
|
||||
_id = ++__id;
|
||||
|
||||
lastSent = System.currentTimeMillis();
|
||||
quit = false;
|
||||
}
|
||||
|
||||
public void startup() {
|
||||
thread = new I2PThread(this, "Snark sender " + _id + ": " + peer);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuesly monitors for more outgoing messages that have to be send.
|
||||
* Stops if quit is true of an IOException occurs.
|
||||
*/
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!quit && peer.isConnected())
|
||||
{
|
||||
Message m = null;
|
||||
PeerState state = null;
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
while (!quit && peer.isConnected() && sendQueue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Make sure everything will reach the other side.
|
||||
dout.flush();
|
||||
|
||||
// Wait till more data arrives.
|
||||
sendQueue.wait(60*1000);
|
||||
}
|
||||
catch (InterruptedException ie)
|
||||
{
|
||||
/* ignored */
|
||||
}
|
||||
}
|
||||
state = peer.state;
|
||||
if (!quit && state != null && peer.isConnected())
|
||||
{
|
||||
// Piece messages are big. So if there are other
|
||||
// (control) messages make sure they are send first.
|
||||
// Also remove request messages from the queue if
|
||||
// we are currently being choked to prevent them from
|
||||
// being send even if we get unchoked a little later.
|
||||
// (Since we will resent them anyway in that case.)
|
||||
// And remove piece messages if we are choking.
|
||||
|
||||
// this should get fixed for starvation
|
||||
Iterator it = sendQueue.iterator();
|
||||
while (m == null && it.hasNext())
|
||||
{
|
||||
Message nm = (Message)it.next();
|
||||
if (nm.type == Message.PIECE)
|
||||
{
|
||||
if (state.choking) {
|
||||
it.remove();
|
||||
SimpleTimer.getInstance().removeEvent(nm.expireEvent);
|
||||
}
|
||||
nm = null;
|
||||
}
|
||||
else if (nm.type == Message.REQUEST && state.choked)
|
||||
{
|
||||
it.remove();
|
||||
SimpleTimer.getInstance().removeEvent(nm.expireEvent);
|
||||
nm = null;
|
||||
}
|
||||
|
||||
if (m == null && nm != null)
|
||||
{
|
||||
m = nm;
|
||||
SimpleTimer.getInstance().removeEvent(nm.expireEvent);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
if (m == null && sendQueue.size() > 0) {
|
||||
m = (Message)sendQueue.remove(0);
|
||||
SimpleTimer.getInstance().removeEvent(m.expireEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m != null)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Send " + peer + ": " + m + " on " + peer.metainfo.getName());
|
||||
m.sendMessage(dout);
|
||||
lastSent = System.currentTimeMillis();
|
||||
|
||||
// Remove all piece messages after sending a choke message.
|
||||
if (m.type == Message.CHOKE)
|
||||
removeMessage(Message.PIECE);
|
||||
|
||||
// XXX - Should also register overhead...
|
||||
if (m.type == Message.PIECE)
|
||||
state.uploaded(m.len);
|
||||
|
||||
m = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
// Ignore, probably other side closed connection.
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("IOError sending to " + peer, ioe);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
_log.error("Error sending to " + peer, t);
|
||||
if (t instanceof OutOfMemoryError)
|
||||
throw (OutOfMemoryError)t;
|
||||
}
|
||||
finally
|
||||
{
|
||||
quit = true;
|
||||
peer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public void disconnect()
|
||||
{
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
//if (quit == true)
|
||||
// return;
|
||||
|
||||
quit = true;
|
||||
if (thread != null)
|
||||
thread.interrupt();
|
||||
|
||||
sendQueue.clear();
|
||||
sendQueue.notify();
|
||||
}
|
||||
if (dout != null) {
|
||||
try {
|
||||
dout.close();
|
||||
} catch (IOException ioe) {
|
||||
_log.warn("Error closing the stream to " + peer, ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the sendQueue and notifies the method waiting
|
||||
* on the sendQueue to change.
|
||||
*/
|
||||
private void addMessage(Message m)
|
||||
{
|
||||
SimpleTimer.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT);
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
sendQueue.add(m);
|
||||
sendQueue.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/** remove messages not sent in 3m */
|
||||
private static final int SEND_TIMEOUT = 3*60*1000;
|
||||
private class RemoveTooSlow implements SimpleTimer.TimedEvent {
|
||||
private Message _m;
|
||||
public RemoveTooSlow(Message m) {
|
||||
_m = m;
|
||||
m.expireEvent = RemoveTooSlow.this;
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
boolean removed = false;
|
||||
synchronized (sendQueue) {
|
||||
removed = sendQueue.remove(_m);
|
||||
sendQueue.notifyAll();
|
||||
}
|
||||
if (removed)
|
||||
_log.info("Took too long to send " + _m + " to " + peer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a particular message type from the queue.
|
||||
*
|
||||
* @param type the Message type to remove.
|
||||
* @returns true when a message of the given type was removed, false
|
||||
* otherwise.
|
||||
*/
|
||||
private boolean removeMessage(int type)
|
||||
{
|
||||
boolean removed = false;
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
Iterator it = sendQueue.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Message m = (Message)it.next();
|
||||
if (m.type == type)
|
||||
{
|
||||
it.remove();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
sendQueue.notifyAll();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
void sendAlive()
|
||||
{
|
||||
Message m = new Message();
|
||||
m.type = Message.KEEP_ALIVE;
|
||||
addMessage(m);
|
||||
}
|
||||
|
||||
void sendChoke(boolean choke)
|
||||
{
|
||||
// We cancel the (un)choke but keep PIECE messages.
|
||||
// PIECE messages are purged if a choke is actually send.
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
int inverseType = choke ? Message.UNCHOKE
|
||||
: Message.CHOKE;
|
||||
if (!removeMessage(inverseType))
|
||||
{
|
||||
Message m = new Message();
|
||||
if (choke)
|
||||
m.type = Message.CHOKE;
|
||||
else
|
||||
m.type = Message.UNCHOKE;
|
||||
addMessage(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sendInterest(boolean interest)
|
||||
{
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
int inverseType = interest ? Message.UNINTERESTED
|
||||
: Message.INTERESTED;
|
||||
if (!removeMessage(inverseType))
|
||||
{
|
||||
Message m = new Message();
|
||||
if (interest)
|
||||
m.type = Message.INTERESTED;
|
||||
else
|
||||
m.type = Message.UNINTERESTED;
|
||||
addMessage(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sendHave(int piece)
|
||||
{
|
||||
Message m = new Message();
|
||||
m.type = Message.HAVE;
|
||||
m.piece = piece;
|
||||
addMessage(m);
|
||||
}
|
||||
|
||||
void sendBitfield(BitField bitfield)
|
||||
{
|
||||
Message m = new Message();
|
||||
m.type = Message.BITFIELD;
|
||||
m.data = bitfield.getFieldBytes();
|
||||
m.off = 0;
|
||||
m.len = m.data.length;
|
||||
addMessage(m);
|
||||
}
|
||||
|
||||
void sendRequests(List requests)
|
||||
{
|
||||
Iterator it = requests.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Request req = (Request)it.next();
|
||||
sendRequest(req);
|
||||
}
|
||||
}
|
||||
|
||||
void sendRequest(Request req)
|
||||
{
|
||||
Message m = new Message();
|
||||
m.type = Message.REQUEST;
|
||||
m.piece = req.piece;
|
||||
m.begin = req.off;
|
||||
m.length = req.len;
|
||||
addMessage(m);
|
||||
}
|
||||
|
||||
void sendPiece(int piece, int begin, int length, byte[] bytes)
|
||||
{
|
||||
Message m = new Message();
|
||||
m.type = Message.PIECE;
|
||||
m.piece = piece;
|
||||
m.begin = begin;
|
||||
m.length = length;
|
||||
m.data = bytes;
|
||||
m.off = begin;
|
||||
m.len = length;
|
||||
addMessage(m);
|
||||
}
|
||||
|
||||
void sendCancel(Request req)
|
||||
{
|
||||
// See if it is still in our send queue
|
||||
synchronized(sendQueue)
|
||||
{
|
||||
Iterator it = sendQueue.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Message m = (Message)it.next();
|
||||
if (m.type == Message.REQUEST
|
||||
&& m.piece == req.piece
|
||||
&& m.begin == req.off
|
||||
&& m.length == req.len)
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Always send, just to be sure it it is really canceled.
|
||||
Message m = new Message();
|
||||
m.type = Message.CANCEL;
|
||||
m.piece = req.piece;
|
||||
m.begin = req.off;
|
||||
m.length = req.len;
|
||||
addMessage(m);
|
||||
}
|
||||
|
||||
// Called by the PeerState when the other side doesn't want this
|
||||
// request to be handled anymore. Removes any pending Piece Message
|
||||
// from out send queue.
|
||||
void cancelRequest(int piece, int begin, int length)
|
||||
{
|
||||
synchronized (sendQueue)
|
||||
{
|
||||
Iterator it = sendQueue.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Message m = (Message)it.next();
|
||||
if (m.type == Message.PIECE
|
||||
&& m.piece == piece
|
||||
&& m.begin == begin
|
||||
&& m.length == length)
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
585
apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
Normal file
585
apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
Normal file
@@ -0,0 +1,585 @@
|
||||
/* PeerCoordinator - Coordinates which peers do what (up and downloading).
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Coordinates what peer does what.
|
||||
*/
|
||||
public class PeerCoordinator implements PeerListener
|
||||
{
|
||||
private final Log _log = new Log(PeerCoordinator.class);
|
||||
final MetaInfo metainfo;
|
||||
final Storage storage;
|
||||
final Snark snark;
|
||||
|
||||
// package local for access by CheckDownLoadersTask
|
||||
final static long CHECK_PERIOD = 20*1000; // 20 seconds
|
||||
final static int MAX_CONNECTIONS = 24;
|
||||
final static int MAX_UPLOADERS = 12; // i2p: might as well balance it out
|
||||
|
||||
// Approximation of the number of current uploaders.
|
||||
// Resynced by PeerChecker once in a while.
|
||||
int uploaders = 0;
|
||||
|
||||
// final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
|
||||
// int downloaders = 0;
|
||||
|
||||
private long uploaded;
|
||||
private long downloaded;
|
||||
|
||||
// synchronize on this when changing peers or downloaders
|
||||
final List peers = new ArrayList();
|
||||
/** estimate of the peers, without requiring any synchronization */
|
||||
volatile int peerCount;
|
||||
|
||||
/** Timer to handle all periodical tasks. */
|
||||
private final Timer timer = new Timer(true);
|
||||
|
||||
private final byte[] id;
|
||||
|
||||
// Some random wanted pieces
|
||||
private final List wantedPieces;
|
||||
|
||||
private boolean halted = false;
|
||||
|
||||
private final CoordinatorListener listener;
|
||||
|
||||
public String trackerProblems = null;
|
||||
public int trackerSeenPeers = 0;
|
||||
|
||||
public PeerCoordinator(byte[] id, MetaInfo metainfo, Storage storage,
|
||||
CoordinatorListener listener, Snark torrent)
|
||||
{
|
||||
this.id = id;
|
||||
this.metainfo = metainfo;
|
||||
this.storage = storage;
|
||||
this.listener = listener;
|
||||
this.snark = torrent;
|
||||
|
||||
// Make a list of pieces
|
||||
wantedPieces = new ArrayList();
|
||||
BitField bitfield = storage.getBitField();
|
||||
for(int i = 0; i < metainfo.getPieces(); i++)
|
||||
if (!bitfield.get(i))
|
||||
wantedPieces.add(new Piece(i));
|
||||
Collections.shuffle(wantedPieces);
|
||||
|
||||
// Install a timer to check the uploaders.
|
||||
timer.schedule(new PeerCheckerTask(this), CHECK_PERIOD, CHECK_PERIOD);
|
||||
}
|
||||
|
||||
public Storage getStorage() { return storage; }
|
||||
public CoordinatorListener getListener() { return listener; }
|
||||
|
||||
public byte[] getID()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean completed()
|
||||
{
|
||||
return storage.complete();
|
||||
}
|
||||
|
||||
public int getPeerCount() { return peerCount; }
|
||||
|
||||
public int getPeers()
|
||||
{
|
||||
synchronized(peers)
|
||||
{
|
||||
int rv = peers.size();
|
||||
peerCount = rv;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many bytes are still needed to get the complete file.
|
||||
*/
|
||||
public long getLeft()
|
||||
{
|
||||
// XXX - Only an approximation.
|
||||
return storage.needed() * metainfo.getPieceLength(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of uploaded bytes of all peers.
|
||||
*/
|
||||
public long getUploaded()
|
||||
{
|
||||
return uploaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of downloaded bytes of all peers.
|
||||
*/
|
||||
public long getDownloaded()
|
||||
{
|
||||
return downloaded;
|
||||
}
|
||||
|
||||
public MetaInfo getMetaInfo()
|
||||
{
|
||||
return metainfo;
|
||||
}
|
||||
|
||||
public boolean needPeers()
|
||||
{
|
||||
synchronized(peers)
|
||||
{
|
||||
return !halted && peers.size() < MAX_CONNECTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean halted() { return halted; }
|
||||
|
||||
public void halt()
|
||||
{
|
||||
halted = true;
|
||||
List removed = new ArrayList();
|
||||
synchronized(peers)
|
||||
{
|
||||
// Stop peer checker task.
|
||||
timer.cancel();
|
||||
|
||||
// Stop peers.
|
||||
removed.addAll(peers);
|
||||
peers.clear();
|
||||
peerCount = 0;
|
||||
}
|
||||
|
||||
while (removed.size() > 0) {
|
||||
Peer peer = (Peer)removed.remove(0);
|
||||
peer.disconnect();
|
||||
removePeerFromPieces(peer);
|
||||
}
|
||||
}
|
||||
|
||||
public void connected(Peer peer)
|
||||
{
|
||||
if (halted)
|
||||
{
|
||||
peer.disconnect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Peer toDisconnect = null;
|
||||
synchronized(peers)
|
||||
{
|
||||
Peer old = peerIDInList(peer.getPeerID(), peers);
|
||||
if ( (old != null) && (old.getInactiveTime() > 2*60*1000) ) {
|
||||
// idle for 2 minutes, kill the old con
|
||||
peers.remove(old);
|
||||
toDisconnect = old;
|
||||
old = null;
|
||||
}
|
||||
if (old != null)
|
||||
{
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Already connected to: " + peer + ": " + old + ", inactive for " + old.getInactiveTime());
|
||||
peer.disconnect(false); // Don't deregister this connection/peer.
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("New connection to peer: " + peer + " for " + metainfo.getName());
|
||||
|
||||
// Add it to the beginning of the list.
|
||||
// And try to optimistically make it a uploader.
|
||||
peers.add(0, peer);
|
||||
peerCount = peers.size();
|
||||
unchokePeer();
|
||||
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
}
|
||||
}
|
||||
if (toDisconnect != null) {
|
||||
toDisconnect.disconnect(false);
|
||||
removePeerFromPieces(toDisconnect);
|
||||
}
|
||||
}
|
||||
|
||||
private static Peer peerIDInList(PeerID pid, List peers)
|
||||
{
|
||||
Iterator it = peers.iterator();
|
||||
while (it.hasNext()) {
|
||||
Peer cur = (Peer)it.next();
|
||||
if (pid.sameID(cur.getPeerID()))
|
||||
return cur;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addPeer(final Peer peer)
|
||||
{
|
||||
if (halted)
|
||||
{
|
||||
peer.disconnect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean need_more;
|
||||
synchronized(peers)
|
||||
{
|
||||
need_more = !peer.isConnected() && peers.size() < MAX_CONNECTIONS;
|
||||
}
|
||||
|
||||
if (need_more)
|
||||
{
|
||||
_log.debug("Adding a peer " + peer.getPeerID().getAddress().calculateHash().toBase64() + " for " + metainfo.getName(), new Exception("add/run"));
|
||||
|
||||
// Run the peer with us as listener and the current bitfield.
|
||||
final PeerListener listener = this;
|
||||
final BitField bitfield = storage.getBitField();
|
||||
Runnable r = new Runnable()
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
peer.runConnection(listener, bitfield);
|
||||
}
|
||||
};
|
||||
String threadName = peer.toString();
|
||||
new I2PThread(r, threadName).start();
|
||||
}
|
||||
else
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
if (peer.isConnected())
|
||||
_log.info("Add peer already connected: " + peer);
|
||||
else
|
||||
_log.info("MAX_CONNECTIONS = " + MAX_CONNECTIONS
|
||||
+ " not accepting extra peer: " + peer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (Optimistically) unchoke. Should be called with peers synchronized
|
||||
void unchokePeer()
|
||||
{
|
||||
// linked list will contain all interested peers that we choke.
|
||||
// At the start are the peers that have us unchoked at the end the
|
||||
// other peer that are interested, but are choking us.
|
||||
List interested = new LinkedList();
|
||||
synchronized (peers) {
|
||||
Iterator it = peers.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Peer peer = (Peer)it.next();
|
||||
boolean remove = false;
|
||||
if (uploaders < MAX_UPLOADERS
|
||||
&& peer.isChoking()
|
||||
&& peer.isInterested())
|
||||
{
|
||||
if (!peer.isChoked())
|
||||
interested.add(0, peer);
|
||||
else
|
||||
interested.add(peer);
|
||||
}
|
||||
}
|
||||
|
||||
while (uploaders < MAX_UPLOADERS && interested.size() > 0)
|
||||
{
|
||||
Peer peer = (Peer)interested.remove(0);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Unchoke: " + peer);
|
||||
peer.setChoking(false);
|
||||
uploaders++;
|
||||
// Put peer back at the end of the list.
|
||||
peers.remove(peer);
|
||||
peers.add(peer);
|
||||
peerCount = peers.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getBitMap()
|
||||
{
|
||||
return storage.getBitField().getFieldBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we don't have the given piece yet.
|
||||
*/
|
||||
public boolean gotHave(Peer peer, int piece)
|
||||
{
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
|
||||
synchronized(wantedPieces)
|
||||
{
|
||||
return wantedPieces.contains(new Piece(piece));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given bitfield contains at least one piece we
|
||||
* are interested in.
|
||||
*/
|
||||
public boolean gotBitField(Peer peer, BitField bitfield)
|
||||
{
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
|
||||
synchronized(wantedPieces)
|
||||
{
|
||||
Iterator it = wantedPieces.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Piece p = (Piece)it.next();
|
||||
int i = p.getId();
|
||||
if (bitfield.get(i))
|
||||
p.addPeer(peer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one of pieces in the given BitField that is still wanted or
|
||||
* -1 if none of the given pieces are wanted.
|
||||
*/
|
||||
public int wantPiece(Peer peer, BitField havePieces)
|
||||
{
|
||||
if (halted) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("We don't want anything from the peer, as we are halted! peer=" + peer);
|
||||
return -1;
|
||||
}
|
||||
|
||||
synchronized(wantedPieces)
|
||||
{
|
||||
Piece piece = null;
|
||||
Collections.sort(wantedPieces); // Sort in order of rarest first.
|
||||
List requested = new ArrayList();
|
||||
Iterator it = wantedPieces.iterator();
|
||||
while (piece == null && it.hasNext())
|
||||
{
|
||||
Piece p = (Piece)it.next();
|
||||
if (havePieces.get(p.getId()) && !p.isRequested())
|
||||
{
|
||||
piece = p;
|
||||
}
|
||||
else if (p.isRequested())
|
||||
{
|
||||
requested.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
//Only request a piece we've requested before if there's no other choice.
|
||||
if (piece == null) {
|
||||
Iterator it2 = requested.iterator();
|
||||
while (piece == null && it2.hasNext())
|
||||
{
|
||||
Piece p = (Piece)it2.next();
|
||||
if (havePieces.get(p.getId()))
|
||||
{
|
||||
piece = p;
|
||||
}
|
||||
}
|
||||
if (piece == null) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("nothing to even rerequest from " + peer + ": requested = " + requested
|
||||
+ " wanted = " + wantedPieces + " peerHas = " + havePieces);
|
||||
return -1; //If we still can't find a piece we want, so be it.
|
||||
}
|
||||
}
|
||||
piece.setRequested(true);
|
||||
return piece.getId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte array containing the requested piece or null of
|
||||
* the piece is unknown.
|
||||
*/
|
||||
public byte[] gotRequest(Peer peer, int piece)
|
||||
{
|
||||
if (halted)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return storage.getPiece(piece);
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
snark.stopTorrent();
|
||||
_log.error("Error reading the storage for " + metainfo.getName(), ioe);
|
||||
throw new RuntimeException("B0rked");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a peer has uploaded some bytes of a piece.
|
||||
*/
|
||||
public void uploaded(Peer peer, int size)
|
||||
{
|
||||
uploaded += size;
|
||||
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a peer has downloaded some bytes of a piece.
|
||||
*/
|
||||
public void downloaded(Peer peer, int size)
|
||||
{
|
||||
downloaded += size;
|
||||
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the piece is no good (according to the hash).
|
||||
* In that case the peer that supplied the piece should probably be
|
||||
* blacklisted.
|
||||
*/
|
||||
public boolean gotPiece(Peer peer, int piece, byte[] bs)
|
||||
{
|
||||
if (halted) {
|
||||
_log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
|
||||
return true; // We don't actually care anymore.
|
||||
}
|
||||
|
||||
synchronized(wantedPieces)
|
||||
{
|
||||
Piece p = new Piece(piece);
|
||||
if (!wantedPieces.contains(p))
|
||||
{
|
||||
_log.info("Got unwanted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
|
||||
|
||||
// No need to announce have piece to peers.
|
||||
// Assume we got a good piece, we don't really care anymore.
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (storage.putPiece(piece, bs))
|
||||
{
|
||||
_log.info("Got valid piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Oops. We didn't actually download this then... :(
|
||||
downloaded -= metainfo.getPieceLength(piece);
|
||||
_log.warn("Got BAD piece " + piece + "/" + metainfo.getPieces() + " from " + peer + " for " + metainfo.getName());
|
||||
return false; // No need to announce BAD piece to peers.
|
||||
}
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
snark.stopTorrent();
|
||||
_log.error("Error writing storage for " + metainfo.getName(), ioe);
|
||||
throw new RuntimeException("B0rked");
|
||||
}
|
||||
wantedPieces.remove(p);
|
||||
}
|
||||
|
||||
// Announce to the world we have it!
|
||||
synchronized(peers)
|
||||
{
|
||||
Iterator it = peers.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Peer p = (Peer)it.next();
|
||||
if (p.isConnected())
|
||||
p.have(piece);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void gotChoke(Peer peer, boolean choke)
|
||||
{
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Got choke(" + choke + "): " + peer);
|
||||
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
}
|
||||
|
||||
public void gotInterest(Peer peer, boolean interest)
|
||||
{
|
||||
if (interest)
|
||||
{
|
||||
synchronized(peers)
|
||||
{
|
||||
if (uploaders < MAX_UPLOADERS)
|
||||
{
|
||||
if(peer.isChoking())
|
||||
{
|
||||
uploaders++;
|
||||
peer.setChoking(false);
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Unchoke: " + peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
}
|
||||
|
||||
public void disconnected(Peer peer)
|
||||
{
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Disconnected " + peer, new Exception("Disconnected by"));
|
||||
|
||||
synchronized(peers)
|
||||
{
|
||||
// Make sure it is no longer in our lists
|
||||
if (peers.remove(peer))
|
||||
{
|
||||
// Unchoke some random other peer
|
||||
unchokePeer();
|
||||
removePeerFromPieces(peer);
|
||||
}
|
||||
peerCount = peers.size();
|
||||
}
|
||||
|
||||
if (listener != null)
|
||||
listener.peerChange(this, peer);
|
||||
}
|
||||
|
||||
/** Called when a peer is removed, to prevent it from being used in
|
||||
* rarest-first calculations.
|
||||
*/
|
||||
public void removePeerFromPieces(Peer peer) {
|
||||
synchronized(wantedPieces) {
|
||||
for(Iterator iter = wantedPieces.iterator(); iter.hasNext(); ) {
|
||||
Piece piece = (Piece)iter.next();
|
||||
piece.removePeer(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Hmm, any guesses as to what this is? Used by the multitorrent functionality
|
||||
* in the PeerAcceptor to pick the right PeerCoordinator to accept the con for.
|
||||
* Each PeerCoordinator is added to the set from within the Snark (and removed
|
||||
* from it there too)
|
||||
*/
|
||||
public class PeerCoordinatorSet {
|
||||
private static final PeerCoordinatorSet _instance = new PeerCoordinatorSet();
|
||||
public static final PeerCoordinatorSet instance() { return _instance; }
|
||||
private Set _coordinators;
|
||||
|
||||
private PeerCoordinatorSet() {
|
||||
_coordinators = new HashSet();
|
||||
}
|
||||
|
||||
public Iterator iterator() {
|
||||
synchronized (_coordinators) {
|
||||
return new ArrayList(_coordinators).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
public void add(PeerCoordinator coordinator) {
|
||||
synchronized (_coordinators) {
|
||||
_coordinators.add(coordinator);
|
||||
}
|
||||
}
|
||||
public void remove(PeerCoordinator coordinator) {
|
||||
synchronized (_coordinators) {
|
||||
_coordinators.remove(coordinator);
|
||||
}
|
||||
}
|
||||
}
|
||||
208
apps/i2psnark/java/src/org/klomp/snark/PeerID.java
Normal file
208
apps/i2psnark/java/src/org/klomp/snark/PeerID.java
Normal file
@@ -0,0 +1,208 @@
|
||||
/* PeerID - All public information concerning a peer.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.DataFormatException;
|
||||
|
||||
public class PeerID implements Comparable
|
||||
{
|
||||
private final byte[] id;
|
||||
private final Destination address;
|
||||
private final int port;
|
||||
|
||||
private final int hash;
|
||||
|
||||
public PeerID(byte[] id, Destination address)
|
||||
{
|
||||
this.id = id;
|
||||
this.address = address;
|
||||
this.port = 6881;
|
||||
|
||||
hash = calculateHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PeerID from a BDecoder.
|
||||
*/
|
||||
public PeerID(BDecoder be)
|
||||
throws IOException
|
||||
{
|
||||
this(be.bdecodeMap().getMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PeerID from a Map containing BEncoded peer id, ip and
|
||||
* port.
|
||||
*/
|
||||
public PeerID(Map m)
|
||||
throws InvalidBEncodingException, UnknownHostException
|
||||
{
|
||||
BEValue bevalue = (BEValue)m.get("peer id");
|
||||
if (bevalue == null)
|
||||
throw new InvalidBEncodingException("peer id missing");
|
||||
id = bevalue.getBytes();
|
||||
|
||||
bevalue = (BEValue)m.get("ip");
|
||||
if (bevalue == null)
|
||||
throw new InvalidBEncodingException("ip missing");
|
||||
address = I2PSnarkUtil.instance().getDestination(bevalue.getString());
|
||||
if (address == null)
|
||||
throw new InvalidBEncodingException("Invalid destination [" + bevalue.getString() + "]");
|
||||
|
||||
port = 6881;
|
||||
|
||||
hash = calculateHash();
|
||||
}
|
||||
|
||||
public byte[] getID()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public Destination getAddress()
|
||||
{
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getPort()
|
||||
{
|
||||
return port;
|
||||
}
|
||||
|
||||
private int calculateHash()
|
||||
{
|
||||
int b = 0;
|
||||
for (int i = 0; i < id.length; i++)
|
||||
b ^= id[i];
|
||||
return (b ^ address.hashCode()) ^ port;
|
||||
}
|
||||
|
||||
/**
|
||||
* The hash code of a PeerID is the exclusive or of all id bytes.
|
||||
*/
|
||||
public int hashCode()
|
||||
{
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if this peerID and the given peerID have
|
||||
* the same 20 bytes as ID.
|
||||
*/
|
||||
public boolean sameID(PeerID pid)
|
||||
{
|
||||
boolean equal = true;
|
||||
for (int i = 0; equal && i < id.length; i++)
|
||||
equal = id[i] == pid.id[i];
|
||||
return equal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Two PeerIDs are equal when they have the same id, address and port.
|
||||
*/
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (o instanceof PeerID)
|
||||
{
|
||||
PeerID pid = (PeerID)o;
|
||||
|
||||
return port == pid.port
|
||||
&& address.equals(pid.address)
|
||||
&& sameID(pid);
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares port, address and id.
|
||||
*/
|
||||
public int compareTo(Object o)
|
||||
{
|
||||
PeerID pid = (PeerID)o;
|
||||
|
||||
int result = port - pid.port;
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
result = address.hashCode() - pid.address.hashCode();
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
for (int i = 0; i < id.length; i++)
|
||||
{
|
||||
result = id[i] - pid.id[i];
|
||||
if (result != 0)
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the String "id@address" where id is the base64 encoded id.
|
||||
*/
|
||||
public String toString()
|
||||
{
|
||||
int nonZero = 0;
|
||||
for (int i = 0; i < id.length; i++) {
|
||||
if (id[i] != 0) {
|
||||
nonZero = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Base64.encode(id, nonZero, id.length-nonZero).substring(0,4) + "@" + address.calculateHash().toBase64().substring(0,6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an id as a hex encoded string and remove leading zeros.
|
||||
*/
|
||||
public static String idencode(byte[] bs)
|
||||
{
|
||||
boolean leading_zeros = true;
|
||||
|
||||
StringBuffer sb = new StringBuffer(bs.length*2);
|
||||
for (int i = 0; i < bs.length; i++)
|
||||
{
|
||||
int c = bs[i] & 0xFF;
|
||||
if (leading_zeros && c == 0)
|
||||
continue;
|
||||
else
|
||||
leading_zeros = false;
|
||||
|
||||
if (c < 16)
|
||||
sb.append('0');
|
||||
sb.append(Integer.toHexString(c));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
145
apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
Normal file
145
apps/i2psnark/java/src/org/klomp/snark/PeerListener.java
Normal file
@@ -0,0 +1,145 @@
|
||||
/* PeerListener - Interface for listening to peer events.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
/**
|
||||
* Listener for Peer events.
|
||||
*/
|
||||
public interface PeerListener
|
||||
{
|
||||
/**
|
||||
* Called when the connection to the peer has started and the
|
||||
* handshake was successfull.
|
||||
*
|
||||
* @param peer the Peer that just got connected.
|
||||
*/
|
||||
void connected(Peer peer);
|
||||
|
||||
/**
|
||||
* Called when the connection to the peer was terminated or the
|
||||
* connection handshake failed.
|
||||
*
|
||||
* @param peer the Peer that just got disconnected.
|
||||
*/
|
||||
void disconnected(Peer peer);
|
||||
|
||||
/**
|
||||
* Called when a choke message is received.
|
||||
*
|
||||
* @param peer the Peer that got the message.
|
||||
* @param choke true when the peer got a choke message, false when
|
||||
* the peer got an unchoke message.
|
||||
*/
|
||||
void gotChoke(Peer peer, boolean choke);
|
||||
|
||||
/**
|
||||
* Called when an interested message is received.
|
||||
*
|
||||
* @param peer the Peer that got the message.
|
||||
* @param interest true when the peer got a interested message, false when
|
||||
* the peer got an uninterested message.
|
||||
*/
|
||||
void gotInterest(Peer peer, boolean interest);
|
||||
|
||||
/**
|
||||
* Called when a have piece message is received. If the method
|
||||
* returns true and the peer has not yet received a interested
|
||||
* message or we indicated earlier to be not interested then an
|
||||
* interested message will be send.
|
||||
*
|
||||
* @param peer the Peer that got the message.
|
||||
* @param piece the piece number that the per just got.
|
||||
*
|
||||
* @return true when it is a piece that we want, false if the piece is
|
||||
* already known.
|
||||
*/
|
||||
boolean gotHave(Peer peer, int piece);
|
||||
|
||||
/**
|
||||
* Called when a bitmap message is received. If this method returns
|
||||
* true a interested message will be send back to the peer.
|
||||
*
|
||||
* @param peer the Peer that got the message.
|
||||
* @param bitfield a BitField containing the pieces that the other
|
||||
* side has.
|
||||
*
|
||||
* @return true when the BitField contains pieces we want, false if
|
||||
* the piece is already known.
|
||||
*/
|
||||
boolean gotBitField(Peer peer, BitField bitfield);
|
||||
|
||||
/**
|
||||
* Called when a piece is received from the peer. The piece must be
|
||||
* requested by Peer.request() first. If this method returns false
|
||||
* that means the Peer provided a corrupted piece and the connection
|
||||
* will be closed.
|
||||
*
|
||||
* @param peer the Peer that got the piece.
|
||||
* @param piece the piece number received.
|
||||
* @param bs the byte array containing the piece.
|
||||
*
|
||||
* @return true when the bytes represent the piece, false otherwise.
|
||||
*/
|
||||
boolean gotPiece(Peer peer, int piece, byte[] bs);
|
||||
|
||||
/**
|
||||
* Called when the peer wants (part of) a piece from us. Only called
|
||||
* when the peer is not choked by us (<code>peer.choke(false)</code>
|
||||
* was called).
|
||||
*
|
||||
* @param peer the Peer that wants the piece.
|
||||
* @param piece the piece number requested.
|
||||
*
|
||||
* @return a byte array containing the piece or null when the piece
|
||||
* is not available (which is a protocol error).
|
||||
*/
|
||||
byte[] gotRequest(Peer peer, int piece);
|
||||
|
||||
/**
|
||||
* Called when a (partial) piece has been downloaded from the peer.
|
||||
*
|
||||
* @param peer the Peer from which size bytes where downloaded.
|
||||
* @param size the number of bytes that where downloaded.
|
||||
*/
|
||||
void downloaded(Peer peer, int size);
|
||||
|
||||
/**
|
||||
* Called when a (partial) piece has been uploaded to the peer.
|
||||
*
|
||||
* @param peer the Peer to which size bytes where uploaded.
|
||||
* @param size the number of bytes that where uploaded.
|
||||
*/
|
||||
void uploaded(Peer peer, int size);
|
||||
|
||||
/**
|
||||
* Called when we are downloading from the peer and need to ask for
|
||||
* a new piece. Might be called multiple times before
|
||||
* <code>gotPiece()</code> is called.
|
||||
*
|
||||
* @param peer the Peer that will be asked to provide the piece.
|
||||
* @param bitfield a BitField containing the pieces that the other
|
||||
* side has.
|
||||
*
|
||||
* @return one of the pieces from the bitfield that we want or -1 if
|
||||
* we are no longer interested in the peer.
|
||||
*/
|
||||
int wantPiece(Peer peer, BitField bitfield);
|
||||
}
|
||||
128
apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java
Normal file
128
apps/i2psnark/java/src/org/klomp/snark/PeerMonitorTask.java
Normal file
@@ -0,0 +1,128 @@
|
||||
/* PeerMonitorTasks - TimerTask that monitors the peers and total up/down speed
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* TimerTask that monitors the peers and total up/download speeds.
|
||||
* Works together with the main Snark class to report periodical statistics.
|
||||
*/
|
||||
class PeerMonitorTask extends TimerTask
|
||||
{
|
||||
final static long MONITOR_PERIOD = 10 * 1000; // Ten seconds.
|
||||
private final long KILOPERSECOND = 1024 * (MONITOR_PERIOD / 1000);
|
||||
|
||||
private final PeerCoordinator coordinator;
|
||||
|
||||
private long lastDownloaded = 0;
|
||||
private long lastUploaded = 0;
|
||||
|
||||
PeerMonitorTask(PeerCoordinator coordinator)
|
||||
{
|
||||
this.coordinator = coordinator;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
// Get some statistics
|
||||
int peers = 0;
|
||||
int uploaders = 0;
|
||||
int downloaders = 0;
|
||||
int interested = 0;
|
||||
int interesting = 0;
|
||||
int choking = 0;
|
||||
int choked = 0;
|
||||
|
||||
synchronized(coordinator.peers)
|
||||
{
|
||||
Iterator it = coordinator.peers.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Peer peer = (Peer)it.next();
|
||||
|
||||
// Don't list dying peers
|
||||
if (!peer.isConnected())
|
||||
continue;
|
||||
|
||||
peers++;
|
||||
|
||||
if (!peer.isChoking())
|
||||
uploaders++;
|
||||
if (!peer.isChoked() && peer.isInteresting())
|
||||
downloaders++;
|
||||
if (peer.isInterested())
|
||||
interested++;
|
||||
if (peer.isInteresting())
|
||||
interesting++;
|
||||
if (peer.isChoking())
|
||||
choking++;
|
||||
if (peer.isChoked())
|
||||
choked++;
|
||||
}
|
||||
}
|
||||
|
||||
// Print some statistics
|
||||
long downloaded = coordinator.getDownloaded();
|
||||
String totalDown;
|
||||
if (downloaded >= 10 * 1024 * 1024)
|
||||
totalDown = (downloaded / (1024 * 1024)) + "MB";
|
||||
else
|
||||
totalDown = (downloaded / 1024 )+ "KB";
|
||||
long uploaded = coordinator.getUploaded();
|
||||
String totalUp;
|
||||
if (uploaded >= 10 * 1024 * 1024)
|
||||
totalUp = (uploaded / (1024 * 1024)) + "MB";
|
||||
else
|
||||
totalUp = (uploaded / 1024) + "KB";
|
||||
|
||||
int needP = coordinator.storage.needed();
|
||||
long needMB
|
||||
= needP * coordinator.metainfo.getPieceLength(0) / (1024 * 1024);
|
||||
int totalP = coordinator.metainfo.getPieces();
|
||||
long totalMB = coordinator.metainfo.getTotalLength() / (1024 * 1024);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("Down: "
|
||||
+ (downloaded - lastDownloaded) / KILOPERSECOND
|
||||
+ "KB/s"
|
||||
+ " (" + totalDown + ")"
|
||||
+ " Up: "
|
||||
+ (uploaded - lastUploaded) / KILOPERSECOND
|
||||
+ "KB/s"
|
||||
+ " (" + totalUp + ")"
|
||||
+ " Need " + needP
|
||||
+ " (" + needMB + "MB)"
|
||||
+ " of " + totalP
|
||||
+ " (" + totalMB + "MB)"
|
||||
+ " pieces");
|
||||
System.out.println(peers + ": Download #" + downloaders
|
||||
+ " Upload #" + uploaders
|
||||
+ " Interested #" + interested
|
||||
+ " Interesting #" + interesting
|
||||
+ " Choking #" + choking
|
||||
+ " Choked #" + choked);
|
||||
System.out.println();
|
||||
|
||||
lastDownloaded = downloaded;
|
||||
lastUploaded = uploaded;
|
||||
}
|
||||
}
|
||||
526
apps/i2psnark/java/src/org/klomp/snark/PeerState.java
Normal file
526
apps/i2psnark/java/src/org/klomp/snark/PeerState.java
Normal file
@@ -0,0 +1,526 @@
|
||||
/* PeerState - Keeps track of the Peer state through connection callbacks.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
import net.i2p.util.Log;
|
||||
|
||||
class PeerState
|
||||
{
|
||||
private Log _log = new Log(PeerState.class);
|
||||
final Peer peer;
|
||||
final PeerListener listener;
|
||||
final MetaInfo metainfo;
|
||||
|
||||
// Interesting and choking describes whether we are interested in or
|
||||
// are choking the other side.
|
||||
boolean interesting = false;
|
||||
boolean choking = true;
|
||||
|
||||
// Interested and choked describes whether the other side is
|
||||
// interested in us or choked us.
|
||||
boolean interested = false;
|
||||
boolean choked = true;
|
||||
|
||||
// Package local for use by Peer.
|
||||
long downloaded;
|
||||
long uploaded;
|
||||
|
||||
BitField bitfield;
|
||||
|
||||
// Package local for use by Peer.
|
||||
final PeerConnectionIn in;
|
||||
final PeerConnectionOut out;
|
||||
|
||||
// Outstanding request
|
||||
private final List outstandingRequests = new ArrayList();
|
||||
private Request lastRequest = null;
|
||||
|
||||
// If we have te resend outstanding requests (true after we got choked).
|
||||
private boolean resend = false;
|
||||
|
||||
private final static int MAX_PIPELINE = 1;
|
||||
private final static int PARTSIZE = 64*1024; // default was 16K, i2p-bt uses 64KB
|
||||
|
||||
PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
|
||||
PeerConnectionIn in, PeerConnectionOut out)
|
||||
{
|
||||
this.peer = peer;
|
||||
this.listener = listener;
|
||||
this.metainfo = metainfo;
|
||||
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
// NOTE Methods that inspect or change the state synchronize (on this).
|
||||
|
||||
void keepAliveMessage()
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " rcv alive");
|
||||
/* XXX - ignored */
|
||||
}
|
||||
|
||||
void chokeMessage(boolean choke)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " rcv " + (choke ? "" : "un") + "choked");
|
||||
|
||||
choked = choke;
|
||||
if (choked)
|
||||
resend = true;
|
||||
|
||||
listener.gotChoke(peer, choke);
|
||||
|
||||
if (!choked && interesting)
|
||||
request();
|
||||
}
|
||||
|
||||
void interestedMessage(boolean interest)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " rcv " + (interest ? "" : "un")
|
||||
+ "interested");
|
||||
interested = interest;
|
||||
listener.gotInterest(peer, interest);
|
||||
}
|
||||
|
||||
void haveMessage(int piece)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " rcv have(" + piece + ")");
|
||||
// Sanity check
|
||||
if (piece < 0 || piece >= metainfo.getPieces())
|
||||
{
|
||||
// XXX disconnect?
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got strange 'have: " + piece + "' message from " + peer);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
// Can happen if the other side never send a bitfield message.
|
||||
if (bitfield == null)
|
||||
bitfield = new BitField(metainfo.getPieces());
|
||||
|
||||
bitfield.set(piece);
|
||||
}
|
||||
|
||||
if (listener.gotHave(peer, piece))
|
||||
setInteresting(true);
|
||||
}
|
||||
|
||||
void bitfieldMessage(byte[] bitmap)
|
||||
{
|
||||
synchronized(this)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " rcv bitfield");
|
||||
if (bitfield != null)
|
||||
{
|
||||
// XXX - Be liberal in what you except?
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got unexpected bitfield message from " + peer);
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX - Check for weird bitfield and disconnect?
|
||||
bitfield = new BitField(bitmap, metainfo.getPieces());
|
||||
}
|
||||
setInteresting(listener.gotBitField(peer, bitfield));
|
||||
}
|
||||
|
||||
void requestMessage(int piece, int begin, int length)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " rcv request("
|
||||
+ piece + ", " + begin + ", " + length + ") ");
|
||||
if (choking)
|
||||
{
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Request received, but choking " + peer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
if (piece < 0
|
||||
|| piece >= metainfo.getPieces()
|
||||
|| begin < 0
|
||||
|| begin > metainfo.getPieceLength(piece)
|
||||
|| length <= 0
|
||||
|| length > 4*PARTSIZE)
|
||||
{
|
||||
// XXX - Protocol error -> disconnect?
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got strange 'request: " + piece
|
||||
+ ", " + begin
|
||||
+ ", " + length
|
||||
+ "' message from " + peer);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] pieceBytes = listener.gotRequest(peer, piece);
|
||||
if (pieceBytes == null)
|
||||
{
|
||||
// XXX - Protocol error-> diconnect?
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got request for unknown piece: " + piece);
|
||||
return;
|
||||
}
|
||||
|
||||
// More sanity checks
|
||||
if (begin >= pieceBytes.length || begin + length > pieceBytes.length)
|
||||
{
|
||||
// XXX - Protocol error-> disconnect?
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got out of range 'request: " + piece
|
||||
+ ", " + begin
|
||||
+ ", " + length
|
||||
+ "' message from " + peer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Sending (" + piece + ", " + begin + ", "
|
||||
+ length + ")" + " to " + peer);
|
||||
out.sendPiece(piece, begin, length, pieceBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when some bytes have left the outgoing connection.
|
||||
* XXX - Should indicate whether it was a real piece or overhead.
|
||||
*/
|
||||
void uploaded(int size)
|
||||
{
|
||||
uploaded += size;
|
||||
listener.uploaded(peer, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a partial piece request has been handled by
|
||||
* PeerConnectionIn.
|
||||
*/
|
||||
void pieceMessage(Request req)
|
||||
{
|
||||
int size = req.len;
|
||||
downloaded += size;
|
||||
listener.downloaded(peer, size);
|
||||
|
||||
// Last chunk needed for this piece?
|
||||
if (getFirstOutstandingRequest(req.piece) == -1)
|
||||
{
|
||||
if (listener.gotPiece(peer, req.piece, req.bs))
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Got " + req.piece + ": " + peer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got BAD " + req.piece + " from " + peer);
|
||||
// XXX ARGH What now !?!
|
||||
downloaded = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized private int getFirstOutstandingRequest(int piece)
|
||||
{
|
||||
for (int i = 0; i < outstandingRequests.size(); i++)
|
||||
if (((Request)outstandingRequests.get(i)).piece == piece)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a piece message is being processed by the incoming
|
||||
* connection. Returns null when there was no such request. It also
|
||||
* requeues/sends requests when it thinks that they must have been
|
||||
* lost.
|
||||
*/
|
||||
Request getOutstandingRequest(int piece, int begin, int length)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("getChunk("
|
||||
+ piece + "," + begin + "," + length + ") "
|
||||
+ peer);
|
||||
|
||||
int r = getFirstOutstandingRequest(piece);
|
||||
|
||||
// Unrequested piece number?
|
||||
if (r == -1)
|
||||
{
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Unrequested 'piece: " + piece + ", "
|
||||
+ begin + ", " + length + "' received from "
|
||||
+ peer);
|
||||
downloaded = 0; // XXX - punishment?
|
||||
return null;
|
||||
}
|
||||
|
||||
// Lookup the correct piece chunk request from the list.
|
||||
Request req;
|
||||
synchronized(this)
|
||||
{
|
||||
req = (Request)outstandingRequests.get(r);
|
||||
while (req.piece == piece && req.off != begin
|
||||
&& r < outstandingRequests.size() - 1)
|
||||
{
|
||||
r++;
|
||||
req = (Request)outstandingRequests.get(r);
|
||||
}
|
||||
|
||||
// Something wrong?
|
||||
if (req.piece != piece || req.off != begin || req.len != length)
|
||||
{
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Unrequested or unneeded 'piece: "
|
||||
+ piece + ", "
|
||||
+ begin + ", "
|
||||
+ length + "' received from "
|
||||
+ peer);
|
||||
downloaded = 0; // XXX - punishment?
|
||||
return null;
|
||||
}
|
||||
|
||||
// Report missing requests.
|
||||
if (r != 0)
|
||||
{
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Some requests dropped, got " + req
|
||||
+ ", wanted for peer: " + peer);
|
||||
for (int i = 0; i < r; i++)
|
||||
{
|
||||
Request dropReq = (Request)outstandingRequests.remove(0);
|
||||
outstandingRequests.add(dropReq);
|
||||
// We used to rerequest the missing chunks but that mostly
|
||||
// just confuses the other side. So now we just keep
|
||||
// waiting for them. They will be rerequested when we get
|
||||
// choked/unchoked again.
|
||||
/*
|
||||
if (!choked)
|
||||
out.sendRequest(dropReq);
|
||||
*/
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("dropped " + dropReq + " with peer " + peer);
|
||||
}
|
||||
}
|
||||
outstandingRequests.remove(0);
|
||||
}
|
||||
|
||||
// Request more if necessary to keep the pipeline filled.
|
||||
addRequest();
|
||||
|
||||
return req;
|
||||
|
||||
}
|
||||
|
||||
void cancelMessage(int piece, int begin, int length)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Got cancel message ("
|
||||
+ piece + ", " + begin + ", " + length + ")");
|
||||
out.cancelRequest(piece, begin, length);
|
||||
}
|
||||
|
||||
void unknownMessage(int type, byte[] bs)
|
||||
{
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Warning: Ignoring unknown message type: " + type
|
||||
+ " length: " + bs.length);
|
||||
}
|
||||
|
||||
void havePiece(int piece)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Tell " + peer + " havePiece(" + piece + ")");
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
// Tell the other side that we are no longer interested in any of
|
||||
// the outstanding requests for this piece.
|
||||
if (lastRequest != null && lastRequest.piece == piece)
|
||||
lastRequest = null;
|
||||
|
||||
Iterator it = outstandingRequests.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Request req = (Request)it.next();
|
||||
if (req.piece == piece)
|
||||
{
|
||||
it.remove();
|
||||
// Send cancel even when we are choked to make sure that it is
|
||||
// really never ever send.
|
||||
out.sendCancel(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the other side that we really have this piece.
|
||||
out.sendHave(piece);
|
||||
|
||||
// Request something else if necessary.
|
||||
addRequest();
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
// Is the peer still interesting?
|
||||
if (lastRequest == null)
|
||||
setInteresting(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Starts or resumes requesting pieces.
|
||||
private void request()
|
||||
{
|
||||
// Are there outstanding requests that have to be resend?
|
||||
if (resend)
|
||||
{
|
||||
synchronized (this) {
|
||||
out.sendRequests(outstandingRequests);
|
||||
}
|
||||
resend = false;
|
||||
}
|
||||
|
||||
// Add/Send some more requests if necessary.
|
||||
addRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new request to the outstanding requests list.
|
||||
*/
|
||||
private void addRequest()
|
||||
{
|
||||
boolean more_pieces = true;
|
||||
while (more_pieces)
|
||||
{
|
||||
synchronized(this)
|
||||
{
|
||||
more_pieces = outstandingRequests.size() < MAX_PIPELINE;
|
||||
}
|
||||
|
||||
// We want something and we don't have outstanding requests?
|
||||
if (more_pieces && lastRequest == null)
|
||||
more_pieces = requestNextPiece();
|
||||
else if (more_pieces) // We want something
|
||||
{
|
||||
int pieceLength;
|
||||
boolean isLastChunk;
|
||||
synchronized(this)
|
||||
{
|
||||
pieceLength = metainfo.getPieceLength(lastRequest.piece);
|
||||
isLastChunk = lastRequest.off + lastRequest.len == pieceLength;
|
||||
}
|
||||
|
||||
// Last part of a piece?
|
||||
if (isLastChunk)
|
||||
more_pieces = requestNextPiece();
|
||||
else
|
||||
{
|
||||
synchronized(this)
|
||||
{
|
||||
int nextPiece = lastRequest.piece;
|
||||
int nextBegin = lastRequest.off + PARTSIZE;
|
||||
byte[] bs = lastRequest.bs;
|
||||
int maxLength = pieceLength - nextBegin;
|
||||
int nextLength = maxLength > PARTSIZE ? PARTSIZE
|
||||
: maxLength;
|
||||
Request req
|
||||
= new Request(nextPiece, bs, nextBegin, nextLength);
|
||||
outstandingRequests.add(req);
|
||||
if (!choked)
|
||||
out.sendRequest(req);
|
||||
lastRequest = req;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " requests " + outstandingRequests);
|
||||
}
|
||||
|
||||
// Starts requesting first chunk of next piece. Returns true if
|
||||
// something has been added to the requests, false otherwise.
|
||||
private boolean requestNextPiece()
|
||||
{
|
||||
// Check that we already know what the other side has.
|
||||
if (bitfield != null)
|
||||
{
|
||||
int nextPiece = listener.wantPiece(peer, bitfield);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " want piece " + nextPiece);
|
||||
synchronized(this)
|
||||
{
|
||||
if (nextPiece != -1
|
||||
&& (lastRequest == null || lastRequest.piece != nextPiece))
|
||||
{
|
||||
int piece_length = metainfo.getPieceLength(nextPiece);
|
||||
byte[] bs = new byte[piece_length];
|
||||
|
||||
int length = Math.min(piece_length, PARTSIZE);
|
||||
Request req = new Request(nextPiece, bs, 0, length);
|
||||
outstandingRequests.add(req);
|
||||
if (!choked)
|
||||
out.sendRequest(req);
|
||||
lastRequest = req;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
synchronized void setInteresting(boolean interest)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " setInteresting(" + interest + ")");
|
||||
|
||||
if (interest != interesting)
|
||||
{
|
||||
interesting = interest;
|
||||
out.sendInterest(interest);
|
||||
|
||||
if (interesting && !choked)
|
||||
request();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void setChoking(boolean choke)
|
||||
{
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(peer + " setChoking(" + choke + ")");
|
||||
|
||||
if (choking != choke)
|
||||
{
|
||||
choking = choke;
|
||||
out.sendChoke(choke);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
apps/i2psnark/java/src/org/klomp/snark/Piece.java
Normal file
42
apps/i2psnark/java/src/org/klomp/snark/Piece.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.Collections;
|
||||
|
||||
public class Piece implements Comparable {
|
||||
|
||||
private int id;
|
||||
private Set peers;
|
||||
private boolean requested;
|
||||
|
||||
public Piece(int id) {
|
||||
this.id = id;
|
||||
this.peers = Collections.synchronizedSet(new HashSet());
|
||||
this.requested = false;
|
||||
}
|
||||
|
||||
public int compareTo(Object o) throws ClassCastException {
|
||||
return this.peers.size() - ((Piece)o).peers.size();
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (o == null) return false;
|
||||
try {
|
||||
return this.id == ((Piece)o).id;
|
||||
} catch (ClassCastException cce) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int getId() { return this.id; }
|
||||
public Set getPeers() { return this.peers; }
|
||||
public boolean addPeer(Peer peer) { return this.peers.add(peer.getPeerID()); }
|
||||
public boolean removePeer(Peer peer) { return this.peers.remove(peer.getPeerID()); }
|
||||
public boolean isRequested() { return this.requested; }
|
||||
public void setRequested(boolean requested) { this.requested = requested; }
|
||||
|
||||
public String toString() {
|
||||
return String.valueOf(id);
|
||||
}
|
||||
}
|
||||
73
apps/i2psnark/java/src/org/klomp/snark/Request.java
Normal file
73
apps/i2psnark/java/src/org/klomp/snark/Request.java
Normal file
@@ -0,0 +1,73 @@
|
||||
/* Request - Holds all information needed for a (partial) piece request.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
/**
|
||||
* Holds all information needed for a partial piece request.
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
final int piece;
|
||||
final byte[] bs;
|
||||
final int off;
|
||||
final int len;
|
||||
|
||||
/**
|
||||
* Creates a new Request.
|
||||
*
|
||||
* @param piece Piece number requested.
|
||||
* @param bs byte array where response should be stored.
|
||||
* @param off the offset in the array.
|
||||
* @param len the number of bytes requested.
|
||||
*/
|
||||
Request(int piece, byte[] bs, int off, int len)
|
||||
{
|
||||
this.piece = piece;
|
||||
this.bs = bs;
|
||||
this.off = off;
|
||||
this.len = len;
|
||||
|
||||
// Sanity check
|
||||
if (piece < 0 || off < 0 || len <= 0 || off + len > bs.length)
|
||||
throw new IndexOutOfBoundsException("Illegal Request " + toString());
|
||||
}
|
||||
|
||||
public int hashCode()
|
||||
{
|
||||
return piece ^ off ^ len;
|
||||
}
|
||||
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (o instanceof Request)
|
||||
{
|
||||
Request req = (Request)o;
|
||||
return req.piece == piece && req.off == off && req.len == len;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return "(" + piece + "," + off + "," + len + ")";
|
||||
}
|
||||
}
|
||||
34
apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java
Normal file
34
apps/i2psnark/java/src/org/klomp/snark/ShutdownListener.java
Normal file
@@ -0,0 +1,34 @@
|
||||
/* ShutdownListener - Callback for end of shutdown sequence
|
||||
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
/**
|
||||
* Callback for end of shutdown sequence.
|
||||
*/
|
||||
interface ShutdownListener
|
||||
{
|
||||
/**
|
||||
* Called when the SnarkShutdown hook has finished shutting down all
|
||||
* subcomponents.
|
||||
*/
|
||||
void shutdown();
|
||||
}
|
||||
706
apps/i2psnark/java/src/org/klomp/snark/Snark.java
Normal file
706
apps/i2psnark/java/src/org/klomp/snark/Snark.java
Normal file
@@ -0,0 +1,706 @@
|
||||
/* Snark - Main snark program startup class.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.client.streaming.I2PServerSocket;
|
||||
import net.i2p.util.I2PThread;
|
||||
|
||||
/**
|
||||
* Main Snark program startup class.
|
||||
*
|
||||
* @author Mark Wielaard (mark@klomp.org)
|
||||
*/
|
||||
public class Snark
|
||||
implements StorageListener, CoordinatorListener, ShutdownListener
|
||||
{
|
||||
private final static int MIN_PORT = 6881;
|
||||
private final static int MAX_PORT = 6889;
|
||||
|
||||
// Error messages (non-fatal)
|
||||
public final static int ERROR = 1;
|
||||
|
||||
// Warning messages
|
||||
public final static int WARNING = 2;
|
||||
|
||||
// Notices (peer level)
|
||||
public final static int NOTICE = 3;
|
||||
|
||||
// Info messages (protocol policy level)
|
||||
public final static int INFO = 4;
|
||||
|
||||
// Debug info (protocol level)
|
||||
public final static int DEBUG = 5;
|
||||
|
||||
// Very low level stuff (network level)
|
||||
public final static int ALL = 6;
|
||||
|
||||
/**
|
||||
* What level of debug info to show.
|
||||
*/
|
||||
public static int debug = NOTICE;
|
||||
|
||||
// Whether or not to ask the user for commands while sharing
|
||||
private static boolean command_interpreter = true;
|
||||
|
||||
private static final String newline = System.getProperty("line.separator");
|
||||
|
||||
private static final String copyright =
|
||||
"The Hunting of the Snark Project - Copyright (C) 2003 Mark J. Wielaard"
|
||||
+ newline + newline
|
||||
+ "Snark comes with ABSOLUTELY NO WARRANTY. This is free software, and"
|
||||
+ newline
|
||||
+ "you are welcome to redistribute it under certain conditions; read the"
|
||||
+ newline
|
||||
+ "COPYING file for details." + newline + newline
|
||||
+ "This is the I2P port, allowing anonymous bittorrent (http://www.i2p.net/)" + newline
|
||||
+ "It will not work with normal torrents, so don't even try ;)";
|
||||
|
||||
private static final String usage =
|
||||
"Press return for help. Type \"quit\" and return to stop.";
|
||||
private static final String help =
|
||||
"Commands: 'info', 'list', 'quit'.";
|
||||
|
||||
// String indicating main activity
|
||||
String activity = "Not started";
|
||||
|
||||
private static class OOMListener implements I2PThread.OOMEventListener {
|
||||
public void outOfMemory(OutOfMemoryError err) {
|
||||
try {
|
||||
err.printStackTrace();
|
||||
I2PSnarkUtil.instance().debug("OOM in the snark", Snark.ERROR, err);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("OOM in the OOM");
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
System.out.println(copyright);
|
||||
System.out.println();
|
||||
|
||||
if ( (args.length > 0) && ("--config".equals(args[0])) ) {
|
||||
I2PThread.addOOMEventListener(new OOMListener());
|
||||
SnarkManager sm = SnarkManager.instance();
|
||||
if (args.length > 1)
|
||||
sm.loadConfig(args[1]);
|
||||
System.out.println("Running in multitorrent mode");
|
||||
while (true) {
|
||||
try {
|
||||
synchronized (sm) {
|
||||
sm.wait();
|
||||
}
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse debug, share/ip and torrent file options.
|
||||
Snark snark = parseArguments(args);
|
||||
|
||||
SnarkShutdown snarkhook
|
||||
= new SnarkShutdown(snark.storage,
|
||||
snark.coordinator,
|
||||
snark.acceptor,
|
||||
snark.trackerclient,
|
||||
snark);
|
||||
//Runtime.getRuntime().addShutdownHook(snarkhook);
|
||||
|
||||
Timer timer = new Timer(true);
|
||||
TimerTask monitor = new PeerMonitorTask(snark.coordinator);
|
||||
timer.schedule(monitor,
|
||||
PeerMonitorTask.MONITOR_PERIOD,
|
||||
PeerMonitorTask.MONITOR_PERIOD);
|
||||
|
||||
// Start command interpreter
|
||||
if (Snark.command_interpreter)
|
||||
{
|
||||
boolean quit = false;
|
||||
|
||||
System.out.println();
|
||||
System.out.println(usage);
|
||||
System.out.println();
|
||||
|
||||
try
|
||||
{
|
||||
BufferedReader br = new BufferedReader
|
||||
(new InputStreamReader(System.in));
|
||||
String line = br.readLine();
|
||||
while(!quit && line != null)
|
||||
{
|
||||
line = line.toLowerCase();
|
||||
if ("quit".equals(line))
|
||||
quit = true;
|
||||
else if ("list".equals(line))
|
||||
{
|
||||
synchronized(snark.coordinator.peers)
|
||||
{
|
||||
System.out.println(snark.coordinator.peers.size()
|
||||
+ " peers -"
|
||||
+ " (i)nterested,"
|
||||
+ " (I)nteresting,"
|
||||
+ " (c)hoking,"
|
||||
+ " (C)hoked:");
|
||||
Iterator it = snark.coordinator.peers.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Peer peer = (Peer)it.next();
|
||||
System.out.println(peer);
|
||||
System.out.println("\ti: " + peer.isInterested()
|
||||
+ " I: " + peer.isInteresting()
|
||||
+ " c: " + peer.isChoking()
|
||||
+ " C: " + peer.isChoked());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ("info".equals(line))
|
||||
{
|
||||
System.out.println("Name: " + snark.meta.getName());
|
||||
System.out.println("Torrent: " + snark.torrent);
|
||||
System.out.println("Tracker: " + snark.meta.getAnnounce());
|
||||
List files = snark.meta.getFiles();
|
||||
System.out.println("Files: "
|
||||
+ ((files == null) ? 1 : files.size()));
|
||||
System.out.println("Pieces: " + snark.meta.getPieces());
|
||||
System.out.println("Piece size: "
|
||||
+ snark.meta.getPieceLength(0) / 1024
|
||||
+ " KB");
|
||||
System.out.println("Total size: "
|
||||
+ snark.meta.getTotalLength() / (1024 * 1024)
|
||||
+ " MB");
|
||||
}
|
||||
else if ("".equals(line) || "help".equals(line))
|
||||
{
|
||||
System.out.println(usage);
|
||||
System.out.println(help);
|
||||
}
|
||||
else
|
||||
{
|
||||
System.out.println("Unknown command: " + line);
|
||||
System.out.println(usage);
|
||||
}
|
||||
|
||||
if (!quit)
|
||||
{
|
||||
System.out.println();
|
||||
line = br.readLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
debug("ERROR while reading stdin: " + ioe, ERROR);
|
||||
}
|
||||
|
||||
// Explicit shutdown.
|
||||
Runtime.getRuntime().removeShutdownHook(snarkhook);
|
||||
snarkhook.start();
|
||||
}
|
||||
}
|
||||
|
||||
public String torrent;
|
||||
public MetaInfo meta;
|
||||
public Storage storage;
|
||||
public PeerCoordinator coordinator;
|
||||
public ConnectionAcceptor acceptor;
|
||||
public TrackerClient trackerclient;
|
||||
public String rootDataDir = ".";
|
||||
public CompleteListener completeListener;
|
||||
public boolean stopped;
|
||||
|
||||
Snark(String torrent, String ip, int user_port,
|
||||
StorageListener slistener, CoordinatorListener clistener) {
|
||||
this(torrent, ip, user_port, slistener, clistener, true, ".");
|
||||
}
|
||||
Snark(String torrent, String ip, int user_port,
|
||||
StorageListener slistener, CoordinatorListener clistener, boolean start, String rootDir)
|
||||
{
|
||||
if (slistener == null)
|
||||
slistener = this;
|
||||
|
||||
if (clistener == null)
|
||||
clistener = this;
|
||||
|
||||
this.torrent = torrent;
|
||||
this.rootDataDir = rootDir;
|
||||
|
||||
stopped = true;
|
||||
activity = "Network setup";
|
||||
|
||||
// "Taking Three as the subject to reason about--
|
||||
// A convenient number to state--
|
||||
// We add Seven, and Ten, and then multiply out
|
||||
// By One Thousand diminished by Eight.
|
||||
//
|
||||
// "The result we proceed to divide, as you see,
|
||||
// By Nine Hundred and Ninety Two:
|
||||
// Then subtract Seventeen, and the answer must be
|
||||
// Exactly and perfectly true.
|
||||
|
||||
// Create a new ID and fill it with something random. First nine
|
||||
// zeros bytes, then three bytes filled with snark and then
|
||||
// sixteen random bytes.
|
||||
byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
|
||||
byte[] id = new byte[20];
|
||||
Random random = new Random();
|
||||
int i;
|
||||
for (i = 0; i < 9; i++)
|
||||
id[i] = 0;
|
||||
id[i++] = snark;
|
||||
id[i++] = snark;
|
||||
id[i++] = snark;
|
||||
while (i < 20)
|
||||
id[i++] = (byte)random.nextInt(256);
|
||||
|
||||
Snark.debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
|
||||
|
||||
int port;
|
||||
IOException lastException = null;
|
||||
boolean ok = I2PSnarkUtil.instance().connect();
|
||||
if (!ok) fatal("Unable to connect to I2P");
|
||||
I2PServerSocket serversocket = I2PSnarkUtil.instance().getServerSocket();
|
||||
if (serversocket == null)
|
||||
fatal("Unable to listen for I2P connections");
|
||||
else {
|
||||
Destination d = serversocket.getManager().getSession().getMyDestination();
|
||||
debug("Listening on I2P destination " + d.toBase64() + " / " + d.calculateHash().toBase64(), NOTICE);
|
||||
}
|
||||
|
||||
// Figure out what the torrent argument represents.
|
||||
meta = null;
|
||||
File f = null;
|
||||
try
|
||||
{
|
||||
InputStream in = null;
|
||||
f = new File(torrent);
|
||||
if (f.exists())
|
||||
in = new FileInputStream(f);
|
||||
else
|
||||
{
|
||||
activity = "Getting torrent";
|
||||
File torrentFile = I2PSnarkUtil.instance().get(torrent);
|
||||
if (torrentFile == null) {
|
||||
fatal("Unable to fetch " + torrent);
|
||||
if (false) return; // never reached - fatal(..) throws
|
||||
} else {
|
||||
torrentFile.deleteOnExit();
|
||||
in = new FileInputStream(torrentFile);
|
||||
}
|
||||
}
|
||||
meta = new MetaInfo(new BDecoder(in));
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
// OK, so it wasn't a torrent metainfo file.
|
||||
if (f != null && f.exists())
|
||||
if (ip == null)
|
||||
fatal("'" + torrent + "' exists,"
|
||||
+ " but is not a valid torrent metainfo file."
|
||||
+ System.getProperty("line.separator"), ioe);
|
||||
else
|
||||
fatal("I2PSnark does not support creating and tracking a torrent at the moment");
|
||||
/*
|
||||
{
|
||||
// Try to create a new metainfo file
|
||||
Snark.debug
|
||||
("Trying to create metainfo torrent for '" + torrent + "'",
|
||||
NOTICE);
|
||||
try
|
||||
{
|
||||
activity = "Creating torrent";
|
||||
storage = new Storage
|
||||
(f, "http://" + ip + ":" + port + "/announce", slistener);
|
||||
storage.create();
|
||||
meta = storage.getMetaInfo();
|
||||
}
|
||||
catch (IOException ioe2)
|
||||
{
|
||||
fatal("Could not create torrent for '" + torrent + "'", ioe2);
|
||||
}
|
||||
}
|
||||
*/
|
||||
else
|
||||
fatal("Cannot open '" + torrent + "'", ioe);
|
||||
}
|
||||
|
||||
debug(meta.toString(), INFO);
|
||||
|
||||
// When the metainfo torrent was created from an existing file/dir
|
||||
// it already exists.
|
||||
if (storage == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
activity = "Checking storage";
|
||||
storage = new Storage(meta, slistener);
|
||||
storage.check(rootDataDir);
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
try { storage.close(); } catch (IOException ioee) {
|
||||
ioee.printStackTrace();
|
||||
}
|
||||
fatal("Could not create storage", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
activity = "Collecting pieces";
|
||||
coordinator = new PeerCoordinator(id, meta, storage, clistener, this);
|
||||
PeerCoordinatorSet set = PeerCoordinatorSet.instance();
|
||||
set.add(coordinator);
|
||||
ConnectionAcceptor acceptor = ConnectionAcceptor.instance();
|
||||
acceptor.startAccepting(set, serversocket);
|
||||
|
||||
trackerclient = new TrackerClient(meta, coordinator);
|
||||
if (start)
|
||||
startTorrent();
|
||||
}
|
||||
/**
|
||||
* Start up contacting peers and querying the tracker
|
||||
*/
|
||||
public void startTorrent() {
|
||||
stopped = false;
|
||||
boolean coordinatorChanged = false;
|
||||
if (coordinator.halted()) {
|
||||
// ok, we have already started and stopped, but the coordinator seems a bit annoying to
|
||||
// restart safely, so lets build a new one to replace the old
|
||||
PeerCoordinatorSet set = PeerCoordinatorSet.instance();
|
||||
set.remove(coordinator);
|
||||
PeerCoordinator newCoord = new PeerCoordinator(coordinator.getID(), coordinator.getMetaInfo(),
|
||||
coordinator.getStorage(), coordinator.getListener(), this);
|
||||
set.add(newCoord);
|
||||
coordinator = newCoord;
|
||||
coordinatorChanged = true;
|
||||
}
|
||||
if (!trackerclient.started() && !coordinatorChanged) {
|
||||
trackerclient.start();
|
||||
} else if (trackerclient.halted() || coordinatorChanged) {
|
||||
TrackerClient newClient = new TrackerClient(coordinator.getMetaInfo(), coordinator);
|
||||
if (!trackerclient.halted())
|
||||
trackerclient.halt();
|
||||
trackerclient = newClient;
|
||||
trackerclient.start();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Stop contacting the tracker and talking with peers
|
||||
*/
|
||||
public void stopTorrent() {
|
||||
stopped = true;
|
||||
TrackerClient tc = trackerclient;
|
||||
if (tc != null)
|
||||
tc.halt();
|
||||
PeerCoordinator pc = coordinator;
|
||||
if (pc != null)
|
||||
pc.halt();
|
||||
Storage st = storage;
|
||||
if (st != null) {
|
||||
try {
|
||||
storage.close();
|
||||
} catch (IOException ioe) {
|
||||
System.out.println("Error closing " + torrent);
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (pc != null)
|
||||
PeerCoordinatorSet.instance().remove(pc);
|
||||
}
|
||||
|
||||
static Snark parseArguments(String[] args)
|
||||
{
|
||||
return parseArguments(args, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets debug, ip and torrent variables then creates a Snark
|
||||
* instance. Calls usage(), which terminates the program, if
|
||||
* non-valid argument list. The given listeners will be
|
||||
* passed to all components that take one.
|
||||
*/
|
||||
static Snark parseArguments(String[] args,
|
||||
StorageListener slistener,
|
||||
CoordinatorListener clistener)
|
||||
{
|
||||
int user_port = -1;
|
||||
String ip = null;
|
||||
String torrent = null;
|
||||
|
||||
boolean configured = I2PSnarkUtil.instance().configured();
|
||||
|
||||
int i = 0;
|
||||
while (i < args.length)
|
||||
{
|
||||
if (args[i].equals("--debug"))
|
||||
{
|
||||
debug = INFO;
|
||||
i++;
|
||||
|
||||
// Try if there is an level argument.
|
||||
if (i < args.length)
|
||||
{
|
||||
try
|
||||
{
|
||||
int level = Integer.parseInt(args[i]);
|
||||
if (level >= 0)
|
||||
{
|
||||
debug = level;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException nfe) { }
|
||||
}
|
||||
}
|
||||
else if (args[i].equals("--port"))
|
||||
{
|
||||
if (args.length - 1 < i + 1)
|
||||
usage("--port needs port number to listen on");
|
||||
try
|
||||
{
|
||||
user_port = Integer.parseInt(args[i + 1]);
|
||||
}
|
||||
catch (NumberFormatException nfe)
|
||||
{
|
||||
usage("--port argument must be a number (" + nfe + ")");
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
else if (args[i].equals("--no-commands"))
|
||||
{
|
||||
command_interpreter = false;
|
||||
i++;
|
||||
}
|
||||
else if (args[i].equals("--eepproxy"))
|
||||
{
|
||||
String proxyHost = args[i+1];
|
||||
String proxyPort = args[i+2];
|
||||
if (!configured)
|
||||
I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort));
|
||||
i += 3;
|
||||
}
|
||||
else if (args[i].equals("--i2cp"))
|
||||
{
|
||||
String i2cpHost = args[i+1];
|
||||
String i2cpPort = args[i+2];
|
||||
Properties opts = null;
|
||||
if (i+3 < args.length) {
|
||||
if (!args[i+3].startsWith("--")) {
|
||||
opts = new Properties();
|
||||
StringTokenizer tok = new StringTokenizer(args[i+3], " \t");
|
||||
while (tok.hasMoreTokens()) {
|
||||
String str = tok.nextToken();
|
||||
int split = str.indexOf('=');
|
||||
if (split > 0) {
|
||||
opts.setProperty(str.substring(0, split), str.substring(split+1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!configured)
|
||||
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts);
|
||||
i += 3 + (opts != null ? 1 : 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
torrent = args[i];
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent == null || i != args.length)
|
||||
if (torrent != null && torrent.startsWith("-"))
|
||||
usage("Unknow option '" + torrent + "'.");
|
||||
else
|
||||
usage("Need exactly one <url>, <file> or <dir>.");
|
||||
|
||||
return new Snark(torrent, ip, user_port, slistener, clistener);
|
||||
}
|
||||
|
||||
private static void usage(String s)
|
||||
{
|
||||
System.out.println("snark: " + s);
|
||||
usage();
|
||||
}
|
||||
|
||||
private static void usage()
|
||||
{
|
||||
System.out.println
|
||||
("Usage: snark [--debug [level]] [--no-commands] [--port <port>]");
|
||||
System.out.println
|
||||
(" [--eepproxy hostname portnum]");
|
||||
System.out.println
|
||||
(" [--i2cp routerHost routerPort ['name=val name=val name=val']]");
|
||||
System.out.println
|
||||
(" (<url>|<file>)");
|
||||
System.out.println
|
||||
(" --debug\tShows some extra info and stacktraces");
|
||||
System.out.println
|
||||
(" level\tHow much debug details to show");
|
||||
System.out.println
|
||||
(" \t(defaults to "
|
||||
+ NOTICE + ", with --debug to "
|
||||
+ INFO + ", highest level is "
|
||||
+ ALL + ").");
|
||||
System.out.println
|
||||
(" --no-commands\tDon't read interactive commands or show usage info.");
|
||||
System.out.println
|
||||
(" --port\tThe port to listen on for incomming connections");
|
||||
System.out.println
|
||||
(" \t(if not given defaults to first free port between "
|
||||
+ MIN_PORT + "-" + MAX_PORT + ").");
|
||||
System.out.println
|
||||
(" --share\tStart torrent tracker on <ip> address or <host> name.");
|
||||
System.out.println
|
||||
(" --eepproxy\thttp proxy to use (default of 127.0.0.1 port 4444)");
|
||||
System.out.println
|
||||
(" --i2cp\tlocation of your I2P router (default of 127.0.0.1 port 7654)");
|
||||
System.out.println
|
||||
(" \toptional settings may be included, such as");
|
||||
System.out.println
|
||||
(" \tinbound.length=2 outbound.length=2 inbound.lengthVariance=-1 ");
|
||||
System.out.println
|
||||
(" <url> \tURL pointing to .torrent metainfo file to download/share.");
|
||||
System.out.println
|
||||
(" <file> \tEither a local .torrent metainfo file to download");
|
||||
System.out.println
|
||||
(" \tor (with --share) a file to share.");
|
||||
System.exit(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts program abnormally.
|
||||
*/
|
||||
public void fatal(String s)
|
||||
{
|
||||
fatal(s, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts program abnormally.
|
||||
*/
|
||||
public void fatal(String s, Throwable t)
|
||||
{
|
||||
I2PSnarkUtil.instance().debug(s, ERROR, t);
|
||||
//System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
|
||||
//if (debug >= INFO && t != null)
|
||||
// t.printStackTrace();
|
||||
stopTorrent();
|
||||
throw new RuntimeException("die bart die");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show debug info if debug is true.
|
||||
*/
|
||||
public static void debug(String s, int level)
|
||||
{
|
||||
I2PSnarkUtil.instance().debug(s, level, null);
|
||||
//if (debug >= level)
|
||||
// System.out.println(s);
|
||||
}
|
||||
|
||||
public void peerChange(PeerCoordinator coordinator, Peer peer)
|
||||
{
|
||||
// System.out.println(peer.toString());
|
||||
}
|
||||
|
||||
boolean allocating = false;
|
||||
public void storageCreateFile(Storage storage, String name, long length)
|
||||
{
|
||||
//if (allocating)
|
||||
// System.out.println(); // Done with last file.
|
||||
|
||||
//System.out.print("Creating file '" + name
|
||||
// + "' of length " + length + ": ");
|
||||
allocating = true;
|
||||
}
|
||||
|
||||
// How much storage space has been allocated
|
||||
private long allocated = 0;
|
||||
|
||||
public void storageAllocated(Storage storage, long length)
|
||||
{
|
||||
allocating = true;
|
||||
//System.out.print(".");
|
||||
allocated += length;
|
||||
//if (allocated == meta.getTotalLength())
|
||||
// System.out.println(); // We have all the disk space we need.
|
||||
}
|
||||
|
||||
boolean allChecked = false;
|
||||
boolean checking = false;
|
||||
boolean prechecking = true;
|
||||
public void storageChecked(Storage storage, int num, boolean checked)
|
||||
{
|
||||
allocating = false;
|
||||
if (!allChecked && !checking)
|
||||
{
|
||||
// Use the MetaInfo from the storage since our own might not
|
||||
// yet be setup correctly.
|
||||
MetaInfo meta = storage.getMetaInfo();
|
||||
//if (meta != null)
|
||||
// System.out.print("Checking existing "
|
||||
// + meta.getPieces()
|
||||
// + " pieces: ");
|
||||
checking = true;
|
||||
}
|
||||
if (!checking)
|
||||
Snark.debug("Got " + (checked ? "" : "BAD ") + "piece: " + num,
|
||||
Snark.INFO);
|
||||
}
|
||||
|
||||
public void storageAllChecked(Storage storage)
|
||||
{
|
||||
//if (checking)
|
||||
// System.out.println();
|
||||
|
||||
allChecked = true;
|
||||
checking = false;
|
||||
}
|
||||
|
||||
public void storageCompleted(Storage storage)
|
||||
{
|
||||
Snark.debug("Completely received " + torrent, Snark.INFO);
|
||||
//storage.close();
|
||||
//System.out.println("Completely received: " + torrent);
|
||||
if (completeListener != null)
|
||||
completeListener.torrentComplete(this);
|
||||
}
|
||||
|
||||
public void shutdown()
|
||||
{
|
||||
// Should not be necessary since all non-deamon threads should
|
||||
// have died. But in reality this does not always happen.
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public interface CompleteListener {
|
||||
public void torrentComplete(Snark snark);
|
||||
}
|
||||
}
|
||||
506
apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
Normal file
506
apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
Normal file
@@ -0,0 +1,506 @@
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Manage multiple snarks
|
||||
*/
|
||||
public class SnarkManager implements Snark.CompleteListener {
|
||||
private static SnarkManager _instance = new SnarkManager();
|
||||
public static SnarkManager instance() { return _instance; }
|
||||
|
||||
/** map of (canonical) filename to Snark instance (unsynchronized) */
|
||||
private Map _snarks;
|
||||
private String _configFile;
|
||||
private Properties _config;
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private List _messages;
|
||||
|
||||
public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost";
|
||||
public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort";
|
||||
public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions";
|
||||
public static final String PROP_EEP_HOST = "i2psnark.eepHost";
|
||||
public static final String PROP_EEP_PORT = "i2psnark.eepPort";
|
||||
public static final String PROP_DIR = "i2psnark.dir";
|
||||
|
||||
public static final String PROP_AUTO_START = "i2snark.autoStart";
|
||||
public static final String DEFAULT_AUTO_START = "false";
|
||||
|
||||
private SnarkManager() {
|
||||
_snarks = new HashMap();
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_log = _context.logManager().getLog(SnarkManager.class);
|
||||
_messages = new ArrayList(16);
|
||||
loadConfig("i2psnark.config");
|
||||
int minutes = getStartupDelayMinutes();
|
||||
_messages.add("Starting up torrents in " + minutes + (minutes == 1 ? " minute" : " minutes"));
|
||||
I2PThread monitor = new I2PThread(new DirMonitor(), "Snark DirMonitor");
|
||||
monitor.setDaemon(true);
|
||||
monitor.start();
|
||||
}
|
||||
|
||||
private static final int MAX_MESSAGES = 5;
|
||||
public void addMessage(String message) {
|
||||
synchronized (_messages) {
|
||||
_messages.add(message);
|
||||
while (_messages.size() > MAX_MESSAGES)
|
||||
_messages.remove(0);
|
||||
}
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("MSG: " + message);
|
||||
}
|
||||
|
||||
/** newest last */
|
||||
public List getMessages() {
|
||||
synchronized (_messages) {
|
||||
return new ArrayList(_messages);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldAutoStart() {
|
||||
return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue();
|
||||
}
|
||||
private int getStartupDelayMinutes() { return 1; }
|
||||
public File getDataDir() {
|
||||
String dir = _config.getProperty(PROP_DIR);
|
||||
if ( (dir == null) || (dir.trim().length() <= 0) )
|
||||
dir = "i2psnark";
|
||||
return new File(dir);
|
||||
}
|
||||
|
||||
public void loadConfig(String filename) {
|
||||
_configFile = filename;
|
||||
if (_config == null)
|
||||
_config = new Properties();
|
||||
File cfg = new File(filename);
|
||||
if (cfg.exists()) {
|
||||
try {
|
||||
DataHelper.loadProps(_config, cfg);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error loading I2PSnark config '" + filename + "'", ioe);
|
||||
}
|
||||
}
|
||||
// now add sane defaults
|
||||
if (!_config.containsKey(PROP_I2CP_HOST))
|
||||
_config.setProperty(PROP_I2CP_HOST, "localhost");
|
||||
if (!_config.containsKey(PROP_I2CP_PORT))
|
||||
_config.setProperty(PROP_I2CP_PORT, "7654");
|
||||
if (!_config.containsKey(PROP_EEP_HOST))
|
||||
_config.setProperty(PROP_EEP_HOST, "localhost");
|
||||
if (!_config.containsKey(PROP_EEP_PORT))
|
||||
_config.setProperty(PROP_EEP_PORT, "4444");
|
||||
if (!_config.containsKey(PROP_DIR))
|
||||
_config.setProperty(PROP_DIR, "i2psnark");
|
||||
if (!_config.containsKey(PROP_AUTO_START))
|
||||
_config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START);
|
||||
updateConfig();
|
||||
}
|
||||
|
||||
private void updateConfig() {
|
||||
String i2cpHost = _config.getProperty(PROP_I2CP_HOST);
|
||||
int i2cpPort = getInt(PROP_I2CP_PORT, 7654);
|
||||
String opts = _config.getProperty(PROP_I2CP_OPTS);
|
||||
Map i2cpOpts = new HashMap();
|
||||
if (opts != null) {
|
||||
StringTokenizer tok = new StringTokenizer(opts, " ");
|
||||
while (tok.hasMoreTokens()) {
|
||||
String pair = tok.nextToken();
|
||||
int split = pair.indexOf('=');
|
||||
if (split > 0)
|
||||
i2cpOpts.put(pair.substring(0, split), pair.substring(split+1));
|
||||
}
|
||||
}
|
||||
if (i2cpHost != null) {
|
||||
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
|
||||
_log.debug("Configuring with I2CP options " + i2cpOpts);
|
||||
}
|
||||
//I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties());
|
||||
String eepHost = _config.getProperty(PROP_EEP_HOST);
|
||||
int eepPort = getInt(PROP_EEP_PORT, 4444);
|
||||
if (eepHost != null)
|
||||
I2PSnarkUtil.instance().setProxy(eepHost, eepPort);
|
||||
getDataDir().mkdirs();
|
||||
}
|
||||
|
||||
private int getInt(String prop, int defaultVal) {
|
||||
String p = _config.getProperty(prop);
|
||||
try {
|
||||
if ( (p != null) && (p.trim().length() > 0) )
|
||||
return Integer.parseInt(p.trim());
|
||||
} catch (NumberFormatException nfe) {
|
||||
// ignore
|
||||
}
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
public void updateConfig(String dataDir, boolean autoStart, String seedPct, String eepHost,
|
||||
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts) {
|
||||
boolean changed = false;
|
||||
if (eepHost != null) {
|
||||
int port = I2PSnarkUtil.instance().getEepProxyPort();
|
||||
try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {}
|
||||
String host = I2PSnarkUtil.instance().getEepProxyHost();
|
||||
if ( (eepHost.trim().length() > 0) && (port > 0) &&
|
||||
((!host.equals(eepHost) || (port != I2PSnarkUtil.instance().getEepProxyPort()) )) ) {
|
||||
I2PSnarkUtil.instance().setProxy(eepHost, port);
|
||||
changed = true;
|
||||
_config.setProperty(PROP_EEP_HOST, eepHost);
|
||||
_config.setProperty(PROP_EEP_PORT, eepPort+"");
|
||||
addMessage("EepProxy location changed to " + eepHost + ":" + port);
|
||||
}
|
||||
}
|
||||
if (i2cpHost != null) {
|
||||
int oldI2CPPort = I2PSnarkUtil.instance().getI2CPPort();
|
||||
String oldI2CPHost = I2PSnarkUtil.instance().getI2CPHost();
|
||||
int port = oldI2CPPort;
|
||||
try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
|
||||
String host = oldI2CPHost;
|
||||
Map opts = new HashMap();
|
||||
if (i2cpOpts == null) i2cpOpts = "";
|
||||
StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n");
|
||||
while (tok.hasMoreTokens()) {
|
||||
String pair = tok.nextToken();
|
||||
int split = pair.indexOf('=');
|
||||
if (split > 0)
|
||||
opts.put(pair.substring(0, split), pair.substring(split+1));
|
||||
}
|
||||
Map oldOpts = new HashMap();
|
||||
String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS);
|
||||
if (oldI2CPOpts == null) oldI2CPOpts = "";
|
||||
tok = new StringTokenizer(oldI2CPOpts, " \t\n");
|
||||
while (tok.hasMoreTokens()) {
|
||||
String pair = tok.nextToken();
|
||||
int split = pair.indexOf('=');
|
||||
if (split > 0)
|
||||
oldOpts.put(pair.substring(0, split), pair.substring(split+1));
|
||||
}
|
||||
|
||||
if ( (i2cpHost.trim().length() > 0) && (port > 0) &&
|
||||
((!host.equals(i2cpHost) ||
|
||||
(port != I2PSnarkUtil.instance().getI2CPPort()) ||
|
||||
(!oldOpts.equals(opts)))) ) {
|
||||
boolean snarksActive = false;
|
||||
Set names = listTorrentFiles();
|
||||
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
||||
Snark snark = getTorrent((String)iter.next());
|
||||
if ( (snark != null) && (!snark.stopped) ) {
|
||||
snarksActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (snarksActive) {
|
||||
addMessage("Cannot change the I2CP settings while torrents are active");
|
||||
_log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts
|
||||
+ "] oldOpts [" + oldOpts + "]");
|
||||
} else {
|
||||
if (I2PSnarkUtil.instance().connected()) {
|
||||
I2PSnarkUtil.instance().disconnect();
|
||||
addMessage("Disconnecting old I2CP destination");
|
||||
}
|
||||
Properties p = new Properties();
|
||||
p.putAll(opts);
|
||||
addMessage("I2CP settings changed to " + i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")");
|
||||
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, port, p);
|
||||
boolean ok = I2PSnarkUtil.instance().connect();
|
||||
if (!ok) {
|
||||
addMessage("Unable to connect with the new settings, reverting to the old I2CP settings");
|
||||
I2PSnarkUtil.instance().setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts);
|
||||
ok = I2PSnarkUtil.instance().connect();
|
||||
if (!ok)
|
||||
addMessage("Unable to reconnect with the old settings!");
|
||||
} else {
|
||||
addMessage("Reconnected on the new I2CP destination");
|
||||
_config.setProperty(PROP_I2CP_HOST, i2cpHost.trim());
|
||||
_config.setProperty(PROP_I2CP_PORT, "" + port);
|
||||
_config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim());
|
||||
changed = true;
|
||||
// no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive
|
||||
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
Snark snark = getTorrent(name);
|
||||
if ( (snark != null) && (snark.acceptor != null) ) {
|
||||
snark.acceptor.restart();
|
||||
addMessage("I2CP listener restarted for " + snark.meta.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (shouldAutoStart() != autoStart) {
|
||||
_config.setProperty(PROP_AUTO_START, autoStart + "");
|
||||
addMessage("Adjusted autostart to " + autoStart);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
saveConfig();
|
||||
} else {
|
||||
addMessage("Configuration unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
public void saveConfig() {
|
||||
try {
|
||||
DataHelper.storeProps(_config, new File(_configFile));
|
||||
} catch (IOException ioe) {
|
||||
addMessage("Unable to save the config to '" + _configFile + "'");
|
||||
}
|
||||
}
|
||||
|
||||
public Properties getConfig() { return _config; }
|
||||
|
||||
/** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
|
||||
private static final int MAX_FILES_PER_TORRENT = 128;
|
||||
|
||||
/** set of filenames that we are dealing with */
|
||||
public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } }
|
||||
/**
|
||||
* Grab the torrent given the (canonical) filename
|
||||
*/
|
||||
public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } }
|
||||
public void addTorrent(String filename) { addTorrent(filename, false); }
|
||||
public void addTorrent(String filename, boolean dontAutoStart) {
|
||||
if (!I2PSnarkUtil.instance().connected()) {
|
||||
addMessage("Connecting to I2P");
|
||||
boolean ok = I2PSnarkUtil.instance().connect();
|
||||
if (!ok) {
|
||||
addMessage("Error connecting to I2P - check your I2CP settings");
|
||||
return;
|
||||
}
|
||||
}
|
||||
File sfile = new File(filename);
|
||||
try {
|
||||
filename = sfile.getCanonicalPath();
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Unable to add the torrent " + filename, ioe);
|
||||
addMessage("ERR: Could not add the torrent '" + filename + "': " + ioe.getMessage());
|
||||
return;
|
||||
}
|
||||
File dataDir = getDataDir();
|
||||
Snark torrent = null;
|
||||
synchronized (_snarks) {
|
||||
torrent = (Snark)_snarks.get(filename);
|
||||
if (torrent == null) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(sfile);
|
||||
MetaInfo info = new MetaInfo(fis);
|
||||
fis.close();
|
||||
fis = null;
|
||||
|
||||
String rejectMessage = locked_validateTorrent(info);
|
||||
if (rejectMessage != null) {
|
||||
sfile.delete();
|
||||
addMessage(rejectMessage);
|
||||
return;
|
||||
} else {
|
||||
torrent = new Snark(filename, null, -1, null, null, false, dataDir.getPath());
|
||||
torrent.completeListener = this;
|
||||
_snarks.put(filename, torrent);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
addMessage("Torrent in " + sfile.getName() + " is invalid: " + ioe.getMessage());
|
||||
if (sfile.exists())
|
||||
sfile.delete();
|
||||
return;
|
||||
} finally {
|
||||
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ok, snark created, now lets start it up or configure it further
|
||||
File f = new File(filename);
|
||||
if (!dontAutoStart && shouldAutoStart()) {
|
||||
torrent.startTorrent();
|
||||
addMessage("Torrent added and started: '" + f.getName() + "'");
|
||||
} else {
|
||||
addMessage("Torrent added: '" + f.getName() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private String locked_validateTorrent(MetaInfo info) throws IOException {
|
||||
List files = info.getFiles();
|
||||
if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
|
||||
return "Too many files in " + info.getName() + " (" + files.size() + "), deleting it";
|
||||
} else if (info.getPieces() <= 0) {
|
||||
return "No pieces in " + info.getName() + "? deleting it";
|
||||
} else if (info.getPieceLength(0) > 10*1024*1024) {
|
||||
return "Pieces are too large in " + info.getName() + " (" + info.getPieceLength(0)/1024 + "KB, deleting it";
|
||||
} else if (info.getTotalLength() > 10*1024*1024*1024l) {
|
||||
System.out.println("torrent info: " + info.toString());
|
||||
List lengths = info.getLengths();
|
||||
if (lengths != null)
|
||||
for (int i = 0; i < lengths.size(); i++)
|
||||
System.out.println("File " + i + " is " + lengths.get(i) + " long");
|
||||
|
||||
return "Torrents larger than 10GB are not supported yet (because we're paranoid): " + info.getName() + ", deleting it";
|
||||
} else {
|
||||
// ok
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the torrent, leaving it on the list of torrents unless told to remove it
|
||||
*/
|
||||
public Snark stopTorrent(String filename, boolean shouldRemove) {
|
||||
File sfile = new File(filename);
|
||||
try {
|
||||
filename = sfile.getCanonicalPath();
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Unable to remove the torrent " + filename, ioe);
|
||||
addMessage("ERR: Could not remove the torrent '" + filename + "': " + ioe.getMessage());
|
||||
return null;
|
||||
}
|
||||
int remaining = 0;
|
||||
Snark torrent = null;
|
||||
synchronized (_snarks) {
|
||||
if (shouldRemove)
|
||||
torrent = (Snark)_snarks.remove(filename);
|
||||
else
|
||||
torrent = (Snark)_snarks.get(filename);
|
||||
remaining = _snarks.size();
|
||||
}
|
||||
if (torrent != null) {
|
||||
boolean wasStopped = torrent.stopped;
|
||||
torrent.stopTorrent();
|
||||
if (remaining == 0) {
|
||||
// should we disconnect/reconnect here (taking care to deal with the other thread's
|
||||
// I2PServerSocket.accept() call properly?)
|
||||
////I2PSnarkUtil.instance().
|
||||
}
|
||||
if (!wasStopped)
|
||||
addMessage("Torrent stopped: '" + sfile.getName() + "'");
|
||||
}
|
||||
return torrent;
|
||||
}
|
||||
/**
|
||||
* Stop the torrent and delete the torrent file itself, but leaving the data
|
||||
* behind.
|
||||
*/
|
||||
public void removeTorrent(String filename) {
|
||||
Snark torrent = stopTorrent(filename, true);
|
||||
if (torrent != null) {
|
||||
File torrentFile = new File(filename);
|
||||
torrentFile.delete();
|
||||
addMessage("Torrent removed: '" + torrentFile.getName() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private class DirMonitor implements Runnable {
|
||||
public void run() {
|
||||
try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {}
|
||||
// the first message was a "We are starting up in 1m"
|
||||
synchronized (_messages) {
|
||||
if (_messages.size() == 1)
|
||||
_messages.remove(0);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
File dir = getDataDir();
|
||||
_log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
|
||||
try {
|
||||
monitorTorrents(dir);
|
||||
} catch (Exception e) {
|
||||
_log.error("Error in the DirectoryMonitor", e);
|
||||
}
|
||||
try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void torrentComplete(Snark snark) {
|
||||
File f = new File(snark.torrent);
|
||||
long len = snark.meta.getTotalLength();
|
||||
addMessage("Download complete of " + f.getName()
|
||||
+ (len < 5*1024*1024 ? " (size: " + (len/1024) + "KB)" : " (size: " + (len/(1024*1024l)) + "MB)"));
|
||||
}
|
||||
|
||||
private void monitorTorrents(File dir) {
|
||||
String fileNames[] = dir.list(TorrentFilenameFilter.instance());
|
||||
List foundNames = new ArrayList(0);
|
||||
if (fileNames != null) {
|
||||
for (int i = 0; i < fileNames.length; i++) {
|
||||
try {
|
||||
foundNames.add(new File(dir, fileNames[i]).getCanonicalPath());
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set existingNames = listTorrentFiles();
|
||||
// lets find new ones first...
|
||||
for (int i = 0; i < foundNames.size(); i++) {
|
||||
if (existingNames.contains(foundNames.get(i))) {
|
||||
// already known. noop
|
||||
} else {
|
||||
if (I2PSnarkUtil.instance().connect())
|
||||
addTorrent((String)foundNames.get(i));
|
||||
else
|
||||
addMessage("Unable to connect to I2P");
|
||||
}
|
||||
}
|
||||
// now lets see which ones have been removed...
|
||||
for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
if (foundNames.contains(name)) {
|
||||
// known and still there. noop
|
||||
} else {
|
||||
// known, but removed. drop it
|
||||
stopTorrent(name, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String DEFAULT_TRACKERS[] = {
|
||||
"Postman's tracker", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php"
|
||||
, "Orion's tracker", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php"
|
||||
// , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php"
|
||||
};
|
||||
|
||||
/** comma delimited list of name=announceURL for the trackers to be displayed */
|
||||
public static final String PROP_TRACKERS = "i2psnark.trackers";
|
||||
/** unordered map of announceURL to name */
|
||||
public Map getTrackers() {
|
||||
HashMap rv = new HashMap();
|
||||
String trackers = _config.getProperty(PROP_TRACKERS);
|
||||
if ( (trackers == null) || (trackers.trim().length() <= 0) )
|
||||
trackers = _context.getProperty(PROP_TRACKERS);
|
||||
if ( (trackers == null) || (trackers.trim().length() <= 0) ) {
|
||||
for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2)
|
||||
rv.put(DEFAULT_TRACKERS[i+1], DEFAULT_TRACKERS[i]);
|
||||
} else {
|
||||
StringTokenizer tok = new StringTokenizer(trackers, ",");
|
||||
while (tok.hasMoreTokens()) {
|
||||
String pair = tok.nextToken();
|
||||
int split = pair.indexOf('=');
|
||||
if (split <= 0)
|
||||
continue;
|
||||
String name = pair.substring(0, split).trim();
|
||||
String url = pair.substring(split+1).trim();
|
||||
if ( (name.length() > 0) && (url.length() > 0) )
|
||||
rv.put(url, name);
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static class TorrentFilenameFilter implements FilenameFilter {
|
||||
private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter();
|
||||
public static TorrentFilenameFilter instance() { return _filter; }
|
||||
public boolean accept(File dir, String name) {
|
||||
return (name != null) && (name.endsWith(".torrent"));
|
||||
}
|
||||
}
|
||||
}
|
||||
92
apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java
Normal file
92
apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java
Normal file
@@ -0,0 +1,92 @@
|
||||
/* TrackerShutdown - Makes sure everything ends correctly when shutting down.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import net.i2p.util.I2PThread;
|
||||
|
||||
/**
|
||||
* Makes sure everything ends correctly when shutting down.
|
||||
*/
|
||||
public class SnarkShutdown extends I2PThread
|
||||
{
|
||||
private final Storage storage;
|
||||
private final PeerCoordinator coordinator;
|
||||
private final ConnectionAcceptor acceptor;
|
||||
private final TrackerClient trackerclient;
|
||||
|
||||
private final ShutdownListener listener;
|
||||
|
||||
public SnarkShutdown(Storage storage,
|
||||
PeerCoordinator coordinator,
|
||||
ConnectionAcceptor acceptor,
|
||||
TrackerClient trackerclient,
|
||||
ShutdownListener listener)
|
||||
{
|
||||
this.storage = storage;
|
||||
this.coordinator = coordinator;
|
||||
this.acceptor = acceptor;
|
||||
this.trackerclient = trackerclient;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
Snark.debug("Shutting down...", Snark.NOTICE);
|
||||
|
||||
Snark.debug("Halting ConnectionAcceptor...", Snark.INFO);
|
||||
if (acceptor != null)
|
||||
acceptor.halt();
|
||||
|
||||
Snark.debug("Halting TrackerClient...", Snark.INFO);
|
||||
if (trackerclient != null)
|
||||
trackerclient.halt();
|
||||
|
||||
Snark.debug("Halting PeerCoordinator...", Snark.INFO);
|
||||
if (coordinator != null)
|
||||
coordinator.halt();
|
||||
|
||||
Snark.debug("Closing Storage...", Snark.INFO);
|
||||
if (storage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
storage.close();
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
I2PSnarkUtil.instance().debug("Couldn't properly close storage", Snark.ERROR, ioe);
|
||||
throw new RuntimeException("b0rking");
|
||||
}
|
||||
}
|
||||
|
||||
// XXX - Should actually wait till done...
|
||||
try
|
||||
{
|
||||
Snark.debug("Waiting 5 seconds...", Snark.INFO);
|
||||
Thread.sleep(5*1000);
|
||||
}
|
||||
catch (InterruptedException ie) { /* ignored */ }
|
||||
|
||||
listener.shutdown();
|
||||
}
|
||||
}
|
||||
47
apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
Normal file
47
apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java
Normal file
@@ -0,0 +1,47 @@
|
||||
/* StaticSnark - Main snark startup class for staticly linking with gcj.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
|
||||
/**
|
||||
* Main snark startup class for staticly linking with gcj.
|
||||
* It references somee necessary classes that are normally loaded through
|
||||
* reflection.
|
||||
*
|
||||
* @author Mark Wielaard (mark@klomp.org)
|
||||
*/
|
||||
public class StaticSnark
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// The GNU security provider is needed for SHA-1 MessageDigest checking.
|
||||
// So make sure it is available as a security provider.
|
||||
//Provider gnu = new gnu.java.security.provider.Gnu();
|
||||
//Security.addProvider(gnu);
|
||||
|
||||
// And finally call the normal starting point.
|
||||
Snark.main(args);
|
||||
}
|
||||
}
|
||||
588
apps/i2psnark/java/src/org/klomp/snark/Storage.java
Normal file
588
apps/i2psnark/java/src/org/klomp/snark/Storage.java
Normal file
@@ -0,0 +1,588 @@
|
||||
/* Storage - Class used to store and retrieve pieces.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import net.i2p.crypto.SHA1;
|
||||
|
||||
/**
|
||||
* Maintains pieces on disk. Can be used to store and retrieve pieces.
|
||||
*/
|
||||
public class Storage
|
||||
{
|
||||
private MetaInfo metainfo;
|
||||
private long[] lengths;
|
||||
private RandomAccessFile[] rafs;
|
||||
private String[] names;
|
||||
|
||||
private final StorageListener listener;
|
||||
|
||||
private final BitField bitfield; // BitField to represent the pieces
|
||||
private int needed; // Number of pieces needed
|
||||
|
||||
// XXX - Not always set correctly
|
||||
int piece_size;
|
||||
int pieces;
|
||||
|
||||
/** The default piece size. */
|
||||
private static int MIN_PIECE_SIZE = 256*1024;
|
||||
/** The maximum number of pieces in a torrent. */
|
||||
private static long MAX_PIECES = 100*1024/20;
|
||||
|
||||
/**
|
||||
* Creates a new storage based on the supplied MetaInfo. This will
|
||||
* try to create and/or check all needed files in the MetaInfo.
|
||||
*
|
||||
* @exception IOException when creating and/or checking files fails.
|
||||
*/
|
||||
public Storage(MetaInfo metainfo, StorageListener listener)
|
||||
throws IOException
|
||||
{
|
||||
this.metainfo = metainfo;
|
||||
this.listener = listener;
|
||||
needed = metainfo.getPieces();
|
||||
bitfield = new BitField(needed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a storage from the existing file or directory together
|
||||
* with an appropriate MetaInfo file as can be announced on the
|
||||
* given announce String location.
|
||||
*/
|
||||
public Storage(File baseFile, String announce, StorageListener listener)
|
||||
throws IOException
|
||||
{
|
||||
this.listener = listener;
|
||||
|
||||
// Create names, rafs and lengths arrays.
|
||||
getFiles(baseFile);
|
||||
|
||||
long total = 0;
|
||||
ArrayList lengthsList = new ArrayList();
|
||||
for (int i = 0; i < lengths.length; i++)
|
||||
{
|
||||
long length = lengths[i];
|
||||
total += length;
|
||||
lengthsList.add(new Long(length));
|
||||
}
|
||||
|
||||
piece_size = MIN_PIECE_SIZE;
|
||||
pieces = (int) ((total - 1)/piece_size) + 1;
|
||||
while (pieces > MAX_PIECES)
|
||||
{
|
||||
piece_size = piece_size*2;
|
||||
pieces = (int) ((total - 1)/piece_size) +1;
|
||||
}
|
||||
|
||||
// Note that piece_hashes and the bitfield will be filled after
|
||||
// the MetaInfo is created.
|
||||
byte[] piece_hashes = new byte[20*pieces];
|
||||
bitfield = new BitField(pieces);
|
||||
needed = 0;
|
||||
|
||||
List files = new ArrayList();
|
||||
for (int i = 0; i < names.length; i++)
|
||||
{
|
||||
List file = new ArrayList();
|
||||
StringTokenizer st = new StringTokenizer(names[i], File.separator);
|
||||
while (st.hasMoreTokens())
|
||||
{
|
||||
String part = st.nextToken();
|
||||
file.add(part);
|
||||
}
|
||||
files.add(file);
|
||||
}
|
||||
|
||||
String name = baseFile.getName();
|
||||
if (files.size() == 1)
|
||||
{
|
||||
files = null;
|
||||
lengthsList = null;
|
||||
}
|
||||
|
||||
// Note that the piece_hashes are not correctly setup yet.
|
||||
metainfo = new MetaInfo(announce, baseFile.getName(), null, files,
|
||||
lengthsList, piece_size, piece_hashes, total);
|
||||
|
||||
}
|
||||
|
||||
// Creates piece hases for a new storage.
|
||||
public void create() throws IOException
|
||||
{
|
||||
if (true) {
|
||||
fast_digestCreate();
|
||||
} else {
|
||||
orig_digestCreate();
|
||||
}
|
||||
}
|
||||
|
||||
private void orig_digestCreate() throws IOException {
|
||||
// Calculate piece_hashes
|
||||
MessageDigest digest = null;
|
||||
try
|
||||
{
|
||||
digest = MessageDigest.getInstance("SHA");
|
||||
}
|
||||
catch(NoSuchAlgorithmException nsa)
|
||||
{
|
||||
throw new InternalError(nsa.toString());
|
||||
}
|
||||
|
||||
byte[] piece_hashes = metainfo.getPieceHashes();
|
||||
|
||||
byte[] piece = new byte[piece_size];
|
||||
for (int i = 0; i < pieces; i++)
|
||||
{
|
||||
int length = getUncheckedPiece(i, piece, 0);
|
||||
digest.update(piece, 0, length);
|
||||
byte[] hash = digest.digest();
|
||||
for (int j = 0; j < 20; j++)
|
||||
piece_hashes[20 * i + j] = hash[j];
|
||||
|
||||
bitfield.set(i);
|
||||
|
||||
if (listener != null)
|
||||
listener.storageChecked(this, i, true);
|
||||
}
|
||||
|
||||
if (listener != null)
|
||||
listener.storageAllChecked(this);
|
||||
|
||||
// Reannounce to force recalculating the info_hash.
|
||||
metainfo = metainfo.reannounce(metainfo.getAnnounce());
|
||||
}
|
||||
|
||||
private void fast_digestCreate() throws IOException {
|
||||
// Calculate piece_hashes
|
||||
SHA1 digest = new SHA1();
|
||||
|
||||
byte[] piece_hashes = metainfo.getPieceHashes();
|
||||
|
||||
byte[] piece = new byte[piece_size];
|
||||
for (int i = 0; i < pieces; i++)
|
||||
{
|
||||
int length = getUncheckedPiece(i, piece, 0);
|
||||
digest.update(piece, 0, length);
|
||||
byte[] hash = digest.digest();
|
||||
for (int j = 0; j < 20; j++)
|
||||
piece_hashes[20 * i + j] = hash[j];
|
||||
|
||||
bitfield.set(i);
|
||||
|
||||
if (listener != null)
|
||||
listener.storageChecked(this, i, true);
|
||||
}
|
||||
|
||||
if (listener != null)
|
||||
listener.storageAllChecked(this);
|
||||
|
||||
// Reannounce to force recalculating the info_hash.
|
||||
metainfo = metainfo.reannounce(metainfo.getAnnounce());
|
||||
}
|
||||
|
||||
private void getFiles(File base) throws IOException
|
||||
{
|
||||
ArrayList files = new ArrayList();
|
||||
addFiles(files, base);
|
||||
|
||||
int size = files.size();
|
||||
names = new String[size];
|
||||
lengths = new long[size];
|
||||
rafs = new RandomAccessFile[size];
|
||||
|
||||
int i = 0;
|
||||
Iterator it = files.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
File f = (File)it.next();
|
||||
names[i] = f.getPath();
|
||||
lengths[i] = f.length();
|
||||
rafs[i] = new RandomAccessFile(f, "r");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addFiles(List l, File f)
|
||||
{
|
||||
if (!f.isDirectory())
|
||||
l.add(f);
|
||||
else
|
||||
{
|
||||
File[] files = f.listFiles();
|
||||
if (files == null)
|
||||
{
|
||||
Snark.debug("WARNING: Skipping '" + f
|
||||
+ "' not a normal file.", Snark.WARNING);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < files.length; i++)
|
||||
addFiles(l, files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MetaInfo associated with this Storage.
|
||||
*/
|
||||
public MetaInfo getMetaInfo()
|
||||
{
|
||||
return metainfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many pieces are still missing from this storage.
|
||||
*/
|
||||
public int needed()
|
||||
{
|
||||
return needed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this storage contains all pieces if the MetaInfo.
|
||||
*/
|
||||
public boolean complete()
|
||||
{
|
||||
return needed == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The BitField that tells which pieces this storage contains.
|
||||
* Do not change this since this is the current state of the storage.
|
||||
*/
|
||||
public BitField getBitField()
|
||||
{
|
||||
return bitfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates (and/or checks) all files from the metainfo file list.
|
||||
*/
|
||||
public void check(String rootDir) throws IOException
|
||||
{
|
||||
File base = new File(rootDir, filterName(metainfo.getName()));
|
||||
|
||||
List files = metainfo.getFiles();
|
||||
if (files == null)
|
||||
{
|
||||
// Create base as file.
|
||||
Snark.debug("Creating/Checking file: " + base, Snark.NOTICE);
|
||||
if (!base.createNewFile() && !base.exists())
|
||||
throw new IOException("Could not create file " + base);
|
||||
|
||||
lengths = new long[1];
|
||||
rafs = new RandomAccessFile[1];
|
||||
names = new String[1];
|
||||
lengths[0] = metainfo.getTotalLength();
|
||||
if (base.exists() && !base.canWrite()) // hope we can get away with this, if we are only seeding...
|
||||
rafs[0] = new RandomAccessFile(base, "r");
|
||||
else
|
||||
rafs[0] = new RandomAccessFile(base, "rw");
|
||||
names[0] = base.getName();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create base as dir.
|
||||
Snark.debug("Creating/Checking directory: " + base, Snark.NOTICE);
|
||||
if (!base.mkdir() && !base.isDirectory())
|
||||
throw new IOException("Could not create directory " + base);
|
||||
|
||||
List ls = metainfo.getLengths();
|
||||
int size = files.size();
|
||||
long total = 0;
|
||||
lengths = new long[size];
|
||||
rafs = new RandomAccessFile[size];
|
||||
names = new String[size];
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
File f = createFileFromNames(base, (List)files.get(i));
|
||||
lengths[i] = ((Long)ls.get(i)).longValue();
|
||||
total += lengths[i];
|
||||
if (f.exists() && !f.canWrite()) // see above re: only seeding
|
||||
rafs[i] = new RandomAccessFile(f, "r");
|
||||
else
|
||||
rafs[i] = new RandomAccessFile(f, "rw");
|
||||
names[i] = f.getName();
|
||||
}
|
||||
|
||||
// Sanity check for metainfo file.
|
||||
long metalength = metainfo.getTotalLength();
|
||||
if (total != metalength)
|
||||
throw new IOException("File lengths do not add up "
|
||||
+ total + " != " + metalength);
|
||||
}
|
||||
checkCreateFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes 'suspicious' characters from the give file name.
|
||||
*/
|
||||
private String filterName(String name)
|
||||
{
|
||||
// XXX - Is this enough?
|
||||
return name.replace(File.separatorChar, '_');
|
||||
}
|
||||
|
||||
private File createFileFromNames(File base, List names) throws IOException
|
||||
{
|
||||
File f = null;
|
||||
Iterator it = names.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
String name = filterName((String)it.next());
|
||||
if (it.hasNext())
|
||||
{
|
||||
// Another dir in the hierarchy.
|
||||
f = new File(base, name);
|
||||
if (!f.mkdir() && !f.isDirectory())
|
||||
throw new IOException("Could not create directory " + f);
|
||||
base = f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The final element (file) in the hierarchy.
|
||||
f = new File(base, name);
|
||||
if (!f.createNewFile() && !f.exists())
|
||||
throw new IOException("Could not create file " + f);
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
private void checkCreateFiles() throws IOException
|
||||
{
|
||||
// Whether we are resuming or not,
|
||||
// if any of the files already exists we assume we are resuming.
|
||||
boolean resume = false;
|
||||
|
||||
// Make sure all files are available and of correct length
|
||||
for (int i = 0; i < rafs.length; i++)
|
||||
{
|
||||
long length = rafs[i].length();
|
||||
if(length == lengths[i])
|
||||
{
|
||||
if (listener != null)
|
||||
listener.storageAllocated(this, length);
|
||||
resume = true; // XXX Could dynamicly check
|
||||
}
|
||||
else if (length == 0)
|
||||
allocateFile(i);
|
||||
else
|
||||
throw new IOException("File '" + names[i]
|
||||
+ "' exists, but has wrong length");
|
||||
}
|
||||
|
||||
// Check which pieces match and which don't
|
||||
if (resume)
|
||||
{
|
||||
pieces = metainfo.getPieces();
|
||||
byte[] piece = new byte[metainfo.getPieceLength(0)];
|
||||
for (int i = 0; i < pieces; i++)
|
||||
{
|
||||
int length = getUncheckedPiece(i, piece, 0);
|
||||
boolean correctHash = metainfo.checkPiece(i, piece, 0, length);
|
||||
if (correctHash)
|
||||
{
|
||||
bitfield.set(i);
|
||||
needed--;
|
||||
}
|
||||
|
||||
if (listener != null)
|
||||
listener.storageChecked(this, i, correctHash);
|
||||
}
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.storageAllChecked(this);
|
||||
if (needed <= 0)
|
||||
listener.storageCompleted(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void allocateFile(int nr) throws IOException
|
||||
{
|
||||
// XXX - Is this the best way to make sure we have enough space for
|
||||
// the whole file?
|
||||
listener.storageCreateFile(this, names[nr], lengths[nr]);
|
||||
final int ZEROBLOCKSIZE = metainfo.getPieceLength(0);
|
||||
byte[] zeros = new byte[ZEROBLOCKSIZE];
|
||||
int i;
|
||||
for (i = 0; i < lengths[nr]/ZEROBLOCKSIZE; i++)
|
||||
{
|
||||
rafs[nr].write(zeros);
|
||||
if (listener != null)
|
||||
listener.storageAllocated(this, ZEROBLOCKSIZE);
|
||||
}
|
||||
int size = (int)(lengths[nr] - i*ZEROBLOCKSIZE);
|
||||
rafs[nr].write(zeros, 0, size);
|
||||
if (listener != null)
|
||||
listener.storageAllocated(this, size);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the Storage and makes sure that all RandomAccessFiles are
|
||||
* closed. The Storage is unusable after this.
|
||||
*/
|
||||
public void close() throws IOException
|
||||
{
|
||||
if (rafs == null) return;
|
||||
for (int i = 0; i < rafs.length; i++)
|
||||
{
|
||||
try {
|
||||
synchronized(rafs[i])
|
||||
{
|
||||
rafs[i].close();
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
I2PSnarkUtil.instance().debug("Error closing " + rafs[i], Snark.ERROR, ioe);
|
||||
// gobble gobble
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte array containing the requested piece or null if
|
||||
* the storage doesn't contain the piece yet.
|
||||
*/
|
||||
public byte[] getPiece(int piece) throws IOException
|
||||
{
|
||||
if (!bitfield.get(piece))
|
||||
return null;
|
||||
|
||||
byte[] bs = new byte[metainfo.getPieceLength(piece)];
|
||||
getUncheckedPiece(piece, bs, 0);
|
||||
return bs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the piece in the Storage if it is correct.
|
||||
*
|
||||
* @return true if the piece was correct (sha metainfo hash
|
||||
* matches), otherwise false.
|
||||
* @exception IOException when some storage related error occurs.
|
||||
*/
|
||||
public boolean putPiece(int piece, byte[] bs) throws IOException
|
||||
{
|
||||
// First check if the piece is correct.
|
||||
// If we were paranoia we could copy the array first.
|
||||
int length = bs.length;
|
||||
boolean correctHash = metainfo.checkPiece(piece, bs, 0, length);
|
||||
if (listener != null)
|
||||
listener.storageChecked(this, piece, correctHash);
|
||||
if (!correctHash)
|
||||
return false;
|
||||
|
||||
boolean complete;
|
||||
synchronized(bitfield)
|
||||
{
|
||||
if (bitfield.get(piece))
|
||||
return true; // No need to store twice.
|
||||
else
|
||||
{
|
||||
bitfield.set(piece);
|
||||
needed--;
|
||||
complete = needed == 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Early typecast, avoid possibly overflowing a temp integer
|
||||
long start = (long) piece * (long) metainfo.getPieceLength(0);
|
||||
int i = 0;
|
||||
long raflen = lengths[i];
|
||||
while (start > raflen)
|
||||
{
|
||||
i++;
|
||||
start -= raflen;
|
||||
raflen = lengths[i];
|
||||
}
|
||||
|
||||
int written = 0;
|
||||
int off = 0;
|
||||
while (written < length)
|
||||
{
|
||||
int need = length - written;
|
||||
int len = (start + need < raflen) ? need : (int)(raflen - start);
|
||||
synchronized(rafs[i])
|
||||
{
|
||||
rafs[i].seek(start);
|
||||
rafs[i].write(bs, off + written, len);
|
||||
}
|
||||
written += len;
|
||||
if (need - len > 0)
|
||||
{
|
||||
i++;
|
||||
raflen = lengths[i];
|
||||
start = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
listener.storageCompleted(this);
|
||||
// do we also need to close all of the files and reopen
|
||||
// them readonly?
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int getUncheckedPiece(int piece, byte[] bs, int off)
|
||||
throws IOException
|
||||
{
|
||||
// XXX - copy/paste code from putPiece().
|
||||
|
||||
// Early typecast, avoid possibly overflowing a temp integer
|
||||
long start = (long) piece * (long) metainfo.getPieceLength(0);
|
||||
|
||||
int length = metainfo.getPieceLength(piece);
|
||||
int i = 0;
|
||||
long raflen = lengths[i];
|
||||
while (start > raflen)
|
||||
{
|
||||
i++;
|
||||
start -= raflen;
|
||||
raflen = lengths[i];
|
||||
}
|
||||
|
||||
int read = 0;
|
||||
while (read < length)
|
||||
{
|
||||
int need = length - read;
|
||||
int len = (start + need < raflen) ? need : (int)(raflen - start);
|
||||
synchronized(rafs[i])
|
||||
{
|
||||
rafs[i].seek(start);
|
||||
rafs[i].readFully(bs, off + read, len);
|
||||
}
|
||||
read += len;
|
||||
if (need - len > 0)
|
||||
{
|
||||
i++;
|
||||
raflen = lengths[i];
|
||||
start = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
58
apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
Normal file
58
apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
Normal file
@@ -0,0 +1,58 @@
|
||||
/* StorageListener.java - Interface used as callback when storage changes.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
/**
|
||||
* Callback used when Storage changes.
|
||||
*/
|
||||
public interface StorageListener
|
||||
{
|
||||
/**
|
||||
* Called when the storage creates a new file of a given length.
|
||||
*/
|
||||
void storageCreateFile(Storage storage, String name, long length);
|
||||
|
||||
/**
|
||||
* Called to indicate that length bytes have been allocated.
|
||||
*/
|
||||
void storageAllocated(Storage storage, long length);
|
||||
|
||||
/**
|
||||
* Called when storage is being checked and the num piece of that
|
||||
* total pieces has been checked. When the piece hash matches the
|
||||
* expected piece hash checked will be true, otherwise it will be
|
||||
* false.
|
||||
*/
|
||||
void storageChecked(Storage storage, int num, boolean checked);
|
||||
|
||||
/**
|
||||
* Called when all pieces in the storage have been checked. Does not
|
||||
* mean that the storage is complete, just that the state of the
|
||||
* storage is known.
|
||||
*/
|
||||
void storageAllChecked(Storage storage);
|
||||
|
||||
/**
|
||||
* Called the one time when the data is completely received and checked.
|
||||
*
|
||||
*/
|
||||
void storageCompleted(Storage storage);
|
||||
}
|
||||
317
apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
Normal file
317
apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
Normal file
@@ -0,0 +1,317 @@
|
||||
/* TrackerClient - Class that informs a tracker and gets new peers.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Informs metainfo tracker of events and gets new peers for peer
|
||||
* coordinator.
|
||||
*
|
||||
* @author Mark Wielaard (mark@klomp.org)
|
||||
*/
|
||||
public class TrackerClient extends I2PThread
|
||||
{
|
||||
private static final Log _log = new Log(TrackerClient.class);
|
||||
private static final String NO_EVENT = "";
|
||||
private static final String STARTED_EVENT = "started";
|
||||
private static final String COMPLETED_EVENT = "completed";
|
||||
private static final String STOPPED_EVENT = "stopped";
|
||||
|
||||
private final static int SLEEP = 5; // 5 minutes.
|
||||
|
||||
private final MetaInfo meta;
|
||||
private final PeerCoordinator coordinator;
|
||||
private final int port;
|
||||
|
||||
private boolean stop;
|
||||
private boolean started;
|
||||
|
||||
private long interval;
|
||||
private long lastRequestTime;
|
||||
|
||||
public TrackerClient(MetaInfo meta, PeerCoordinator coordinator)
|
||||
{
|
||||
// Set unique name.
|
||||
super("TrackerClient-" + urlencode(coordinator.getID()));
|
||||
this.meta = meta;
|
||||
this.coordinator = coordinator;
|
||||
|
||||
this.port = 6881; //(port == -1) ? 9 : port;
|
||||
|
||||
stop = false;
|
||||
started = false;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (stop) throw new RuntimeException("Dont rerun me, create a copy");
|
||||
super.start();
|
||||
started = true;
|
||||
}
|
||||
|
||||
public boolean halted() { return stop; }
|
||||
public boolean started() { return started; }
|
||||
|
||||
/**
|
||||
* Interrupts this Thread to stop it.
|
||||
*/
|
||||
public void halt()
|
||||
{
|
||||
stop = true;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
private boolean verifyConnected() {
|
||||
while (!stop && !I2PSnarkUtil.instance().connected()) {
|
||||
boolean ok = I2PSnarkUtil.instance().connect();
|
||||
if (!ok) {
|
||||
try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
return !stop && I2PSnarkUtil.instance().connected();
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
// XXX - Support other IPs
|
||||
String announce = meta.getAnnounce(); //I2PSnarkUtil.instance().rewriteAnnounce(meta.getAnnounce());
|
||||
String infoHash = urlencode(meta.getInfoHash());
|
||||
String peerID = urlencode(coordinator.getID());
|
||||
|
||||
_log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash
|
||||
+ " xmitAnnounce: [" + announce + "]");
|
||||
|
||||
long uploaded = coordinator.getUploaded();
|
||||
long downloaded = coordinator.getDownloaded();
|
||||
long left = coordinator.getLeft();
|
||||
|
||||
boolean completed = (left == 0);
|
||||
|
||||
try
|
||||
{
|
||||
if (!verifyConnected()) return;
|
||||
boolean started = false;
|
||||
while (!started)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Send start.
|
||||
TrackerInfo info = doRequest(announce, infoHash, peerID,
|
||||
uploaded, downloaded, left,
|
||||
STARTED_EVENT);
|
||||
Set peers = info.getPeers();
|
||||
coordinator.trackerSeenPeers = peers.size();
|
||||
if (!completed) {
|
||||
Iterator it = peers.iterator();
|
||||
while (it.hasNext()) {
|
||||
Peer cur = (Peer)it.next();
|
||||
coordinator.addPeer(cur);
|
||||
int delay = 3000;
|
||||
int c = ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10;
|
||||
try { Thread.sleep(delay * c); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
started = true;
|
||||
coordinator.trackerProblems = null;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
// Probably not fatal (if it doesn't last to long...)
|
||||
Snark.debug
|
||||
("WARNING: Could not contact tracker at '"
|
||||
+ announce + "': " + ioe, Snark.WARNING);
|
||||
coordinator.trackerProblems = ioe.getMessage();
|
||||
}
|
||||
|
||||
if (!started && !stop)
|
||||
{
|
||||
Snark.debug(" Retrying in one minute...", Snark.DEBUG);
|
||||
try
|
||||
{
|
||||
// Sleep one minutes...
|
||||
Thread.sleep(60*1000);
|
||||
}
|
||||
catch(InterruptedException interrupt)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Random r = new Random();
|
||||
while(!stop)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Sleep some minutes...
|
||||
int delay = SLEEP*60*1000 + r.nextInt(120*1000);
|
||||
Thread.sleep(delay);
|
||||
}
|
||||
catch(InterruptedException interrupt)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (stop)
|
||||
break;
|
||||
|
||||
if (!verifyConnected()) return;
|
||||
|
||||
uploaded = coordinator.getUploaded();
|
||||
downloaded = coordinator.getDownloaded();
|
||||
left = coordinator.getLeft();
|
||||
|
||||
// First time we got a complete download?
|
||||
String event;
|
||||
if (!completed && left == 0)
|
||||
{
|
||||
completed = true;
|
||||
event = COMPLETED_EVENT;
|
||||
}
|
||||
else
|
||||
event = NO_EVENT;
|
||||
|
||||
// Only do a request when necessary.
|
||||
if (event == COMPLETED_EVENT
|
||||
|| coordinator.needPeers()
|
||||
|| System.currentTimeMillis() > lastRequestTime + interval)
|
||||
{
|
||||
try
|
||||
{
|
||||
TrackerInfo info = doRequest(announce, infoHash, peerID,
|
||||
uploaded, downloaded, left,
|
||||
event);
|
||||
|
||||
Set peers = info.getPeers();
|
||||
coordinator.trackerSeenPeers = peers.size();
|
||||
if ( (left > 0) && (!completed) ) {
|
||||
// we only want to talk to new people if we need things
|
||||
// from them (duh)
|
||||
List ordered = new ArrayList(peers);
|
||||
Collections.shuffle(ordered);
|
||||
Iterator it = ordered.iterator();
|
||||
while (it.hasNext()) {
|
||||
Peer cur = (Peer)it.next();
|
||||
coordinator.addPeer(cur);
|
||||
int delay = 3000;
|
||||
int c = ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10;
|
||||
try { Thread.sleep(delay * c); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
// Probably not fatal (if it doesn't last to long...)
|
||||
Snark.debug
|
||||
("WARNING: Could not contact tracker at '"
|
||||
+ announce + "': " + ioe, Snark.WARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
I2PSnarkUtil.instance().debug("TrackerClient: " + t, Snark.ERROR, t);
|
||||
if (t instanceof OutOfMemoryError)
|
||||
throw (OutOfMemoryError)t;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!verifyConnected()) return;
|
||||
TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded,
|
||||
downloaded, left, STOPPED_EVENT);
|
||||
}
|
||||
catch(IOException ioe) { /* ignored */ }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private TrackerInfo doRequest(String announce, String infoHash,
|
||||
String peerID, long uploaded,
|
||||
long downloaded, long left, String event)
|
||||
throws IOException
|
||||
{
|
||||
String s = announce
|
||||
+ "?info_hash=" + infoHash
|
||||
+ "&peer_id=" + peerID
|
||||
+ "&port=" + port
|
||||
+ "&ip=" + I2PSnarkUtil.instance().getOurIPString() + ".i2p"
|
||||
+ "&uploaded=" + uploaded
|
||||
+ "&downloaded=" + downloaded
|
||||
+ "&left=" + left
|
||||
+ ((event != NO_EVENT) ? ("&event=" + event) : "");
|
||||
if (Snark.debug >= Snark.INFO)
|
||||
Snark.debug("Sending TrackerClient request: " + s, Snark.INFO);
|
||||
|
||||
File fetched = I2PSnarkUtil.instance().get(s);
|
||||
if (fetched == null) {
|
||||
throw new IOException("Error fetching " + s);
|
||||
}
|
||||
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(fetched);
|
||||
|
||||
TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
|
||||
coordinator.getMetaInfo());
|
||||
if (Snark.debug >= Snark.INFO)
|
||||
Snark.debug("TrackerClient response: " + info, Snark.INFO);
|
||||
lastRequestTime = System.currentTimeMillis();
|
||||
|
||||
String failure = info.getFailureReason();
|
||||
if (failure != null)
|
||||
throw new IOException(failure);
|
||||
|
||||
interval = info.getInterval() * 1000;
|
||||
return info;
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
fetched.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Very lazy byte[] to URL encoder. Just encodes everything, even
|
||||
* "normal" chars.
|
||||
*/
|
||||
static String urlencode(byte[] bs)
|
||||
{
|
||||
StringBuffer sb = new StringBuffer(bs.length*3);
|
||||
for (int i = 0; i < bs.length; i++)
|
||||
{
|
||||
int c = bs[i] & 0xFF;
|
||||
sb.append('%');
|
||||
if (c < 16)
|
||||
sb.append('0');
|
||||
sb.append(Integer.toHexString(c));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
128
apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
Normal file
128
apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
Normal file
@@ -0,0 +1,128 @@
|
||||
/* TrackerInfo - Holds information returned by a tracker, mainly the peer list.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.klomp.snark.bencode.*;
|
||||
|
||||
public class TrackerInfo
|
||||
{
|
||||
private final String failure_reason;
|
||||
private final int interval;
|
||||
private final Set peers;
|
||||
|
||||
public TrackerInfo(InputStream in, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
this(new BDecoder(in), my_id, metainfo);
|
||||
}
|
||||
|
||||
public TrackerInfo(BDecoder be, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
this(be.bdecodeMap().getMap(), my_id, metainfo);
|
||||
}
|
||||
|
||||
public TrackerInfo(Map m, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
BEValue reason = (BEValue)m.get("failure reason");
|
||||
if (reason != null)
|
||||
{
|
||||
failure_reason = reason.getString();
|
||||
interval = -1;
|
||||
peers = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
failure_reason = null;
|
||||
BEValue beInterval = (BEValue)m.get("interval");
|
||||
if (beInterval == null)
|
||||
throw new InvalidBEncodingException("No interval given");
|
||||
else
|
||||
interval = beInterval.getInt();
|
||||
BEValue bePeers = (BEValue)m.get("peers");
|
||||
if (bePeers == null)
|
||||
throw new InvalidBEncodingException("No peer list");
|
||||
else
|
||||
peers = getPeers(bePeers.getList(), my_id, metainfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static Set getPeers(InputStream in, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
return getPeers(new BDecoder(in), my_id, metainfo);
|
||||
}
|
||||
|
||||
public static Set getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
return getPeers(be.bdecodeList().getList(), my_id, metainfo);
|
||||
}
|
||||
|
||||
public static Set getPeers(List l, byte[] my_id, MetaInfo metainfo)
|
||||
throws IOException
|
||||
{
|
||||
Set peers = new HashSet(l.size());
|
||||
|
||||
Iterator it = l.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
PeerID peerID = new PeerID(((BEValue)it.next()).getMap());
|
||||
peers.add(new Peer(peerID, my_id, metainfo));
|
||||
}
|
||||
|
||||
return peers;
|
||||
}
|
||||
|
||||
public Set getPeers()
|
||||
{
|
||||
return peers;
|
||||
}
|
||||
|
||||
public String getFailureReason()
|
||||
{
|
||||
return failure_reason;
|
||||
}
|
||||
|
||||
public int getInterval()
|
||||
{
|
||||
return interval;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
if (failure_reason != null)
|
||||
return "TrackerInfo[FAILED: " + failure_reason + "]";
|
||||
else
|
||||
return "TrackerInfo[interval=" + interval
|
||||
+ ", peers=" + peers + "]";
|
||||
}
|
||||
}
|
||||
355
apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java
Normal file
355
apps/i2psnark/java/src/org/klomp/snark/bencode/BDecoder.java
Normal file
@@ -0,0 +1,355 @@
|
||||
/* BDecoder - Converts an InputStream to BEValues.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark.bencode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Decodes a bencoded stream to <code>BEValue</code>s.
|
||||
*
|
||||
* A bencoded byte stream can represent byte arrays, numbers, lists and
|
||||
* maps (dictionaries).
|
||||
*
|
||||
* It currently contains a hack to indicate a name of a dictionary of
|
||||
* which a SHA-1 digest hash should be calculated (the hash over the
|
||||
* original bencoded bytes).
|
||||
*
|
||||
* @author Mark Wielaard (mark@klomp.org).
|
||||
*/
|
||||
public class BDecoder
|
||||
{
|
||||
// The InputStream to BDecode.
|
||||
private final InputStream in;
|
||||
|
||||
// The last indicator read.
|
||||
// Zero if unknown.
|
||||
// '0'..'9' indicates a byte[].
|
||||
// 'i' indicates an Number.
|
||||
// 'l' indicates a List.
|
||||
// 'd' indicates a Map.
|
||||
// 'e' indicates end of Number, List or Map (only used internally).
|
||||
// -1 indicates end of stream.
|
||||
// Call getNextIndicator to get the current value (will never return zero).
|
||||
private int indicator = 0;
|
||||
|
||||
// Used for ugly hack to get SHA hash over the metainfo info map
|
||||
private String special_map = "info";
|
||||
private boolean in_special_map = false;
|
||||
private final MessageDigest sha_digest;
|
||||
|
||||
// Ugly hack. Return the SHA has over bytes that make up the special map.
|
||||
public byte[] get_special_map_digest()
|
||||
{
|
||||
byte[] result = sha_digest.digest();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ugly hack. Name defaults to "info".
|
||||
public void set_special_map_name(String name)
|
||||
{
|
||||
special_map = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalizes a new BDecoder. Nothing is read from the given
|
||||
* <code>InputStream</code> yet.
|
||||
*/
|
||||
public BDecoder(InputStream in)
|
||||
{
|
||||
this.in = in;
|
||||
// XXX - Used for ugly hack.
|
||||
try
|
||||
{
|
||||
sha_digest = MessageDigest.getInstance("SHA");
|
||||
}
|
||||
catch(NoSuchAlgorithmException nsa)
|
||||
{
|
||||
throw new InternalError(nsa.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new BDecoder and immediatly decodes the first value it
|
||||
* sees.
|
||||
*
|
||||
* @return The first BEValue on the stream or null when the stream
|
||||
* has ended.
|
||||
*
|
||||
* @exception InvalidBEncoding when the stream doesn't start with a
|
||||
* bencoded value or the stream isn't a bencoded stream at all.
|
||||
* @exception IOException when somthing bad happens with the stream
|
||||
* to read from.
|
||||
*/
|
||||
public static BEValue bdecode(InputStream in) throws IOException
|
||||
{
|
||||
return new BDecoder(in).bdecode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns what the next bencoded object will be on the stream or -1
|
||||
* when the end of stream has been reached. Can return something
|
||||
* unexpected (not '0' .. '9', 'i', 'l' or 'd') when the stream
|
||||
* isn't bencoded.
|
||||
*
|
||||
* This might or might not read one extra byte from the stream.
|
||||
*/
|
||||
public int getNextIndicator() throws IOException
|
||||
{
|
||||
if (indicator == 0)
|
||||
{
|
||||
indicator = in.read();
|
||||
// XXX - Used for ugly hack
|
||||
if (in_special_map) sha_digest.update((byte)indicator);
|
||||
}
|
||||
return indicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next indicator and returns either null when the stream
|
||||
* has ended or bdecodes the rest of the stream and returns the
|
||||
* appropriate BEValue encoded object.
|
||||
*/
|
||||
public BEValue bdecode() throws IOException
|
||||
{
|
||||
indicator = getNextIndicator();
|
||||
if (indicator == -1)
|
||||
return null;
|
||||
|
||||
if (indicator >= '0' && indicator <= '9')
|
||||
return bdecodeBytes();
|
||||
else if (indicator == 'i')
|
||||
return bdecodeNumber();
|
||||
else if (indicator == 'l')
|
||||
return bdecodeList();
|
||||
else if (indicator == 'd')
|
||||
return bdecodeMap();
|
||||
else
|
||||
throw new InvalidBEncodingException
|
||||
("Unknown indicator '" + indicator + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next bencoded value on the stream and makes sure it
|
||||
* is a byte array. If it is not a bencoded byte array it will throw
|
||||
* InvalidBEncodingException.
|
||||
*/
|
||||
public BEValue bdecodeBytes() throws IOException
|
||||
{
|
||||
int c = getNextIndicator();
|
||||
int num = c - '0';
|
||||
if (num < 0 || num > 9)
|
||||
throw new InvalidBEncodingException("Number expected, not '"
|
||||
+ (char)c + "'");
|
||||
indicator = 0;
|
||||
|
||||
c = read();
|
||||
int i = c - '0';
|
||||
while (i >= 0 && i <= 9)
|
||||
{
|
||||
// XXX - This can overflow!
|
||||
num = num*10 + i;
|
||||
c = read();
|
||||
i = c - '0';
|
||||
}
|
||||
|
||||
if (c != ':')
|
||||
throw new InvalidBEncodingException("Colon expected, not '"
|
||||
+ (char)c + "'");
|
||||
|
||||
return new BEValue(read(num));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next bencoded value on the stream and makes sure it
|
||||
* is a number. If it is not a number it will throw
|
||||
* InvalidBEncodingException.
|
||||
*/
|
||||
public BEValue bdecodeNumber() throws IOException
|
||||
{
|
||||
int c = getNextIndicator();
|
||||
if (c != 'i')
|
||||
throw new InvalidBEncodingException("Expected 'i', not '"
|
||||
+ (char)c + "'");
|
||||
indicator = 0;
|
||||
|
||||
c = read();
|
||||
if (c == '0')
|
||||
{
|
||||
c = read();
|
||||
if (c == 'e')
|
||||
return new BEValue(BigInteger.ZERO);
|
||||
else
|
||||
throw new InvalidBEncodingException("'e' expected after zero,"
|
||||
+ " not '" + (char)c + "'");
|
||||
}
|
||||
|
||||
// XXX - We don't support more the 255 char big integers
|
||||
char[] chars = new char[256];
|
||||
int off = 0;
|
||||
|
||||
if (c == '-')
|
||||
{
|
||||
c = read();
|
||||
if (c == '0')
|
||||
throw new InvalidBEncodingException("Negative zero not allowed");
|
||||
chars[off] = (char)c;
|
||||
off++;
|
||||
}
|
||||
|
||||
if (c < '1' || c > '9')
|
||||
throw new InvalidBEncodingException("Invalid Integer start '"
|
||||
+ (char)c + "'");
|
||||
chars[off] = (char)c;
|
||||
off++;
|
||||
|
||||
c = read();
|
||||
int i = c - '0';
|
||||
while(i >= 0 && i <= 9)
|
||||
{
|
||||
chars[off] = (char)c;
|
||||
off++;
|
||||
c = read();
|
||||
i = c - '0';
|
||||
}
|
||||
|
||||
if (c != 'e')
|
||||
throw new InvalidBEncodingException("Integer should end with 'e'");
|
||||
|
||||
String s = new String(chars, 0, off);
|
||||
return new BEValue(new BigInteger(s));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next bencoded value on the stream and makes sure it
|
||||
* is a list. If it is not a list it will throw
|
||||
* InvalidBEncodingException.
|
||||
*/
|
||||
public BEValue bdecodeList() throws IOException
|
||||
{
|
||||
int c = getNextIndicator();
|
||||
if (c != 'l')
|
||||
throw new InvalidBEncodingException("Expected 'l', not '"
|
||||
+ (char)c + "'");
|
||||
indicator = 0;
|
||||
|
||||
List result = new ArrayList();
|
||||
c = getNextIndicator();
|
||||
while (c != 'e')
|
||||
{
|
||||
result.add(bdecode());
|
||||
c = getNextIndicator();
|
||||
}
|
||||
indicator = 0;
|
||||
|
||||
return new BEValue(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next bencoded value on the stream and makes sure it
|
||||
* is a map (dictonary). If it is not a map it will throw
|
||||
* InvalidBEncodingException.
|
||||
*/
|
||||
public BEValue bdecodeMap() throws IOException
|
||||
{
|
||||
int c = getNextIndicator();
|
||||
if (c != 'd')
|
||||
throw new InvalidBEncodingException("Expected 'd', not '"
|
||||
+ (char)c + "'");
|
||||
indicator = 0;
|
||||
|
||||
Map result = new HashMap();
|
||||
c = getNextIndicator();
|
||||
while (c != 'e')
|
||||
{
|
||||
// Dictonary keys are always strings.
|
||||
String key = bdecode().getString();
|
||||
|
||||
// XXX ugly hack
|
||||
boolean special = special_map.equals(key);
|
||||
if (special)
|
||||
in_special_map = true;
|
||||
|
||||
BEValue value = bdecode();
|
||||
result.put(key, value);
|
||||
|
||||
// XXX ugly hack continued
|
||||
if (special)
|
||||
in_special_map = false;
|
||||
|
||||
c = getNextIndicator();
|
||||
}
|
||||
indicator = 0;
|
||||
|
||||
return new BEValue(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next byte read from the InputStream (as int).
|
||||
* Throws EOFException if InputStream.read() returned -1.
|
||||
*/
|
||||
private int read() throws IOException
|
||||
{
|
||||
int c = in.read();
|
||||
if (c == -1)
|
||||
throw new EOFException();
|
||||
if (in_special_map) sha_digest.update((byte)c);
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte[] containing length valid bytes starting at offset
|
||||
* zero. Throws EOFException if InputStream.read() returned -1
|
||||
* before all requested bytes could be read. Note that the byte[]
|
||||
* returned might be bigger then requested but will only contain
|
||||
* length valid bytes. The returned byte[] will be reused when this
|
||||
* method is called again.
|
||||
*/
|
||||
private byte[] read(int length) throws IOException
|
||||
{
|
||||
byte[] result = new byte[length];
|
||||
|
||||
int read = 0;
|
||||
while (read < length)
|
||||
{
|
||||
int i = in.read(result, read, length - read);
|
||||
if (i == -1)
|
||||
throw new EOFException();
|
||||
read += i;
|
||||
}
|
||||
|
||||
if (in_special_map) sha_digest.update(result, 0, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
193
apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
Normal file
193
apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
Normal file
@@ -0,0 +1,193 @@
|
||||
/* BEValue - Holds different types that a bencoded byte array can represent.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark.bencode;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Holds different types that a bencoded byte array can represent.
|
||||
* You need to call the correct get method to get the correct java
|
||||
* type object. If the BEValue wasn't actually of the requested type
|
||||
* you will get a InvalidBEncodingException.
|
||||
*
|
||||
* @author Mark Wielaard (mark@klomp.org)
|
||||
*/
|
||||
public class BEValue
|
||||
{
|
||||
// This is either a byte[], Number, List or Map.
|
||||
private final Object value;
|
||||
|
||||
public BEValue(byte[] value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public BEValue(Number value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public BEValue(List value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public BEValue(Map value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as a String. This operation only succeeds
|
||||
* when the BEValue is a byte[], otherwise it will throw a
|
||||
* InvalidBEncodingException. The byte[] will be interpreted as
|
||||
* UTF-8 encoded characters.
|
||||
*/
|
||||
public String getString() throws InvalidBEncodingException
|
||||
{
|
||||
try
|
||||
{
|
||||
return new String(getBytes(), "UTF-8");
|
||||
}
|
||||
catch (ClassCastException cce)
|
||||
{
|
||||
throw new InvalidBEncodingException(cce.toString());
|
||||
}
|
||||
catch (UnsupportedEncodingException uee)
|
||||
{
|
||||
throw new InternalError(uee.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as a byte[]. This operation only succeeds
|
||||
* when the BEValue is actually a byte[], otherwise it will throw a
|
||||
* InvalidBEncodingException.
|
||||
*/
|
||||
public byte[] getBytes() throws InvalidBEncodingException
|
||||
{
|
||||
try
|
||||
{
|
||||
return (byte[])value;
|
||||
}
|
||||
catch (ClassCastException cce)
|
||||
{
|
||||
throw new InvalidBEncodingException(cce.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as a Number. This operation only succeeds
|
||||
* when the BEValue is actually a Number, otherwise it will throw a
|
||||
* InvalidBEncodingException.
|
||||
*/
|
||||
public Number getNumber() throws InvalidBEncodingException
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Number)value;
|
||||
}
|
||||
catch (ClassCastException cce)
|
||||
{
|
||||
throw new InvalidBEncodingException(cce.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as int. This operation only succeeds when
|
||||
* the BEValue is actually a Number, otherwise it will throw a
|
||||
* InvalidBEncodingException. The returned int is the result of
|
||||
* <code>Number.intValue()</code>.
|
||||
*/
|
||||
public int getInt() throws InvalidBEncodingException
|
||||
{
|
||||
return getNumber().intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as long. This operation only succeeds when
|
||||
* the BEValue is actually a Number, otherwise it will throw a
|
||||
* InvalidBEncodingException. The returned long is the result of
|
||||
* <code>Number.longValue()</code>.
|
||||
*/
|
||||
public long getLong() throws InvalidBEncodingException
|
||||
{
|
||||
return getNumber().longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as a List of BEValues. This operation only
|
||||
* succeeds when the BEValue is actually a List, otherwise it will
|
||||
* throw a InvalidBEncodingException.
|
||||
*/
|
||||
public List getList() throws InvalidBEncodingException
|
||||
{
|
||||
try
|
||||
{
|
||||
return (List)value;
|
||||
}
|
||||
catch (ClassCastException cce)
|
||||
{
|
||||
throw new InvalidBEncodingException(cce.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this BEValue as a Map of BEValue keys and BEValue
|
||||
* values. This operation only succeeds when the BEValue is actually
|
||||
* a Map, otherwise it will throw a InvalidBEncodingException.
|
||||
*/
|
||||
public Map getMap() throws InvalidBEncodingException
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Map)value;
|
||||
}
|
||||
catch (ClassCastException cce)
|
||||
{
|
||||
throw new InvalidBEncodingException(cce.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/** return the untyped value */
|
||||
public Object getValue() { return value; }
|
||||
|
||||
public String toString()
|
||||
{
|
||||
String valueString;
|
||||
if (value instanceof byte[])
|
||||
{
|
||||
byte[] bs = (byte[])value;
|
||||
// XXX - Stupid heuristic...
|
||||
if (bs.length <= 12)
|
||||
valueString = new String(bs);
|
||||
else
|
||||
valueString = "bytes:" + bs.length;
|
||||
}
|
||||
else
|
||||
valueString = value.toString();
|
||||
|
||||
return "BEValue[" + valueString + "]";
|
||||
}
|
||||
}
|
||||
193
apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
Normal file
193
apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java
Normal file
@@ -0,0 +1,193 @@
|
||||
/* BDecoder - Converts an InputStream to BEValues.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark.bencode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class BEncoder
|
||||
{
|
||||
|
||||
public static byte[] bencode(Object o) throws IllegalArgumentException
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bencode(o, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
throw new InternalError(ioe.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void bencode(Object o, OutputStream out)
|
||||
throws IOException, IllegalArgumentException
|
||||
{
|
||||
if (o instanceof String)
|
||||
bencode((String)o, out);
|
||||
else if (o instanceof byte[])
|
||||
bencode((byte[])o, out);
|
||||
else if (o instanceof Number)
|
||||
bencode((Number)o, out);
|
||||
else if (o instanceof List)
|
||||
bencode((List)o, out);
|
||||
else if (o instanceof Map)
|
||||
bencode((Map)o, out);
|
||||
else if (o instanceof BEValue)
|
||||
bencode(((BEValue)o).getValue(), out);
|
||||
else
|
||||
throw new IllegalArgumentException("Cannot bencode: " + o.getClass());
|
||||
}
|
||||
|
||||
public static byte[] bencode(String s)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bencode(s, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
throw new InternalError(ioe.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void bencode(String s, OutputStream out) throws IOException
|
||||
{
|
||||
byte[] bs = s.getBytes("UTF-8");
|
||||
bencode(bs, out);
|
||||
}
|
||||
|
||||
public static byte[] bencode(Number n)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bencode(n, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
throw new InternalError(ioe.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void bencode(Number n, OutputStream out) throws IOException
|
||||
{
|
||||
out.write('i');
|
||||
String s = n.toString();
|
||||
out.write(s.getBytes("UTF-8"));
|
||||
out.write('e');
|
||||
}
|
||||
|
||||
public static byte[] bencode(List l)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bencode(l, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
throw new InternalError(ioe.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void bencode(List l, OutputStream out) throws IOException
|
||||
{
|
||||
out.write('l');
|
||||
Iterator it = l.iterator();
|
||||
while (it.hasNext())
|
||||
bencode(it.next(), out);
|
||||
out.write('e');
|
||||
}
|
||||
|
||||
public static byte[] bencode(byte[] bs)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bencode(bs, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
throw new InternalError(ioe.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void bencode(byte[] bs, OutputStream out) throws IOException
|
||||
{
|
||||
String l = Integer.toString(bs.length);
|
||||
out.write(l.getBytes("UTF-8"));
|
||||
out.write(':');
|
||||
out.write(bs);
|
||||
}
|
||||
|
||||
public static byte[] bencode(Map m)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bencode(m, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
throw new InternalError(ioe.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void bencode(Map m, OutputStream out) throws IOException
|
||||
{
|
||||
out.write('d');
|
||||
|
||||
// Keys must be sorted. XXX - But is this the correct order?
|
||||
Set s = m.keySet();
|
||||
List l = new ArrayList(s);
|
||||
Collections.sort(l);
|
||||
|
||||
Iterator it = l.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
// Keys must be Strings.
|
||||
String key = (String)it.next();
|
||||
Object value = m.get(key);
|
||||
bencode(key, out);
|
||||
bencode(value, out);
|
||||
}
|
||||
|
||||
out.write('e');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* InvalidBEncodingException - Thrown when a bencoded stream is corrupted.
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
This file is part of Snark.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
package org.klomp.snark.bencode;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a bencoded stream is corrupted.
|
||||
*
|
||||
* @author Mark Wielaard (mark@klomp.org)
|
||||
*/
|
||||
public class InvalidBEncodingException extends IOException
|
||||
{
|
||||
public InvalidBEncodingException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
641
apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
Normal file
641
apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
Normal file
@@ -0,0 +1,641 @@
|
||||
package org.klomp.snark.web;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
import org.klomp.snark.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class I2PSnarkServlet extends HttpServlet {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private SnarkManager _manager;
|
||||
private static long _nonce;
|
||||
|
||||
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
|
||||
|
||||
public void init(ServletConfig cfg) throws ServletException {
|
||||
super.init(cfg);
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_log = _context.logManager().getLog(I2PSnarkServlet.class);
|
||||
_nonce = _context.random().nextLong();
|
||||
_manager = SnarkManager.instance();
|
||||
String configFile = _context.getProperty(PROP_CONFIG_FILE);
|
||||
if ( (configFile == null) || (configFile.trim().length() <= 0) )
|
||||
configFile = "i2psnark.config";
|
||||
_manager.loadConfig(configFile);
|
||||
}
|
||||
|
||||
public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
req.setCharacterEncoding("UTF-8");
|
||||
resp.setCharacterEncoding("UTF-8");
|
||||
resp.setContentType("text/html; charset=UTF-8");
|
||||
|
||||
String nonce = req.getParameter("nonce");
|
||||
if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) )
|
||||
processRequest(req);
|
||||
|
||||
PrintWriter out = resp.getWriter();
|
||||
out.write(HEADER_BEGIN);
|
||||
// we want it to go to the base URI so we don't refresh with some funky action= value
|
||||
out.write("<meta http-equiv=\"refresh\" content=\"60;" + req.getRequestURI() + "\">\n");
|
||||
out.write(HEADER);
|
||||
|
||||
out.write("<table border=\"0\" width=\"100%\">\n");
|
||||
out.write("<tr><td width=\"5%\" class=\"snarkTitle\" valign=\"top\" align=\"left\">");
|
||||
out.write("I2PSnark<br />\n");
|
||||
out.write("<a href=\"" + req.getRequestURI() + "\" class=\"snarkRefresh\">Refresh</a>\n");
|
||||
out.write("</td><td width=\"95%\" class=\"snarkMessages\" valign=\"top\" align=\"left\"><pre>");
|
||||
List msgs = _manager.getMessages();
|
||||
for (int i = msgs.size()-1; i >= 0; i--) {
|
||||
String msg = (String)msgs.get(i);
|
||||
out.write(msg + "\n");
|
||||
}
|
||||
out.write("</pre></td></tr></table>\n");
|
||||
|
||||
out.write(TABLE_HEADER);
|
||||
|
||||
List snarks = getSortedSnarks(req);
|
||||
String uri = req.getRequestURI();
|
||||
for (int i = 0; i < snarks.size(); i++) {
|
||||
Snark snark = (Snark)snarks.get(i);
|
||||
displaySnark(out, snark, uri, i);
|
||||
}
|
||||
if (snarks.size() <= 0) {
|
||||
out.write(TABLE_EMPTY);
|
||||
}
|
||||
|
||||
out.write(TABLE_FOOTER);
|
||||
writeAddForm(out, req);
|
||||
if (true) // seeding needs to register the torrent first, so we can't start it automatically (boo, hiss)
|
||||
writeSeedForm(out, req);
|
||||
writeConfigForm(out, req);
|
||||
out.write(FOOTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do what they ask, adding messages to _manager.addMessage as necessary
|
||||
*/
|
||||
private void processRequest(HttpServletRequest req) {
|
||||
String action = req.getParameter("action");
|
||||
if (action == null) {
|
||||
// noop
|
||||
} else if ("Add torrent".equals(action)) {
|
||||
String newFile = req.getParameter("newFile");
|
||||
String newURL = req.getParameter("newURL");
|
||||
File f = null;
|
||||
if ( (newFile != null) && (newFile.trim().length() > 0) )
|
||||
f = new File(newFile.trim());
|
||||
if ( (f != null) && (!f.exists()) ) {
|
||||
_manager.addMessage("Torrent file " + newFile +" does not exist");
|
||||
}
|
||||
if ( (f != null) && (f.exists()) ) {
|
||||
File local = new File(_manager.getDataDir(), f.getName());
|
||||
String canonical = null;
|
||||
try {
|
||||
canonical = local.getCanonicalPath();
|
||||
|
||||
if (local.exists()) {
|
||||
if (_manager.getTorrent(canonical) != null)
|
||||
_manager.addMessage("Torrent already running: " + newFile);
|
||||
else
|
||||
_manager.addMessage("Torrent already in the queue: " + newFile);
|
||||
} else {
|
||||
boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true);
|
||||
if (ok) {
|
||||
_manager.addMessage("Copying torrent to " + local.getAbsolutePath());
|
||||
_manager.addTorrent(canonical);
|
||||
} else {
|
||||
_manager.addMessage("Unable to copy the torrent to " + local.getAbsolutePath() + " from " + f.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
_log.warn("hrm: " + local, ioe);
|
||||
}
|
||||
} else if ( (newURL != null) && (newURL.trim().length() > "http://.i2p/".length()) ) {
|
||||
_manager.addMessage("Fetching " + newURL);
|
||||
I2PThread fetch = new I2PThread(new FetchAndAdd(_manager, newURL), "Fetch and add");
|
||||
fetch.start();
|
||||
} else {
|
||||
// no file or URL specified
|
||||
}
|
||||
} else if ("Stop".equals(action)) {
|
||||
String torrent = req.getParameter("torrent");
|
||||
if (torrent != null) {
|
||||
byte infoHash[] = Base64.decode(torrent);
|
||||
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
|
||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
Snark snark = _manager.getTorrent(name);
|
||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
||||
_manager.stopTorrent(name, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("Start".equals(action)) {
|
||||
String torrent = req.getParameter("torrent");
|
||||
if (torrent != null) {
|
||||
byte infoHash[] = Base64.decode(torrent);
|
||||
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
|
||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
Snark snark = _manager.getTorrent(name);
|
||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
||||
snark.startTorrent();
|
||||
_manager.addMessage("Starting up torrent " + name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("Remove".equals(action)) {
|
||||
String torrent = req.getParameter("torrent");
|
||||
if (torrent != null) {
|
||||
byte infoHash[] = Base64.decode(torrent);
|
||||
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
|
||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
Snark snark = _manager.getTorrent(name);
|
||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
||||
_manager.stopTorrent(name, true);
|
||||
// should we delete the torrent file?
|
||||
// yeah, need to, otherwise it'll get autoadded again (at the moment
|
||||
File f = new File(name);
|
||||
f.delete();
|
||||
_manager.addMessage("Torrent file deleted: " + f.getAbsolutePath());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("Delete".equals(action)) {
|
||||
String torrent = req.getParameter("torrent");
|
||||
if (torrent != null) {
|
||||
byte infoHash[] = Base64.decode(torrent);
|
||||
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
|
||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
Snark snark = _manager.getTorrent(name);
|
||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
||||
_manager.stopTorrent(name, true);
|
||||
File f = new File(name);
|
||||
f.delete();
|
||||
_manager.addMessage("Torrent file deleted: " + f.getAbsolutePath());
|
||||
List files = snark.meta.getFiles();
|
||||
String dataFile = snark.meta.getName();
|
||||
for (int i = 0; files != null && i < files.size(); i++) {
|
||||
// multifile torrents have the getFiles() return lists of lists of filenames, but
|
||||
// each of those lists just contain a single file afaict...
|
||||
File df = new File(_manager.getDataDir(), files.get(i).toString());
|
||||
boolean deleted = FileUtil.rmdir(df, false);
|
||||
if (deleted)
|
||||
_manager.addMessage("Data dir deleted: " + df.getAbsolutePath());
|
||||
else
|
||||
_manager.addMessage("Data dir could not be deleted: " + df.getAbsolutePath());
|
||||
}
|
||||
if (dataFile != null) {
|
||||
f = new File(_manager.getDataDir(), dataFile);
|
||||
boolean deleted = f.delete();
|
||||
if (deleted)
|
||||
_manager.addMessage("Data file deleted: " + f.getAbsolutePath());
|
||||
else
|
||||
_manager.addMessage("Data file could not be deleted: " + f.getAbsolutePath());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("Save configuration".equals(action)) {
|
||||
String dataDir = req.getParameter("dataDir");
|
||||
boolean autoStart = req.getParameter("autoStart") != null;
|
||||
String seedPct = req.getParameter("seedPct");
|
||||
String eepHost = req.getParameter("eepHost");
|
||||
String eepPort = req.getParameter("eepPort");
|
||||
String i2cpHost = req.getParameter("i2cpHost");
|
||||
String i2cpPort = req.getParameter("i2cpPort");
|
||||
String i2cpOpts = req.getParameter("i2cpOpts");
|
||||
_manager.updateConfig(dataDir, autoStart, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts);
|
||||
} else if ("Create torrent".equals(action)) {
|
||||
String baseData = req.getParameter("baseFile");
|
||||
if (baseData != null) {
|
||||
File baseFile = new File(_manager.getDataDir(), baseData);
|
||||
String announceURL = req.getParameter("announceURL");
|
||||
String announceURLOther = req.getParameter("announceURLOther");
|
||||
if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
|
||||
announceURL = announceURLOther;
|
||||
|
||||
if (baseFile.exists() && baseFile.isFile()) {
|
||||
try {
|
||||
Storage s = new Storage(baseFile, announceURL, null);
|
||||
s.create();
|
||||
MetaInfo info = s.getMetaInfo();
|
||||
File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
|
||||
if (torrentFile.exists())
|
||||
throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
|
||||
FileOutputStream out = new FileOutputStream(torrentFile);
|
||||
out.write(info.getTorrentData());
|
||||
out.close();
|
||||
_manager.addMessage("Torrent created for " + baseFile.getName() + ": " + torrentFile.getAbsolutePath());
|
||||
// now fire it up, but don't automatically seed it
|
||||
_manager.addTorrent(torrentFile.getCanonicalPath(), false);
|
||||
_manager.addMessage("Many I2P trackers require you to register new torrents before seeding - please do so before starting " + baseFile.getName());
|
||||
} catch (IOException ioe) {
|
||||
_manager.addMessage("Error creating a torrent for " + baseFile.getAbsolutePath() + ": " + ioe.getMessage());
|
||||
}
|
||||
} else if (baseFile.exists()) {
|
||||
_manager.addMessage("I2PSnark doesn't yet support creating multifile torrents");
|
||||
} else {
|
||||
_manager.addMessage("Cannot create a torrent for the nonexistant data: " + baseFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List getSortedSnarks(HttpServletRequest req) {
|
||||
Set files = _manager.listTorrentFiles();
|
||||
TreeSet fileNames = new TreeSet(files); // sorts it alphabetically
|
||||
ArrayList rv = new ArrayList(fileNames.size());
|
||||
for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
Snark snark = _manager.getTorrent(name);
|
||||
if (snark != null)
|
||||
rv.add(snark);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static final int MAX_DISPLAYED_FILENAME_LENGTH = 60;
|
||||
private void displaySnark(PrintWriter out, Snark snark, String uri, int row) throws IOException {
|
||||
String filename = snark.torrent;
|
||||
File f = new File(filename);
|
||||
filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
|
||||
if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH)
|
||||
filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "...";
|
||||
long total = snark.meta.getTotalLength();
|
||||
// Early typecast, avoid possibly overflowing a temp integer
|
||||
long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0);
|
||||
if (remaining > total)
|
||||
remaining = total;
|
||||
int totalBps = 4096; // should probably grab this from the snark...
|
||||
long remainingSeconds = remaining / totalBps;
|
||||
long uploaded = snark.coordinator.getUploaded();
|
||||
|
||||
boolean isRunning = !snark.stopped;
|
||||
boolean isValid = snark.meta != null;
|
||||
boolean singleFile = snark.meta.getFiles() == null;
|
||||
|
||||
String err = snark.coordinator.trackerProblems;
|
||||
int curPeers = snark.coordinator.getPeerCount();
|
||||
int knownPeers = snark.coordinator.trackerSeenPeers;
|
||||
|
||||
String statusString = "Unknown";
|
||||
if (err != null) {
|
||||
if (isRunning)
|
||||
statusString = "TrackerErr (" + curPeers + "/" + knownPeers + " peers)";
|
||||
else
|
||||
statusString = "TrackerErr (" + err + ")";
|
||||
} else if (remaining <= 0) {
|
||||
if (isRunning)
|
||||
statusString = "Seeding (" + curPeers + "/" + knownPeers + " peers)";
|
||||
else
|
||||
statusString = "Complete";
|
||||
} else {
|
||||
if (isRunning)
|
||||
statusString = "OK (" + curPeers + "/" + knownPeers + " peers)";
|
||||
else
|
||||
statusString = "Stopped";
|
||||
}
|
||||
|
||||
String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
|
||||
out.write("<tr class=\"" + rowClass + "\">");
|
||||
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentStatus " + rowClass + "\">");
|
||||
out.write(statusString + "</td>\n\t");
|
||||
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentName " + rowClass + "\">");
|
||||
|
||||
if (remaining == 0)
|
||||
out.write("<a href=\"file:///" + _manager.getDataDir().getAbsolutePath() + File.separatorChar + snark.meta.getName()
|
||||
+ "\" title=\"Download the completed file\">");
|
||||
out.write(filename);
|
||||
if (remaining == 0)
|
||||
out.write("</a>");
|
||||
out.write("</td>\n\t");
|
||||
|
||||
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
|
||||
if (remaining > 0) {
|
||||
out.write(formatSize(total-remaining) + "/" + formatSize(total)); // 18MB/3GB
|
||||
// lets hold off on the ETA until we have rates sorted...
|
||||
//out.write(" (eta " + DataHelper.formatDuration(remainingSeconds*1000) + ")"); // (eta 6h)
|
||||
} else {
|
||||
out.write(formatSize(total)); // 3GB
|
||||
}
|
||||
out.write("</td>\n\t");
|
||||
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentUploaded " + rowClass
|
||||
+ "\">" + formatSize(uploaded) + "</td>\n\t");
|
||||
//out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentRate\">");
|
||||
//out.write("n/a"); //2KBps/12KBps/4KBps
|
||||
//out.write("</td>\n\t");
|
||||
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentAction " + rowClass + "\">");
|
||||
if (isRunning) {
|
||||
out.write("<a href=\"" + uri + "?action=Stop&nonce=" + _nonce
|
||||
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
|
||||
+ "\" title=\"Stop the torrent\">Stop</a>");
|
||||
} else {
|
||||
if (isValid)
|
||||
out.write("<a href=\"" + uri + "?action=Start&nonce=" + _nonce
|
||||
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
|
||||
+ "\" title=\"Start the torrent\">Start</a> ");
|
||||
out.write("<a href=\"" + uri + "?action=Remove&nonce=" + _nonce
|
||||
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
|
||||
+ "\" title=\"Remove the torrent from the active list, deleting the .torrent file\">Remove</a><br />");
|
||||
out.write("<a href=\"" + uri + "?action=Delete&nonce=" + _nonce
|
||||
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
|
||||
+ "\" title=\"Delete the .torrent file and the associated data file(s)\">Delete</a> ");
|
||||
}
|
||||
out.write("</td>\n</tr>\n");
|
||||
}
|
||||
|
||||
private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
|
||||
String uri = req.getRequestURI();
|
||||
String newURL = req.getParameter("newURL");
|
||||
if ( (newURL == null) || (newURL.trim().length() <= 0) ) newURL = "http://";
|
||||
String newFile = req.getParameter("newFile");
|
||||
if ( (newFile == null) || (newFile.trim().length() <= 0) ) newFile = "";
|
||||
|
||||
out.write("<span class=\"snarkNewTorrent\">\n");
|
||||
// *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
|
||||
out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
|
||||
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
|
||||
out.write("From URL : <input type=\"text\" name=\"newURL\" size=\"50\" value=\"" + newURL + "\" /> \n");
|
||||
// not supporting from file at the moment, since the file name passed isn't always absolute (so it may not resolve)
|
||||
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n");
|
||||
out.write("<input type=\"submit\" value=\"Add torrent\" name=\"action\" /><br />\n");
|
||||
out.write("<span class=\"snarkAddInfo\">Alternately, you can copy .torrent files to " + _manager.getDataDir().getAbsolutePath() + "<br />\n");
|
||||
out.write("Removing that .torrent file will cause the torrent to stop.<br /></span>\n");
|
||||
out.write("</form>\n</span>\n");
|
||||
}
|
||||
|
||||
private void writeSeedForm(PrintWriter out, HttpServletRequest req) throws IOException {
|
||||
String uri = req.getRequestURI();
|
||||
String baseFile = req.getParameter("baseFile");
|
||||
if (baseFile == null)
|
||||
baseFile = "";
|
||||
|
||||
out.write("<span class=\"snarkNewTorrent\">\n");
|
||||
// *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
|
||||
out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
|
||||
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
|
||||
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n");
|
||||
out.write("Data to seed: " + _manager.getDataDir().getAbsolutePath() + File.separatorChar
|
||||
+ "<input type=\"text\" name=\"baseFile\" size=\"20\" value=\"" + baseFile
|
||||
+ "\" title=\"File to seed (must be within the specified path)\" /><br />\n");
|
||||
out.write("Tracker: <select name=\"announceURL\"><option value=\"\">Select a tracker</option>\n");
|
||||
Map trackers = sort(_manager.getTrackers());
|
||||
for (Iterator iter = trackers.keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
String announceURL = (String)trackers.get(name);
|
||||
// we inject whitespace in sort(...) to guarantee uniqueness, but we can strip it off here
|
||||
out.write("\t<option value=\"" + announceURL + "\">" + name.trim() + "</option>\n");
|
||||
}
|
||||
out.write("</select>\n");
|
||||
out.write("or <input type=\"text\" name=\"announceURLOther\" size=\"50\" value=\"http://\" " +
|
||||
"title=\"Custom tracker URL\" /> ");
|
||||
out.write("<input type=\"submit\" value=\"Create torrent\" name=\"action\" />\n");
|
||||
out.write("</form>\n</span>\n");
|
||||
}
|
||||
|
||||
private Map sort(Map trackers) {
|
||||
TreeMap rv = new TreeMap();
|
||||
for (Iterator iter = trackers.keySet().iterator(); iter.hasNext(); ) {
|
||||
String url = (String)iter.next();
|
||||
String name = (String)trackers.get(url);
|
||||
while (rv.containsKey(name))
|
||||
name = name + " ";
|
||||
rv.put(name, url);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException {
|
||||
String uri = req.getRequestURI();
|
||||
String dataDir = _manager.getDataDir().getAbsolutePath();
|
||||
boolean autoStart = _manager.shouldAutoStart();
|
||||
int seedPct = 0;
|
||||
|
||||
out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
|
||||
out.write("<span class=\"snarkConfig\"><hr />\n");
|
||||
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
|
||||
out.write("<span class=\"snarkConfigTitle\">Configuration:</span><br />\n");
|
||||
out.write("Data directory: <input type=\"text\" size=\"40\" name=\"dataDir\" value=\"" + dataDir + "\" ");
|
||||
out.write("title=\"Directory to store torrents and data\" disabled=\"true\" /><br />\n");
|
||||
out.write("Auto start: <input type=\"checkbox\" name=\"autoStart\" value=\"true\" "
|
||||
+ (autoStart ? "checked " : "")
|
||||
+ "title=\"If true, automatically start torrents that are added\" />");
|
||||
//Auto add: <input type="checkbox" name="autoAdd" value="true" title="If true, automatically add torrents that are found in the data directory" />
|
||||
//Auto stop: <input type="checkbox" name="autoStop" value="true" title="If true, automatically stop torrents that are removed from the data directory" />
|
||||
//out.write("<br />\n");
|
||||
out.write("Seed percentage: <select name=\"seedPct\" disabled=\"true\" >\n\t");
|
||||
if (seedPct <= 0)
|
||||
out.write("<option value=\"0\" selected=\"true\">Unlimited</option>\n\t");
|
||||
else
|
||||
out.write("<option value=\"0\">Unlimited</option>\n\t");
|
||||
if (seedPct == 100)
|
||||
out.write("<option value=\"100\" selected=\"true\">100%</option>\n\t");
|
||||
else
|
||||
out.write("<option value=\"100\">100%</option>\n\t");
|
||||
if (seedPct == 150)
|
||||
out.write("<option value=\"150\" selected=\"true\">150%</option>\n\t");
|
||||
else
|
||||
out.write("<option value=\"150\">150%</option>\n\t");
|
||||
out.write("</select><br />\n");
|
||||
|
||||
//out.write("<hr />\n");
|
||||
out.write("EepProxy host: <input type=\"text\" name=\"eepHost\" value=\""
|
||||
+ I2PSnarkUtil.instance().getEepProxyHost() + "\" size=\"15\" /> ");
|
||||
out.write("port: <input type=\"text\" name=\"eepPort\" value=\""
|
||||
+ I2PSnarkUtil.instance().getEepProxyPort() + "\" size=\"5\" /><br />\n");
|
||||
out.write("I2CP host: <input type=\"text\" name=\"i2cpHost\" value=\""
|
||||
+ I2PSnarkUtil.instance().getI2CPHost() + "\" size=\"15\" /> ");
|
||||
out.write("port: <input type=\"text\" name=\"i2cpPort\" value=\"" +
|
||||
+ I2PSnarkUtil.instance().getI2CPPort() + "\" size=\"5\" /> <br />\n");
|
||||
StringBuffer opts = new StringBuffer(64);
|
||||
Map options = new TreeMap(I2PSnarkUtil.instance().getI2CPOptions());
|
||||
for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) {
|
||||
String key = (String)iter.next();
|
||||
String val = (String)options.get(key);
|
||||
opts.append(key).append('=').append(val).append(' ');
|
||||
}
|
||||
out.write("I2CP opts: <input type=\"text\" name=\"i2cpOpts\" size=\"40\" value=\""
|
||||
+ opts.toString() + "\" /><br />\n");
|
||||
out.write("<input type=\"submit\" value=\"Save configuration\" name=\"action\" />\n");
|
||||
out.write("</span>\n");
|
||||
out.write("</form>\n");
|
||||
}
|
||||
|
||||
private String formatSize(long bytes) {
|
||||
if (bytes < 5*1024)
|
||||
return bytes + "B";
|
||||
else if (bytes < 5*1024*1024)
|
||||
return (bytes/1024) + "KB";
|
||||
else if (bytes < 5*1024*1024*1024l)
|
||||
return (bytes/(1024*1024)) + "MB";
|
||||
else
|
||||
return (bytes/(1024*1024*1024)) + "GB";
|
||||
}
|
||||
|
||||
private static final String HEADER_BEGIN = "<html>\n" +
|
||||
"<head>\n" +
|
||||
"<title>I2PSnark - anonymous bittorrent</title>\n";
|
||||
|
||||
private static final String HEADER = "<style>\n" +
|
||||
"body {\n" +
|
||||
" background-color: #C7CFB4;\n" +
|
||||
"}\n" +
|
||||
".snarkTitle {\n" +
|
||||
" text-align: left;\n" +
|
||||
" float: left;\n" +
|
||||
" margin: 0px 0px 5px 5px;\n" +
|
||||
" display: inline;\n" +
|
||||
" font-size: 16pt;\n" +
|
||||
" font-weight: bold;\n" +
|
||||
"}\n" +
|
||||
".snarkRefresh {\n" +
|
||||
" font-size: 10pt;\n" +
|
||||
"}\n" +
|
||||
".snarkMessages {\n" +
|
||||
" border: none;\n" +
|
||||
" background-color: #CECFC6;\n" +
|
||||
" font-family: monospace;\n" +
|
||||
" font-size: 10pt;\n" +
|
||||
" font-weight: 100;\n" +
|
||||
" width: 100%;\n" +
|
||||
" text-align: left;\n" +
|
||||
" margin: 0px 0px 0px 0px;\n" +
|
||||
" border: 0px;\n" +
|
||||
" padding: 5px;\n" +
|
||||
" border-width: 0px;\n" +
|
||||
" border-spacing: 0px;\n" +
|
||||
"}\n" +
|
||||
"table {\n" +
|
||||
" margin: 0px 0px 0px 0px;\n" +
|
||||
" border: 0px;\n" +
|
||||
" padding: 0px;\n" +
|
||||
" border-width: 0px;\n" +
|
||||
" border-spacing: 0px;\n" +
|
||||
"}\n" +
|
||||
"th {\n" +
|
||||
" background-color: #C7D5D5;\n" +
|
||||
" margin: 0px 0px 0px 0px;\n" +
|
||||
"}\n" +
|
||||
".snarkTorrentEven {\n" +
|
||||
" background-color: #E7E7E7;\n" +
|
||||
"}\n" +
|
||||
".snarkTorrentOdd {\n" +
|
||||
" background-color: #DDDDCC;\n" +
|
||||
"}\n" +
|
||||
".snarkNewTorrent {\n" +
|
||||
" font-size: 10pt;\n" +
|
||||
" font-family: monospace;\n" +
|
||||
" background-color: #ADAE9;\n" +
|
||||
"}\n" +
|
||||
".snarkAddInfo {\n" +
|
||||
" font-size: 10pt;\n" +
|
||||
"}\n" +
|
||||
".snarkConfigTitle {\n" +
|
||||
" font-size: 12pt;\n" +
|
||||
" font-weight: bold;\n" +
|
||||
"}\n" +
|
||||
".snarkConfig {\n" +
|
||||
" font-size: 10pt;\n" +
|
||||
"}\n" +
|
||||
"</style>\n" +
|
||||
"</head>\n" +
|
||||
"<body>\n";
|
||||
|
||||
|
||||
private static final String TABLE_HEADER = "<table border=\"0\" class=\"snarkTorrents\" width=\"100%\">\n" +
|
||||
"<thead>\n" +
|
||||
"<tr><th align=\"left\" valign=\"top\">Status</th>\n" +
|
||||
" <th align=\"left\" valign=\"top\">Torrent</th>\n" +
|
||||
" <th align=\"left\" valign=\"top\">Downloaded</th>\n" +
|
||||
" <th align=\"left\" valign=\"top\">Uploaded</th>\n" +
|
||||
//" <th align=\"left\" valign=\"top\">Rate</th>\n" +
|
||||
" <th> </th></tr>\n" +
|
||||
"</thead>\n";
|
||||
|
||||
private static final String TABLE_EMPTY = "<tr class=\"snarkTorrentEven\">" +
|
||||
"<td class=\"snarkTorrentEven\" align=\"left\"" +
|
||||
" valign=\"top\" colspan=\"5\">No torrents</td></tr>\n";
|
||||
|
||||
private static final String TABLE_FOOTER = "</table>\n";
|
||||
|
||||
private static final String FOOTER = "</body></html>";
|
||||
}
|
||||
|
||||
|
||||
class FetchAndAdd implements Runnable {
|
||||
private SnarkManager _manager;
|
||||
private String _url;
|
||||
public FetchAndAdd(SnarkManager mgr, String url) {
|
||||
_manager = mgr;
|
||||
_url = url;
|
||||
}
|
||||
public void run() {
|
||||
_url = _url.trim();
|
||||
File file = I2PSnarkUtil.instance().get(_url, false);
|
||||
try {
|
||||
if ( (file != null) && (file.exists()) && (file.length() > 0) ) {
|
||||
_manager.addMessage("Torrent fetched from " + _url);
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(file);
|
||||
MetaInfo info = new MetaInfo(in);
|
||||
String name = info.getName();
|
||||
name = name.replace('/', '_');
|
||||
name = name.replace('\\', '_');
|
||||
name = name.replace('&', '+');
|
||||
name = name.replace('\'', '_');
|
||||
name = name.replace('"', '_');
|
||||
name = name.replace('`', '_');
|
||||
name = name + ".torrent";
|
||||
File torrentFile = new File(_manager.getDataDir(), name);
|
||||
|
||||
String canonical = torrentFile.getCanonicalPath();
|
||||
|
||||
if (torrentFile.exists()) {
|
||||
if (_manager.getTorrent(canonical) != null)
|
||||
_manager.addMessage("Torrent already running: " + name);
|
||||
else
|
||||
_manager.addMessage("Torrent already in the queue: " + name);
|
||||
} else {
|
||||
FileUtil.copy(file.getAbsolutePath(), canonical, true);
|
||||
_manager.addTorrent(canonical);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
_manager.addMessage("Torrent at " + _url + " was not valid: " + ioe.getMessage());
|
||||
} finally {
|
||||
try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
} else {
|
||||
_manager.addMessage("Torrent was not retrieved from " + _url);
|
||||
}
|
||||
} finally {
|
||||
if (file != null) file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.klomp.snark.web;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import net.i2p.util.FileUtil;
|
||||
import org.mortbay.jetty.Server;
|
||||
|
||||
public class RunStandalone {
|
||||
private Server _server;
|
||||
|
||||
static {
|
||||
System.setProperty("org.mortbay.http.Version.paranoid", "true");
|
||||
System.setProperty("org.mortbay.xml.XmlParser.NotValidating", "true");
|
||||
}
|
||||
|
||||
private RunStandalone(String args[]) {}
|
||||
|
||||
public static void main(String args[]) {
|
||||
RunStandalone runner = new RunStandalone(args);
|
||||
runner.start();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
File workDir = new File("work");
|
||||
boolean workDirRemoved = FileUtil.rmdir(workDir, false);
|
||||
if (!workDirRemoved)
|
||||
System.err.println("ERROR: Unable to remove Jetty temporary work directory");
|
||||
boolean workDirCreated = workDir.mkdirs();
|
||||
if (!workDirCreated)
|
||||
System.err.println("ERROR: Unable to create Jetty temporary work directory");
|
||||
|
||||
try {
|
||||
_server = new Server("jetty-i2psnark.xml");
|
||||
_server.start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
_server.stop();
|
||||
} catch (InterruptedException ie) {
|
||||
ie.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
114
apps/i2psnark/jetty-i2psnark.xml
Normal file
114
apps/i2psnark/jetty-i2psnark.xml
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.2//EN" "http://jetty.mortbay.org/configure_1_2.dtd">
|
||||
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Jetty Server -->
|
||||
<!-- =============================================================== -->
|
||||
<Configure class="org.mortbay.jetty.Server">
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Request Listeners -->
|
||||
<!-- =============================================================== -->
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Add and configure a HTTP listener to port 8080 -->
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<Call name="addListener">
|
||||
<Arg>
|
||||
<New class="org.mortbay.http.SocketListener">
|
||||
<Arg>
|
||||
<New class="org.mortbay.util.InetAddrPort">
|
||||
<Set name="host">127.0.0.1</Set>
|
||||
<Set name="port">8002</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
<Set name="MinThreads">3</Set>
|
||||
<Set name="MaxThreads">10</Set>
|
||||
<Set name="MaxIdleTimeMs">30000</Set>
|
||||
<Set name="LowResourcePersistTimeMs">1000</Set>
|
||||
<Set name="ConfidentialPort">8443</Set>
|
||||
<Set name="IntegralPort">8443</Set>
|
||||
<Set name="PoolName">main</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Add a HTTPS SSL listener on port 8443 -->
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- UNCOMMENT TO ACTIVATE
|
||||
<Call name="addListener">
|
||||
<Arg>
|
||||
<New class="org.mortbay.http.SunJsseListener">
|
||||
<Set name="Port">8443</Set>
|
||||
<Set name="PoolName">main</Set>
|
||||
<Set name="Keystore"><SystemProperty name="jetty.home" default="."/>/etc/demokeystore</Set>
|
||||
<Set name="Password">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set>
|
||||
<Set name="KeyPassword">OBF:1u2u1wml1z7s1z7a1wnl1u2g</Set>
|
||||
<Set name="NonPersistentUserAgent">MSIE 5</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
-->
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Add a AJP13 listener on port 8009 -->
|
||||
<!-- This protocol can be used with mod_jk in apache, IIS etc. -->
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!--
|
||||
<Call name="addListener">
|
||||
<Arg>
|
||||
<New class="org.mortbay.http.ajp.AJP13Listener">
|
||||
<Set name="PoolName">ajp</Set>
|
||||
<Set name="Port">8009</Set>
|
||||
<Set name="MinThreads">3</Set>
|
||||
<Set name="MaxThreads">20</Set>
|
||||
<Set name="MaxIdleTimeMs">0</Set>
|
||||
<Set name="confidentialPort">443</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
-->
|
||||
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Contexts -->
|
||||
<!-- =============================================================== -->
|
||||
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Add a all web application within the webapps directory. -->
|
||||
<!-- + No virtual host specified -->
|
||||
<!-- + Look in the webapps directory relative to jetty.home or . -->
|
||||
<!-- + Use the default webdefault.xml in jetty's install -->
|
||||
<!-- + Upack the war file -->
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<Set name="rootWebApp">i2psnark</Set>
|
||||
<Call name="addWebApplication">
|
||||
<Arg>/</Arg>
|
||||
<Arg>webapps/i2psnark.war</Arg>
|
||||
</Call>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Request Log -->
|
||||
<!-- =============================================================== -->
|
||||
<Set name="RequestLog">
|
||||
<New class="org.mortbay.http.NCSARequestLog">
|
||||
<Arg>./logs/yyyy_mm_dd.i2psnark-request.log</Arg>
|
||||
<Set name="retainDays">90</Set>
|
||||
<Set name="append">true</Set>
|
||||
<Set name="extended">false</Set>
|
||||
<Set name="buffered">false</Set>
|
||||
<Set name="LogTimeZone">GMT</Set>
|
||||
</New>
|
||||
</Set>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Other Server Options -->
|
||||
<!-- =============================================================== -->
|
||||
<Set name="requestsPerGC">2000</Set>
|
||||
<Set name="statsOn">false</Set>
|
||||
|
||||
</Configure>
|
||||
6
apps/i2psnark/readme-standalone.txt
Normal file
6
apps/i2psnark/readme-standalone.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
To run I2PSnark from the command line, run "java -jar lib/i2psnark.jar", but
|
||||
to run it with the web UI, run "java -jar launch-i2psnark.jar". I2PSnark is
|
||||
GPL'ed software, based on Snark (http://www.klomp.org/) to run on top of I2P
|
||||
(http://www.i2p.net/) within a webserver (such as the bundled Jetty from
|
||||
http://jetty.mortbay.org/). For more information about I2PSnark, get in touch
|
||||
with the folks at http://forum.i2p.net/
|
||||
11
apps/i2psnark/readme.txt
Normal file
11
apps/i2psnark/readme.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
This is an I2P port of snark [http://klomp.org/snark], a GPL'ed bittorrent client
|
||||
|
||||
The build in tracker has been removed for simplicity.
|
||||
|
||||
Example usage:
|
||||
java -jar lib/i2psnark.jar myFile.torrent
|
||||
|
||||
or, a more verbose setting:
|
||||
java -jar lib/i2psnark.jar --eepproxy 127.0.0.1 4444 \
|
||||
--i2cp 127.0.0.1 7654 "inbound.length=2 outbound.length=2" \
|
||||
--debug 6 myFile.torrent
|
||||
141
apps/i2psnark/readme.txt.snark
Normal file
141
apps/i2psnark/readme.txt.snark
Normal file
@@ -0,0 +1,141 @@
|
||||
The Hunting of the Snark Project - BitTorrent Application Suite
|
||||
0.5 - The Beaver's Lesson (27 June 2003)
|
||||
|
||||
"It's a Snark!" was the sound that first came to their ears,
|
||||
And seemed almost too good to be true.
|
||||
Then followed a torrent of laughter and cheers:
|
||||
Then the ominous words "It's a Boo-"
|
||||
|
||||
-- from The Hunting Of The Snark by Lewis Carroll
|
||||
|
||||
Snark is a client for downloading and sharing files distributed with
|
||||
the BitTorrent protocol. It is mainly used for exploring the BitTorrent
|
||||
protocol and experimenting with the the GNU Compiler for Java (gcj).
|
||||
But it can also be used as a regular BitTorrent Client.
|
||||
|
||||
Snark can also act as a torrent creator, micro http server for delivering
|
||||
metainfo.torrent files and has an integrated Tracker for making sharing of
|
||||
files as easy as possible.
|
||||
|
||||
When you give the option --share Snark will automatically
|
||||
create a .torrent file, start a very simple webserver to distribute
|
||||
the metainfo.torrent file and a local tracker that other BitTorrent
|
||||
clients can connect to.
|
||||
|
||||
Distribution
|
||||
------------
|
||||
|
||||
Copyright (C) 2003 Mark J. Wielaard
|
||||
|
||||
Snark is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
Requirements/Installation
|
||||
-------------------------
|
||||
|
||||
The GNU Compiler for java (gcj) version 3.3 or later.
|
||||
(Earlier versions have a faulty SHA message digest implementation.)
|
||||
On Debian GNU/Linux based distributions just install the gcj-3.3 package.
|
||||
Edit the GCJ variable in the Makefile if your gcj binary is not gcj-3.3.
|
||||
|
||||
Typing 'make' will create the native snark binary and a snark.jar file
|
||||
for use with traditional java byte code interpreters.
|
||||
|
||||
It is possible to compile the sources with other java compilers
|
||||
like jikes or kjc to produce the snark.jar file. Edit the JAVAC and
|
||||
JAVAC_FLAGS variables on top of the Makefile for this. And type
|
||||
'make snark.jar' to create a jar file that can be used by traditional
|
||||
java bytecode interpreters like kaffe: 'kaffe -jar snark.jar'.
|
||||
You will need at least version 1.1 of kaffe for all functionality to work
|
||||
correctly ('--share' does not work with older versions).
|
||||
|
||||
When trying out the experimental Gnome frontend you also need the java-gnome
|
||||
bindings. On Debian GNU/Linux systems install the package libgnome0-java.
|
||||
You can try it out by typing 'make snark-gnome' and then run 'snark-gnome.sh'
|
||||
like you would with the normal command line client.
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
To use the program start it with:
|
||||
|
||||
snark [--debug [level]] [--no-commands] [--port <port>]
|
||||
[--share (<ip>|<host>)] (<url>|<file>|<dir>)
|
||||
--debug Shows some extra info and stacktraces.
|
||||
level How much debug details to show
|
||||
(defaults to 3, with --debug to 4, highest level is 6).
|
||||
--no-commands Don't read interactive commands or show usage info.
|
||||
--port The port to listen on for incomming connections
|
||||
(if not given defaults to first free port between 6881-6889).
|
||||
--share Start torrent tracker on <ip> address or <host> name.
|
||||
<url> URL pointing to .torrent metainfo file to download/share.
|
||||
<file> Either a local .torrent metainfo file to download
|
||||
or (with --share) a file to share.
|
||||
<dir> A directory with files to share (needs --share).
|
||||
|
||||
Since this is an early beta release there are probably still some bugs
|
||||
in the program. To help find them run the program with the --debug
|
||||
option which shows more information on what it going on. You can also give
|
||||
the level of debug output you want. Zero will give (almost) no output at all.
|
||||
Everything above debug level 4 is probably to much (only really useful to
|
||||
see what goes on on the protocol/network level).
|
||||
|
||||
Examples
|
||||
|
||||
- To simple start downloading/sharing a file.
|
||||
Either download the .torrent file to disk and start snark with:
|
||||
./snark somefile.torrent
|
||||
|
||||
Or give it the complete URL:
|
||||
./snark http://somehost.example.com/cd-images/bbc-lnx.iso.torrent
|
||||
|
||||
- To start seeding/sharing a local file:
|
||||
./snark --share my-host.example.com some-file
|
||||
|
||||
Snark will respond with:
|
||||
Listening on port: 6881
|
||||
Trying to create metainfo torrent for 'some-file'
|
||||
Creating torrent piece hashes: ++++++++++
|
||||
Torrent available on http://my-host.example.com:6881/metainfo.torrent
|
||||
|
||||
You can now point other people to the above URL so they can share
|
||||
the file with their own BitTorrent client.
|
||||
|
||||
Commands
|
||||
|
||||
While the program is running in text mode you can currently give the
|
||||
following commands: 'info', 'list' and 'quit'.
|
||||
|
||||
Interactive commands are disabled when the '--no-commands' flag is given.
|
||||
This is sometimes desireable for running snark in the background.
|
||||
|
||||
More information
|
||||
----------------
|
||||
|
||||
- The Evolution of Cooperation - Robert Axelrod
|
||||
ISBN 0-465-02121-2
|
||||
|
||||
- The BitTorrent protocol description:
|
||||
<http://bitconjurer.org/BitTorrent/protocol.html>
|
||||
|
||||
- The GNU Compiler for Java (gcj):
|
||||
<http://gcc.gnu.org/java/>
|
||||
|
||||
- java-gnome bindings : <http://java-gnome.sourceforge.net/>
|
||||
|
||||
- The Hunting of the Snark - Lewis Carroll
|
||||
|
||||
Comments welcome
|
||||
|
||||
- Mark Wielaard <mark@klomp.org>
|
||||
25
apps/i2psnark/web.xml
Normal file
25
apps/i2psnark/web.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE web-app
|
||||
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
|
||||
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">
|
||||
|
||||
<web-app>
|
||||
<servlet>
|
||||
<servlet-name>org.klomp.snark.web.I2PSnarkServlet</servlet-name>
|
||||
<servlet-class>org.klomp.snark.web.I2PSnarkServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<!-- precompiled servlets -->
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>org.klomp.snark.web.I2PSnarkServlet</servlet-name>
|
||||
<url-pattern>/</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<session-config>
|
||||
<session-timeout>
|
||||
30
|
||||
</session-timeout>
|
||||
</session-config>
|
||||
</web-app>
|
||||
@@ -4,24 +4,73 @@
|
||||
<target name="build" depends="builddep, jar" />
|
||||
<target name="builddep">
|
||||
<ant dir="../../ministreaming/java/" target="build" />
|
||||
<ant dir="../../../core/java/" target="build" />
|
||||
<ant dir="../../jetty/" target="build" />
|
||||
<!-- ministreaming will build core -->
|
||||
</target>
|
||||
<target name="compile">
|
||||
<mkdir dir="./build" />
|
||||
<mkdir dir="./build/obj" />
|
||||
<javac
|
||||
srcdir="./src"
|
||||
debug="true"
|
||||
debug="true" deprecation="on" source="1.3" target="1.3"
|
||||
destdir="./build/obj"
|
||||
classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" />
|
||||
</target>
|
||||
<target name="jar" depends="compile">
|
||||
<target name="jar" depends="builddep, compile">
|
||||
<jar destfile="./build/i2ptunnel.jar" basedir="./build/obj" includes="**/*.class">
|
||||
<manifest>
|
||||
<attribute name="Main-Class" value="net.i2p.i2ptunnel.I2PTunnel" />
|
||||
<attribute name="Class-Path" value="i2p.jar mstreaming.jar" />
|
||||
</manifest>
|
||||
</jar>
|
||||
<ant target="war" />
|
||||
</target>
|
||||
<target name="war" depends="precompilejsp">
|
||||
<war destfile="build/i2ptunnel.war" webxml="../jsp/web-out.xml"
|
||||
basedir="../jsp/" excludes="web.xml, *.java, *.jsp">
|
||||
</war>
|
||||
</target>
|
||||
<target name="precompilejsp">
|
||||
<delete dir="../jsp/WEB-INF/" />
|
||||
<delete file="../jsp/web-fragment.xml" />
|
||||
<delete file="../jsp/web-out.xml" />
|
||||
<mkdir dir="../jsp/WEB-INF/" />
|
||||
<mkdir dir="../jsp/WEB-INF/classes" />
|
||||
<!-- there are various jspc ant tasks, but they all seem a bit flakey -->
|
||||
<java classname="org.apache.jasper.JspC" fork="true" >
|
||||
<classpath>
|
||||
<pathelement location="../../jetty/jettylib/jasper-compiler.jar" />
|
||||
<pathelement location="../../jetty/jettylib/jasper-runtime.jar" />
|
||||
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
|
||||
<pathelement location="../../jetty/jettylib/commons-logging.jar" />
|
||||
<pathelement location="../../jetty/jettylib/commons-el.jar" />
|
||||
<pathelement location="../../jetty/jettylib/ant.jar" />
|
||||
<pathelement location="build/i2ptunnel.jar" />
|
||||
</classpath>
|
||||
<arg value="-d" />
|
||||
<arg value="../jsp/WEB-INF/classes" />
|
||||
<arg value="-p" />
|
||||
<arg value="net.i2p.i2ptunnel.jsp" />
|
||||
<arg value="-webinc" />
|
||||
<arg value="../jsp/web-fragment.xml" />
|
||||
<arg value="-webapp" />
|
||||
<arg value="../jsp/" />
|
||||
</java>
|
||||
<javac debug="true" deprecation="on" source="1.3" target="1.3"
|
||||
destdir="../jsp/WEB-INF/classes/" srcdir="../jsp/WEB-INF/classes" includes="**/*.java">
|
||||
<classpath>
|
||||
<pathelement location="../../jetty/jettylib/jasper-runtime.jar" />
|
||||
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
|
||||
<pathelement location="../../jetty/jettylib/commons-logging.jar" />
|
||||
<pathelement location="../../jetty/jettylib/commons-el.jar" />
|
||||
<pathelement location="build/i2ptunnel.jar" />
|
||||
</classpath>
|
||||
</javac>
|
||||
<copy file="../jsp/web.xml" tofile="../jsp/web-out.xml" />
|
||||
<loadfile property="jspc.web.fragment" srcfile="../jsp/web-fragment.xml" />
|
||||
<replace file="../jsp/web-out.xml">
|
||||
<replacefilter token="<!-- precompiled servlets -->" value="${jspc.web.fragment}" />
|
||||
</replace>
|
||||
</target>
|
||||
<target name="javadoc">
|
||||
<mkdir dir="./build" />
|
||||
@@ -37,11 +86,11 @@
|
||||
<delete dir="./build" />
|
||||
</target>
|
||||
<target name="cleandep" depends="clean">
|
||||
<ant dir="../../../core/java/" target="cleandep" />
|
||||
<!-- ministreaming will clean core -->
|
||||
<ant dir="../../ministreaming/java/" target="distclean" />
|
||||
</target>
|
||||
<target name="distclean" depends="clean">
|
||||
<ant dir="../../../core/java/" target="distclean" />
|
||||
<!-- ministreaming will clean core -->
|
||||
<ant dir="../../ministreaming/java/" target="distclean" />
|
||||
</target>
|
||||
</project>
|
||||
|
||||
@@ -16,16 +16,26 @@ class BufferLogger implements Logging {
|
||||
private final static Log _log = new Log(BufferLogger.class);
|
||||
private ByteArrayOutputStream _baos;
|
||||
private boolean _ignore;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a buffered logger.
|
||||
*/
|
||||
public BufferLogger() {
|
||||
_baos = new ByteArrayOutputStream(512);
|
||||
_ignore = false;
|
||||
_baos = new ByteArrayOutputStream(512);
|
||||
_ignore = false;
|
||||
}
|
||||
|
||||
private final static String EMPTY = "";
|
||||
public String getBuffer() {
|
||||
if (_ignore) return EMPTY;
|
||||
else return new String(_baos.toByteArray());
|
||||
|
||||
/**
|
||||
* Retrieves the buffer
|
||||
* @return the buffer
|
||||
*/
|
||||
public String getBuffer() {
|
||||
if (_ignore)
|
||||
return EMPTY;
|
||||
|
||||
return new String(_baos.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,26 +46,27 @@ class BufferLogger implements Logging {
|
||||
*
|
||||
*/
|
||||
public void ignoreFurtherActions() {
|
||||
_ignore = true;
|
||||
synchronized (_baos) {
|
||||
_baos.reset();
|
||||
}
|
||||
_baos = null;
|
||||
_ignore = true;
|
||||
synchronized (_baos) {
|
||||
_baos.reset();
|
||||
}
|
||||
_baos = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pass in some random data
|
||||
* @param s String containing what we're logging.
|
||||
*/
|
||||
public void log(String s) {
|
||||
if (_ignore) return;
|
||||
if (s != null) {
|
||||
_log.debug("logging [" + s + "]");
|
||||
try {
|
||||
_baos.write(s.getBytes());
|
||||
_baos.write('\n');
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error logging [" + s + "]");
|
||||
}
|
||||
}
|
||||
if (_ignore) return;
|
||||
if (s != null) {
|
||||
_log.debug("logging [" + s + "]");
|
||||
try {
|
||||
_baos.write(s.getBytes());
|
||||
_baos.write('\n');
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error logging [" + s + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package net.i2p.i2ptunnel;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by jrandom in 2005 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.util.ByteCache;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Simple stream for delivering an HTTP response to
|
||||
* the client, trivially filtered to make sure "Connection: close"
|
||||
* is always in the response. Perhaps add transparent handling of the
|
||||
* Content-encoding: x-i2p-gzip, adjusting the headers to say Content-encoding: identity?
|
||||
* Content-encoding: gzip is trivial as well, but Transfer-encoding: chunked makes it
|
||||
* more work than is worthwhile at the moment.
|
||||
*
|
||||
*/
|
||||
class HTTPResponseOutputStream extends FilterOutputStream {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private ByteCache _cache;
|
||||
protected ByteArray _headerBuffer;
|
||||
private boolean _headerWritten;
|
||||
private byte _buf1[];
|
||||
protected boolean _gzip;
|
||||
private long _dataWritten;
|
||||
private InternalGZIPInputStream _in;
|
||||
private static final int CACHE_SIZE = 8*1024;
|
||||
|
||||
public HTTPResponseOutputStream(OutputStream raw) {
|
||||
super(raw);
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_context.statManager().createRateStat("i2ptunnel.httpCompressionRatio", "ratio of compressed size to decompressed size after transfer", "i2ptunnel", new long[] { 60*1000, 30*60*1000 });
|
||||
_context.statManager().createRateStat("i2ptunnel.httpCompressed", "compressed size transferred", "i2ptunnel", new long[] { 60*1000, 30*60*1000 });
|
||||
_context.statManager().createRateStat("i2ptunnel.httpExpanded", "size transferred after expansion", "i2ptunnel", new long[] { 60*1000, 30*60*1000 });
|
||||
_log = _context.logManager().getLog(getClass());
|
||||
_cache = ByteCache.getInstance(8, CACHE_SIZE);
|
||||
_headerBuffer = _cache.acquire();
|
||||
_headerWritten = false;
|
||||
_gzip = false;
|
||||
_dataWritten = 0;
|
||||
_buf1 = new byte[1];
|
||||
}
|
||||
|
||||
public void write(int c) throws IOException {
|
||||
_buf1[0] = (byte)c;
|
||||
write(_buf1, 0, 1);
|
||||
}
|
||||
public void write(byte buf[]) throws IOException {
|
||||
write(buf, 0, buf.length);
|
||||
}
|
||||
public void write(byte buf[], int off, int len) throws IOException {
|
||||
if (_headerWritten) {
|
||||
out.write(buf, off, len);
|
||||
_dataWritten += len;
|
||||
//out.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
ensureCapacity();
|
||||
_headerBuffer.getData()[_headerBuffer.getValid()] = buf[off+i];
|
||||
_headerBuffer.setValid(_headerBuffer.getValid()+1);
|
||||
|
||||
if (headerReceived()) {
|
||||
writeHeader();
|
||||
_headerWritten = true;
|
||||
if (i + 1 < len) {
|
||||
// write out the remaining
|
||||
out.write(buf, off+i+1, len-i-1);
|
||||
_dataWritten += len-i-1;
|
||||
//out.flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** grow (and free) the buffer as necessary */
|
||||
private void ensureCapacity() {
|
||||
if (_headerBuffer.getValid() + 1 >= _headerBuffer.getData().length) {
|
||||
int newSize = (int)(_headerBuffer.getData().length * 1.5);
|
||||
ByteArray newBuf = new ByteArray(new byte[newSize]);
|
||||
System.arraycopy(_headerBuffer.getData(), 0, newBuf.getData(), 0, _headerBuffer.getValid());
|
||||
newBuf.setValid(_headerBuffer.getValid());
|
||||
newBuf.setOffset(0);
|
||||
if (_headerBuffer.getData().length == CACHE_SIZE)
|
||||
_cache.release(_headerBuffer);
|
||||
_headerBuffer = newBuf;
|
||||
}
|
||||
}
|
||||
|
||||
/** are the headers finished? */
|
||||
private boolean headerReceived() {
|
||||
if (_headerBuffer.getValid() < 3) return false;
|
||||
byte first = _headerBuffer.getData()[_headerBuffer.getValid()-3];
|
||||
byte second = _headerBuffer.getData()[_headerBuffer.getValid()-2];
|
||||
byte third = _headerBuffer.getData()[_headerBuffer.getValid()-1];
|
||||
return (isNL(second) && isNL(third)) || // \n\n
|
||||
(isNL(first) && isNL(third)); // \n\r\n
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweak that first HTTP response line (HTTP 200 OK, etc)
|
||||
*
|
||||
*/
|
||||
protected String filterResponseLine(String line) {
|
||||
return line;
|
||||
}
|
||||
|
||||
/** we ignore any potential \r, since we trim it on write anyway */
|
||||
private static final byte NL = '\n';
|
||||
private boolean isNL(byte b) { return (b == NL); }
|
||||
|
||||
/** ok, received, now munge & write it */
|
||||
private void writeHeader() throws IOException {
|
||||
String responseLine = null;
|
||||
|
||||
boolean connectionSent = false;
|
||||
boolean proxyConnectionSent = false;
|
||||
|
||||
int lastEnd = -1;
|
||||
for (int i = 0; i < _headerBuffer.getValid(); i++) {
|
||||
if (isNL(_headerBuffer.getData()[i])) {
|
||||
if (lastEnd == -1) {
|
||||
responseLine = new String(_headerBuffer.getData(), 0, i+1); // includes NL
|
||||
responseLine = filterResponseLine(responseLine);
|
||||
responseLine = (responseLine.trim() + "\r\n");
|
||||
out.write(responseLine.getBytes());
|
||||
} else {
|
||||
for (int j = lastEnd+1; j < i; j++) {
|
||||
if (_headerBuffer.getData()[j] == ':') {
|
||||
int keyLen = j-(lastEnd+1);
|
||||
int valLen = i-(j+1);
|
||||
if ( (keyLen <= 0) || (valLen < 0) )
|
||||
throw new IOException("Invalid header @ " + j);
|
||||
String key = new String(_headerBuffer.getData(), lastEnd+1, keyLen);
|
||||
String val = null;
|
||||
if (valLen == 0)
|
||||
val = "";
|
||||
else
|
||||
val = new String(_headerBuffer.getData(), j+2, valLen).trim();
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Response header [" + key + "] = [" + val + "]");
|
||||
|
||||
if ("Connection".equalsIgnoreCase(key)) {
|
||||
out.write("Connection: close\r\n".getBytes());
|
||||
connectionSent = true;
|
||||
} else if ("Proxy-Connection".equalsIgnoreCase(key)) {
|
||||
out.write("Proxy-Connection: close\r\n".getBytes());
|
||||
proxyConnectionSent = true;
|
||||
} else if ( ("Content-encoding".equalsIgnoreCase(key)) && ("x-i2p-gzip".equalsIgnoreCase(val)) ) {
|
||||
_gzip = true;
|
||||
} else {
|
||||
out.write((key.trim() + ": " + val.trim() + "\r\n").getBytes());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (!connectionSent)
|
||||
out.write("Connection: close\r\n".getBytes());
|
||||
if (!proxyConnectionSent)
|
||||
out.write("Proxy-Connection: close\r\n".getBytes());
|
||||
|
||||
finishHeaders();
|
||||
|
||||
boolean shouldCompress = shouldCompress();
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("After headers: gzip? " + _gzip + " compress? " + shouldCompress);
|
||||
|
||||
// done, shove off
|
||||
if (_headerBuffer.getData().length == CACHE_SIZE)
|
||||
_cache.release(_headerBuffer);
|
||||
else
|
||||
_headerBuffer = null;
|
||||
if (shouldCompress) {
|
||||
beginProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean shouldCompress() { return _gzip; }
|
||||
|
||||
protected void finishHeaders() throws IOException {
|
||||
out.write("\r\n".getBytes()); // end of the headers
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
out.close();
|
||||
}
|
||||
|
||||
protected void beginProcessing() throws IOException {
|
||||
//out.flush();
|
||||
PipedInputStream pi = new PipedInputStream();
|
||||
PipedOutputStream po = new PipedOutputStream(pi);
|
||||
new I2PThread(new Pusher(pi, out), "HTTP decompresser").start();
|
||||
out = po;
|
||||
}
|
||||
|
||||
private class Pusher implements Runnable {
|
||||
private InputStream _inRaw;
|
||||
private OutputStream _out;
|
||||
public Pusher(InputStream in, OutputStream out) {
|
||||
_inRaw = in;
|
||||
_out = out;
|
||||
}
|
||||
public void run() {
|
||||
OutputStream to = null;
|
||||
_in = null;
|
||||
long start = System.currentTimeMillis();
|
||||
long written = 0;
|
||||
try {
|
||||
_in = new InternalGZIPInputStream(_inRaw);
|
||||
byte buf[] = new byte[8192];
|
||||
int read = -1;
|
||||
while ( (read = _in.read(buf)) != -1) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Read " + read + " and writing it to the browser/streams");
|
||||
_out.write(buf, 0, read);
|
||||
_out.flush();
|
||||
written += read;
|
||||
}
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Decompressed: " + written + ", " + _in.getTotalRead() + "/" + _in.getTotalExpanded());
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Error decompressing: " + written + ", " + (_in != null ? _in.getTotalRead() + "/" + _in.getTotalExpanded() : ""), ioe);
|
||||
} finally {
|
||||
if (_log.shouldLog(Log.WARN) && (_in != null))
|
||||
_log.warn("After decompression, written=" + written +
|
||||
(_in != null ?
|
||||
" read=" + _in.getTotalRead()
|
||||
+ ", expanded=" + _in.getTotalExpanded() + ", remaining=" + _in.getRemaining()
|
||||
+ ", finished=" + _in.getFinished()
|
||||
: ""));
|
||||
if (_out != null) try {
|
||||
_out.close();
|
||||
} catch (IOException ioe) {}
|
||||
}
|
||||
long end = System.currentTimeMillis();
|
||||
double compressed = (_in != null ? _in.getTotalRead() : 0);
|
||||
double expanded = (_in != null ? _in.getTotalExpanded() : 0);
|
||||
double ratio = 0;
|
||||
if (expanded > 0)
|
||||
ratio = compressed/expanded;
|
||||
|
||||
_context.statManager().addRateData("i2ptunnel.httpCompressionRatio", (int)(100d*ratio), end-start);
|
||||
_context.statManager().addRateData("i2ptunnel.httpCompressed", (long)compressed, end-start);
|
||||
_context.statManager().addRateData("i2ptunnel.httpExpanded", (long)expanded, end-start);
|
||||
}
|
||||
}
|
||||
private class InternalGZIPInputStream extends GZIPInputStream {
|
||||
public InternalGZIPInputStream(InputStream in) throws IOException {
|
||||
super(in);
|
||||
}
|
||||
public long getTotalRead() {
|
||||
try {
|
||||
return super.inf.getTotalIn();
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
public long getTotalExpanded() {
|
||||
try {
|
||||
return super.inf.getTotalOut();
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
public long getRemaining() {
|
||||
try {
|
||||
return super.inf.getRemaining();
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
public boolean getFinished() {
|
||||
try {
|
||||
return super.inf.finished();
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public String toString() {
|
||||
return "Read: " + getTotalRead() + " expanded: " + getTotalExpanded() + " remaining: " + getRemaining() + " finished: " + getFinished();
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return super.toString() + ": " + _in;
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
String simple = "HTTP/1.1 200 OK\n" +
|
||||
"foo: bar\n" +
|
||||
"baz: bat\n" +
|
||||
"\n" +
|
||||
"hi ho, this is the body";
|
||||
String filtered = "HTTP/1.1 200 OK\n" +
|
||||
"Connection: keep-alive\n" +
|
||||
"foo: bar\n" +
|
||||
"baz: bat\n" +
|
||||
"\n" +
|
||||
"hi ho, this is the body";
|
||||
String winfilter= "HTTP/1.1 200 OK\r\n" +
|
||||
"Connection: keep-alive\r\n" +
|
||||
"foo: bar\r\n" +
|
||||
"baz: bat\r\n" +
|
||||
"\r\n" +
|
||||
"hi ho, this is the body";
|
||||
String minimal = "HTTP/1.1 200 OK\n" +
|
||||
"\n" +
|
||||
"hi ho, this is the body";
|
||||
String winmin = "HTTP/1.1 200 OK\r\n" +
|
||||
"\r\n" +
|
||||
"hi ho, this is the body";
|
||||
String invalid1 = "HTTP/1.1 200 OK\n";
|
||||
String invalid2 = "HTTP/1.1 200 OK";
|
||||
String invalid3 = "HTTP 200 OK\r\n";
|
||||
String invalid4 = "HTTP 200 OK\r";
|
||||
String invalid5 = "HTTP/1.1 200 OK\r\n" +
|
||||
"I am broken, and I smell\r\n" +
|
||||
"\r\n";
|
||||
String invalid6 = "HTTP/1.1 200 OK\r\n" +
|
||||
":I am broken, and I smell\r\n" +
|
||||
"\r\n";
|
||||
String invalid7 = "HTTP/1.1 200 OK\n" +
|
||||
"I am broken, and I smell:\n" +
|
||||
":asdf\n" +
|
||||
":\n" +
|
||||
"\n";
|
||||
String large = "HTTP/1.1 200 OK\n" +
|
||||
"Last-modified: Tue, 25 Nov 2003 12:05:38 GMT\n" +
|
||||
"Expires: Tue, 25 Nov 2003 12:05:38 GMT\n" +
|
||||
"Content-length: 32\n" +
|
||||
"\n" +
|
||||
"hi ho, this is the body";
|
||||
String blankval = "HTTP/1.0 200 OK\n" +
|
||||
"A:\n" +
|
||||
"\n";
|
||||
|
||||
/* */
|
||||
test("Simple", simple, true);
|
||||
test("Filtered", filtered, true);
|
||||
test("Filtered windows", winfilter, true);
|
||||
test("Minimal", minimal, true);
|
||||
test("Windows", winmin, true);
|
||||
test("Large", large, true);
|
||||
test("Blank whitespace", blankval, true);
|
||||
test("Invalid (short headers)", invalid1, true);
|
||||
test("Invalid (no headers)", invalid2, true);
|
||||
test("Invalid (windows with short headers)", invalid3, true);
|
||||
test("Invalid (windows no headers)", invalid4, true);
|
||||
test("Invalid (bad headers)", invalid5, true);
|
||||
test("Invalid (bad headers2)", invalid6, false);
|
||||
test("Invalid (bad headers3)", invalid7, false);
|
||||
/* */
|
||||
}
|
||||
|
||||
private static void test(String name, String orig, boolean shouldPass) {
|
||||
System.out.println("====Testing: " + name + "\n" + orig + "\n------------");
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
|
||||
HTTPResponseOutputStream resp = new HTTPResponseOutputStream(baos);
|
||||
resp.write(orig.getBytes());
|
||||
resp.flush();
|
||||
String received = new String(baos.toByteArray());
|
||||
System.out.println(received);
|
||||
} catch (Exception e) {
|
||||
if (shouldPass)
|
||||
e.printStackTrace();
|
||||
else
|
||||
System.out.println("Properly fails with " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,11 @@
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.Destination;
|
||||
@@ -16,46 +19,86 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
|
||||
|
||||
private static final Log _log = new Log(I2PTunnelClient.class);
|
||||
|
||||
protected Destination dest;
|
||||
/** list of Destination objects that we point at */
|
||||
protected List dests;
|
||||
private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
|
||||
protected long readTimeout = DEFAULT_READ_TIMEOUT;
|
||||
|
||||
public I2PTunnelClient(int localPort, String destination,
|
||||
Logging l, boolean ownDest,
|
||||
EventDispatcher notifyThis) {
|
||||
super(localPort, ownDest, l, notifyThis, "SynSender");
|
||||
/**
|
||||
* @param destinations comma delimited list of peers we target
|
||||
* @throws IllegalArgumentException if the I2PTunnel does not contain
|
||||
* valid config to contact the router
|
||||
*/
|
||||
public I2PTunnelClient(int localPort, String destinations, Logging l,
|
||||
boolean ownDest, EventDispatcher notifyThis,
|
||||
I2PTunnel tunnel) throws IllegalArgumentException {
|
||||
super(localPort, ownDest, l, notifyThis, "SynSender", tunnel);
|
||||
|
||||
if (waitEventValue("openBaseClientResult").equals("error")) {
|
||||
notifyEvent("openClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dest=I2PTunnel.destFromName(destination);
|
||||
if (dest == null) {
|
||||
l.log("Could not resolve " + destination + ".");
|
||||
return;
|
||||
}
|
||||
} catch (DataFormatException e) {
|
||||
l.log("Bad format in destination \"" + destination + "\".");
|
||||
notifyEvent("openClientResult", "error");
|
||||
return;
|
||||
}
|
||||
if (waitEventValue("openBaseClientResult").equals("error")) {
|
||||
notifyEvent("openClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setName(getLocalPort() + " -> " + destination);
|
||||
StringTokenizer tok = new StringTokenizer(destinations, ",");
|
||||
dests = new ArrayList(1);
|
||||
while (tok.hasMoreTokens()) {
|
||||
String destination = tok.nextToken();
|
||||
try {
|
||||
Destination dest = I2PTunnel.destFromName(destination);
|
||||
if (dest == null)
|
||||
l.log("Could not resolve " + destination);
|
||||
else
|
||||
dests.add(dest);
|
||||
} catch (DataFormatException dfe) {
|
||||
l.log("Bad format parsing \"" + destination + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
startRunning();
|
||||
if (dests.size() <= 0) {
|
||||
l.log("No target destinations found");
|
||||
notifyEvent("openClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
notifyEvent("openClientResult", "ok");
|
||||
setName(getLocalPort() + " -> " + destinations);
|
||||
|
||||
startRunning();
|
||||
|
||||
notifyEvent("openClientResult", "ok");
|
||||
}
|
||||
|
||||
public void setReadTimeout(long ms) { readTimeout = ms; }
|
||||
public long getReadTimeout() { return readTimeout; }
|
||||
|
||||
protected void clientConnectionRun(Socket s) {
|
||||
try {
|
||||
I2PSocket i2ps = createI2PSocket(dest);
|
||||
new I2PTunnelRunner(s, i2ps, sockLock, null);
|
||||
} catch (I2PException ex) {
|
||||
_log.info("Error connecting", ex);
|
||||
l.log("Unable to reach peer");
|
||||
// s has been initialized before the try block...
|
||||
closeSocket(s);
|
||||
}
|
||||
Destination dest = pickDestination();
|
||||
I2PSocket i2ps = null;
|
||||
try {
|
||||
i2ps = createI2PSocket(dest);
|
||||
i2ps.setReadTimeout(readTimeout);
|
||||
new I2PTunnelRunner(s, i2ps, sockLock, null, mySockets);
|
||||
} catch (Exception ex) {
|
||||
_log.info("Error connecting", ex);
|
||||
l.log(ex.getMessage());
|
||||
closeSocket(s);
|
||||
if (i2ps != null) {
|
||||
synchronized (sockLock) {
|
||||
mySockets.remove(sockLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Destination pickDestination() {
|
||||
int size = dests.size();
|
||||
if (size <= 0) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("No client targets?!");
|
||||
return null;
|
||||
}
|
||||
if (size == 1) // skip the rand in the most common case
|
||||
return (Destination)dests.get(0);
|
||||
int index = I2PAppContext.getGlobalContext().random().nextInt(size);
|
||||
return (Destination)dests.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NoRouteToHostException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -14,25 +17,31 @@ import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory;
|
||||
import net.i2p.client.streaming.I2PSocketOptions;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.EventDispatcher;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
public abstract class I2PTunnelClientBase extends I2PTunnelTask
|
||||
implements Runnable {
|
||||
|
||||
private static final Log _log = new Log(I2PTunnelClientBase.class);
|
||||
protected Logging l;
|
||||
|
||||
private static final long DEFAULT_CONNECT_TIMEOUT = 60*1000;
|
||||
public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable {
|
||||
|
||||
private static final Log _log = new Log(I2PTunnelClientBase.class);
|
||||
protected I2PAppContext _context;
|
||||
protected Logging l;
|
||||
|
||||
static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000;
|
||||
|
||||
private static volatile long __clientId = 0;
|
||||
protected long _clientId;
|
||||
protected Object sockLock = new Object(); // Guards sockMgr and mySockets
|
||||
private I2PSocketManager sockMgr;
|
||||
private List mySockets = new ArrayList();
|
||||
protected I2PSocketManager sockMgr;
|
||||
protected List mySockets = new ArrayList();
|
||||
|
||||
protected Destination dest = null;
|
||||
private int localPort;
|
||||
@@ -40,116 +49,250 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask
|
||||
private boolean listenerReady = false;
|
||||
|
||||
private ServerSocket ss;
|
||||
|
||||
|
||||
private Object startLock = new Object();
|
||||
private boolean startRunning = false;
|
||||
|
||||
private Object closeLock = new Object();
|
||||
|
||||
|
||||
private byte[] pubkey;
|
||||
|
||||
private String handlerName;
|
||||
|
||||
private Object conLock = new Object();
|
||||
|
||||
/** List of Socket for those accept()ed but not yet started up */
|
||||
private List _waitingSockets = new ArrayList();
|
||||
/** How many connections will we allow to be in the process of being built at once? */
|
||||
private int _numConnectionBuilders;
|
||||
/** How long will we allow sockets to sit in the _waitingSockets map before killing them? */
|
||||
private int _maxWaitTime;
|
||||
|
||||
/**
|
||||
* How many concurrent connections this I2PTunnel instance will allow to be
|
||||
* in the process of connecting (or if less than 1, there is no limit)?
|
||||
*/
|
||||
public static final String PROP_NUM_CONNECTION_BUILDERS = "i2ptunnel.numConnectionBuilders";
|
||||
/**
|
||||
* How long will we let a socket wait after being accept()ed without getting
|
||||
* pumped through a connection builder (in milliseconds). If this time is
|
||||
* reached, the socket is unceremoniously closed and discarded. If the max
|
||||
* wait time is less than 1, there is no limit.
|
||||
*
|
||||
*/
|
||||
public static final String PROP_MAX_WAIT_TIME = "i2ptunnel.maxWaitTime";
|
||||
|
||||
private static final int DEFAULT_NUM_CONNECTION_BUILDERS = 5;
|
||||
private static final int DEFAULT_MAX_WAIT_TIME = 30*1000;
|
||||
|
||||
//public I2PTunnelClientBase(int localPort, boolean ownDest,
|
||||
// Logging l) {
|
||||
// I2PTunnelClientBase(localPort, ownDest, l, (EventDispatcher)null);
|
||||
//}
|
||||
|
||||
public I2PTunnelClientBase(int localPort, boolean ownDest,
|
||||
Logging l, EventDispatcher notifyThis,
|
||||
String handlerName) {
|
||||
super(localPort+" (uninitialized)", notifyThis);
|
||||
this.localPort=localPort;
|
||||
this.l = l;
|
||||
this.handlerName=handlerName;
|
||||
/**
|
||||
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
||||
* badly that we cant create a socketManager
|
||||
*/
|
||||
public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l,
|
||||
EventDispatcher notifyThis, String handlerName,
|
||||
I2PTunnel tunnel) throws IllegalArgumentException{
|
||||
super(localPort + " (uninitialized)", notifyThis, tunnel);
|
||||
_clientId = ++__clientId;
|
||||
this.localPort = localPort;
|
||||
this.l = l;
|
||||
this.handlerName = handlerName + _clientId;
|
||||
|
||||
synchronized(sockLock) {
|
||||
if (ownDest) {
|
||||
sockMgr=buildSocketManager();
|
||||
} else {
|
||||
sockMgr=getSocketManager();
|
||||
}
|
||||
}
|
||||
if (sockMgr == null) throw new NullPointerException();
|
||||
l.log("I2P session created");
|
||||
_context = tunnel.getContext();
|
||||
_context.statManager().createRateStat("i2ptunnel.client.closeBacklog", "How many pending sockets remain when we close one due to backlog?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("i2ptunnel.client.closeNoBacklog", "How many pending sockets remain when it was removed prior to backlog timeout?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("i2ptunnel.client.manageTime", "How long it takes to accept a socket and fire it into an i2ptunnel runner (or queue it for the pool)?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("i2ptunnel.client.buildRunTime", "How long it takes to run a queued socket into an i2ptunnel runner?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
|
||||
// no need to load the netDb with leaseSets for destinations that will never
|
||||
// be looked up
|
||||
tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true");
|
||||
|
||||
Thread t = new Thread(this);
|
||||
t.setName("Client");
|
||||
listenerReady=false;
|
||||
t.start();
|
||||
open=true;
|
||||
synchronized (this) {
|
||||
while (!listenerReady) {
|
||||
try {
|
||||
wait();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open && listenerReady) {
|
||||
l.log("Ready! Port " + getLocalPort());
|
||||
notifyEvent("openBaseClientResult", "ok");
|
||||
} else {
|
||||
l.log("Error!");
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
}
|
||||
while (sockMgr == null) {
|
||||
synchronized (sockLock) {
|
||||
if (ownDest) {
|
||||
sockMgr = buildSocketManager();
|
||||
} else {
|
||||
sockMgr = getSocketManager();
|
||||
}
|
||||
}
|
||||
if (sockMgr == null) {
|
||||
_log.log(Log.CRIT, "Unable to create socket manager (our own? " + ownDest + ")");
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
if (sockMgr == null) {
|
||||
l.log("Invalid I2CP configuration");
|
||||
throw new IllegalArgumentException("Socket manager could not be created");
|
||||
}
|
||||
l.log("I2P session created");
|
||||
|
||||
getTunnel().addSession(sockMgr.getSession());
|
||||
|
||||
Thread t = new I2PThread(this);
|
||||
t.setName("Client " + _clientId);
|
||||
listenerReady = false;
|
||||
t.start();
|
||||
open = true;
|
||||
synchronized (this) {
|
||||
while (!listenerReady && open) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurePool(tunnel);
|
||||
|
||||
if (open && listenerReady) {
|
||||
l.log("Ready! Port " + getLocalPort());
|
||||
notifyEvent("openBaseClientResult", "ok");
|
||||
} else {
|
||||
l.log("Error listening - please see the logs!");
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* build and configure the pool handling accept()ed but not yet
|
||||
* established connections
|
||||
*
|
||||
*/
|
||||
private void configurePool(I2PTunnel tunnel) {
|
||||
_waitingSockets = new ArrayList(4);
|
||||
|
||||
Properties opts = tunnel.getClientOptions();
|
||||
String maxWait = opts.getProperty(PROP_MAX_WAIT_TIME, DEFAULT_MAX_WAIT_TIME+"");
|
||||
try {
|
||||
_maxWaitTime = Integer.parseInt(maxWait);
|
||||
} catch (NumberFormatException nfe) {
|
||||
_maxWaitTime = DEFAULT_MAX_WAIT_TIME;
|
||||
}
|
||||
|
||||
String numBuild = opts.getProperty(PROP_NUM_CONNECTION_BUILDERS, DEFAULT_NUM_CONNECTION_BUILDERS+"");
|
||||
try {
|
||||
_numConnectionBuilders = Integer.parseInt(numBuild);
|
||||
} catch (NumberFormatException nfe) {
|
||||
_numConnectionBuilders = DEFAULT_NUM_CONNECTION_BUILDERS;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _numConnectionBuilders; i++) {
|
||||
String name = "ClientBuilder" + _clientId + '.' + i;
|
||||
I2PThread b = new I2PThread(new TunnelConnectionBuilder(), name);
|
||||
b.setDaemon(true);
|
||||
b.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static I2PSocketManager socketManager;
|
||||
|
||||
protected static synchronized I2PSocketManager getSocketManager() {
|
||||
if (socketManager == null) {
|
||||
socketManager = buildSocketManager();
|
||||
}
|
||||
return socketManager;
|
||||
|
||||
protected synchronized I2PSocketManager getSocketManager() {
|
||||
return getSocketManager(getTunnel());
|
||||
}
|
||||
|
||||
protected static I2PSocketManager buildSocketManager() {
|
||||
Properties props = new Properties();
|
||||
props.putAll(System.getProperties());
|
||||
return I2PSocketManagerFactory.createManager
|
||||
(I2PTunnel.host, Integer.parseInt(I2PTunnel.port), props);
|
||||
protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel) {
|
||||
if (socketManager != null) {
|
||||
I2PSession s = socketManager.getSession();
|
||||
if ( (s == null) || (s.isClosed()) ) {
|
||||
_log.info("Building a new socket manager since the old one closed [s=" + s + "]");
|
||||
socketManager = buildSocketManager(tunnel);
|
||||
} else {
|
||||
_log.info("Not building a new socket manager since the old one is open [s=" + s + "]");
|
||||
}
|
||||
} else {
|
||||
_log.info("Building a new socket manager since there is no other one");
|
||||
socketManager = buildSocketManager(tunnel);
|
||||
}
|
||||
return socketManager;
|
||||
}
|
||||
|
||||
|
||||
protected I2PSocketManager buildSocketManager() {
|
||||
return buildSocketManager(getTunnel());
|
||||
}
|
||||
protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel) {
|
||||
Properties props = new Properties();
|
||||
if (tunnel == null)
|
||||
props.putAll(System.getProperties());
|
||||
else
|
||||
props.putAll(tunnel.getClientOptions());
|
||||
int portNum = 7654;
|
||||
if (tunnel.port != null) {
|
||||
try {
|
||||
portNum = Integer.parseInt(tunnel.port);
|
||||
} catch (NumberFormatException nfe) {
|
||||
_log.log(Log.CRIT, "Invalid port specified [" + tunnel.port + "], reverting to " + portNum);
|
||||
}
|
||||
}
|
||||
|
||||
I2PSocketManager sockManager = null;
|
||||
while (sockManager == null) {
|
||||
sockManager = I2PSocketManagerFactory.createManager(tunnel.host, portNum, props);
|
||||
|
||||
if (sockManager == null) {
|
||||
_log.log(Log.CRIT, "Unable to create socket manager");
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
sockManager.setName("Client");
|
||||
return sockManager;
|
||||
}
|
||||
|
||||
public final int getLocalPort() {
|
||||
return localPort;
|
||||
}
|
||||
|
||||
protected final InetAddress getListenHost(Logging l) {
|
||||
try {
|
||||
return InetAddress.getByName(I2PTunnel.listenHost);
|
||||
} catch (UnknownHostException uhe) {
|
||||
l.log("Could not find listen host to bind to [" +
|
||||
I2PTunnel.host + "]");
|
||||
_log.error("Error finding host to bind", uhe);
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(getTunnel().listenHost);
|
||||
} catch (UnknownHostException uhe) {
|
||||
l.log("Could not find listen host to bind to [" + getTunnel().host + "]");
|
||||
_log.error("Error finding host to bind", uhe);
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Actually start working on incoming connections. *Must* be
|
||||
* called by derived classes after initialization.
|
||||
*
|
||||
*/
|
||||
public final void startRunning() {
|
||||
synchronized (startLock) {
|
||||
startRunning = true;
|
||||
startLock.notify();
|
||||
}
|
||||
synchronized (startLock) {
|
||||
startRunning = true;
|
||||
startLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create the default options (using the default timeout, etc)
|
||||
*
|
||||
*/
|
||||
protected I2PSocketOptions getDefaultOptions() {
|
||||
Properties defaultOpts = getTunnel().getClientOptions();
|
||||
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
||||
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* create the default options (using the default timeout, etc)
|
||||
*
|
||||
*/
|
||||
private I2PSocketOptions getDefaultOptions() {
|
||||
I2PSocketOptions opts = new I2PSocketOptions();
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
return opts;
|
||||
protected I2PSocketOptions getDefaultOptions(Properties overrides) {
|
||||
Properties defaultOpts = getTunnel().getClientOptions();
|
||||
defaultOpts.putAll(overrides);
|
||||
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
||||
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,8 +303,8 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask
|
||||
* @param dest The destination to connect to
|
||||
* @return a new I2PSocket
|
||||
*/
|
||||
public I2PSocket createI2PSocket(Destination dest) throws I2PException {
|
||||
return createI2PSocket(dest, getDefaultOptions());
|
||||
public I2PSocket createI2PSocket(Destination dest) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
|
||||
return createI2PSocket(dest, getDefaultOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,55 +315,80 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask
|
||||
* @param dest The destination to connect to
|
||||
* @param opt Option to be used to open when opening the socket
|
||||
* @return a new I2PSocket
|
||||
*
|
||||
* @throws ConnectException if the peer refuses the connection
|
||||
* @throws NoRouteToHostException if the peer is not found or not reachable
|
||||
* @throws InterruptedIOException if the connection timeouts
|
||||
* @throws I2PException if there is some other I2P-related problem
|
||||
*/
|
||||
public I2PSocket createI2PSocket(Destination dest, I2PSocketOptions opt) throws I2PException {
|
||||
I2PSocket i2ps;
|
||||
public I2PSocket createI2PSocket(Destination dest, I2PSocketOptions opt) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
|
||||
I2PSocket i2ps;
|
||||
|
||||
synchronized (sockLock) {
|
||||
i2ps = sockMgr.connect(dest, opt);
|
||||
mySockets.add(i2ps);
|
||||
}
|
||||
i2ps = sockMgr.connect(dest, opt);
|
||||
synchronized (sockLock) {
|
||||
mySockets.add(i2ps);
|
||||
}
|
||||
|
||||
return i2ps;
|
||||
return i2ps;
|
||||
}
|
||||
|
||||
public final void run() {
|
||||
try {
|
||||
InetAddress addr = getListenHost(l);
|
||||
if (addr == null) return;
|
||||
ss = new ServerSocket(localPort, 0, addr);
|
||||
|
||||
// If a free port was requested, find out what we got
|
||||
if (localPort == 0) {
|
||||
localPort = ss.getLocalPort();
|
||||
}
|
||||
notifyEvent("clientLocalPort", new Integer(ss.getLocalPort()));
|
||||
l.log("Listening for clients on port " + localPort +
|
||||
" of " + I2PTunnel.listenHost);
|
||||
|
||||
// Notify constructor that port is ready
|
||||
synchronized(this) {
|
||||
listenerReady = true;
|
||||
notify();
|
||||
}
|
||||
try {
|
||||
InetAddress addr = getListenHost(l);
|
||||
if (addr == null) {
|
||||
open = false;
|
||||
synchronized (this) {
|
||||
notifyAll();
|
||||
}
|
||||
synchronized (_waitingSockets) { _waitingSockets.notifyAll(); }
|
||||
return;
|
||||
}
|
||||
ss = new ServerSocket(localPort, 0, addr);
|
||||
|
||||
// Wait until we are authorized to process data
|
||||
synchronized (startLock) {
|
||||
while (!startRunning) {
|
||||
try {
|
||||
startLock.wait();
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
// If a free port was requested, find out what we got
|
||||
if (localPort == 0) {
|
||||
localPort = ss.getLocalPort();
|
||||
}
|
||||
notifyEvent("clientLocalPort", new Integer(ss.getLocalPort()));
|
||||
l.log("Listening for clients on port " + localPort + " of " + getTunnel().listenHost);
|
||||
|
||||
while (true) {
|
||||
Socket s = ss.accept();
|
||||
manageConnection(s);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error listening for connections", ex);
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
}
|
||||
// Notify constructor that port is ready
|
||||
synchronized (this) {
|
||||
listenerReady = true;
|
||||
notify();
|
||||
}
|
||||
|
||||
// Wait until we are authorized to process data
|
||||
synchronized (startLock) {
|
||||
while (!startRunning) {
|
||||
try {
|
||||
startLock.wait();
|
||||
} catch (InterruptedException ie) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
Socket s = ss.accept();
|
||||
long before = System.currentTimeMillis();
|
||||
manageConnection(s);
|
||||
long total = System.currentTimeMillis() - before;
|
||||
_context.statManager().addRateData("i2ptunnel.client.manageTime", total, total);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error listening for connections on " + localPort, ex);
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
synchronized (sockLock) {
|
||||
mySockets.clear();
|
||||
}
|
||||
open = false;
|
||||
synchronized (this) {
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
synchronized (_waitingSockets) {
|
||||
_waitingSockets.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,59 +397,131 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask
|
||||
* @param s Socket to take care of
|
||||
*/
|
||||
protected void manageConnection(Socket s) {
|
||||
new ClientConnectionRunner(s, handlerName);
|
||||
if (s == null) return;
|
||||
if (_numConnectionBuilders <= 0) {
|
||||
new I2PThread(new BlockingRunner(s), "Clinet run").start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_maxWaitTime > 0)
|
||||
SimpleTimer.getInstance().addEvent(new CloseEvent(s), _maxWaitTime);
|
||||
|
||||
synchronized (_waitingSockets) {
|
||||
_waitingSockets.add(s);
|
||||
_waitingSockets.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocking runner, used during the connection establishment whenever we
|
||||
* are not using the queued builders.
|
||||
*
|
||||
*/
|
||||
private class BlockingRunner implements Runnable {
|
||||
private Socket _s;
|
||||
public BlockingRunner(Socket s) { _s = s; }
|
||||
public void run() {
|
||||
clientConnectionRun(_s);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and close the socket from the waiting list, if it is still there.
|
||||
*
|
||||
*/
|
||||
private class CloseEvent implements SimpleTimer.TimedEvent {
|
||||
private Socket _s;
|
||||
public CloseEvent(Socket s) { _s = s; }
|
||||
public void timeReached() {
|
||||
int remaining = 0;
|
||||
boolean stillWaiting = false;
|
||||
synchronized (_waitingSockets) {
|
||||
stillWaiting = _waitingSockets.remove(_s);
|
||||
remaining = _waitingSockets.size();
|
||||
}
|
||||
if (stillWaiting) {
|
||||
try { _s.close(); } catch (IOException ioe) {}
|
||||
if (_log.shouldLog(Log.INFO)) {
|
||||
_context.statManager().addRateData("i2ptunnel.client.closeBacklog", remaining, 0);
|
||||
_log.info("Closed a waiting socket because of backlog");
|
||||
}
|
||||
} else {
|
||||
_context.statManager().addRateData("i2ptunnel.client.closeNoBacklog", remaining, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean close(boolean forced) {
|
||||
if (!open) return true;
|
||||
// FIXME: here we might have to wait quite a long time if
|
||||
// there is a connection attempt atm. But without waiting we
|
||||
// might risk to create an orphan socket. Would be better
|
||||
// to return with an error in that situation quickly.
|
||||
synchronized(sockLock) {
|
||||
mySockets.retainAll(sockMgr.listSockets());
|
||||
if (!forced && mySockets.size() != 0) {
|
||||
l.log("There are still active connections!");
|
||||
_log.debug("can't close: there are still active connections!");
|
||||
for (Iterator it = mySockets.iterator(); it.hasNext();) {
|
||||
l.log("->"+it.next());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
l.log("Closing client "+toString());
|
||||
try {
|
||||
if (ss != null) ss.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
l.log("Client closed.");
|
||||
open=false;
|
||||
return true;
|
||||
}
|
||||
if (!open) return true;
|
||||
// FIXME: here we might have to wait quite a long time if
|
||||
// there is a connection attempt atm. But without waiting we
|
||||
// might risk to create an orphan socket. Would be better
|
||||
// to return with an error in that situation quickly.
|
||||
synchronized (sockLock) {
|
||||
mySockets.retainAll(sockMgr.listSockets());
|
||||
if (!forced && mySockets.size() != 0) {
|
||||
l.log("There are still active connections!");
|
||||
_log.debug("can't close: there are still active connections!");
|
||||
for (Iterator it = mySockets.iterator(); it.hasNext();) {
|
||||
l.log("->" + it.next());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
I2PSession session = sockMgr.getSession();
|
||||
if (session != null) {
|
||||
getTunnel().removeSession(session);
|
||||
}
|
||||
l.log("Closing client " + toString());
|
||||
try {
|
||||
if (ss != null) ss.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
l.log("Client closed.");
|
||||
open = false;
|
||||
}
|
||||
|
||||
synchronized (_waitingSockets) { _waitingSockets.notifyAll(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void closeSocket(Socket s) {
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException ex) {
|
||||
_log.error("Could not close socket", ex);
|
||||
}
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException ex) {
|
||||
_log.error("Could not close socket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public class ClientConnectionRunner extends Thread {
|
||||
private Socket s;
|
||||
|
||||
public ClientConnectionRunner(Socket s, String name) {
|
||||
this.s=s;
|
||||
setName(name);
|
||||
start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
clientConnectionRun(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool runner pulling sockets off the waiting list and pushing them
|
||||
* through clientConnectionRun. This dies when the I2PTunnel instance
|
||||
* is closed.
|
||||
*
|
||||
*/
|
||||
private class TunnelConnectionBuilder implements Runnable {
|
||||
public void run() {
|
||||
Socket s = null;
|
||||
while (open) {
|
||||
try {
|
||||
synchronized (_waitingSockets) {
|
||||
if (_waitingSockets.size() <= 0)
|
||||
_waitingSockets.wait();
|
||||
else
|
||||
s = (Socket)_waitingSockets.remove(0);
|
||||
}
|
||||
} catch (InterruptedException ie) {}
|
||||
|
||||
if (s != null) {
|
||||
long before = System.currentTimeMillis();
|
||||
clientConnectionRun(s);
|
||||
long total = System.currentTimeMillis() - before;
|
||||
_context.statManager().addRateData("i2ptunnel.client.buildRunTime", total, 0);
|
||||
}
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,30 +19,30 @@ public class I2PTunnelGUI extends Frame implements ActionListener, Logging {
|
||||
TextField input;
|
||||
TextArea log;
|
||||
I2PTunnel t;
|
||||
|
||||
|
||||
public I2PTunnelGUI(I2PTunnel t) {
|
||||
super("I2PTunnel control panel");
|
||||
this.t=t;
|
||||
setLayout(new BorderLayout());
|
||||
add("South", input=new TextField());
|
||||
input.addActionListener(this);
|
||||
Font font = new Font("Monospaced",Font.PLAIN,12);
|
||||
add("Center",log=new TextArea("",20,80,TextArea.SCROLLBARS_VERTICAL_ONLY));
|
||||
log.setFont(font);
|
||||
log.setEditable(false);
|
||||
log("enter 'help' for help.");
|
||||
pack();
|
||||
show();
|
||||
super("I2PTunnel control panel");
|
||||
this.t = t;
|
||||
setLayout(new BorderLayout());
|
||||
add("South", input = new TextField());
|
||||
input.addActionListener(this);
|
||||
Font font = new Font("Monospaced", Font.PLAIN, 12);
|
||||
add("Center", log = new TextArea("", 20, 80, TextArea.SCROLLBARS_VERTICAL_ONLY));
|
||||
log.setFont(font);
|
||||
log.setEditable(false);
|
||||
log("enter 'help' for help.");
|
||||
pack();
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
public void log(String s) {
|
||||
log.append(s+"\n");
|
||||
log.append(s + "\n");
|
||||
}
|
||||
|
||||
public void actionPerformed(ActionEvent evt) {
|
||||
log("I2PTunnel>"+input.getText());
|
||||
t.runCommand(input.getText(), this);
|
||||
log("---");
|
||||
input.setText("");
|
||||
log("I2PTunnel>" + input.getText());
|
||||
t.runCommand(input.getText(), this);
|
||||
log("---");
|
||||
input.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,327 +8,661 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.client.streaming.I2PSocketOptions;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Clock;
|
||||
import net.i2p.util.EventDispatcher;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
public class I2PTunnelHTTPClient extends I2PTunnelClientBase
|
||||
implements Runnable {
|
||||
private static final Log _log =
|
||||
new Log(I2PTunnelHTTPClient.class);
|
||||
/**
|
||||
* Act as a mini HTTP proxy, handling various different types of requests,
|
||||
* forwarding them through I2P appropriately, and displaying the reply. Supported
|
||||
* request formats are: <pre>
|
||||
* $method http://$site[$port]/$path $protocolVersion
|
||||
* or
|
||||
* $method $path $protocolVersion\nHost: $site
|
||||
* or
|
||||
* $method http://i2p/$site/$path $protocolVersion
|
||||
* or
|
||||
* $method /$site/$path $protocolVersion
|
||||
* </pre>
|
||||
*
|
||||
* If the $site resolves with the I2P naming service, then it is directed towards
|
||||
* that eepsite, otherwise it is directed towards this client's outproxy (typically
|
||||
* "squid.i2p"). Only HTTP is supported (no HTTPS, ftp, mailto, etc). Both GET
|
||||
* and POST have been tested, though other $methods should work.
|
||||
*
|
||||
*/
|
||||
public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable {
|
||||
private static final Log _log = new Log(I2PTunnelHTTPClient.class);
|
||||
|
||||
private String wwwProxy;
|
||||
private List proxyList;
|
||||
|
||||
private final static byte[] ERR_REQUEST_DENIED = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html; charset=iso-8859-1\r\nCache-control: no-cache\r\n\r\n<html><body><H1>I2P ERROR: REQUEST DENIED</H1>You attempted to connect to a non-I2P website or location.<BR>".getBytes();
|
||||
private final static byte[] ERR_DESTINATION_UNKNOWN = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html; charset=iso-8859-1\r\nCache-control: no-cache\r\n\r\n<html><body><H1>I2P ERROR: NOT FOUND</H1>That Desitination was not found. Perhaps you pasted in the wrong BASE64 I2P Destination or the link you are following is bad. The host (or the WWW proxy, if you're using one) could also be temporarily offline. Could not find the following Destination:<BR><BR>".getBytes();
|
||||
private final static byte[] ERR_TIMEOUT = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html; charset=iso-8859-1\r\nCache-control: no-cache\r\n\r\n<html><body><H1>I2P ERROR: TIMEOUT</H1>That Desitination was reachable, but timed out getting a response. This may be a temporary error, so you should simply try to refresh, though if the problem persists, the remote destination may have issues. Could not get a response from the following Destination:<BR><BR>".getBytes();
|
||||
private HashMap addressHelpers = new HashMap();
|
||||
|
||||
private final static byte[] ERR_REQUEST_DENIED =
|
||||
("HTTP/1.1 403 Access Denied\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n"+
|
||||
"Cache-control: no-cache\r\n"+
|
||||
"\r\n"+
|
||||
"<html><body><H1>I2P ERROR: REQUEST DENIED</H1>"+
|
||||
"You attempted to connect to a non-I2P website or location.<BR>")
|
||||
.getBytes();
|
||||
|
||||
//public I2PTunnelHTTPClient(int localPort, Logging l,
|
||||
// boolean ownDest,
|
||||
// String wwwProxy) {
|
||||
// I2PTunnelHTTPClient(localPort, l, ownDest, wwwProxy,
|
||||
// (EventDispatcher)null);
|
||||
//}
|
||||
private final static byte[] ERR_DESTINATION_UNKNOWN =
|
||||
("HTTP/1.1 503 Service Unavailable\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n"+
|
||||
"Cache-control: no-cache\r\n"+
|
||||
"\r\n"+
|
||||
"<html><body><H1>I2P ERROR: DESTINATION NOT FOUND</H1>"+
|
||||
"That I2P Destination was not found. Perhaps you pasted in the "+
|
||||
"wrong BASE64 I2P Destination or the link you are following is "+
|
||||
"bad. The host (or the WWW proxy, if you're using one) could also "+
|
||||
"be temporarily offline. You may want to <b>retry</b>. "+
|
||||
"Could not find the following Destination:<BR><BR><div>")
|
||||
.getBytes();
|
||||
|
||||
private final static byte[] ERR_TIMEOUT =
|
||||
("HTTP/1.1 504 Gateway Timeout\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n"+
|
||||
"Cache-control: no-cache\r\n\r\n"+
|
||||
"<html><body><H1>I2P ERROR: TIMEOUT</H1>"+
|
||||
"That Destination was reachable, but timed out getting a "+
|
||||
"response. This is likely a temporary error, so you should simply "+
|
||||
"try to refresh, though if the problem persists, the remote "+
|
||||
"destination may have issues. Could not get a response from "+
|
||||
"the following Destination:<BR><BR>")
|
||||
.getBytes();
|
||||
|
||||
public I2PTunnelHTTPClient(int localPort, Logging l,
|
||||
boolean ownDest,
|
||||
String wwwProxy, EventDispatcher notifyThis) {
|
||||
super(localPort, ownDest, l, notifyThis, "HTTPHandler");
|
||||
private final static byte[] ERR_NO_OUTPROXY =
|
||||
("HTTP/1.1 503 Service Unavailable\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n"+
|
||||
"Cache-control: no-cache\r\n"+
|
||||
"\r\n"+
|
||||
"<html><body><H1>I2P ERROR: No outproxy found</H1>"+
|
||||
"Your request was for a site outside of I2P, but you have no "+
|
||||
"HTTP outproxy configured. Please configure an outproxy in I2PTunnel")
|
||||
.getBytes();
|
||||
|
||||
private final static byte[] ERR_AHELPER_CONFLICT =
|
||||
("HTTP/1.1 409 Conflict\r\n"+
|
||||
"Content-Type: text/html; charset=iso-8859-1\r\n"+
|
||||
"Cache-control: no-cache\r\n"+
|
||||
"\r\n"+
|
||||
"<html><body><H1>I2P ERROR: Destination key conflict</H1>"+
|
||||
"The addresshelper link you followed specifies a different destination key "+
|
||||
"than a host entry in your host database. "+
|
||||
"Someone could be trying to impersonate another eepsite, "+
|
||||
"or people have given two eepsites identical names.<P/>"+
|
||||
"You can resolve the conflict by considering which key you trust, "+
|
||||
"and either discarding the addresshelper link, "+
|
||||
"discarding the host entry from your host database, "+
|
||||
"or naming one of them differently.<P/>")
|
||||
.getBytes();
|
||||
|
||||
/** used to assign unique IDs to the threads / clients. no logic or functionality */
|
||||
private static volatile long __clientId = 0;
|
||||
|
||||
if (waitEventValue("openBaseClientResult").equals("error")) {
|
||||
notifyEvent("openHTTPClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.wwwProxy = wwwProxy;
|
||||
/**
|
||||
* @throws IllegalArgumentException if the I2PTunnel does not contain
|
||||
* valid config to contact the router
|
||||
*/
|
||||
public I2PTunnelHTTPClient(int localPort, Logging l, boolean ownDest,
|
||||
String wwwProxy, EventDispatcher notifyThis,
|
||||
I2PTunnel tunnel) throws IllegalArgumentException {
|
||||
super(localPort, ownDest, l, notifyThis, "HTTPHandler " + (++__clientId), tunnel);
|
||||
|
||||
setName(getLocalPort()
|
||||
+ " -> HTTPClient [WWW outproxy: " + this.wwwProxy + "]");
|
||||
if (waitEventValue("openBaseClientResult").equals("error")) {
|
||||
notifyEvent("openHTTPClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
startRunning();
|
||||
proxyList = new ArrayList();
|
||||
if (wwwProxy != null) {
|
||||
StringTokenizer tok = new StringTokenizer(wwwProxy, ",");
|
||||
while (tok.hasMoreTokens())
|
||||
proxyList.add(tok.nextToken().trim());
|
||||
}
|
||||
|
||||
notifyEvent("openHTTPClientResult", "ok");
|
||||
setName(getLocalPort() + " -> HTTPClient [WWW outproxy list: " + wwwProxy + "]");
|
||||
|
||||
startRunning();
|
||||
|
||||
notifyEvent("openHTTPClientResult", "ok");
|
||||
}
|
||||
|
||||
private String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; }
|
||||
|
||||
private String selectProxy() {
|
||||
synchronized (proxyList) {
|
||||
int size = proxyList.size();
|
||||
if (size <= 0) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Proxy list is empty - no outproxy available");
|
||||
l.log("Proxy list is emtpy - no outproxy available");
|
||||
return null;
|
||||
}
|
||||
int index = I2PAppContext.getGlobalContext().random().nextInt(size);
|
||||
String proxy = (String)proxyList.get(index);
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int DEFAULT_READ_TIMEOUT = 60*1000;
|
||||
|
||||
/**
|
||||
* create the default options (using the default timeout, etc)
|
||||
*
|
||||
*/
|
||||
protected I2PSocketOptions getDefaultOptions() {
|
||||
Properties defaultOpts = getTunnel().getClientOptions();
|
||||
if (!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT))
|
||||
defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT);
|
||||
//if (!defaultOpts.contains("i2p.streaming.inactivityTimeout"))
|
||||
// defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT);
|
||||
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
||||
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* create the default options (using the default timeout, etc)
|
||||
*
|
||||
*/
|
||||
protected I2PSocketOptions getDefaultOptions(Properties overrides) {
|
||||
Properties defaultOpts = getTunnel().getClientOptions();
|
||||
defaultOpts.putAll(overrides);
|
||||
if (!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT))
|
||||
defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT);
|
||||
if (!defaultOpts.contains("i2p.streaming.inactivityTimeout"))
|
||||
defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT);
|
||||
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
||||
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
return opts;
|
||||
}
|
||||
|
||||
private static final boolean DEFAULT_GZIP = true;
|
||||
|
||||
private static long __requestId = 0;
|
||||
protected void clientConnectionRun(Socket s) {
|
||||
OutputStream out = null;
|
||||
String targetRequest = null;
|
||||
boolean usingWWWProxy = false;
|
||||
InactivityTimeoutThread timeoutThread = null;
|
||||
try {
|
||||
out = s.getOutputStream();
|
||||
BufferedReader br = new BufferedReader
|
||||
(new InputStreamReader(s.getInputStream(),
|
||||
"ISO-8859-1"));
|
||||
String line, method=null, protocol=null, host=null, destination=null;
|
||||
StringBuffer newRequest=new StringBuffer();
|
||||
while ((line=br.readLine()) != null) {
|
||||
if (method==null) { // first line (GET /base64/realaddr)
|
||||
int pos=line.indexOf(" ");
|
||||
if (pos == -1) break;
|
||||
method=line.substring(0, pos);
|
||||
String request = line.substring(pos+1);
|
||||
if (request.startsWith("/") &&
|
||||
System.getProperty("i2ptunnel.noproxy") != null) {
|
||||
request="http://i2p"+request;
|
||||
}
|
||||
pos = request.indexOf("//");
|
||||
if (pos == -1) {
|
||||
method=null;
|
||||
break;
|
||||
}
|
||||
protocol=request.substring(0,pos+2);
|
||||
request=request.substring(pos+2);
|
||||
OutputStream out = null;
|
||||
String targetRequest = null;
|
||||
boolean usingWWWProxy = false;
|
||||
String currentProxy = null;
|
||||
long requestId = ++__requestId;
|
||||
try {
|
||||
out = s.getOutputStream();
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(), "ISO-8859-1"));
|
||||
String line, method = null, protocol = null, host = null, destination = null;
|
||||
StringBuffer newRequest = new StringBuffer();
|
||||
int ahelper = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix(requestId) + "Line=[" + line + "]");
|
||||
|
||||
String lowercaseLine = line.toLowerCase();
|
||||
if (lowercaseLine.startsWith("connection: ") ||
|
||||
lowercaseLine.startsWith("keep-alive: ") ||
|
||||
lowercaseLine.startsWith("proxy-connection: "))
|
||||
continue;
|
||||
|
||||
if (method == null) { // first line (GET /base64/realaddr)
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix(requestId) + "Method is null for [" + line + "]");
|
||||
|
||||
line = line.trim();
|
||||
int pos = line.indexOf(" ");
|
||||
if (pos == -1) break;
|
||||
method = line.substring(0, pos);
|
||||
String request = line.substring(pos + 1);
|
||||
if (request.startsWith("/") && getTunnel().getClientOptions().getProperty("i2ptunnel.noproxy") != null) {
|
||||
request = "http://i2p" + request;
|
||||
} else if (request.startsWith("/eepproxy/")) {
|
||||
// /eepproxy/foo.i2p/bar/baz.html HTTP/1.0
|
||||
String subRequest = request.substring("/eepproxy/".length());
|
||||
int protopos = subRequest.indexOf(" ");
|
||||
String uri = subRequest.substring(0, protopos);
|
||||
if (uri.indexOf("/") == -1) {
|
||||
uri = uri + "/";
|
||||
}
|
||||
// "http://" + "foo.i2p/bar/baz.html" + " HTTP/1.0"
|
||||
request = "http://" + uri + subRequest.substring(protopos);
|
||||
}
|
||||
|
||||
targetRequest = request;
|
||||
pos = request.indexOf("//");
|
||||
if (pos == -1) {
|
||||
method = null;
|
||||
break;
|
||||
}
|
||||
protocol = request.substring(0, pos + 2);
|
||||
request = request.substring(pos + 2);
|
||||
|
||||
pos = request.indexOf("/");
|
||||
if (pos == -1) {
|
||||
method=null;
|
||||
break;
|
||||
}
|
||||
host=request.substring(0,pos);
|
||||
targetRequest = request;
|
||||
|
||||
// Quick hack for foo.bar.i2p
|
||||
if (host.toLowerCase().endsWith( ".i2p")) {
|
||||
destination=host;
|
||||
host=getHostName(destination);
|
||||
line=method+" "+request.substring(pos);
|
||||
} else if (host.indexOf(".") != -1) {
|
||||
// The request must be forwarded to a WWW proxy
|
||||
destination = wwwProxy;
|
||||
usingWWWProxy = true;
|
||||
} else {
|
||||
request=request.substring(pos+1);
|
||||
pos = request.indexOf("/");
|
||||
destination=request.substring(0,pos);
|
||||
line=method+" "+request.substring(pos);
|
||||
}
|
||||
pos = request.indexOf("/");
|
||||
if (pos == -1) {
|
||||
method = null;
|
||||
break;
|
||||
}
|
||||
host = request.substring(0, pos);
|
||||
|
||||
boolean isValid = usingWWWProxy ||
|
||||
isSupportedAddress(host, protocol);
|
||||
if (!isValid) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("notValid(" + host + ")");
|
||||
method=null;
|
||||
destination=null;
|
||||
break;
|
||||
} else if (!usingWWWProxy) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("host=getHostName(" + destination + ")");
|
||||
host=getHostName(destination); // hide original host
|
||||
}
|
||||
// Quick hack for foo.bar.i2p
|
||||
if (host.toLowerCase().endsWith(".i2p")) {
|
||||
// Destination gets the host name
|
||||
destination = host;
|
||||
// Host becomes the destination key
|
||||
host = getHostName(destination);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug("METHOD:"+method+":");
|
||||
_log.debug("PROTOC:"+protocol+":");
|
||||
_log.debug("HOST :"+host+":");
|
||||
_log.debug("DEST :"+destination+":");
|
||||
}
|
||||
int pos2;
|
||||
if ((pos2 = request.indexOf("?")) != -1) {
|
||||
// Try to find an address helper in the fragments
|
||||
// and split the request into it's component parts for rebuilding later
|
||||
String ahelperKey = null;
|
||||
boolean ahelperConflict = false;
|
||||
|
||||
String fragments = request.substring(pos2 + 1);
|
||||
String uriPath = request.substring(0, pos2);
|
||||
pos2 = fragments.indexOf(" ");
|
||||
String protocolVersion = fragments.substring(pos2 + 1);
|
||||
String urlEncoding = "";
|
||||
fragments = fragments.substring(0, pos2);
|
||||
String initialFragments = fragments;
|
||||
fragments = fragments + "&";
|
||||
String fragment;
|
||||
while(fragments.length() > 0) {
|
||||
pos2 = fragments.indexOf("&");
|
||||
fragment = fragments.substring(0, pos2);
|
||||
fragments = fragments.substring(pos2 + 1);
|
||||
|
||||
// Fragment looks like addresshelper key
|
||||
if (fragment.startsWith("i2paddresshelper=")) {
|
||||
pos2 = fragment.indexOf("=");
|
||||
ahelperKey = fragment.substring(pos2 + 1);
|
||||
|
||||
// Key contains data, lets not ignore it
|
||||
if (ahelperKey != null) {
|
||||
|
||||
// Host resolvable only with addresshelper
|
||||
if ( (host == null) || ("i2p".equals(host)) )
|
||||
{
|
||||
// Cannot check, use addresshelper key
|
||||
addressHelpers.put(destination,ahelperKey);
|
||||
} else {
|
||||
// Host resolvable from database, verify addresshelper key
|
||||
// Silently bypass correct keys, otherwise alert
|
||||
if (!host.equals(ahelperKey))
|
||||
{
|
||||
// Conflict: handle when URL reconstruction done
|
||||
ahelperConflict = true;
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getPrefix(requestId) + "Addresshelper key conflict for site [" + destination + "], trusted key [" + host + "], specified key [" + ahelperKey + "].");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Other fragments, just pass along
|
||||
// Append each fragment to urlEncoding
|
||||
if ("".equals(urlEncoding)) {
|
||||
urlEncoding = "?" + fragment;
|
||||
} else {
|
||||
urlEncoding = urlEncoding + "&" + fragment;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reconstruct the request minus the i2paddresshelper GET var
|
||||
request = uriPath + urlEncoding + " " + protocolVersion;
|
||||
|
||||
// Did addresshelper key conflict?
|
||||
if (ahelperConflict)
|
||||
{
|
||||
String str;
|
||||
byte[] header;
|
||||
str = FileUtil.readTextFile("docs/ahelper-conflict-header.ht", 100, true);
|
||||
if (str != null) header = str.getBytes();
|
||||
else header = ERR_AHELPER_CONFLICT;
|
||||
|
||||
} else if (line.startsWith("Host: ") && !usingWWWProxy) {
|
||||
line="Host: "+host;
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Setting host = " + host);
|
||||
}
|
||||
newRequest.append(line).append("\r\n"); // HTTP spec
|
||||
if (line.length()==0) break;
|
||||
}
|
||||
while (br.ready()) { // empty the buffer (POST requests)
|
||||
int i=br.read();
|
||||
if (i != -1) {
|
||||
newRequest.append((char)i);
|
||||
}
|
||||
}
|
||||
if (method==null || destination==null) {
|
||||
l.log("No HTTP method found in the request.");
|
||||
if (out != null) {
|
||||
out.write(ERR_REQUEST_DENIED);
|
||||
out.write("<p /><i>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
Destination dest=I2PTunnel.destFromName(destination);
|
||||
if (dest == null) {
|
||||
l.log("Could not resolve "+destination+".");
|
||||
writeErrorMessage(ERR_DESTINATION_UNKNOWN, out, targetRequest,
|
||||
usingWWWProxy, destination);
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
String remoteID;
|
||||
I2PSocket i2ps = createI2PSocket(dest);
|
||||
byte[] data=newRequest.toString().getBytes("ISO-8859-1");
|
||||
I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data);
|
||||
timeoutThread = new InactivityTimeoutThread(runner, out, targetRequest, usingWWWProxy, s);
|
||||
timeoutThread.start();
|
||||
} catch (IOException ex) {
|
||||
if (timeoutThread != null) timeoutThread.disable();
|
||||
_log.error("Error sending syn", ex);
|
||||
handleHTTPClientException(ex, out, targetRequest,
|
||||
usingWWWProxy, wwwProxy);
|
||||
closeSocket(s);
|
||||
} catch (I2PException ex) {
|
||||
if (timeoutThread != null) timeoutThread.disable();
|
||||
_log.info("Error sending syn", ex);
|
||||
l.log("Unable to reach peer");
|
||||
handleHTTPClientException(ex, out, targetRequest,
|
||||
usingWWWProxy, wwwProxy);
|
||||
closeSocket(s);
|
||||
}
|
||||
}
|
||||
if (out != null) {
|
||||
long alias = I2PAppContext.getGlobalContext().random().nextLong();
|
||||
String trustedURL = protocol + uriPath + urlEncoding;
|
||||
String conflictURL = protocol + alias + ".i2p/?" + initialFragments;
|
||||
out.write(header);
|
||||
out.write(("To visit the destination in your host database, click <a href=\"" + trustedURL + "\">here</a>. To visit the conflicting addresshelper link by temporarily giving it a random alias, click <a href=\"" + conflictURL + "\">here</a>.<P/>").getBytes());
|
||||
out.write("</div><p><i>I2P HTTP Proxy Server<br>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static final long INACTIVITY_TIMEOUT = 120*1000;
|
||||
|
||||
private class InactivityTimeoutThread extends I2PThread {
|
||||
|
||||
private Socket s;
|
||||
private I2PTunnelRunner _runner;
|
||||
private OutputStream _out;
|
||||
private String _targetRequest;
|
||||
private boolean _useWWWProxy;
|
||||
private boolean _disabled;
|
||||
private Object _disableLock = new Object();
|
||||
|
||||
public InactivityTimeoutThread(I2PTunnelRunner runner, OutputStream out, String targetRequest, boolean useWWWProxy, Socket s) {
|
||||
this.s=s;
|
||||
_runner = runner;
|
||||
_out = out;
|
||||
_targetRequest = targetRequest;
|
||||
_useWWWProxy = useWWWProxy;
|
||||
_disabled = false;
|
||||
}
|
||||
public void disable() {
|
||||
_disabled = true;
|
||||
synchronized (_disableLock) { _disableLock.notifyAll(); }
|
||||
}
|
||||
public void run() {
|
||||
while (!_disabled) {
|
||||
if (_runner.isFinished()) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("HTTP client request completed prior to timeout");
|
||||
return;
|
||||
}
|
||||
if (_runner.getLastActivityOn() < Clock.getInstance().now() - INACTIVITY_TIMEOUT) {
|
||||
if (_runner.getStartedOn() < Clock.getInstance().now() - INACTIVITY_TIMEOUT) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("HTTP client request timed out (lastActivity: " + new Date(_runner.getLastActivityOn()) + ", startedOn: " + new Date(_runner.getLastActivityOn()) + ")");
|
||||
timeout();
|
||||
return;
|
||||
} else {
|
||||
// runner hasn't been going to long enough
|
||||
}
|
||||
} else {
|
||||
// there has been activity in the period
|
||||
}
|
||||
synchronized (_disableLock) {
|
||||
try {
|
||||
_disableLock.wait(INACTIVITY_TIMEOUT);
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
private void timeout() {
|
||||
_log.info("Inactivity timeout reached");
|
||||
l.log("Inactivity timeout reached");
|
||||
if (_out != null) {
|
||||
try {
|
||||
if (_runner.getLastActivityOn() > 0) {
|
||||
// some data has been sent, so don't 404 it
|
||||
} else {
|
||||
writeErrorMessage(ERR_TIMEOUT, _out, _targetRequest,
|
||||
_useWWWProxy, wwwProxy);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
_log.warn("Error writing out the 'timeout' message", ioe);
|
||||
}
|
||||
} else {
|
||||
_log.warn("Client disconnected before we could say we timed out");
|
||||
}
|
||||
closeSocket(s);
|
||||
}
|
||||
String addressHelper = (String) addressHelpers.get(destination);
|
||||
if (addressHelper != null) {
|
||||
destination = addressHelper;
|
||||
host = getHostName(destination);
|
||||
ahelper = 1;
|
||||
}
|
||||
|
||||
line = method + " " + request.substring(pos);
|
||||
} else if (host.indexOf(".") != -1) {
|
||||
// The request must be forwarded to a WWW proxy
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Before selecting outproxy for " + host);
|
||||
currentProxy = selectProxy();
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("After selecting outproxy for " + host + ": " + currentProxy);
|
||||
if (currentProxy == null) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!");
|
||||
l.log("No HTTP outproxy found for the request.");
|
||||
if (out != null) {
|
||||
out.write(ERR_NO_OUTPROXY);
|
||||
out.write("<p /><i>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
destination = currentProxy;
|
||||
usingWWWProxy = true;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix(requestId) + "Host doesnt end with .i2p and it contains a period [" + host + "]: wwwProxy!");
|
||||
} else {
|
||||
request = request.substring(pos + 1);
|
||||
pos = request.indexOf("/");
|
||||
if (pos < 0) {
|
||||
l.log("Invalid request url [" + request + "]");
|
||||
if (out != null) {
|
||||
out.write(ERR_REQUEST_DENIED);
|
||||
out.write("<p /><i>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
destination = request.substring(0, pos);
|
||||
line = method + " " + request.substring(pos);
|
||||
}
|
||||
|
||||
boolean isValid = usingWWWProxy || isSupportedAddress(host, protocol);
|
||||
if (!isValid) {
|
||||
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "notValid(" + host + ")");
|
||||
method = null;
|
||||
destination = null;
|
||||
break;
|
||||
} else if (!usingWWWProxy) {
|
||||
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "host=getHostName(" + destination + ")");
|
||||
host = getHostName(destination); // hide original host
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getPrefix(requestId) + "METHOD:" + method + ":");
|
||||
_log.debug(getPrefix(requestId) + "PROTOC:" + protocol + ":");
|
||||
_log.debug(getPrefix(requestId) + "HOST :" + host + ":");
|
||||
_log.debug(getPrefix(requestId) + "DEST :" + destination + ":");
|
||||
}
|
||||
|
||||
} else {
|
||||
if (lowercaseLine.startsWith("host: ") && !usingWWWProxy) {
|
||||
line = "Host: " + host;
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(getPrefix(requestId) + "Setting host = " + host);
|
||||
} else if (lowercaseLine.startsWith("user-agent: ")) {
|
||||
// always stripped, added back at the end
|
||||
line = null;
|
||||
continue;
|
||||
} else if (lowercaseLine.startsWith("accept")) {
|
||||
// strip the accept-blah headers, as they vary dramatically from
|
||||
// browser to browser
|
||||
line = null;
|
||||
continue;
|
||||
} else if (lowercaseLine.startsWith("referer: ")) {
|
||||
// Shouldn't we be more specific, like accepting in-site referers ?
|
||||
//line = "Referer: i2p";
|
||||
line = null;
|
||||
continue; // completely strip the line
|
||||
} else if (lowercaseLine.startsWith("via: ")) {
|
||||
//line = "Via: i2p";
|
||||
line = null;
|
||||
continue; // completely strip the line
|
||||
} else if (lowercaseLine.startsWith("from: ")) {
|
||||
//line = "From: i2p";
|
||||
line = null;
|
||||
continue; // completely strip the line
|
||||
}
|
||||
}
|
||||
|
||||
if (line.length() == 0) {
|
||||
|
||||
String ok = getTunnel().getContext().getProperty("i2ptunnel.gzip");
|
||||
boolean gzip = DEFAULT_GZIP;
|
||||
if (ok != null)
|
||||
gzip = Boolean.valueOf(ok).booleanValue();
|
||||
if (gzip) {
|
||||
// according to rfc2616 s14.3, this *should* force identity, even if
|
||||
// an explicit q=0 for gzip doesn't. tested against orion.i2p, and it
|
||||
// seems to work.
|
||||
newRequest.append("Accept-Encoding: \r\n");
|
||||
newRequest.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n");
|
||||
}
|
||||
newRequest.append("User-Agent: MYOB/6.66 (AN/ON)\r\n");
|
||||
newRequest.append("Connection: close\r\n\r\n");
|
||||
break;
|
||||
} else {
|
||||
newRequest.append(line.trim()).append("\r\n"); // HTTP spec
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix(requestId) + "NewRequest header: [" + newRequest.toString() + "]");
|
||||
|
||||
while (br.ready()) { // empty the buffer (POST requests)
|
||||
int i = br.read();
|
||||
if (i != -1) {
|
||||
newRequest.append((char) i);
|
||||
}
|
||||
}
|
||||
if (method == null || destination == null) {
|
||||
l.log("No HTTP method found in the request.");
|
||||
if (out != null) {
|
||||
out.write(ERR_REQUEST_DENIED);
|
||||
out.write("<p /><i>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix(requestId) + "Destination: " + destination);
|
||||
|
||||
Destination dest = I2PTunnel.destFromName(destination);
|
||||
if (dest == null) {
|
||||
l.log("Could not resolve " + destination + ".");
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Unable to resolve " + destination + " (proxy? " + usingWWWProxy + ", request: " + targetRequest);
|
||||
String str;
|
||||
byte[] header;
|
||||
boolean showAddrHelper = false;
|
||||
if (usingWWWProxy)
|
||||
str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
|
||||
else if(ahelper != 0)
|
||||
str = FileUtil.readTextFile("docs/dnfb-header.ht", 100, true);
|
||||
else {
|
||||
str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true);
|
||||
showAddrHelper = true;
|
||||
}
|
||||
if (str != null)
|
||||
header = str.getBytes();
|
||||
else
|
||||
header = ERR_DESTINATION_UNKNOWN;
|
||||
writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination, showAddrHelper);
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
String remoteID;
|
||||
|
||||
Properties opts = new Properties();
|
||||
//opts.setProperty("i2p.streaming.inactivityTimeout", ""+120*1000);
|
||||
// 1 == disconnect. see ConnectionOptions in the new streaming lib, which i
|
||||
// dont want to hard link to here
|
||||
//opts.setProperty("i2p.streaming.inactivityTimeoutAction", ""+1);
|
||||
I2PSocket i2ps = createI2PSocket(dest, getDefaultOptions(opts));
|
||||
byte[] data = newRequest.toString().getBytes("ISO-8859-1");
|
||||
Runnable onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
|
||||
I2PTunnelRunner runner = new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout);
|
||||
} catch (SocketException ex) {
|
||||
_log.info(getPrefix(requestId) + "Error trying to connect", ex);
|
||||
l.log(ex.getMessage());
|
||||
handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
|
||||
closeSocket(s);
|
||||
} catch (IOException ex) {
|
||||
_log.info(getPrefix(requestId) + "Error trying to connect", ex);
|
||||
l.log(ex.getMessage());
|
||||
handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
|
||||
closeSocket(s);
|
||||
} catch (I2PException ex) {
|
||||
_log.info("getPrefix(requestId) + Error trying to connect", ex);
|
||||
l.log(ex.getMessage());
|
||||
handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
|
||||
closeSocket(s);
|
||||
}
|
||||
}
|
||||
|
||||
private final static String getHostName(String host) {
|
||||
try {
|
||||
Destination dest=I2PTunnel.destFromName(host);
|
||||
if (dest == null) return "i2p";
|
||||
return dest.toBase64();
|
||||
} catch (DataFormatException dfe) {
|
||||
return "i2p";
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeErrorMessage(byte[] errMessage, OutputStream out,
|
||||
String targetRequest,
|
||||
boolean usingWWWProxy,
|
||||
String wwwProxy)
|
||||
throws IOException {
|
||||
if (out != null) {
|
||||
out.write(errMessage);
|
||||
if (targetRequest != null) {
|
||||
out.write(targetRequest.getBytes());
|
||||
if (usingWWWProxy)
|
||||
out.write(("<br>WWW proxy: " +
|
||||
wwwProxy).getBytes());
|
||||
}
|
||||
out.write("<p /><i>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
if (host == null) return null;
|
||||
try {
|
||||
Destination dest = I2PTunnel.destFromName(host);
|
||||
if (dest == null) return "i2p";
|
||||
return dest.toBase64();
|
||||
} catch (DataFormatException dfe) {
|
||||
return "i2p";
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleHTTPClientException (Exception ex, OutputStream out,
|
||||
String targetRequest,
|
||||
boolean usingWWWProxy,
|
||||
String wwwProxy) {
|
||||
if (out != null) {
|
||||
try {
|
||||
writeErrorMessage(ERR_DESTINATION_UNKNOWN, out, targetRequest,
|
||||
usingWWWProxy, wwwProxy);
|
||||
} catch (IOException ioe) {
|
||||
_log.warn("Error writing out the 'destination was unknown' "+
|
||||
"message", ioe);
|
||||
}
|
||||
} else {
|
||||
_log.warn("Client disconnected before we could say that destination "+
|
||||
"was unknown", ex);
|
||||
}
|
||||
private class OnTimeout implements Runnable {
|
||||
private Socket _socket;
|
||||
private OutputStream _out;
|
||||
private String _target;
|
||||
private boolean _usingProxy;
|
||||
private String _wwwProxy;
|
||||
private long _requestId;
|
||||
public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
|
||||
_socket = s;
|
||||
_out = out;
|
||||
_target = target;
|
||||
_usingProxy = usingProxy;
|
||||
_wwwProxy = wwwProxy;
|
||||
_requestId = id;
|
||||
}
|
||||
public void run() {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Timeout occured requesting " + _target);
|
||||
handleHTTPClientException(new RuntimeException("Timeout"), _out,
|
||||
_target, _usingProxy, _wwwProxy, _requestId);
|
||||
closeSocket(_socket);
|
||||
}
|
||||
}
|
||||
|
||||
private final static String SUPPORTED_HOSTS[] = { "i2p", "www.i2p.com",
|
||||
"i2p." };
|
||||
|
||||
private boolean isSupportedAddress(String host, String protocol) {
|
||||
if ( (host == null) || (protocol == null) ) return false;
|
||||
boolean found = false;
|
||||
String lcHost = host.toLowerCase();
|
||||
for (int i = 0; i < SUPPORTED_HOSTS.length; i++) {
|
||||
if (SUPPORTED_HOSTS[i].equals(lcHost)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
private static void writeErrorMessage(byte[] errMessage, OutputStream out, String targetRequest,
|
||||
boolean usingWWWProxy, String wwwProxy, boolean showAddrHelper) throws IOException {
|
||||
if (out != null) {
|
||||
out.write(errMessage);
|
||||
if (targetRequest != null) {
|
||||
int protopos = targetRequest.indexOf(" ");
|
||||
String uri = null;
|
||||
if (protopos >= 0)
|
||||
uri = targetRequest.substring(0, protopos);
|
||||
else
|
||||
uri = targetRequest;
|
||||
out.write("<a href=\"http://".getBytes());
|
||||
out.write(uri.getBytes());
|
||||
out.write("\">http://".getBytes());
|
||||
out.write(uri.getBytes());
|
||||
out.write("</a>".getBytes());
|
||||
if (usingWWWProxy) out.write(("<br>WWW proxy: " + wwwProxy).getBytes());
|
||||
if (showAddrHelper) {
|
||||
out.write("<br><br>Click below to try an address helper link:<br><br><a href=\"http://orion.i2p/jump/".getBytes());
|
||||
out.write(uri.getBytes());
|
||||
out.write("\">http://orion.i2p/jump/".getBytes());
|
||||
out.write(uri.getBytes());
|
||||
out.write("</a>".getBytes());
|
||||
}
|
||||
}
|
||||
out.write("</div><p><i>I2P HTTP Proxy Server<br>Generated on: ".getBytes());
|
||||
out.write(new Date().toString().getBytes());
|
||||
out.write("</i></body></html>\n".getBytes());
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
try {
|
||||
Destination d = I2PTunnel.destFromName(host);
|
||||
if (d == null) return false;
|
||||
} catch (DataFormatException dfe) {}
|
||||
}
|
||||
|
||||
return protocol.equalsIgnoreCase("http://");
|
||||
private void handleHTTPClientException(Exception ex, OutputStream out, String targetRequest,
|
||||
boolean usingWWWProxy, String wwwProxy, long requestId) {
|
||||
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getPrefix(requestId) + "Error sending to " + wwwProxy + " (proxy? " + usingWWWProxy + ", request: " + targetRequest, ex);
|
||||
if (out != null) {
|
||||
try {
|
||||
String str;
|
||||
byte[] header;
|
||||
if (usingWWWProxy)
|
||||
str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
|
||||
else
|
||||
str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true);
|
||||
if (str != null)
|
||||
header = str.getBytes();
|
||||
else
|
||||
header = ERR_DESTINATION_UNKNOWN;
|
||||
writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy, false);
|
||||
} catch (IOException ioe) {
|
||||
_log.warn(getPrefix(requestId) + "Error writing out the 'destination was unknown' " + "message", ioe);
|
||||
}
|
||||
} else {
|
||||
_log.warn(getPrefix(requestId) + "Client disconnected before we could say that destination " + "was unknown", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final static String SUPPORTED_HOSTS[] = { "i2p", "www.i2p.com", "i2p."};
|
||||
|
||||
private boolean isSupportedAddress(String host, String protocol) {
|
||||
if ((host == null) || (protocol == null)) return false;
|
||||
boolean found = false;
|
||||
String lcHost = host.toLowerCase();
|
||||
for (int i = 0; i < SUPPORTED_HOSTS.length; i++) {
|
||||
if (SUPPORTED_HOSTS[i].equals(lcHost)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
try {
|
||||
Destination d = I2PTunnel.destFromName(host);
|
||||
if (d == null) return false;
|
||||
} catch (DataFormatException dfe) {
|
||||
}
|
||||
}
|
||||
|
||||
return protocol.equalsIgnoreCase("http://");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
|
||||
* (c) 2003 - 2004 mihi
|
||||
*/
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.ByteCache;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Override the response with a stream filtering the HTTP headers
|
||||
* received. Specifically, this makes sure we get Connection: close,
|
||||
* so the browser knows they really shouldn't try to use persistent
|
||||
* connections. The HTTP server *should* already be setting this,
|
||||
* since the HTTP headers sent by the browser specify Connection: close,
|
||||
* and the server should echo it. However, both broken and malicious
|
||||
* servers could ignore that, potentially confusing the user.
|
||||
*
|
||||
*/
|
||||
public class I2PTunnelHTTPClientRunner extends I2PTunnelRunner {
|
||||
private Log _log;
|
||||
public I2PTunnelHTTPClientRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, List sockList, Runnable onTimeout) {
|
||||
super(s, i2ps, slock, initialI2PData, sockList, onTimeout);
|
||||
_log = I2PAppContext.getGlobalContext().logManager().getLog(I2PTunnelHTTPClientRunner.class);
|
||||
}
|
||||
|
||||
protected OutputStream getSocketOut() throws IOException {
|
||||
OutputStream raw = super.getSocketOut();
|
||||
return new HTTPResponseOutputStream(raw);
|
||||
}
|
||||
|
||||
protected void close(OutputStream out, InputStream in, OutputStream i2pout, InputStream i2pin, Socket s, I2PSocket i2ps, Thread t1, Thread t2) throws InterruptedException, IOException {
|
||||
try {
|
||||
i2pin.close();
|
||||
i2pout.close();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Unable to close the i2p socket output stream: " + i2pout, ioe);
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
out.close();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Unable to close the browser output stream: " + out, ioe);
|
||||
}
|
||||
i2ps.close();
|
||||
s.close();
|
||||
t1.join(30*1000);
|
||||
t2.join(30*1000);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
|
||||
* (c) 2003 - 2004 mihi
|
||||
*/
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.streaming.I2PServerSocket;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.EventDispatcher;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Simple extension to the I2PTunnelServer that filters the HTTP
|
||||
* headers sent from the client to the server, replacing the Host
|
||||
* header with whatever this instance has been configured with, and
|
||||
* if the browser set Accept-encoding: x-i2p-gzip, gzip the http
|
||||
* message body and set Content-encoding: x-i2p-gzip.
|
||||
*
|
||||
*/
|
||||
public class I2PTunnelHTTPServer extends I2PTunnelServer {
|
||||
private final static Log _log = new Log(I2PTunnelHTTPServer.class);
|
||||
/** what Host: should we seem to be to the webserver? */
|
||||
private String _spoofHost;
|
||||
|
||||
public I2PTunnelHTTPServer(InetAddress host, int port, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
super(host, port, privData, l, notifyThis, tunnel);
|
||||
_spoofHost = spoofHost;
|
||||
getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpserver.blockingHandleTime", "how long the blocking handle takes to complete", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000, 3*60*60*1000 });
|
||||
getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpNullWorkaround", "How often an http server works around a streaming lib or i2ptunnel bug", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000 });
|
||||
}
|
||||
|
||||
public I2PTunnelHTTPServer(InetAddress host, int port, File privkey, String privkeyname, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
super(host, port, privkey, privkeyname, l, notifyThis, tunnel);
|
||||
_spoofHost = spoofHost;
|
||||
getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpserver.blockingHandleTime", "how long the blocking handle takes to complete", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000, 3*60*60*1000 });
|
||||
getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpNullWorkaround", "How often an http server works around a streaming lib or i2ptunnel bug", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000 });
|
||||
}
|
||||
|
||||
public I2PTunnelHTTPServer(InetAddress host, int port, InputStream privData, String privkeyname, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
super(host, port, privData, privkeyname, l, notifyThis, tunnel);
|
||||
_spoofHost = spoofHost;
|
||||
getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpserver.blockingHandleTime", "how long the blocking handle takes to complete", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000, 3*60*60*1000 });
|
||||
getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpNullWorkaround", "How often an http server works around a streaming lib or i2ptunnel bug", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the thread pool of I2PSocket handlers
|
||||
*
|
||||
*/
|
||||
protected void blockingHandle(I2PSocket socket) {
|
||||
long afterAccept = getTunnel().getContext().clock().now();
|
||||
long afterSocket = -1;
|
||||
//local is fast, so synchronously. Does not need that many
|
||||
//threads.
|
||||
try {
|
||||
// give them 5 seconds to send in the HTTP request
|
||||
socket.setReadTimeout(5*1000);
|
||||
|
||||
InputStream in = socket.getInputStream();
|
||||
|
||||
StringBuffer command = new StringBuffer(128);
|
||||
Properties headers = readHeaders(in, command);
|
||||
if ( (_spoofHost != null) && (_spoofHost.trim().length() > 0) )
|
||||
headers.setProperty("Host", _spoofHost);
|
||||
headers.setProperty("Connection", "close");
|
||||
// we keep the enc sent by the browser before clobbering it, since it may have
|
||||
// been x-i2p-gzip
|
||||
String enc = headers.getProperty("Accept-encoding");
|
||||
String altEnc = headers.getProperty("X-Accept-encoding");
|
||||
|
||||
// according to rfc2616 s14.3, this *should* force identity, even if
|
||||
// "identity;q=1, *;q=0" didn't.
|
||||
headers.setProperty("Accept-encoding", "");
|
||||
String modifiedHeader = formatHeaders(headers, command);
|
||||
|
||||
//String modifiedHeader = getModifiedHeader(socket);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Modified header: [" + modifiedHeader + "]");
|
||||
|
||||
socket.setReadTimeout(readTimeout);
|
||||
Socket s = new Socket(remoteHost, remotePort);
|
||||
afterSocket = getTunnel().getContext().clock().now();
|
||||
// instead of i2ptunnelrunner, use something that reads the HTTP
|
||||
// request from the socket, modifies the headers, sends the request to the
|
||||
// server, reads the response headers, rewriting to include Content-encoding: x-i2p-gzip
|
||||
// if it was one of the Accept-encoding: values, and gzip the payload
|
||||
Properties opts = getTunnel().getClientOptions();
|
||||
boolean allowGZIP = true;
|
||||
if (opts != null) {
|
||||
String val = opts.getProperty("i2ptunnel.gzip");
|
||||
if ( (val != null) && (!Boolean.valueOf(val).booleanValue()) )
|
||||
allowGZIP = false;
|
||||
}
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("HTTP server encoding header: " + enc + "/" + altEnc);
|
||||
boolean useGZIP = ( (enc != null) && (enc.indexOf("x-i2p-gzip") >= 0) );
|
||||
if ( (!useGZIP) && (altEnc != null) && (altEnc.indexOf("x-i2p-gzip") >= 0) )
|
||||
useGZIP = true;
|
||||
|
||||
if (allowGZIP && useGZIP) {
|
||||
I2PThread req = new I2PThread(new CompressedRequestor(s, socket, modifiedHeader), Thread.currentThread().getName()+".hc");
|
||||
req.start();
|
||||
} else {
|
||||
new I2PTunnelRunner(s, socket, slock, null, modifiedHeader.getBytes(), null);
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Error while closing the received i2p con", ex);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Error while receiving the new HTTP request", ex);
|
||||
}
|
||||
|
||||
long afterHandle = getTunnel().getContext().clock().now();
|
||||
long timeToHandle = afterHandle - afterAccept;
|
||||
getTunnel().getContext().statManager().addRateData("i2ptunnel.httpserver.blockingHandleTime", timeToHandle, 0);
|
||||
if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) )
|
||||
_log.warn("Took a while to handle the request [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
|
||||
}
|
||||
|
||||
private class CompressedRequestor implements Runnable {
|
||||
private Socket _webserver;
|
||||
private I2PSocket _browser;
|
||||
private String _headers;
|
||||
public CompressedRequestor(Socket webserver, I2PSocket browser, String headers) {
|
||||
_webserver = webserver;
|
||||
_browser = browser;
|
||||
_headers = headers;
|
||||
}
|
||||
public void run() {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Compressed requestor running");
|
||||
OutputStream serverout = null;
|
||||
OutputStream browserout = null;
|
||||
InputStream browserin = null;
|
||||
InputStream serverin = null;
|
||||
try {
|
||||
serverout = _webserver.getOutputStream();
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("request headers: " + _headers);
|
||||
serverout.write(_headers.getBytes());
|
||||
browserin = _browser.getInputStream();
|
||||
I2PThread sender = new I2PThread(new Sender(serverout, browserin, "server: browser to server"), Thread.currentThread().getName() + "hcs");
|
||||
sender.start();
|
||||
|
||||
browserout = _browser.getOutputStream();
|
||||
serverin = _webserver.getInputStream();
|
||||
CompressedResponseOutputStream compressedOut = new CompressedResponseOutputStream(browserout);
|
||||
Sender s = new Sender(compressedOut, serverin, "server: server to browser");
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Before pumping the compressed response");
|
||||
s.run(); // same thread
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("After pumping the compressed response: " + compressedOut.getTotalRead() + "/" + compressedOut.getTotalCompressed());
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("error compressing", ioe);
|
||||
} finally {
|
||||
if (browserout != null) try { browserout.close(); } catch (IOException ioe) {}
|
||||
if (serverout != null) try { serverout.close(); } catch (IOException ioe) {}
|
||||
if (browserin != null) try { browserin.close(); } catch (IOException ioe) {}
|
||||
if (serverin != null) try { serverin.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
private class Sender implements Runnable {
|
||||
private OutputStream _out;
|
||||
private InputStream _in;
|
||||
private String _name;
|
||||
public Sender(OutputStream out, InputStream in, String name) {
|
||||
_out = out;
|
||||
_in = in;
|
||||
_name = name;
|
||||
}
|
||||
public void run() {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(_name + ": Begin sending");
|
||||
try {
|
||||
byte buf[] = new byte[16*1024];
|
||||
int read = 0;
|
||||
int total = 0;
|
||||
while ( (read = _in.read(buf)) != -1) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(_name + ": read " + read + " and sending through the stream");
|
||||
_out.write(buf, 0, read);
|
||||
total += read;
|
||||
}
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(_name + ": Done sending: " + total);
|
||||
//_out.flush();
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Error sending", ioe);
|
||||
} finally {
|
||||
if (_out != null) try { _out.close(); } catch (IOException ioe) {}
|
||||
if (_in != null) try { _in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
private class CompressedResponseOutputStream extends HTTPResponseOutputStream {
|
||||
private InternalGZIPOutputStream _gzipOut;
|
||||
public CompressedResponseOutputStream(OutputStream o) {
|
||||
super(o);
|
||||
}
|
||||
|
||||
protected boolean shouldCompress() { return true; }
|
||||
protected void finishHeaders() throws IOException {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Including x-i2p-gzip as the content encoding in the response");
|
||||
out.write("Content-encoding: x-i2p-gzip\r\n".getBytes());
|
||||
super.finishHeaders();
|
||||
}
|
||||
|
||||
protected void beginProcessing() throws IOException {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Beginning compression processing");
|
||||
//out.flush();
|
||||
_gzipOut = new InternalGZIPOutputStream(out);
|
||||
out = _gzipOut;
|
||||
}
|
||||
public long getTotalRead() {
|
||||
InternalGZIPOutputStream gzipOut = _gzipOut;
|
||||
if (gzipOut != null)
|
||||
return gzipOut.getTotalRead();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
public long getTotalCompressed() {
|
||||
InternalGZIPOutputStream gzipOut = _gzipOut;
|
||||
if (gzipOut != null)
|
||||
return gzipOut.getTotalCompressed();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
private class InternalGZIPOutputStream extends GZIPOutputStream {
|
||||
public InternalGZIPOutputStream(OutputStream target) throws IOException {
|
||||
super(target);
|
||||
}
|
||||
public long getTotalRead() {
|
||||
try {
|
||||
return def.getTotalIn();
|
||||
} catch (Exception e) {
|
||||
// j2se 1.4.2_08 on linux is sometimes throwing an NPE in the getTotalIn() implementation
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
public long getTotalCompressed() {
|
||||
try {
|
||||
return def.getTotalOut();
|
||||
} catch (Exception e) {
|
||||
// j2se 1.4.2_08 on linux is sometimes throwing an NPE in the getTotalOut() implementation
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String formatHeaders(Properties headers, StringBuffer command) {
|
||||
StringBuffer buf = new StringBuffer(command.length() + headers.size() * 64);
|
||||
buf.append(command.toString().trim()).append("\r\n");
|
||||
for (Iterator iter = headers.keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
String val = headers.getProperty(name);
|
||||
buf.append(name.trim()).append(": ").append(val.trim()).append("\r\n");
|
||||
}
|
||||
buf.append("\r\n");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private Properties readHeaders(InputStream in, StringBuffer command) throws IOException {
|
||||
Properties headers = new Properties();
|
||||
StringBuffer buf = new StringBuffer(128);
|
||||
|
||||
boolean ok = DataHelper.readLine(in, command);
|
||||
if (!ok) throw new IOException("EOF reached while reading the HTTP command [" + command.toString() + "]");
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Read the http command [" + command.toString() + "]");
|
||||
|
||||
int trimmed = 0;
|
||||
if (command.length() > 0) {
|
||||
for (int i = 0; i < command.length(); i++) {
|
||||
if (command.charAt(i) == 0) {
|
||||
command = command.deleteCharAt(i);
|
||||
i--;
|
||||
trimmed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trimmed > 0)
|
||||
getTunnel().getContext().statManager().addRateData("i2ptunnel.httpNullWorkaround", trimmed, 0);
|
||||
|
||||
while (true) {
|
||||
buf.setLength(0);
|
||||
ok = DataHelper.readLine(in, buf);
|
||||
if (!ok) throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]");
|
||||
if ( (buf.length() == 0) ||
|
||||
((buf.charAt(0) == '\n') || (buf.charAt(0) == '\r')) ) {
|
||||
// end of headers reached
|
||||
return headers;
|
||||
} else {
|
||||
int split = buf.indexOf(":");
|
||||
if (split <= 0) throw new IOException("Invalid HTTP header, missing colon [" + buf.toString() + "]");
|
||||
String name = buf.substring(0, split).trim();
|
||||
String value = null;
|
||||
if (buf.length() > split + 1)
|
||||
value = buf.substring(split+1).trim(); // ":"
|
||||
else
|
||||
value = "";
|
||||
if ("Accept-encoding".equalsIgnoreCase(name))
|
||||
name = "Accept-encoding";
|
||||
else if ("X-Accept-encoding".equalsIgnoreCase(name))
|
||||
name = "X-Accept-encoding";
|
||||
headers.setProperty(name, value);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Read the header [" + name + "] = [" + value + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
import java.lang.IndexOutOfBoundsException;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.EventDispatcher;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable {
|
||||
|
||||
private static final Log _log = new Log(I2PTunnelIRCClient.class);
|
||||
|
||||
/** used to assign unique IDs to the threads / clients. no logic or functionality */
|
||||
private static volatile long __clientId = 0;
|
||||
|
||||
/** list of Destination objects that we point at */
|
||||
protected List dests;
|
||||
private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
|
||||
protected long readTimeout = DEFAULT_READ_TIMEOUT;
|
||||
/** this is the pong response the client expects for their last ping. at least, i hope so... */
|
||||
private String _expectedPong;
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if the I2PTunnel does not contain
|
||||
* valid config to contact the router
|
||||
*/
|
||||
public I2PTunnelIRCClient(
|
||||
int localPort,
|
||||
String destinations,
|
||||
Logging l,
|
||||
boolean ownDest,
|
||||
EventDispatcher notifyThis,
|
||||
I2PTunnel tunnel) throws IllegalArgumentException {
|
||||
super(localPort,
|
||||
ownDest,
|
||||
l,
|
||||
notifyThis,
|
||||
"IRCHandler " + (++__clientId), tunnel);
|
||||
|
||||
_expectedPong = null;
|
||||
|
||||
StringTokenizer tok = new StringTokenizer(destinations, ",");
|
||||
dests = new ArrayList(1);
|
||||
while (tok.hasMoreTokens()) {
|
||||
String destination = tok.nextToken();
|
||||
try {
|
||||
Destination dest = I2PTunnel.destFromName(destination);
|
||||
if (dest == null)
|
||||
l.log("Could not resolve " + destination);
|
||||
else
|
||||
dests.add(dest);
|
||||
} catch (DataFormatException dfe) {
|
||||
l.log("Bad format parsing \"" + destination + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (dests.size() <= 0) {
|
||||
l.log("No target destinations found");
|
||||
notifyEvent("openClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setName(getLocalPort() + " -> IRCClient");
|
||||
|
||||
startRunning();
|
||||
|
||||
notifyEvent("openIRCClientResult", "ok");
|
||||
}
|
||||
|
||||
protected void clientConnectionRun(Socket s) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("got a connection.");
|
||||
Destination dest = pickDestination();
|
||||
I2PSocket i2ps = null;
|
||||
try {
|
||||
i2ps = createI2PSocket(dest);
|
||||
i2ps.setReadTimeout(readTimeout);
|
||||
Thread in = new I2PThread(new IrcInboundFilter(s,i2ps));
|
||||
in.start();
|
||||
Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps));
|
||||
out.start();
|
||||
} catch (Exception ex) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Error connecting", ex);
|
||||
l.log(ex.getMessage());
|
||||
closeSocket(s);
|
||||
if (i2ps != null) {
|
||||
synchronized (sockLock) {
|
||||
mySockets.remove(sockLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final Destination pickDestination() {
|
||||
int size = dests.size();
|
||||
if (size <= 0) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("No client targets?!");
|
||||
return null;
|
||||
}
|
||||
if (size == 1) // skip the rand in the most common case
|
||||
return (Destination)dests.get(0);
|
||||
int index = I2PAppContext.getGlobalContext().random().nextInt(size);
|
||||
return (Destination)dests.get(index);
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*
|
||||
*/
|
||||
private class IrcInboundFilter implements Runnable {
|
||||
|
||||
private Socket local;
|
||||
private I2PSocket remote;
|
||||
|
||||
IrcInboundFilter(Socket _local, I2PSocket _remote) {
|
||||
local=_local;
|
||||
remote=_remote;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
BufferedReader in;
|
||||
OutputStream output;
|
||||
try {
|
||||
in = new BufferedReader(new InputStreamReader(remote.getInputStream(), "ISO-8859-1"));
|
||||
output=local.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("IrcInboundFilter: no streams",e);
|
||||
return;
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("IrcInboundFilter: Running.");
|
||||
try {
|
||||
while(true)
|
||||
{
|
||||
try {
|
||||
String inmsg = in.readLine();
|
||||
if(inmsg==null)
|
||||
break;
|
||||
if(inmsg.endsWith("\r"))
|
||||
inmsg=inmsg.substring(0,inmsg.length()-1);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("in: [" + inmsg + "]");
|
||||
String outmsg = inboundFilter(inmsg);
|
||||
if(outmsg!=null)
|
||||
{
|
||||
if(!inmsg.equals(outmsg)) {
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
_log.warn("inbound FILTERED: "+outmsg);
|
||||
_log.warn(" - inbound was: "+inmsg);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("inbound: "+outmsg);
|
||||
}
|
||||
outmsg=outmsg+"\n";
|
||||
output.write(outmsg.getBytes("ISO-8859-1"));
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("inbound BLOCKED: "+inmsg);
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("IrcInboundFilter: disconnected",e1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException re) {
|
||||
_log.error("Error filtering inbound data", re);
|
||||
} finally {
|
||||
if (local != null) try { local.close(); } catch (IOException e) {}
|
||||
}
|
||||
if(_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("IrcInboundFilter: Done.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*
|
||||
*/
|
||||
private class IrcOutboundFilter implements Runnable {
|
||||
|
||||
private Socket local;
|
||||
private I2PSocket remote;
|
||||
|
||||
IrcOutboundFilter(Socket _local, I2PSocket _remote) {
|
||||
local=_local;
|
||||
remote=_remote;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
BufferedReader in;
|
||||
OutputStream output;
|
||||
try {
|
||||
in = new BufferedReader(new InputStreamReader(local.getInputStream(), "ISO-8859-1"));
|
||||
output=remote.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("IrcOutboundFilter: no streams",e);
|
||||
return;
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("IrcOutboundFilter: Running.");
|
||||
try {
|
||||
while(true)
|
||||
{
|
||||
try {
|
||||
String inmsg = in.readLine();
|
||||
if(inmsg==null)
|
||||
break;
|
||||
if(inmsg.endsWith("\r"))
|
||||
inmsg=inmsg.substring(0,inmsg.length()-1);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("out: [" + inmsg + "]");
|
||||
String outmsg = outboundFilter(inmsg);
|
||||
if(outmsg!=null)
|
||||
{
|
||||
if(!inmsg.equals(outmsg)) {
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
_log.warn("outbound FILTERED: "+outmsg);
|
||||
_log.warn(" - outbound was: "+inmsg);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("outbound: "+outmsg);
|
||||
}
|
||||
outmsg=outmsg+"\n";
|
||||
output.write(outmsg.getBytes("ISO-8859-1"));
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("outbound BLOCKED: "+"\""+inmsg+"\"");
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("IrcOutboundFilter: disconnected",e1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException re) {
|
||||
_log.error("Error filtering outbound data", re);
|
||||
} finally {
|
||||
if (remote != null) try { remote.close(); } catch (IOException e) {}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("IrcOutboundFilter: Done.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************
|
||||
*
|
||||
*/
|
||||
|
||||
public String inboundFilter(String s) {
|
||||
|
||||
String field[]=s.split(" ",4);
|
||||
String command;
|
||||
int idx=0;
|
||||
final String[] allowedCommands =
|
||||
{
|
||||
"NOTICE",
|
||||
//"PING",
|
||||
//"PONG",
|
||||
"MODE",
|
||||
"JOIN",
|
||||
"NICK",
|
||||
"QUIT",
|
||||
"PART",
|
||||
"WALLOPS",
|
||||
"ERROR",
|
||||
"TOPIC"
|
||||
};
|
||||
|
||||
if(field[0].charAt(0)==':')
|
||||
idx++;
|
||||
|
||||
try { command = field[idx++]; }
|
||||
catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command?
|
||||
{
|
||||
_log.warn("Dropping defective message: index out of bounds while extracting command.");
|
||||
return null;
|
||||
}
|
||||
|
||||
idx++; //skip victim
|
||||
|
||||
// Allow numerical responses
|
||||
try {
|
||||
new Integer(command);
|
||||
return s;
|
||||
} catch(NumberFormatException nfe){}
|
||||
|
||||
|
||||
if ("PING".equals(command))
|
||||
return "PING 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works
|
||||
if ("PONG".equals(command)) {
|
||||
// Turn the received ":irc.freshcoffee.i2p PONG irc.freshcoffee.i2p :127.0.0.1"
|
||||
// into ":127.0.0.1 PONG 127.0.0.1 " so that the caller can append the client's extra parameter
|
||||
// though, does 127.0.0.1 work for irc clients connecting remotely? and for all of them? sure would
|
||||
// be great if irc clients actually followed the RFCs here, but i guess thats too much to ask.
|
||||
// If we haven't PINGed them, or the PING we sent isn't something we know how to filter, this
|
||||
// is null.
|
||||
String pong = _expectedPong;
|
||||
_expectedPong = null;
|
||||
return pong;
|
||||
}
|
||||
|
||||
// Allow all allowedCommands
|
||||
for(int i=0;i<allowedCommands.length;i++) {
|
||||
if(allowedCommands[i].equals(command))
|
||||
return s;
|
||||
}
|
||||
|
||||
// Allow PRIVMSG, but block CTCP.
|
||||
if("PRIVMSG".equals(command))
|
||||
{
|
||||
String msg;
|
||||
msg = field[idx++];
|
||||
|
||||
byte[] bytes = msg.getBytes();
|
||||
if(bytes[1]==0x01)
|
||||
{
|
||||
// CTCP
|
||||
msg=msg.substring(2);
|
||||
if(msg.startsWith("ACTION ")) {
|
||||
// /me says hello
|
||||
return s;
|
||||
}
|
||||
return null; // Block all other ctcp
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Block the rest
|
||||
return null;
|
||||
}
|
||||
|
||||
public String outboundFilter(String s) {
|
||||
|
||||
String field[]=s.split(" ",3);
|
||||
String command;
|
||||
final String[] allowedCommands =
|
||||
{
|
||||
"NOTICE",
|
||||
"MODE",
|
||||
"JOIN",
|
||||
"NICK",
|
||||
"WHO",
|
||||
"WHOIS",
|
||||
"LIST",
|
||||
"NAMES",
|
||||
"NICK",
|
||||
// "QUIT", // replace with a filtered QUIT to hide client quit messages
|
||||
"SILENCE",
|
||||
"MAP", // seems safe enough, the ircd should protect themselves though
|
||||
"PART",
|
||||
"OPER",
|
||||
// "PONG", // replaced with a filtered PING/PONG since some clients send the server IP (thanks aardvax!)
|
||||
// "PING",
|
||||
"KICK",
|
||||
"HELPME",
|
||||
"RULES",
|
||||
"TOPIC"
|
||||
};
|
||||
|
||||
if(field[0].length()==0)
|
||||
return null; // W T F?
|
||||
|
||||
|
||||
if(field[0].charAt(0)==':')
|
||||
return null; // wtf
|
||||
|
||||
command = field[0].toUpperCase();
|
||||
|
||||
if ("PING".equals(command)) {
|
||||
// Most clients just send a PING and are happy with any old PONG. Others,
|
||||
// like BitchX, actually expect certain behavior. It sends two different pings:
|
||||
// "PING :irc.freshcoffee.i2p" and "PING 1234567890 127.0.0.1" (where the IP is the proxy)
|
||||
// the PONG to the former seems to be "PONG 127.0.0.1", while the PONG to the later is
|
||||
// ":irc.freshcoffee.i2p PONG irc.freshcoffe.i2p :1234567890".
|
||||
// We don't want to send them our proxy's IP address, so we need to rewrite the PING
|
||||
// sent to the server, but when we get a PONG back, use what we expected, rather than
|
||||
// what they sent.
|
||||
//
|
||||
// Yuck.
|
||||
|
||||
String rv = null;
|
||||
if (field.length == 1) { // PING
|
||||
rv = "PING";
|
||||
_expectedPong = "PONG 127.0.0.1";
|
||||
} else if (field.length == 2) { // PING nonce
|
||||
rv = "PING " + field[1];
|
||||
_expectedPong = "PONG " + field[1];
|
||||
} else if (field.length == 3) { // PING nonce serverLocation
|
||||
rv = "PING " + field[1];
|
||||
_expectedPong = "PONG " + field[1];
|
||||
} else {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("IRC client sent a PING we don't understand, filtering it (\"" + s + "\")");
|
||||
rv = null;
|
||||
_expectedPong = null;
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("sending ping " + rv + ", waiting for " + _expectedPong + " orig was [" + s + "]");
|
||||
|
||||
return rv;
|
||||
}
|
||||
if ("PONG".equals(command))
|
||||
return "PONG 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works
|
||||
|
||||
// Allow all allowedCommands
|
||||
for(int i=0;i<allowedCommands.length;i++)
|
||||
{
|
||||
if(allowedCommands[i].equals(command))
|
||||
return s;
|
||||
}
|
||||
|
||||
// Allow PRIVMSG, but block CTCP (except ACTION).
|
||||
if("PRIVMSG".equals(command))
|
||||
{
|
||||
String msg;
|
||||
msg = field[2];
|
||||
|
||||
byte[] bytes = msg.getBytes();
|
||||
if(bytes[1]==0x01)
|
||||
{
|
||||
// CTCP
|
||||
msg=msg.substring(2);
|
||||
if(msg.startsWith("ACTION ")) {
|
||||
// /me says hello
|
||||
return s;
|
||||
}
|
||||
return null; // Block all other ctcp
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
if("USER".equals(command)) {
|
||||
int idx = field[2].lastIndexOf(":");
|
||||
if(idx<0)
|
||||
return "USER user hostname localhost :realname";
|
||||
String realname = field[2].substring(idx+1);
|
||||
String ret = "USER "+field[1]+" hostname localhost :"+realname;
|
||||
return ret;
|
||||
} else if ("QUIT".equals(command)) {
|
||||
return "QUIT :leaving";
|
||||
}
|
||||
|
||||
// Block the rest
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,55 +3,81 @@
|
||||
*/
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.util.ByteCache;
|
||||
import net.i2p.util.Clock;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
public class I2PTunnelRunner extends Thread {
|
||||
public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorListener {
|
||||
private final static Log _log = new Log(I2PTunnelRunner.class);
|
||||
|
||||
|
||||
private static volatile long __runnerId;
|
||||
private long _runnerId;
|
||||
/**
|
||||
* max bytes streamed in a packet - smaller ones might be filled
|
||||
* up to this size. Larger ones are not split (at least not on
|
||||
* Sun's impl of BufferedOutputStream), but that is the streaming
|
||||
* api's job...
|
||||
*/
|
||||
static int MAX_PACKET_SIZE = 1024*32;
|
||||
static int MAX_PACKET_SIZE = 1024 * 4;
|
||||
|
||||
static final int NETWORK_BUFFER_SIZE = MAX_PACKET_SIZE;
|
||||
|
||||
private Socket s;
|
||||
private I2PSocket i2ps;
|
||||
Object slock, finishLock = new Object();
|
||||
boolean finished=false;
|
||||
boolean finished = false;
|
||||
HashMap ostreams, sockets;
|
||||
I2PSession session;
|
||||
byte[] initialData;
|
||||
byte[] initialI2PData;
|
||||
byte[] initialSocketData;
|
||||
/** when the last data was sent/received (or -1 if never) */
|
||||
private long lastActivityOn;
|
||||
/** when the runner started up */
|
||||
private long startedOn;
|
||||
private List sockList;
|
||||
/** if we die before receiving any data, run this job */
|
||||
private Runnable onTimeout;
|
||||
private long totalSent;
|
||||
private long totalReceived;
|
||||
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock,
|
||||
byte[] initialData) {
|
||||
this.s=s;
|
||||
this.i2ps=i2ps;
|
||||
this.slock=slock;
|
||||
this.initialData = initialData;
|
||||
lastActivityOn = -1;
|
||||
startedOn = -1;
|
||||
_log.info("I2PTunnelRunner started");
|
||||
setName("I2PTunnelRunner");
|
||||
start();
|
||||
private volatile long __forwarderId;
|
||||
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, List sockList) {
|
||||
this(s, i2ps, slock, initialI2PData, null, sockList, null);
|
||||
}
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List sockList) {
|
||||
this(s, i2ps, slock, initialI2PData, initialSocketData, sockList, null);
|
||||
}
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, List sockList, Runnable onTimeout) {
|
||||
this(s, i2ps, slock, initialI2PData, null, sockList, onTimeout);
|
||||
}
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List sockList, Runnable onTimeout) {
|
||||
this.sockList = sockList;
|
||||
this.s = s;
|
||||
this.i2ps = i2ps;
|
||||
this.slock = slock;
|
||||
this.initialI2PData = initialI2PData;
|
||||
this.initialSocketData = initialSocketData;
|
||||
this.onTimeout = onTimeout;
|
||||
lastActivityOn = -1;
|
||||
startedOn = Clock.getInstance().now();
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("I2PTunnelRunner started");
|
||||
_runnerId = ++__runnerId;
|
||||
__forwarderId = i2ps.hashCode();
|
||||
setName("I2PTunnelRunner " + _runnerId);
|
||||
start();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,126 +85,249 @@ public class I2PTunnelRunner extends Thread {
|
||||
* [aka we're done running the streams]?
|
||||
*
|
||||
*/
|
||||
public boolean isFinished() { return finished; }
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* When was the last data for this runner sent or received? (-1 if no data
|
||||
* has been transferred yet)
|
||||
* When was the last data for this runner sent or received?
|
||||
*
|
||||
* @return date (ms since the epoch), or -1 if no data has been transferred yet
|
||||
*
|
||||
*/
|
||||
public long getLastActivityOn() { return lastActivityOn; }
|
||||
private void updateActivity() { lastActivityOn = Clock.getInstance().now(); }
|
||||
|
||||
public long getLastActivityOn() {
|
||||
return lastActivityOn;
|
||||
}
|
||||
|
||||
private void updateActivity() {
|
||||
lastActivityOn = Clock.getInstance().now();
|
||||
}
|
||||
|
||||
/**
|
||||
* When this runner started up transferring data
|
||||
*
|
||||
*/
|
||||
public long getStartedOn() { return startedOn; }
|
||||
|
||||
public void run() {
|
||||
startedOn = Clock.getInstance().now();
|
||||
try {
|
||||
InputStream in = s.getInputStream();
|
||||
OutputStream out = new BufferedOutputStream(s.getOutputStream(),
|
||||
NETWORK_BUFFER_SIZE);
|
||||
InputStream i2pin = i2ps.getInputStream();
|
||||
OutputStream i2pout = new BufferedOutputStream
|
||||
(i2ps.getOutputStream(), MAX_PACKET_SIZE);
|
||||
if (initialData != null) {
|
||||
synchronized(slock) {
|
||||
i2pout.write(initialData);
|
||||
i2pout.flush();
|
||||
}
|
||||
}
|
||||
Thread t1 = new StreamForwarder(in, i2pout);
|
||||
Thread t2 = new StreamForwarder(i2pin, out);
|
||||
synchronized(finishLock) {
|
||||
while (!finished) {
|
||||
finishLock.wait();
|
||||
}
|
||||
}
|
||||
// now one connection is dead - kill the other as well.
|
||||
s.close();
|
||||
s = null;
|
||||
i2ps.close();
|
||||
i2ps = null;
|
||||
t1.join();
|
||||
t2.join();
|
||||
} catch (InterruptedException ex) {
|
||||
_log.error("Interrupted", ex);
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
_log.error("Error forwarding", ex);
|
||||
} finally {
|
||||
try {
|
||||
if (s != null) s.close();
|
||||
if (i2ps != null) i2ps.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
_log.error("Could not close socket", ex);
|
||||
}
|
||||
}
|
||||
public long getStartedOn() {
|
||||
return startedOn;
|
||||
}
|
||||
|
||||
private class StreamForwarder extends Thread {
|
||||
protected InputStream getSocketIn() throws IOException { return s.getInputStream(); }
|
||||
protected OutputStream getSocketOut() throws IOException { return s.getOutputStream(); }
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
InputStream in = getSocketIn();
|
||||
OutputStream out = getSocketOut(); // = new BufferedOutputStream(s.getOutputStream(), NETWORK_BUFFER_SIZE);
|
||||
i2ps.setSocketErrorListener(this);
|
||||
InputStream i2pin = i2ps.getInputStream();
|
||||
OutputStream i2pout = i2ps.getOutputStream(); //new BufferedOutputStream(i2ps.getOutputStream(), MAX_PACKET_SIZE);
|
||||
if (initialI2PData != null) {
|
||||
synchronized (slock) {
|
||||
i2pout.write(initialI2PData);
|
||||
//i2pout.flush();
|
||||
}
|
||||
}
|
||||
if (initialSocketData != null) {
|
||||
out.write(initialSocketData);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Initial data " + (initialI2PData != null ? initialI2PData.length : 0)
|
||||
+ " written to I2P, " + (initialSocketData != null ? initialSocketData.length : 0)
|
||||
+ " written to the socket, starting forwarders");
|
||||
Thread t1 = new StreamForwarder(in, i2pout, true);
|
||||
Thread t2 = new StreamForwarder(i2pin, out, false);
|
||||
synchronized (finishLock) {
|
||||
while (!finished) {
|
||||
finishLock.wait();
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("At least one forwarder completed, closing and joining");
|
||||
|
||||
// this task is useful for the httpclient
|
||||
if (onTimeout != null) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("runner has a timeout job, totalReceived = " + totalReceived
|
||||
+ " totalSent = " + totalSent + " job = " + onTimeout);
|
||||
if ( (totalSent <= 0) && (totalReceived <= 0) )
|
||||
onTimeout.run();
|
||||
}
|
||||
|
||||
// now one connection is dead - kill the other as well, after making sure we flush
|
||||
close(out, in, i2pout, i2pin, s, i2ps, t1, t2);
|
||||
} catch (InterruptedException ex) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Interrupted", ex);
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Error forwarding", ex);
|
||||
} catch (Exception e) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Internal error", e);
|
||||
} finally {
|
||||
removeRef();
|
||||
try {
|
||||
if (s != null)
|
||||
s.close();
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Could not close java socket", ex);
|
||||
}
|
||||
if (i2ps != null) {
|
||||
try {
|
||||
i2ps.close();
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Could not close I2PSocket", ex);
|
||||
}
|
||||
i2ps.setSocketErrorListener(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void close(OutputStream out, InputStream in, OutputStream i2pout, InputStream i2pin, Socket s, I2PSocket i2ps, Thread t1, Thread t2) throws InterruptedException, IOException {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
i2pout.flush();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
}
|
||||
in.close();
|
||||
i2pin.close();
|
||||
// ok, yeah, there's a race here in theory, if data comes in after flushing and before
|
||||
// closing, but its better than before...
|
||||
s.close();
|
||||
i2ps.close();
|
||||
t1.join(30*1000);
|
||||
t2.join(30*1000);
|
||||
}
|
||||
|
||||
public void errorOccurred() {
|
||||
synchronized (finishLock) {
|
||||
finished = true;
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeRef() {
|
||||
if (sockList != null) {
|
||||
synchronized (slock) {
|
||||
boolean removed = sockList.remove(i2ps);
|
||||
//System.out.println("Removal of i2psocket " + i2ps + " successful? "
|
||||
// + removed + " remaining: " + sockList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StreamForwarder extends I2PThread {
|
||||
|
||||
InputStream in;
|
||||
OutputStream out;
|
||||
|
||||
private StreamForwarder(InputStream in, OutputStream out) {
|
||||
this.in=in;
|
||||
this.out=out;
|
||||
setName("StreamForwarder");
|
||||
start();
|
||||
}
|
||||
InputStream in;
|
||||
OutputStream out;
|
||||
String direction;
|
||||
private boolean _toI2P;
|
||||
private ByteCache _cache;
|
||||
|
||||
public void run() {
|
||||
byte[] buffer = new byte[NETWORK_BUFFER_SIZE];
|
||||
try {
|
||||
int len;
|
||||
while ((len=in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
|
||||
if (len > 0)
|
||||
updateActivity();
|
||||
|
||||
if (in.available()==0) {
|
||||
try {
|
||||
Thread.sleep(I2PTunnel.PACKET_DELAY);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (in.available()==0) {
|
||||
out.flush(); // make sure the data get though
|
||||
}
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
// this *will* occur when the other threads closes the socket
|
||||
synchronized(finishLock) {
|
||||
if (!finished)
|
||||
_log.error("Error reading and writing", ex);
|
||||
else
|
||||
_log.warn("You may ignore this", ex);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (!finished)
|
||||
_log.error("Error forwarding", ex);
|
||||
else
|
||||
_log.warn("You may ignore this", ex);
|
||||
} finally {
|
||||
try {
|
||||
out.close();
|
||||
in.close();
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error closing streams", ex);
|
||||
}
|
||||
synchronized(finishLock) {
|
||||
finished=true;
|
||||
finishLock.notifyAll();
|
||||
// the main thread will close sockets etc. now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private StreamForwarder(InputStream in, OutputStream out, boolean toI2P) {
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
_toI2P = toI2P;
|
||||
direction = (toI2P ? "toI2P" : "fromI2P");
|
||||
_cache = ByteCache.getInstance(32, NETWORK_BUFFER_SIZE);
|
||||
setName("StreamForwarder " + _runnerId + "." + (++__forwarderId));
|
||||
start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
String from = i2ps.getThisDestination().calculateHash().toBase64().substring(0,6);
|
||||
String to = i2ps.getPeerDestination().calculateHash().toBase64().substring(0,6);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(direction + ": Forwarding between "
|
||||
+ from + " and " + to);
|
||||
}
|
||||
|
||||
// boo, hiss! shouldn't need this - the streaming lib should be configurable, but
|
||||
// somehow the inactivity timer is sometimes failing to get triggered properly
|
||||
//i2ps.setReadTimeout(2*60*1000);
|
||||
|
||||
ByteArray ba = _cache.acquire();
|
||||
byte[] buffer = ba.getData(); // new byte[NETWORK_BUFFER_SIZE];
|
||||
try {
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
if (_toI2P)
|
||||
totalSent += len;
|
||||
else
|
||||
totalReceived += len;
|
||||
|
||||
if (len > 0) updateActivity();
|
||||
|
||||
if (in.available() == 0) {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Flushing after sending " + len + " bytes through");
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(direction + ": " + len + " bytes flushed through to "
|
||||
+ i2ps.getPeerDestination().calculateHash().toBase64().substring(0,6));
|
||||
try {
|
||||
Thread.sleep(I2PTunnel.PACKET_DELAY);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (in.available() <= 0)
|
||||
out.flush(); // make sure the data get though
|
||||
}
|
||||
}
|
||||
//out.flush(); // close() flushes
|
||||
} catch (SocketException ex) {
|
||||
// this *will* occur when the other threads closes the socket
|
||||
synchronized (finishLock) {
|
||||
if (!finished) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(direction + ": Socket closed - error reading and writing",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedIOException ex) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(direction + ": Closing connection due to timeout (error: \""
|
||||
+ ex.getMessage() + "\")");
|
||||
} catch (IOException ex) {
|
||||
if (!finished) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(direction + ": Error forwarding", ex);
|
||||
}
|
||||
//else
|
||||
// _log.warn("You may ignore this", ex);
|
||||
} finally {
|
||||
_cache.release(ba);
|
||||
if (_log.shouldLog(Log.INFO)) {
|
||||
_log.info(direction + ": done forwarding between "
|
||||
+ from + " and " + to);
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(direction + ": Error closing input stream", ex);
|
||||
}
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(direction + ": Error flushing to close", ioe);
|
||||
}
|
||||
synchronized (finishLock) {
|
||||
finished = true;
|
||||
finishLock.notifyAll();
|
||||
// the main thread will close sockets etc. now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.*;
|
||||
import java.net.ConnectException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
@@ -22,117 +25,256 @@ import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.util.EventDispatcher;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
public class I2PTunnelServer extends I2PTunnelTask
|
||||
implements Runnable {
|
||||
|
||||
public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
|
||||
|
||||
private final static Log _log = new Log(I2PTunnelServer.class);
|
||||
|
||||
private I2PSocketManager sockMgr;
|
||||
private I2PServerSocket i2pss;
|
||||
protected I2PSocketManager sockMgr;
|
||||
protected I2PServerSocket i2pss;
|
||||
|
||||
private Object lock = new Object(), slock = new Object();
|
||||
private Object lock = new Object();
|
||||
protected Object slock = new Object();
|
||||
|
||||
private InetAddress remoteHost;
|
||||
private int remotePort;
|
||||
protected InetAddress remoteHost;
|
||||
protected int remotePort;
|
||||
private boolean _usePool;
|
||||
|
||||
private Logging l;
|
||||
|
||||
private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000;
|
||||
/** default timeout to 3 minutes - override if desired */
|
||||
protected long readTimeout = DEFAULT_READ_TIMEOUT;
|
||||
|
||||
private static final boolean DEFAULT_USE_POOL = false;
|
||||
|
||||
public I2PTunnelServer(InetAddress host, int port,
|
||||
String privData, Logging l,
|
||||
EventDispatcher notifyThis) {
|
||||
super(host+":"+port+" <- "+privData, notifyThis);
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(privData));
|
||||
init(host, port, bais, privData, l);
|
||||
}
|
||||
|
||||
public I2PTunnelServer(InetAddress host, int port,
|
||||
File privkey, String privkeyname,
|
||||
Logging l, EventDispatcher notifyThis) {
|
||||
super(host+":"+port+" <- "+privkeyname, notifyThis);
|
||||
try {
|
||||
init(host, port, new FileInputStream(privkey), privkeyname, l);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error starting server", ioe);
|
||||
notifyEvent("openServerResult", "error");
|
||||
}
|
||||
}
|
||||
public I2PTunnelServer(InetAddress host, int port,
|
||||
InputStream privData, String privkeyname,
|
||||
Logging l, EventDispatcher notifyThis) {
|
||||
super(host+":"+port+" <- "+privkeyname, notifyThis);
|
||||
init(host, port, privData, privkeyname, l);
|
||||
}
|
||||
|
||||
private void init(InetAddress host, int port, InputStream privData,
|
||||
String privkeyname, Logging l) {
|
||||
this.l=l;
|
||||
this.remoteHost=host;
|
||||
this.remotePort=port;
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
Properties props = new Properties();
|
||||
props.putAll(System.getProperties());
|
||||
synchronized(slock) {
|
||||
sockMgr = I2PSocketManagerFactory.createManager
|
||||
(privData, I2PTunnel.host,
|
||||
Integer.parseInt(I2PTunnel.port), props);
|
||||
|
||||
}
|
||||
l.log("Ready!");
|
||||
notifyEvent("openServerResult", "ok");
|
||||
open=true;
|
||||
Thread t = new Thread(this);
|
||||
t.setName("Server");
|
||||
t.start();
|
||||
public I2PTunnelServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
super(host + ":" + port + " <- " + privData, notifyThis, tunnel);
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(privData));
|
||||
String usePool = tunnel.getClientOptions().getProperty("i2ptunnel.usePool");
|
||||
if (usePool != null)
|
||||
_usePool = "true".equalsIgnoreCase(usePool);
|
||||
else
|
||||
_usePool = DEFAULT_USE_POOL;
|
||||
init(host, port, bais, privData, l);
|
||||
}
|
||||
|
||||
public I2PTunnelServer(InetAddress host, int port, File privkey, String privkeyname, Logging l,
|
||||
EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
super(host + ":" + port + " <- " + privkeyname, notifyThis, tunnel);
|
||||
String usePool = tunnel.getClientOptions().getProperty("i2ptunnel.usePool");
|
||||
if (usePool != null)
|
||||
_usePool = "true".equalsIgnoreCase(usePool);
|
||||
else
|
||||
_usePool = DEFAULT_USE_POOL;
|
||||
try {
|
||||
init(host, port, new FileInputStream(privkey), privkeyname, l);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error starting server", ioe);
|
||||
notifyEvent("openServerResult", "error");
|
||||
}
|
||||
}
|
||||
|
||||
public I2PTunnelServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
super(host + ":" + port + " <- " + privkeyname, notifyThis, tunnel);
|
||||
String usePool = tunnel.getClientOptions().getProperty("i2ptunnel.usePool");
|
||||
if (usePool != null)
|
||||
_usePool = "true".equalsIgnoreCase(usePool);
|
||||
else
|
||||
_usePool = DEFAULT_USE_POOL;
|
||||
init(host, port, privData, privkeyname, l);
|
||||
}
|
||||
|
||||
private void init(InetAddress host, int port, InputStream privData, String privkeyname, Logging l) {
|
||||
this.l = l;
|
||||
this.remoteHost = host;
|
||||
this.remotePort = port;
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
Properties props = new Properties();
|
||||
props.putAll(getTunnel().getClientOptions());
|
||||
int portNum = 7654;
|
||||
if (getTunnel().port != null) {
|
||||
try {
|
||||
portNum = Integer.parseInt(getTunnel().port);
|
||||
} catch (NumberFormatException nfe) {
|
||||
_log.log(Log.CRIT, "Invalid port specified [" + getTunnel().port + "], reverting to " + portNum);
|
||||
}
|
||||
}
|
||||
|
||||
while (sockMgr == null) {
|
||||
synchronized (slock) {
|
||||
sockMgr = I2PSocketManagerFactory.createManager(privData, getTunnel().host, portNum,
|
||||
props);
|
||||
|
||||
}
|
||||
if (sockMgr == null) {
|
||||
_log.log(Log.CRIT, "Unable to create socket manager");
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
sockMgr.setName("Server");
|
||||
getTunnel().addSession(sockMgr.getSession());
|
||||
l.log("Ready!");
|
||||
notifyEvent("openServerResult", "ok");
|
||||
open = true;
|
||||
}
|
||||
|
||||
|
||||
private static volatile long __serverId = 0;
|
||||
|
||||
/**
|
||||
* Start running the I2PTunnelServer.
|
||||
*
|
||||
*/
|
||||
public void startRunning() {
|
||||
Thread t = new I2PThread(this);
|
||||
t.setName("Server " + (++__serverId));
|
||||
t.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the read idle timeout for newly-created connections (in
|
||||
* milliseconds). After this time expires without data being reached from
|
||||
* the I2P network, the connection itself will be closed.
|
||||
*/
|
||||
public void setReadTimeout(long ms) {
|
||||
readTimeout = ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the read idle timeout for newly-created connections (in
|
||||
* milliseconds).
|
||||
*
|
||||
* @return The read timeout used for connections
|
||||
*/
|
||||
public long getReadTimeout() {
|
||||
return readTimeout;
|
||||
}
|
||||
|
||||
public boolean close(boolean forced) {
|
||||
if (!open) return true;
|
||||
synchronized(lock) {
|
||||
if (!forced && sockMgr.listSockets().size() != 0) {
|
||||
l.log("There are still active connections!");
|
||||
for (Iterator it = sockMgr.listSockets().iterator();
|
||||
it.hasNext();) {
|
||||
l.log("->"+it.next());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
l.log("Shutting down server "+toString());
|
||||
try {
|
||||
if (i2pss != null) i2pss.close();
|
||||
sockMgr.getSession().destroySession();
|
||||
} catch (I2PException ex) {
|
||||
_log.error("Error destroying the session", ex);
|
||||
System.exit(1);
|
||||
}
|
||||
l.log("Server shut down.");
|
||||
open=false;
|
||||
return true;
|
||||
}
|
||||
if (!open) return true;
|
||||
synchronized (lock) {
|
||||
if (!forced && sockMgr.listSockets().size() != 0) {
|
||||
l.log("There are still active connections!");
|
||||
for (Iterator it = sockMgr.listSockets().iterator(); it.hasNext();) {
|
||||
l.log("->" + it.next());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
l.log("Shutting down server " + toString());
|
||||
try {
|
||||
if (i2pss != null) i2pss.close();
|
||||
getTunnel().removeSession(sockMgr.getSession());
|
||||
sockMgr.getSession().destroySession();
|
||||
} catch (I2PException ex) {
|
||||
_log.error("Error destroying the session", ex);
|
||||
System.exit(1);
|
||||
}
|
||||
l.log("Server shut down.");
|
||||
open = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final String PROP_HANDLER_COUNT = "i2ptunnel.blockingHandlerCount";
|
||||
private static final int DEFAULT_HANDLER_COUNT = 10;
|
||||
|
||||
protected int getHandlerCount() {
|
||||
int rv = DEFAULT_HANDLER_COUNT;
|
||||
String cnt = getTunnel().getClientOptions().getProperty(PROP_HANDLER_COUNT);
|
||||
if (cnt != null) {
|
||||
try {
|
||||
rv = Integer.parseInt(cnt);
|
||||
if (rv <= 0)
|
||||
rv = DEFAULT_HANDLER_COUNT;
|
||||
} catch (NumberFormatException nfe) {
|
||||
rv = DEFAULT_HANDLER_COUNT;
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
I2PServerSocket i2pss = sockMgr.getServerSocket();
|
||||
while (true) {
|
||||
I2PSocket i2ps = i2pss.accept();
|
||||
//local is fast, so synchronously. Does not need that many
|
||||
//threads.
|
||||
try {
|
||||
Socket s = new Socket(remoteHost, remotePort);
|
||||
new I2PTunnelRunner(s, i2ps, slock, null);
|
||||
} catch (SocketException ex) {
|
||||
i2ps.close();
|
||||
}
|
||||
}
|
||||
} catch (I2PException ex) {
|
||||
_log.error("Error while waiting for I2PConnections", ex);
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error while waiting for I2PConnections", ex);
|
||||
}
|
||||
}
|
||||
if (shouldUsePool()) {
|
||||
I2PServerSocket i2pss = sockMgr.getServerSocket();
|
||||
int handlers = getHandlerCount();
|
||||
for (int i = 0; i < handlers; i++) {
|
||||
I2PThread handler = new I2PThread(new Handler(i2pss), "Handle Server " + i);
|
||||
handler.start();
|
||||
}
|
||||
} else {
|
||||
I2PServerSocket i2pss = sockMgr.getServerSocket();
|
||||
while (true) {
|
||||
try {
|
||||
final I2PSocket i2ps = i2pss.accept();
|
||||
if (i2ps == null) throw new I2PException("I2PServerSocket closed");
|
||||
new I2PThread(new Runnable() { public void run() { blockingHandle(i2ps); } }).start();
|
||||
} catch (I2PException ipe) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Error accepting - KILLING THE TUNNEL SERVER", ipe);
|
||||
return;
|
||||
} catch (ConnectException ce) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Error accepting", ce);
|
||||
// not killing the server..
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldUsePool() { return _usePool; }
|
||||
|
||||
/**
|
||||
* minor thread pool to pull off the accept() concurrently. there are still lots
|
||||
* (and lots) of wasted threads within the I2PTunnelRunner, but its a start
|
||||
*
|
||||
*/
|
||||
private class Handler implements Runnable {
|
||||
private I2PServerSocket _serverSocket;
|
||||
public Handler(I2PServerSocket serverSocket) {
|
||||
_serverSocket = serverSocket;
|
||||
}
|
||||
public void run() {
|
||||
while (open) {
|
||||
try {
|
||||
blockingHandle(_serverSocket.accept());
|
||||
} catch (I2PException ex) {
|
||||
_log.error("Error while waiting for I2PConnections", ex);
|
||||
return;
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error while waiting for I2PConnections", ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void blockingHandle(I2PSocket socket) {
|
||||
long afterAccept = I2PAppContext.getGlobalContext().clock().now();
|
||||
long afterSocket = -1;
|
||||
//local is fast, so synchronously. Does not need that many
|
||||
//threads.
|
||||
try {
|
||||
socket.setReadTimeout(readTimeout);
|
||||
Socket s = new Socket(remoteHost, remotePort);
|
||||
afterSocket = I2PAppContext.getGlobalContext().clock().now();
|
||||
new I2PTunnelRunner(s, socket, slock, null, null);
|
||||
} catch (SocketException ex) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error while closing the received i2p con", ex);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error while waiting for I2PConnections", ex);
|
||||
}
|
||||
|
||||
long afterHandle = I2PAppContext.getGlobalContext().clock().now();
|
||||
long timeToHandle = afterHandle - afterAccept;
|
||||
if (timeToHandle > 1000)
|
||||
_log.warn("Took a while to handle the request [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,50 +26,91 @@ public abstract class I2PTunnelTask implements EventDispatcher {
|
||||
// I2PTunnelTask(name, (EventDispatcher)null);
|
||||
//}
|
||||
|
||||
protected I2PTunnelTask(String name, EventDispatcher notifyThis) {
|
||||
attachEventDispatcher(notifyThis);
|
||||
this.name=name;
|
||||
this.id = -1;
|
||||
protected I2PTunnelTask(String name, EventDispatcher notifyThis, I2PTunnel tunnel) {
|
||||
attachEventDispatcher(notifyThis);
|
||||
this.name = name;
|
||||
this.id = -1;
|
||||
this.tunnel = tunnel;
|
||||
}
|
||||
|
||||
/** for apps that use multiple I2PTunnel instances */
|
||||
public void setTunnel(I2PTunnel pTunnel) {
|
||||
tunnel = pTunnel;
|
||||
}
|
||||
|
||||
/** for apps that use multiple I2PTunnel instances */
|
||||
public void setTunnel(I2PTunnel pTunnel) { tunnel = pTunnel; }
|
||||
|
||||
public I2PTunnel getTunnel() { return tunnel; }
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public boolean isOpen() {return open;}
|
||||
public boolean isOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
protected void setName(String name) {
|
||||
this.name=name;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
protected void routerDisconnected() {
|
||||
tunnel.routerDisconnected();
|
||||
}
|
||||
|
||||
protected void routerDisconnected() { tunnel.routerDisconnected(); }
|
||||
|
||||
public abstract boolean close(boolean forced);
|
||||
|
||||
public void disconnected(I2PSession session) { routerDisconnected(); }
|
||||
public void errorOccurred(I2PSession session, String message,
|
||||
Throwable error) {}
|
||||
public void reportAbuse(I2PSession session, int severity) {}
|
||||
|
||||
public void disconnected(I2PSession session) {
|
||||
routerDisconnected();
|
||||
getTunnel().removeSession(session);
|
||||
}
|
||||
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
}
|
||||
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return name;
|
||||
return name;
|
||||
}
|
||||
|
||||
/* Required by the EventDispatcher interface */
|
||||
public EventDispatcher getEventDispatcher() { return _event; }
|
||||
public void attachEventDispatcher(EventDispatcher e) { _event.attachEventDispatcher(e.getEventDispatcher()); }
|
||||
public void detachEventDispatcher(EventDispatcher e) { _event.detachEventDispatcher(e.getEventDispatcher()); }
|
||||
public void notifyEvent(String e, Object a) { _event.notifyEvent(e,a); }
|
||||
public Object getEventValue(String n) { return _event.getEventValue(n); }
|
||||
public Set getEvents() { return _event.getEvents(); }
|
||||
public void ignoreEvents() { _event.ignoreEvents(); }
|
||||
public void unIgnoreEvents() { _event.unIgnoreEvents(); }
|
||||
public Object waitEventValue(String n) { return _event.waitEventValue(n); }
|
||||
}
|
||||
public EventDispatcher getEventDispatcher() {
|
||||
return _event;
|
||||
}
|
||||
|
||||
public void attachEventDispatcher(EventDispatcher e) {
|
||||
_event.attachEventDispatcher(e.getEventDispatcher());
|
||||
}
|
||||
|
||||
public void detachEventDispatcher(EventDispatcher e) {
|
||||
_event.detachEventDispatcher(e.getEventDispatcher());
|
||||
}
|
||||
|
||||
public void notifyEvent(String e, Object a) {
|
||||
_event.notifyEvent(e, a);
|
||||
}
|
||||
|
||||
public Object getEventValue(String n) {
|
||||
return _event.getEventValue(n);
|
||||
}
|
||||
|
||||
public Set getEvents() {
|
||||
return _event.getEvents();
|
||||
}
|
||||
|
||||
public void ignoreEvents() {
|
||||
_event.ignoreEvents();
|
||||
}
|
||||
|
||||
public void unIgnoreEvents() {
|
||||
_event.unIgnoreEvents();
|
||||
}
|
||||
|
||||
public Object waitEventValue(String n) {
|
||||
return _event.waitEventValue(n);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user