Compare commits

...

213 Commits

Author SHA1 Message Date
Zlatin Balevsky
041fc3bef3 Release 0.2.1 2019-06-16 09:37:53 +01:00
Zlatin Balevsky
03c3b1ebf1 fix copying of hash if search results are sorted 2019-06-16 09:30:52 +01:00
Zlatin Balevsky
aece390daa right-click menu on the search results tab 2019-06-16 09:17:17 +01:00
Zlatin Balevsky
cf63be68e8 copy search to clipboard 2019-06-16 08:38:47 +01:00
Zlatin Balevsky
88ece4dc23 add option to show search hashes in monitor 2019-06-16 08:29:03 +01:00
Zlatin Balevsky
13767d58f2 detect if a query is hash, get rid of radio buttons 2019-06-16 08:09:51 +01:00
Zlatin Balevsky
05a1ccd3d8 update todo 2019-06-16 07:31:01 +01:00
Zlatin Balevsky
6807c14a5f add copy hash to clipboard 2019-06-16 07:23:22 +01:00
Zlatin Balevsky
684be0c50e start of work on directory watcher 2019-06-16 07:03:16 +01:00
Zlatin Balevsky
6655c262c6 more todo items 2019-06-16 07:01:50 +01:00
Zlatin Balevsky
b1ccd55030 more todo items 2019-06-16 06:26:03 +01:00
Zlatin Balevsky
a3becd0f7e update TODO 2019-06-16 06:19:28 +01:00
Zlatin Balevsky
af2f3e0ebf in/out direction done 2019-06-16 05:56:56 +01:00
Zlatin Balevsky
e2b7ffa1db direction in monitor tab 2019-06-16 05:52:23 +01:00
Zlatin Balevsky
0e0176acfc add web UI to TODO list 2019-06-16 05:35:05 +01:00
Zlatin Balevsky
7f09bb079c Beginnings of a TODO list 2019-06-16 05:28:42 +01:00
Zlatin Balevsky
77e48b01bb Release 0.2.0 2019-06-15 21:10:11 +01:00
Zlatin Balevsky
12db6857c1 disable unshare files popup until implemented 2019-06-15 12:12:08 +01:00
Zlatin Balevsky
acd67733a5 sort the downloads table on updates 2019-06-15 12:08:29 +01:00
Zlatin Balevsky
8d3ce7aa8e use the same sorted row selection logic in downloads table 2019-06-15 09:57:12 +01:00
Zlatin Balevsky
0eb5870e9b Release 0.1.13 2019-06-15 09:19:19 +01:00
Zlatin Balevsky
051efbfaba prevent empty searches 2019-06-15 09:11:42 +01:00
Zlatin Balevsky
6b38d7bffb fix sorting bug try 2 2019-06-15 08:58:51 +01:00
Zlatin Balevsky
5778d537ce Release 0.1.12 2019-06-15 08:39:19 +01:00
Zlatin Balevsky
93664a7985 update readme 2019-06-15 08:37:29 +01:00
Zlatin Balevsky
edd58e0c90 allow cancelling of downloads while hashlist is being fetched 2019-06-15 08:35:23 +01:00
Zlatin Balevsky
9ac52b61dc sort results table on update 2019-06-15 08:33:22 +01:00
Zlatin Balevsky
0a4b9c7029 shut down connection manager last 2019-06-15 08:20:10 +01:00
Zlatin Balevsky
87b366a205 add ability to cancel failed downloads 2019-06-14 22:49:56 +01:00
Zlatin Balevsky
040248560a Release 0.1.11 2019-06-14 22:26:28 +01:00
Zlatin Balevsky
77caaf83de reset instead of close 2019-06-14 22:08:25 +01:00
Zlatin Balevsky
cc5ece5103 do not throw exception on shutdown 2019-06-14 21:36:50 +01:00
Zlatin Balevsky
db7e21e343 close connections in parallel, more shutdown fixes 2019-06-14 21:25:22 +01:00
Zlatin Balevsky
a388eaec1d shutdown all connections on shutdown 2019-06-14 20:53:54 +01:00
Zlatin Balevsky
8ff39072c7 download file on double-clicking a result 2019-06-14 20:42:26 +01:00
Zlatin Balevsky
55d2ac9b24 delete partial files and pieces file on cancel 2019-06-14 20:27:14 +01:00
Zlatin Balevsky
6ebe492fd8 if nothing is enabled cancel and retry buttons are disabled 2019-06-14 18:37:18 +01:00
Zlatin Balevsky
165cd542ec work around not having a selected row while cancelling a download 2019-06-14 18:28:00 +01:00
Zlatin Balevsky
5ca0c8b00d wip on unshare selected files popup menu 2019-06-14 18:08:56 +01:00
Zlatin Balevsky
b6a38e3f23 revert to default lnf if the desired one fails 2019-06-14 18:01:14 +01:00
Zlatin Balevsky
34d9165bd5 Release 0.1.10 2019-06-14 16:43:28 +01:00
Zlatin Balevsky
2e52dd5c49 fix overwriting of custom nickname 2019-06-14 16:20:21 +01:00
Zlatin Balevsky
2a315dd734 add option to exclude local results from searches 2019-06-14 14:48:01 +01:00
Zlatin Balevsky
6b661b99c5 fix sorting by size in shared files table 2019-06-14 13:47:35 +01:00
Zlatin Balevsky
5dacd60bbb hook up cleaning up of cancelled/finished downloads 2019-06-14 13:11:20 +01:00
Zlatin Balevsky
f8f7cfe836 UI options panel 2019-06-14 12:51:27 +01:00
Zlatin Balevsky
0b4f261bc1 ability to not show monitor panel 2019-06-14 12:21:14 +01:00
Zlatin Balevsky
042d67d784 fix selection of size column 2019-06-14 11:46:31 +01:00
Zlatin Balevsky
800df88f14 proper sorting by size 2019-06-14 11:10:19 +01:00
Zlatin Balevsky
4d1eac50a0 update readme for sorting bug 2019-06-14 10:39:58 +01:00
Zlatin Balevsky
c48df7f14b Release 0.1.9 2019-06-13 22:57:08 +01:00
Zlatin Balevsky
9d04148001 remember loaded downloads from previous sessions 2019-06-13 22:53:23 +01:00
Zlatin Balevsky
bb4d522572 Release 0.1.8 2019-06-13 15:27:06 +01:00
Zlatin Balevsky
8052501e52 increase persistence interval to 15 seconds 2019-06-13 15:25:30 +01:00
Zlatin Balevsky
66cc6d8ab7 reduce piece size by factor of 8 2019-06-13 15:24:26 +01:00
Zlatin Balevsky
a45e57f5ec Release 0.1.7 2019-06-13 10:28:44 +01:00
Zlatin Balevsky
7d8ca55d87 fix emiting of download finished event 2019-06-13 10:27:18 +01:00
Zlatin Balevsky
de22f3c6b9 use metal lnf on java 9 or newer 2019-06-13 05:02:11 +01:00
Zlatin Balevsky
3b0eb5678d update wire protocol 2019-06-12 23:46:48 +01:00
Zlatin Balevsky
5a1f32e40b Release 0.1.6 2019-06-12 22:42:34 +01:00
Zlatin Balevsky
ca3f2513e1 sync persisting of hashlist or hashroot for active downloads 2019-06-12 22:39:00 +01:00
Zlatin Balevsky
658d9cf5a8 serialize downloads that do not have a hashlist 2019-06-12 22:22:20 +01:00
Zlatin Balevsky
e389090b7e download side of oob hashlist 2019-06-12 22:13:16 +01:00
Zlatin Balevsky
04ceaba514 do not persist downloaders until they have a hashlist 2019-06-12 21:02:01 +01:00
Zlatin Balevsky
6a01d97a8d enable oob infohash in queries; send V2 search results 2019-06-12 20:55:13 +01:00
Zlatin Balevsky
747663e1dc fix pieece size of shared downloaded files 2019-06-12 18:22:53 +01:00
Zlatin Balevsky
e426b3ccbd refactoring to enable hashlist uploads 2019-06-12 17:33:43 +01:00
Zlatin Balevsky
5172e19627 font-ize more elements 2019-06-12 16:34:24 +01:00
Zlatin Balevsky
e826cfd8d5 start work on ability to configure font 2019-06-12 16:26:40 +01:00
Zlatin Balevsky
51004f6fe9 wip on adding UI options 2019-06-11 08:04:26 +01:00
Zlatin Balevsky
08bb2b614d load some gui props from a separate config file 2019-06-11 02:17:58 +01:00
Zlatin Balevsky
d0e5d0ce8a set default i2cp options if none present 2019-06-10 08:55:44 +01:00
Zlatin Balevsky
9e05802d1b Merge pull request #4 from mikalv/master
Fixes i2cp bug while connecting to remote router
2019-06-10 08:48:27 +01:00
Mikal Villa
fb4f56eec9 Remove debug message 2019-06-10 09:40:32 +02:00
Mikal Villa
be2083d430 Fixes i2cp bug while connecting to remote router 2019-06-10 09:39:46 +02:00
Zlatin Balevsky
af6275d0a3 prevent Cli from hanging if there are no shared files 2019-06-10 07:04:01 +01:00
Zlatin Balevsky
5269815329 update readme 2019-06-10 04:49:09 +01:00
Zlatin Balevsky
bd21cf65ea Release 0.1.5 2019-06-09 20:37:39 +01:00
Zlatin Balevsky
dea592eb27 do not resume cancelled downloads on restart 2019-06-09 20:36:14 +01:00
Zlatin Balevsky
c81f963e0a Release 0.1.4 2019-06-09 17:37:10 +01:00
Zlatin Balevsky
dc6b1199f3 implement resume across restart 2019-06-09 17:35:32 +01:00
Zlatin Balevsky
42621a2dfb wip on persisting downloads between restarts 2019-06-09 16:26:00 +01:00
Zlatin Balevsky
a7125963a7 DownloadManager listens to events, not FileManager 2019-06-09 16:19:35 +01:00
Zlatin Balevsky
f39d7f4fa8 emit an event when the UI loads 2019-06-09 15:44:06 +01:00
Zlatin Balevsky
b88334f19a Release 0.1.3 for sorting fixes 2019-06-08 17:57:36 +01:00
Zlatin Balevsky
81e186ad1f fix sorting by download status and trust, fix events on downloads table 2019-06-08 17:55:39 +01:00
Zlatin Balevsky
33a45c3835 fix buttons when tables are sorted 2019-06-08 17:09:44 +01:00
Zlatin Balevsky
32b7867e44 Release 0.1.2 for search index test 2019-06-08 13:09:28 +01:00
Zlatin Balevsky
5b313276f4 fix tests broken by piece size change 2019-06-08 13:08:20 +01:00
Zlatin Balevsky
abba4cc6fa fix a bug where multi-term search modifies the index 2019-06-08 12:55:47 +01:00
Zlatin Balevsky
15b4804968 update wire protocol with originator and oobHashlist fields 2019-06-08 12:40:38 +01:00
Zlatin Balevsky
942a01a501 forgot to commit 2019-06-08 09:33:16 +01:00
Zlatin Balevsky
502a8d91da print only the root 2019-06-08 09:30:01 +01:00
Zlatin Balevsky
5414e8679b update readme 2019-06-08 09:07:13 +01:00
Zlatin Balevsky
14e42dd7c2 correct element 2019-06-08 08:46:28 +01:00
Zlatin Balevsky
1299fb2512 Release 0.1.1 for fixes and reduced piece size 2019-06-08 08:04:35 +01:00
Zlatin Balevsky
9bafdfe0b1 reduce piece size 2019-06-08 07:57:36 +01:00
Zlatin Balevsky
36eb632756 do not set the flag until it is implemented 2019-06-08 07:53:33 +01:00
Zlatin Balevsky
83ee620402 sort by columns 2019-06-08 07:45:07 +01:00
Zlatin Balevsky
3fe40d317d update readme for custom host:port 2019-06-08 07:28:23 +01:00
Zlatin Balevsky
e9703a2652 support for custom i2cp host:port 2019-06-08 07:23:14 +01:00
Zlatin Balevsky
a3fe89851f OS-specific home dir 2019-06-08 07:10:24 +01:00
Zlatin Balevsky
b9ea0128cd add oobInfohash flag, filter results by that flag 2019-06-08 02:44:49 +01:00
Zlatin Balevsky
53c6db4ec8 de-hardcode piece sizes in results 2019-06-08 01:48:07 +01:00
Zlatin Balevsky
60776829b9 fix disabling sharing of downloaded files 2019-06-08 01:35:03 +01:00
Zlatin Balevsky
b5cb31c23d proposed infohash upgrade document 2019-06-08 01:04:56 +01:00
Zlatin Balevsky
5052c0c993 note about downloads in progress 2019-06-07 21:52:38 +01:00
Zlatin Balevsky
06de007866 update readme 2019-06-07 21:22:49 +01:00
Zlatin Balevsky
7c8a0c9ad9 update readme for 0.1.0 2019-06-07 19:24:13 +01:00
Zlatin Balevsky
cda81a89a2 Release 0.1.0 2019-06-07 18:39:39 +01:00
Zlatin Balevsky
483773422c fix remaining tests 2019-06-07 18:23:16 +01:00
Zlatin Balevsky
1e1e6d0bb0 fix test 2019-06-07 18:17:16 +01:00
Zlatin Balevsky
668d6e087d fix test 2019-06-07 18:15:03 +01:00
Zlatin Balevsky
49af412b96 status update and auto-retry 2019-06-07 16:13:35 +01:00
Zlatin Balevsky
d5513021ed Release 0.0.14 for split search 2019-06-07 15:00:16 +01:00
Zlatin Balevsky
c3154cf717 stray println 2019-06-07 14:58:03 +01:00
Zlatin Balevsky
114940c4c1 fix searches with spaces 2019-06-07 14:51:09 +01:00
Zlatin Balevsky
d4336e9b5d outbound nickname 2019-06-07 14:24:45 +01:00
Zlatin Balevsky
2c1d5508ed outbound nickname 2019-06-07 14:21:03 +01:00
Zlatin Balevsky
1cebf6c7bd cli downloader 2019-06-07 14:02:10 +01:00
Zlatin Balevsky
e12924a207 shadow jar for cli 2019-06-07 14:01:28 +01:00
Zlatin Balevsky
f3b11895e4 utility for hashing files 2019-06-07 12:10:18 +01:00
Zlatin Balevsky
1e084820fb log tweak 2019-06-07 11:55:17 +01:00
Zlatin Balevsky
2198b4846d change wording 2019-06-07 11:43:02 +01:00
Zlatin Balevsky
a5d442d320 Release 0.0.13 for keyword search fix 2019-06-07 06:37:23 +01:00
Zlatin Balevsky
3f9ee887d6 prevent NPE in toString 2019-06-07 06:31:29 +01:00
Zlatin Balevsky
4a9e6d3b6b prevent npe in keyword searches 2019-06-07 06:14:40 +01:00
Zlatin Balevsky
80f2cc5f99 logging and toString() 2019-06-07 06:07:02 +01:00
Zlatin Balevsky
12283dba9d Release 0.0.12 for search by hash 2019-06-06 22:22:43 +01:00
Zlatin Balevsky
5c959bc8b7 name update search tab 2019-06-06 22:07:20 +01:00
Zlatin Balevsky
f3712fe7af delay initial update check a minute 2019-06-06 21:52:35 +01:00
Zlatin Balevsky
3e49b0ec66 infohash may be null 2019-06-06 21:40:44 +01:00
Zlatin Balevsky
f90beb8e3d encode infohash 2019-06-06 21:31:00 +01:00
Zlatin Balevsky
fbad7b6c7e searchHash 2019-06-06 21:27:07 +01:00
Zlatin Balevsky
ec2d89c18c serialize infohash 2019-06-06 21:21:40 +01:00
Zlatin Balevsky
c27fc0a515 update from infohash 2019-06-06 21:08:58 +01:00
Zlatin Balevsky
14681c2060 search by hash ui 2019-06-06 20:30:15 +01:00
Zlatin Balevsky
1aeb230ea8 catch exceptions in event dispatch thread 2019-06-06 19:31:10 +01:00
Zlatin Balevsky
d1dfc73f5a decode infohash 2019-06-06 19:28:29 +01:00
Zlatin Balevsky
0cebe4119c update list of limitations 2019-06-06 14:19:43 +01:00
Zlatin Balevsky
9f21120ec8 print periodic stats 2019-06-06 13:59:05 +01:00
Zlatin Balevsky
7eea8be67d Release 0.0.11 for file loading bug 2019-06-06 09:22:16 +01:00
Zlatin Balevsky
f114302bdb hopefully fix the shared file loss 2019-06-06 09:19:00 +01:00
Zlatin Balevsky
05b9b37488 emit an event when all files are loaded 2019-06-06 09:10:09 +01:00
Zlatin Balevsky
52f317a5b7 prevent division by zero 2019-06-06 07:09:54 +01:00
Zlatin Balevsky
fb8227a1f3 prevent division by zero 2019-06-06 07:09:05 +01:00
Zlatin Balevsky
5677d9f46a release 0.0.10 2019-06-06 00:23:59 +01:00
Zlatin Balevsky
c5192e3845 update readme for fix 2019-06-06 00:21:41 +01:00
Zlatin Balevsky
43c2a55cb8 0 not null 2019-06-06 00:03:22 +01:00
Zlatin Balevsky
94f6de6bea do not create new objects because that clears the successes 2019-06-05 21:07:23 +01:00
Zlatin Balevsky
6782849a12 retry hosts received from hostcache even if marked as failed 2019-06-05 20:58:28 +01:00
Zlatin Balevsky
c07d351c5d switch to jul, reduce aging interval 2019-06-05 20:14:38 +01:00
Zlatin Balevsky
dc2f675dd3 delete pieces file when download finishes 2019-06-05 19:52:50 +01:00
Zlatin Balevsky
a8e795ec51 do not accept connections if already try to connect to them 2019-06-05 19:07:36 +01:00
Zlatin Balevsky
33c5b3b18e option to disable sharing of downloaded files 2019-06-05 17:46:55 +01:00
Zlatin Balevsky
581fce4643 share downloaded files 2019-06-05 17:33:34 +01:00
Zlatin Balevsky
7fe78a0719 more clear name 2019-06-05 16:47:10 +01:00
Zlatin Balevsky
cdb6e22522 ui option for allowing untrusted connections 2019-06-05 15:47:44 +01:00
Zlatin Balevsky
2edeb046be drop neutral queries if configured 2019-06-05 15:38:39 +01:00
Zlatin Balevsky
4021f3c244 fix jullog 2019-06-05 13:04:46 +01:00
Zlatin Balevsky
9008fac24d shutdown cleanly on exit 2019-06-05 12:38:56 +01:00
Zlatin Balevsky
e2f92c5c5e print reported version 2019-06-05 10:07:04 +01:00
Zlatin Balevsky
7b33a16fd8 update list of known issues 2019-06-05 09:22:56 +01:00
Zlatin Balevsky
9a2531b264 release 0.0.9 2019-06-05 09:04:52 +01:00
Zlatin Balevsky
9a8dadff57 center the sources column 2019-06-05 08:43:58 +01:00
Zlatin Balevsky
4a274010f9 fix close tab button not appearing on duplicate searches 2019-06-05 08:34:09 +01:00
Zlatin Balevsky
1eb930435b fix hashing errors in large files 2019-06-05 00:34:38 +01:00
Zlatin Balevsky
9df28552ad try to load persisted files before hashing new ones 2019-06-05 00:22:36 +01:00
Zlatin Balevsky
ac0204dffc hopefully more accurate bandwidth gauge 2019-06-04 23:50:36 +01:00
Zlatin Balevsky
e5c402a400 retry download workers on resume 2019-06-04 23:36:57 +01:00
Zlatin Balevsky
7704c73b68 pass logging.properties to cli 2019-06-04 22:19:19 +01:00
Zlatin Balevsky
a9aa8dd840 do not count finished downloaders towards bandwidth 2019-06-04 21:55:59 +01:00
Zlatin Balevsky
de682a802a options panel for i2p tunnel options 2019-06-04 21:14:23 +01:00
Zlatin Balevsky
5435518212 core-side i2cp options 2019-06-04 20:20:25 +01:00
Zlatin Balevsky
bd01f983c9 break html in search results 2019-06-04 19:27:22 +01:00
Zlatin Balevsky
8b63864b90 utility to share files in headless mode 2019-06-04 18:58:02 +01:00
Zlatin Balevsky
ed3943c1af 0.0.8 for UI tweaks and sanitization 2019-06-04 18:01:08 +01:00
Zlatin Balevsky
e195141a27 simpler sanitization 2019-06-04 17:58:19 +01:00
Zlatin Balevsky
bb02fdbee9 do not use regex in sanitization 2019-06-04 17:46:41 +01:00
Zlatin Balevsky
6e3a2c0d08 update split pattern 2019-06-04 17:30:55 +01:00
Zlatin Balevsky
bd5fecc19d fix 2019-06-04 17:04:24 +01:00
Zlatin Balevsky
d5db49fa79 initialize core 2019-06-04 16:56:58 +01:00
Zlatin Balevsky
f2ea8619bb CLI project 2019-06-04 16:46:32 +01:00
Zlatin Balevsky
b129e79196 do not count finished workers in total count 2019-06-04 16:22:48 +01:00
Zlatin Balevsky
404d5b60bc format length in shared file stable an resize columns 2019-06-04 14:05:33 +01:00
Zlatin Balevsky
de2753ac50 preferred sizes for download table columns 2019-06-04 13:35:18 +01:00
Zlatin Balevsky
2d53999c8e only show download speed if downloading 2019-06-04 13:23:48 +01:00
Zlatin Balevsky
5aecf72d6f format download speed 2019-06-04 13:19:14 +01:00
Zlatin Balevsky
a574a67ec6 format file size 2019-06-04 13:15:24 +01:00
Zlatin Balevsky
6b5ad969b7 pass logging properties 2019-06-04 13:00:10 +01:00
Zlatin Balevsky
617209c4e4 column widths tweaks 2019-06-04 12:46:48 +01:00
Zlatin Balevsky
16b475bd9a 0.0.7 for multi-source downloads 2019-06-04 04:17:29 +01:00
Zlatin Balevsky
3cea1870cd multisource downloads, untested 2019-06-04 03:30:55 +01:00
Zlatin Balevsky
e7240dcb6f keep track of claimed pieces in preparation for multi-source downloads 2019-06-04 02:18:30 +01:00
Zlatin Balevsky
c91440cbfc config option for update check interval 2019-06-03 23:30:39 +01:00
Zlatin Balevsky
294605f5c7 basic update notification 2019-06-03 23:23:07 +01:00
Zlatin Balevsky
986caf3a75 backend for checking updates 2019-06-03 23:11:03 +01:00
Zlatin Balevsky
8524d5309f typo 2019-06-03 21:53:51 +01:00
Zlatin Balevsky
48b3ac2b4a wip on update server 2019-06-03 21:50:46 +01:00
Zlatin Balevsky
18f21dc247 update server 2019-06-03 21:47:31 +01:00
Zlatin Balevsky
e69a5eac18 0.0.6 2019-06-03 18:30:27 +01:00
Zlatin Balevsky
6e0f1778b7 rudimentary speed gauge 2019-06-03 18:02:10 +01:00
Zlatin Balevsky
abbb741d73 show the number of sources for a result, counted by infohash 2019-06-03 17:21:08 +01:00
Zlatin Balevsky
07dfc0a1d1 destroy mvc group on options window close 2019-06-03 15:33:16 +01:00
Zlatin Balevsky
00c12cfd49 hook up download retry logic 2019-06-03 15:02:04 +01:00
Zlatin Balevsky
1ee389ff91 options dialog 2019-06-03 14:40:32 +01:00
Zlatin Balevsky
3642736cfe options dialog, wip 2019-06-03 11:32:34 +01:00
Zlatin Balevsky
b6f7f51476 verify X-Persona header if present 2019-06-03 08:12:33 +01:00
Zlatin Balevsky
4c21f2d5ae show full persona in searches 2019-06-03 08:06:51 +01:00
Zlatin Balevsky
9e0d52d548 show source in incoming searches 2019-06-03 07:43:28 +01:00
Zlatin Balevsky
fad01603de fix replyTo field 2019-06-03 07:35:09 +01:00
Zlatin Balevsky
da007795fb learn about new hosts from incoming connections too 2019-06-03 07:27:12 +01:00
Zlatin Balevsky
881d755dd3 update test work with personas 2019-06-02 22:47:43 +01:00
93 changed files with 2894 additions and 418 deletions

View File

@@ -1,14 +1,14 @@
# MuWire - Easy Anonymous File-Sharing
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net).
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The project is in development. You can find technical documentation in the "doc" folder.
The current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
### Building
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew assemble
@@ -23,17 +23,11 @@ Some of the UI tests will fail because they haven't been written yet :-/
### Running
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside.
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.post=<port>" in there.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you.
At the moment there are very few nodes on the network, so you will see very few connections and search results. It is best to leave MuWire running all the time, just like I2P.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
### Known bugs and limitations
* Any shared files get re-hashed on startup
* Sometimes the list of shared files gets lost
* Many UI features you would expect are not there yet

42
TODO.md Normal file
View File

@@ -0,0 +1,42 @@
# TODO List
Not in any particular order yet
### Big Items
##### Alternate Locations
This helps peers discover new sources for a file while the download is in progress. Also makes sharing of partial files possible.
##### Bloom Filters
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
##### Two-tier Topology
This helps with scalability
##### Trust List Sharing
For helping users make better decisions whom to trust
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
##### Packaging With JRE, Embedded Router
For ease of deployment for new users, and so that users do not need to run a separate I2P router
##### Web UI, REST Interface, etc.
Basically any non-gui non-cli user interface
### Small Items
* Detect if router is dead and show warning or exit
* Wrapper of some kind for in-place upgrades
* Download file sequentially
* Unsharing of files
* Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item)

22
cli/build.gradle Normal file
View File

@@ -0,0 +1,22 @@
buildscript {
repositories {
jcenter()
mavenLocal()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin : 'application'
mainClassName = 'com.muwire.cli.Cli'
apply plugin : 'com.github.johnrengelman.shadow'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile project(":core")
}

View File

@@ -0,0 +1,142 @@
package com.muwire.cli
import java.util.concurrent.CountDownLatch
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
class Cli {
public static void main(String[] args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
if (!home.exists())
home.mkdirs()
def propsFile = new File(home,"MuWire.properties")
if (!propsFile.exists()) {
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
System.exit(1)
}
def props = new Properties()
propsFile.withInputStream { props.load(it) }
props = new MuWireSettings(props)
Core core
try {
core = new Core(props, home, "0.2.1")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"
System.exit(1)
}
def filesList
if (args.length == 0) {
println "Enter a file containing list of files to share"
def reader = new BufferedReader(new InputStreamReader(System.in))
filesList = reader.readLine()
} else
filesList = args[0]
Thread.sleep(1000)
println "loading shared files from $filesList"
// listener for shared files
def sharedListener = new SharedListener()
core.eventBus.register(FileHashedEvent.class, sharedListener)
core.eventBus.register(FileLoadedEvent.class, sharedListener)
// for connections
def connectionsListener = new ConnectionListener()
core.eventBus.register(ConnectionEvent.class, connectionsListener)
core.eventBus.register(DisconnectionEvent.class, connectionsListener)
// for uploads
def uploadsListener = new UploadsListener()
core.eventBus.register(UploadEvent.class, uploadsListener)
core.eventBus.register(UploadFinishedEvent.class, uploadsListener)
Timer timer = new Timer("status-printer", true)
timer.schedule({
println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
} as TimerTask, 60000, 60000)
def latch = new CountDownLatch(1)
def fileLoader = new Object() {
public void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
latch.countDown()
}
}
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
core.startServices()
println "waiting for files to load"
latch.await()
// now we begin
println "MuWire is ready"
filesList = new File(filesList)
filesList.withReader {
def toShare = it.readLine()
core.eventBus.publish(new FileSharedEvent(file : new File(toShare)))
}
Runtime.getRuntime().addShutdownHook({
println "shutting down.."
core.shutdown()
println "shutdown."
})
Thread.sleep(Integer.MAX_VALUE)
}
static class ConnectionListener {
volatile int connections
public void onConnectionEvent(ConnectionEvent e) {
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
connections++
}
public void onDisconnectionEvent(DisconnectionEvent e) {
connections--
}
}
static class UploadsListener {
volatile int uploads
public void onUploadEvent(UploadEvent e) {
uploads++
println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
}
public void onUploadFinishedEvent(UploadFinishedEvent e) {
uploads--
println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
}
}
static class SharedListener {
volatile int shared
void onFileHashedEvent(FileHashedEvent e) {
if (e.error != null)
println "ERROR $e.error"
else {
println "Shared file : $e.sharedFile.file"
shared++
}
}
void onFileLoadedEvent(FileLoadedEvent e) {
shared++
}
}
}

View File

@@ -0,0 +1,166 @@
package com.muwire.cli
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.data.Base64
class CliDownloader {
private static final List<Downloader> downloaders = Collections.synchronizedList(new ArrayList<>())
private static final Map<UUID,ResultsHolder> resultsListeners = new ConcurrentHashMap<>()
public static void main(String []args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
if (!home.exists())
home.mkdirs()
def propsFile = new File(home,"MuWire.properties")
if (!propsFile.exists()) {
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
System.exit(1)
}
def props = new Properties()
propsFile.withInputStream { props.load(it) }
props = new MuWireSettings(props)
def filesList
int connections
int resultWait
if (args.length != 3) {
println "Enter a file containing list of hashes of files to download, " +
"how many connections you want before searching" +
"and how long to wait for results to arrive"
System.exit(1)
} else {
filesList = args[0]
connections = Integer.parseInt(args[1])
resultWait = Integer.parseInt(args[2])
}
Core core
try {
core = new Core(props, home, "0.2.1")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"
System.exit(1)
}
def latch = new CountDownLatch(connections)
def connectionListener = new ConnectionWaiter(latch : latch)
core.eventBus.register(ConnectionEvent.class, connectionListener)
core.startServices()
println "starting to wait until there are $connections connections"
latch.await()
println "connected, searching for files"
def file = new File(filesList)
file.eachLine {
String[] split = it.split(",")
UUID uuid = UUID.randomUUID()
core.eventBus.register(UIResultEvent.class, new ResultsListener(fileName : split[1]))
def hash = Base64.decode(split[0])
def searchEvent = new SearchEvent(searchHash : hash, uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop:true,
replyTo: core.me.destination, receivedOn : core.me.destination, originator: core.me))
}
println "waiting for results to arrive"
Thread.sleep(resultWait * 1000)
core.eventBus.register(DownloadStartedEvent.class, new DownloadListener())
resultsListeners.each { uuid, resultsListener ->
println "starting download of $resultsListener.fileName from ${resultsListener.getResults().size()} hosts"
File target = new File(resultsListener.fileName)
core.eventBus.publish(new UIDownloadEvent(target : target, result : resultsListener.getResults()))
}
Thread.sleep(1000)
Timer timer = new Timer("stats-printer")
timer.schedule({
println "==== STATUS UPDATE ==="
downloaders.each {
int donePieces = it.donePieces()
int totalPieces = it.nPieces
int sources = it.activeWorkers.size()
def root = Base64.encode(it.infoHash.getRoot())
def state = it.getCurrentState()
println "file $it.file hash: $root progress: $donePieces/$totalPieces sources: $sources status: $state}"
it.resume()
}
println "==== END ==="
} as TimerTask, 60000, 60000)
println "waiting for downloads to finish"
while(true) {
boolean allFinished = true
for (Downloader d : downloaders) {
allFinished &= d.getCurrentState() == Downloader.DownloadState.FINISHED
}
if (allFinished)
break
Thread.sleep(1000)
}
println "all downloads finished"
}
static class ResultsHolder {
final List<UIResultEvent> results = Collections.synchronizedList(new ArrayList<>())
String fileName
void add(UIResultEvent e) {
results.add(e)
}
List getResults() {
results
}
}
static class ResultsListener {
UUID uuid
String fileName
public onUIResultEvent(UIResultEvent e) {
println "got a result for $fileName from ${e.sender.getHumanReadableName()}"
ResultsHolder listener = resultsListeners.get(e.uuid)
if (listener == null) {
listener = new ResultsHolder(fileName : fileName)
resultsListeners.put(e.uuid, listener)
}
listener.add(e)
}
}
static class ConnectionWaiter {
CountDownLatch latch
public void onConnectionEvent(ConnectionEvent e) {
if (e.status == ConnectionAttemptStatus.SUCCESSFUL)
latch.countDown()
}
}
static class DownloadListener {
public void onDownloadStartedEvent(DownloadStartedEvent e) {
downloaders.add(e.downloader)
}
}
}

View File

@@ -12,6 +12,7 @@ import com.muwire.core.connection.I2PConnector
import com.muwire.core.connection.LeafConnectionManager
import com.muwire.core.connection.UltrapeerConnectionManager
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
@@ -32,6 +33,7 @@ import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
import com.muwire.core.update.UpdateClient
import com.muwire.core.upload.UploadManager
import com.muwire.core.util.MuWireLogManager
@@ -55,18 +57,23 @@ public class Core {
final EventBus eventBus
final Persona me
final File home
final Properties i2pOptions
final MuWireSettings muOptions
private final TrustService trustService
private final PersisterService persisterService
private final HostCache hostCache
private final ConnectionManager connectionManager
private final CacheClient cacheClient
private final UpdateClient updateClient
private final ConnectionAcceptor connectionAcceptor
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
private final DownloadManager downloadManager
public Core(MuWireSettings props, File home) {
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
this.muOptions = props
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
@@ -81,12 +88,31 @@ public class Core {
}
}
def sysProps = System.getProperties().clone()
sysProps["inbound.nickname"] = "MuWire"
i2pOptions = new Properties()
def i2pOptionsFile = new File(home,"i2p.properties")
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.containsKey("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "2"
i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "2"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654"
}
// options like tunnel length and quantity
I2PSession i2pSession
I2PSocketManager socketManager
keyDat.withInputStream {
socketManager = new I2PSocketManagerFactory().createManager(it, sysProps)
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
}
socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000)
@@ -127,7 +153,7 @@ public class Core {
log.info "initializing file manager"
FileManager fileManager = new FileManager(eventBus)
FileManager fileManager = new FileManager(eventBus, props)
eventBus.register(FileHashedEvent.class, fileManager)
eventBus.register(FileLoadedEvent.class, fileManager)
eventBus.register(FileDownloadedEvent.class, fileManager)
@@ -135,7 +161,7 @@ public class Core {
eventBus.register(SearchEvent.class, fileManager)
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 5000, fileManager)
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
@@ -145,7 +171,8 @@ public class Core {
log.info("initializing connection manager")
connectionManager = props.isLeaf() ?
new LeafConnectionManager(eventBus, me, 3, hostCache) : new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService)
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
eventBus.register(TrustEvent.class, connectionManager)
eventBus.register(ConnectionEvent.class, connectionManager)
eventBus.register(DisconnectionEvent.class, connectionManager)
@@ -154,6 +181,9 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
@@ -166,23 +196,26 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
log.info("initializing upload manager")
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager)
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus)
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
eventBus.register(FileSharedEvent.class, hasherService)
}
@@ -197,9 +230,17 @@ public class Core {
connectionAcceptor.start()
connectionEstablisher.start()
hostCache.waitForLoad()
updateClient.start()
}
public void shutdown() {
log.info("shutting down download manageer")
downloadManager.shutdown()
log.info("shutting down connection acceeptor")
connectionAcceptor.stop()
log.info("shutting down connection establisher")
connectionEstablisher.stop()
log.info("shutting down connection manager")
connectionManager.shutdown()
}
@@ -227,7 +268,7 @@ public class Core {
}
}
Core core = new Core(props, home)
Core core = new Core(props, home, "0.2.1")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -3,6 +3,7 @@ package com.muwire.core
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
import com.muwire.core.files.FileSharedEvent
@@ -23,14 +24,18 @@ class EventBus {
}
private void publishInternal(Event e) {
log.fine "publishing event $e of type ${e.getClass().getSimpleName()}"
log.fine "publishing event $e of type ${e.getClass().getSimpleName()} event $e"
def currentHandlers
final def clazz = e.getClass()
synchronized(this) {
currentHandlers = handlers.getOrDefault(clazz, [])
}
currentHandlers.each {
it."on${clazz.getSimpleName()}"(e)
try {
it."on${clazz.getSimpleName()}"(e)
} catch (Exception bad) {
log.log(Level.SEVERE, "exception dispatching event",bad)
}
}
}

View File

@@ -7,10 +7,13 @@ class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
int downloadRetryInterval
int updateCheckInterval
String nickname
File downloadLocation
String sharedFiles
CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
boolean watchSharedDirectories
MuWireSettings() {
this(new Properties())
@@ -25,6 +28,9 @@ class MuWireSettings {
System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true"))
}
void write(OutputStream out) throws IOException {
@@ -34,7 +40,10 @@ class MuWireSettings {
props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", "15")
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
props.store(out, "")

View File

@@ -0,0 +1,4 @@
package com.muwire.core
class UILoadedEvent extends Event {
}

View File

@@ -6,6 +6,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.search.QueryEvent
@@ -14,6 +16,7 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
@@ -24,7 +27,8 @@ abstract class Connection implements Closeable {
final boolean incoming
final HostCache hostCache
final TrustService trustService
final MuWireSettings settings
private final AtomicBoolean running = new AtomicBoolean()
private final BlockingQueue messages = new LinkedBlockingQueue()
private final Thread reader, writer
@@ -33,12 +37,14 @@ abstract class Connection implements Closeable {
long lastPingSentTime, lastPongReceivedTime
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming, HostCache hostCache, TrustService trustService) {
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
this.eventBus = eventBus
this.incoming = incoming
this.endpoint = endpoint
this.hostCache = hostCache
this.trustService = trustService
this.settings = settings
this.name = endpoint.destination.toBase32().substring(0,8)
@@ -70,9 +76,9 @@ abstract class Connection implements Closeable {
return
}
log.info("closing $name")
endpoint.close()
reader.interrupt()
writer.interrupt()
endpoint.close()
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
}
@@ -120,9 +126,13 @@ abstract class Connection implements Closeable {
query.version = 1
query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop
// TODO: first hop figure out
query.keywords = e.searchEvent.getSearchTerms()
query.replyTo = e.getReceivedOn().toBase64()
query.oobInfohash = e.searchEvent.oobInfohash
if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
messages.put(query)
}
@@ -148,21 +158,43 @@ abstract class Connection implements Closeable {
protected void handleSearch(def search) {
UUID uuid = UUID.fromString(search.uuid)
if (search.infohash != null)
byte [] infohash = null
if (search.infohash != null) {
search.keywords = null
infohash = Base64.decode(search.infohash)
}
Destination replyTo = new Destination(search.replyTo)
if (trustService.getLevel(replyTo) == TrustLevel.DISTRUSTED) {
TrustLevel trustLevel = trustService.getLevel(replyTo)
if (trustLevel == TrustLevel.DISTRUSTED) {
log.info "dropping search from distrusted peer"
return
}
// TODO: add option to respond only to trusted peers
if (trustLevel == TrustLevel.NEUTRAL && !settings.allowUntrusted()) {
log.info("dropping search from neutral peer")
return
}
Persona originator = null
if (search.originator != null) {
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
if (originator.destination != replyTo) {
log.info("originator doesn't match destination")
return
}
}
boolean oob = false
if (search.oobInfohash != null)
oob = search.oobInfohash
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : search.infohash,
uuid : uuid)
searchHash : infohash,
uuid : uuid,
oobInfohash : oob)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
eventBus.publish(event)

View File

@@ -34,13 +34,17 @@ class ConnectionAcceptor {
final TrustService trustService
final SearchManager searchManager
final UploadManager uploadManager
final ConnectionEstablisher establisher
final ExecutorService acceptorThread
final ExecutorService handshakerThreads
private volatile shutdown
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager) {
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
ConnectionEstablisher establisher) {
this.eventBus = eventBus
this.manager = manager
this.settings = settings
@@ -49,7 +53,8 @@ class ConnectionAcceptor {
this.trustService = trustService
this.searchManager = searchManager
this.uploadManager = uploadManager
this.establisher = establisher
acceptorThread = Executors.newSingleThreadExecutor { r ->
def rv = new Thread(r)
rv.setDaemon(true)
@@ -70,11 +75,13 @@ class ConnectionAcceptor {
}
void stop() {
shutdown = true
acceptorThread.shutdownNow()
handshakerThreads.shutdownNow()
}
private void acceptLoop() {
try {
while(true) {
def incoming = acceptor.accept()
log.info("accepted connection from ${incoming.destination.toBase32()}")
@@ -90,6 +97,11 @@ class ConnectionAcceptor {
}
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
}
} catch (Exception e) {
log.log(Level.WARNING, "exception in accept loop",e)
if (!shutdown)
throw e
}
}
private void processIncoming(Endpoint e) {
@@ -105,6 +117,9 @@ class ConnectionAcceptor {
case (byte)'G':
processGET(e)
break
case (byte)'H':
processHashList(e)
break
case (byte)'P':
processPOST(e)
break
@@ -140,7 +155,9 @@ class ConnectionAcceptor {
}
private void handleIncoming(Endpoint e, boolean leaf) {
boolean accept = !manager.isConnected(e.destination) && (leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
boolean accept = !manager.isConnected(e.destination) &&
!establisher.isInProgress(e.destination) &&
(leaf ? manager.hasLeafSlots() : manager.hasPeerSlots())
if (accept) {
log.info("accepting connection, leaf:$leaf")
e.outputStream.write("OK".bytes)
@@ -173,9 +190,18 @@ class ConnectionAcceptor {
dis.readFully(et)
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid GET connection")
uploadManager.processEndpoint(e)
uploadManager.processGET(e)
}
private void processHashList(Endpoint e) {
byte[] ashList = new byte[8]
final DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(ashList)
if (ashList != "ASHLIST ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid HASHLIST connection")
uploadManager.processHashList(e)
}
private void processPOST(final Endpoint e) throws IOException {
byte [] ost = new byte[4]
final DataInputStream dis = new DataInputStream(e.getInputStream())

View File

@@ -35,6 +35,8 @@ class ConnectionEstablisher {
final Set inProgress = new ConcurrentHashSet()
ConnectionEstablisher(){}
ConnectionEstablisher(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings,
ConnectionManager connectionManager, HostCache hostCache) {
this.eventBus = eventBus
@@ -176,4 +178,8 @@ class ConnectionEstablisher {
e.close()
}
}
public boolean isInProgress(Destination d) {
inProgress.contains(d)
}
}

View File

@@ -1,6 +1,7 @@
package com.muwire.core.connection
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent
@@ -19,13 +20,15 @@ abstract class ConnectionManager {
protected final HostCache hostCache
protected final Persona me
protected final MuWireSettings settings
ConnectionManager() {}
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache) {
ConnectionManager(EventBus eventBus, Persona me, HostCache hostCache, MuWireSettings settings) {
this.eventBus = eventBus
this.me = me
this.hostCache = hostCache
this.settings = settings
this.timer = new Timer("connections-pinger",true)
}

View File

@@ -34,7 +34,7 @@ class Endpoint implements Closeable {
try {outputStream.close()} catch (Exception ignore) {}
}
if (toClose != null) {
try {toClose.close()} catch (Exception ignore) {}
try {toClose.reset()} catch (Exception ignore) {}
}
}

View File

@@ -4,6 +4,7 @@ import java.io.InputStream
import java.io.OutputStream
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustService
@@ -16,8 +17,9 @@ import net.i2p.data.Destination
*/
class LeafConnection extends Connection {
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache, TrustService trustService) {
super(eventBus, endpoint, true, hostCache, trustService);
public LeafConnection(EventBus eventBus, Endpoint endpoint, HostCache hostCache,
TrustService trustService, MuWireSettings settings) {
super(eventBus, endpoint, true, hostCache, trustService, settings);
}
@Override

View File

@@ -3,6 +3,7 @@ package com.muwire.core.connection
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent
@@ -17,8 +18,9 @@ class LeafConnectionManager extends ConnectionManager {
final Map<Destination, UltrapeerConnection> connections = new ConcurrentHashMap()
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections, HostCache hostCache) {
super(eventBus, me, hostCache)
public LeafConnectionManager(EventBus eventBus, Persona me, int maxConnections,
HostCache hostCache, MuWireSettings settings) {
super(eventBus, me, hostCache, settings)
this.maxConnections = maxConnections
}

View File

@@ -4,6 +4,7 @@ import java.io.InputStream
import java.io.OutputStream
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil
@@ -29,8 +30,9 @@ class PeerConnection extends Connection {
private final JsonSlurper slurper = new JsonSlurper()
public PeerConnection(EventBus eventBus, Endpoint endpoint,
boolean incoming, HostCache hostCache, TrustService trustService) {
super(eventBus, endpoint, incoming, hostCache, trustService)
boolean incoming, HostCache hostCache, TrustService trustService,
MuWireSettings settings) {
super(eventBus, endpoint, incoming, hostCache, trustService, settings)
this.dis = new DataInputStream(endpoint.inputStream)
this.dos = new DataOutputStream(endpoint.outputStream)
}

View File

@@ -4,6 +4,7 @@ import java.util.Collection
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent
@@ -17,15 +18,15 @@ class UltrapeerConnectionManager extends ConnectionManager {
final int maxPeers, maxLeafs
final TrustService trustService
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
HostCache hostCache, TrustService trustService) {
super(eventBus, me, hostCache)
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
super(eventBus, me, hostCache, settings)
this.maxPeers = maxPeers
this.maxLeafs = maxLeafs
this.trustService = trustService
@@ -85,8 +86,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
return
Connection c = e.leaf ?
new LeafConnection(eventBus, e.endpoint, hostCache, trustService) :
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService)
new LeafConnection(eventBus, e.endpoint, hostCache, trustService, settings) :
new PeerConnection(eventBus, e.endpoint, e.incoming, hostCache, trustService, settings)
def map = e.leaf ? leafConnections : peerConnections
map.put(e.endpoint.destination, c)
c.start()
@@ -103,8 +104,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
@Override
void shutdown() {
peerConnections.each {k,v -> v.close() }
leafConnections.each {k,v -> v.close() }
peerConnections.values().stream().parallel().forEach({v -> v.close()})
leafConnections.values().stream().parallel().forEach({v -> v.close()})
peerConnections.clear()
leafConnections.clear()
}

View File

@@ -1,11 +1,21 @@
package com.muwire.core.download
import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@@ -15,17 +25,17 @@ public class DownloadManager {
private final EventBus eventBus
private final I2PConnector connector
private final Executor executor
private final File incompletes
private final String meB64
private final File incompletes, home
private final Persona me
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
private final Set<Downloader> downloaders = new ConcurrentHashSet<>()
public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
def baos = new ByteArrayOutputStream()
me.write(baos)
this.meB64 = Base64.encode(baos.toByteArray())
this.incompletes = new File(home,"incompletes")
this.home = home
this.me = me
incompletes.mkdir()
@@ -39,14 +49,95 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(this, meB64, e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
def size = e.result[0].size
def infohash = e.result[0].infohash
def pieceSize = e.result[0].pieceSize
Set<Destination> destinations = new HashSet<>()
e.result.each {
destinations.add(it.sender.destination)
}
def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations,
incompletes)
downloaders.add(downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
downloaders.remove(e.downloader)
persistDownloaders()
}
void resume(Downloader downloader) {
executor.execute({downloader.download() as Runnable})
}
void onUILoadedEvent(UILoadedEvent e) {
File downloadsFile = new File(home, "downloads.json")
if (!downloadsFile.exists())
return
def slurper = new JsonSlurper()
downloadsFile.eachLine {
def json = slurper.parseText(it)
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
def destinations = new HashSet<>()
json.destinations.each { destination ->
destinations.add new Destination(destination)
}
InfoHash infoHash
if (json.hashList != null) {
byte[] hashList = Base64.decode(json.hashList)
infoHash = InfoHash.fromHashList(hashList)
} else {
byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root)
}
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
downloaders.add(downloader)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
downloaders.remove(e.downloader)
persistDownloaders()
}
private void persistDownloaders() {
File downloadsFile = new File(home,"downloads.json")
downloadsFile.withPrintWriter { writer ->
downloaders.each { downloader ->
if (!downloader.cancelled) {
def json = [:]
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
json.length = downloader.length
json.pieceSizePow2 = downloader.pieceSizePow2
def destinations = []
downloader.destinations.each {
destinations << it.toBase64()
}
json.destinations = destinations
InfoHash infoHash = downloader.getInfoHash()
if (infoHash.hashList != null)
json.hashList = Base64.encode(infoHash.hashList)
else
json.hashRoot = Base64.encode(infoHash.getRoot())
writer.println(JsonOutput.toJson(json))
}
}
}
}
public void shutdown() {
downloaders.each { it.stop() }
Downloader.executorService.shutdownNow()
}
}

View File

@@ -20,8 +20,10 @@ import java.security.NoSuchAlgorithmException
@Log
class DownloadSession {
private static int SAMPLES = 10
private final String meB64
private final Pieces pieces
private final Pieces downloaded, claimed
private final InfoHash infoHash
private final Endpoint endpoint
private final File file
@@ -29,12 +31,16 @@ class DownloadSession {
private final long fileLength
private final MessageDigest digest
private final LinkedList<Long> timestamps = new LinkedList<>()
private final LinkedList<Integer> reads = new LinkedList<>()
private ByteBuffer mapped
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) {
this.meB64 = meB64
this.pieces = pieces
this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint
this.infoHash = infoHash
this.file = file
@@ -48,11 +54,31 @@ class DownloadSession {
}
}
public void request() throws IOException {
/**
* @return if the request will proceed. The only time it may not
* is if all the pieces have been claimed by other sessions.
* @throws IOException
*/
public boolean request() throws IOException {
OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream()
int piece = pieces.getRandomPiece()
int piece
while(true) {
piece = downloaded.getRandomPiece()
if (claimed.isMarked(piece)) {
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
log.info("all pieces claimed")
return false
}
continue
}
break
}
claimed.markDownloaded(piece)
log.info("will download piece $piece")
long start = piece * pieceSize
long end = Math.min(fileLength, start + pieceSize) - 1
long length = end - start + 1
@@ -80,7 +106,7 @@ class DownloadSession {
if (!code.startsWith("200 ")) {
log.warning("unknown code $code")
endpoint.close()
return
return false
}
// parse all headers
@@ -105,7 +131,7 @@ class DownloadSession {
if (receivedStart != start || receivedEnd != end) {
log.warning("We don't support mismatching ranges yet")
endpoint.close()
return
return false
}
// start the download
@@ -122,6 +148,13 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
}
}
@@ -133,10 +166,12 @@ class DownloadSession {
if (hash != expected)
throw new BadHashException()
pieces.markDownloaded(piece)
downloaded.markDownloaded(piece)
} finally {
claimed.clear(piece)
try { channel?.close() } catch (IOException ignore) {}
}
return true
}
synchronized int positionInPiece() {
@@ -144,4 +179,26 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
int totalRead = 0
int idx = 0
final long now = System.currentTimeMillis()
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
idx++
if (idx == SAMPLES)
return 0
if (idx == SAMPLES - 1)
return reads[idx]
long interval = timestamps.last - timestamps[idx]
if (interval == 0)
interval = 1
for (int i = idx; i < SAMPLES; i++)
totalRead += reads[idx]
(int)(totalRead * 1000.0 / interval)
}
}

View File

@@ -1,49 +1,68 @@
package com.muwire.core.download
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent
import groovy.util.logging.Log
import net.i2p.data.Destination
@Log
public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
Thread rv = new Thread(r)
rv.setName("download worker")
rv.setDaemon(true)
rv
})
private final EventBus eventBus
private final DownloadManager downloadManager
private final String meB64
private final Persona me
private final File file
private final Pieces pieces
private final Pieces downloaded, claimed
private final long length
private final InfoHash infoHash
private InfoHash infoHash
private final int pieceSize
private final I2PConnector connector
private final Destination destination
private final Set<Destination> destinations
private final int nPieces
private final File piecesFile
final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private Endpoint endpoint
private volatile DownloadSession currentSession
private volatile DownloadState currentState
private volatile boolean cancelled
private volatile Thread downloadThread
public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination,
private volatile boolean eventFired
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) {
this.meB64 = meB64
this.eventBus = eventBus
this.me = me
this.downloadManager = downloadManager
this.file = file
this.infoHash = infoHash
this.length = length
this.connector = connector
this.destination = destination
this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2
int nPieces
@@ -53,72 +72,197 @@ public class Downloader {
nPieces = length / pieceSize + 1
this.nPieces = nPieces
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
currentState = DownloadState.CONNECTING
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
claimed = new Pieces(nPieces)
}
public synchronized InfoHash getInfoHash() {
infoHash
}
private synchronized void setInfoHash(InfoHash infoHash) {
this.infoHash = infoHash
}
void download() {
readPieces()
downloadThread = Thread.currentThread()
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
writePieces()
destinations.each {
if (it != me.destination) {
def worker = new DownloadWorker(it)
activeWorkers.put(it, worker)
executorService.submit(worker)
}
currentState = DownloadState.FINISHED
piecesFile.delete()
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
if (cancelled)
currentState = DownloadState.CANCELLED
else if (currentState != DownloadState.FINISHED)
currentState = DownloadState.FAILED
} finally {
endpoint?.close()
}
}
void readPieces() {
if (!piecesFile.exists())
return
piecesFile.withReader {
int piece = Integer.parseInt(it.readLine())
pieces.markDownloaded(piece)
piecesFile.eachLine {
int piece = Integer.parseInt(it)
downloaded.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
downloaded.getDownloaded().each { piece ->
writer.println(piece)
}
}
}
public long donePieces() {
pieces.donePieces()
downloaded.donePieces()
}
public int positionInPiece() {
if (currentSession == null)
return 0
currentSession.positionInPiece()
public int speed() {
int total = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
total += it.speed()
}
}
total
}
public DownloadState getCurrentState() {
currentState
if (cancelled)
return DownloadState.CANCELLED
boolean allFinished = true
activeWorkers.values().each {
allFinished &= it.currentState == WorkerState.FINISHED
}
if (allFinished) {
if (downloaded.isComplete())
return DownloadState.FINISHED
return DownloadState.FAILED
}
// if at least one is downloading...
boolean oneDownloading = false
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING) {
oneDownloading = true
return
}
}
if (oneDownloading)
return DownloadState.DOWNLOADING
// at least one is requesting hashlist
boolean oneHashlist = false
activeWorkers.values().each {
if (it.currentState == WorkerState.HASHLIST) {
oneHashlist = true
return
}
}
if (oneHashlist)
return DownloadState.HASHLIST
return DownloadState.CONNECTING
}
public void cancel() {
cancelled = true
downloadThread?.interrupt()
stop()
file.delete()
piecesFile.delete()
}
void stop() {
activeWorkers.values().each {
it.cancel()
}
}
public int activeWorkers() {
int active = 0
activeWorkers.values().each {
if (it.currentState != WorkerState.FINISHED)
active++
}
active
}
public void resume() {
downloadManager.resume(this)
destinations.each { destination ->
def worker = activeWorkers.get(destination)
if (worker != null) {
if (worker.currentState == WorkerState.FINISHED) {
def newWorker = new DownloadWorker(destination)
activeWorkers.put(destination, newWorker)
executorService.submit(newWorker)
}
} else {
worker = new DownloadWorker(destination)
activeWorkers.put(destination, worker)
executorService.submit(worker)
}
}
}
class DownloadWorker implements Runnable {
private final Destination destination
private volatile WorkerState currentState
private volatile Thread downloadThread
private Endpoint endpoint
private volatile DownloadSession currentSession
DownloadWorker(Destination destination) {
this.destination = destination
}
public void run() {
downloadThread = Thread.currentThread()
currentState = WorkerState.CONNECTING
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
while(getInfoHash().hashList == null) {
currentState = WorkerState.HASHLIST
HashListSession session = new HashListSession(me.toBase64(), infoHash, endpoint)
InfoHash received = session.request()
setInfoHash(received)
}
currentState = WorkerState.DOWNLOADING
boolean requestPerformed
while(!downloaded.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
writePieces()
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
} finally {
currentState = WorkerState.FINISHED
if (downloaded.isComplete() && !eventFired) {
piecesFile.delete()
eventFired = true
eventBus.publish(
new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
downloader : Downloader.this))
}
endpoint?.close()
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}
}
}

View File

@@ -0,0 +1,82 @@
package com.muwire.core.download
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint
import groovy.util.logging.Log
import static com.muwire.core.util.DataUtil.readTillRN
import net.i2p.data.Base64
@Log
class HashListSession {
private final String meB64
private final InfoHash infoHash
private final Endpoint endpoint
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
this.meB64 = meB64
this.infoHash = infoHash
this.endpoint = endpoint
}
InfoHash request() throws IOException {
InputStream is = endpoint.getInputStream()
OutputStream os = endpoint.getOutputStream()
String root = Base64.encode(infoHash.getRoot())
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
String code = readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("unknown code $code")
// parse all headers
Set<String> headers = new HashSet<>()
String header
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
headers.add(header)
long receivedStart = -1
long receivedEnd = -1
for (String receivedHeader : headers) {
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
if (group.size() != 1) {
log.info("ignoring header $receivedHeader")
continue
}
receivedStart = Long.parseLong(group[0][1])
receivedEnd = Long.parseLong(group[0][2])
}
if (receivedStart != 0)
throw new IOException("hashlist started at $receivedStart")
byte[] hashList = new byte[receivedEnd]
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
byte[] tmp = new byte[0x1 << 13]
while(hashListBuf.hasRemaining()) {
if (hashListBuf.remaining() > tmp.length)
tmp = new byte[hashListBuf.remaining()]
int read = is.read(tmp)
if (read == -1)
throw new IOException()
hashListBuf.put(tmp, 0, read)
}
InfoHash received = InfoHash.fromHashList(hashList)
if (received.getRoot() != infoHash.getRoot())
throw new IOException("fetched list doesn't match root")
received
}
}

View File

@@ -28,7 +28,8 @@ class Pieces {
while(true) {
int start = random.nextInt(nPieces)
while(bitSet.get(start) && ++start < nPieces);
if (bitSet.get(start))
continue
return start
}
}
@@ -45,10 +46,18 @@ class Pieces {
bitSet.set(piece)
}
synchronized void clear(int piece) {
bitSet.clear(piece)
}
synchronized boolean isComplete() {
bitSet.cardinality() == nPieces
}
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
}
synchronized int donePieces() {
bitSet.cardinality()
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.download
import com.muwire.core.Event
class UIDownloadCancelledEvent extends Event {
Downloader downloader
}

View File

@@ -5,6 +5,6 @@ import com.muwire.core.search.UIResultEvent
class UIDownloadEvent extends Event {
UIResultEvent result
UIResultEvent[] result
File target
}

View File

@@ -0,0 +1,6 @@
package com.muwire.core.files
import com.muwire.core.Event
class AllFilesLoadedEvent extends Event {
}

View File

@@ -0,0 +1,4 @@
package com.muwire.core.files
class DirectoryWatcher {
}

View File

@@ -2,10 +2,11 @@ package com.muwire.core.files
import com.muwire.core.DownloadedFile
import com.muwire.core.Event
import com.muwire.core.download.Downloader
import net.i2p.data.Destination
class FileDownloadedEvent extends Event {
Downloader downloader
DownloadedFile downloadedFile
}

View File

@@ -1,6 +1,9 @@
package com.muwire.core.files
import com.muwire.core.InfoHash
import net.i2p.data.Base64
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
import java.nio.channels.FileChannel.MapMode
@@ -17,12 +20,12 @@ class FileHasher {
* @return the size of each piece in power of 2
*/
static int getPieceSize(long size) {
if (size <= 0x1 << 25)
return 18
if (size <= 0x1 << 30)
return 17
for (int i = 26; i <= 37; i++) {
for (int i = 31; i <= 37; i++) {
if (size <= 0x1L << i) {
return i-7
return i-13
}
}
@@ -52,11 +55,11 @@ class FileHasher {
try {
MappedByteBuffer buf
for (int i = 0; i < numPieces - 1; i++) {
buf = raf.getChannel().map(MapMode.READ_ONLY, size * i, size)
buf = raf.getChannel().map(MapMode.READ_ONLY, ((long)size) * i, size)
digest.update buf
output.write(digest.digest(), 0, 32)
}
def lastPieceLength = length - (numPieces - 1) * size
def lastPieceLength = length - (numPieces - 1) * ((long)size)
buf = raf.getChannel().map(MapMode.READ_ONLY, length - lastPieceLength, lastPieceLength)
digest.update buf
output.write(digest.digest(), 0, 32)
@@ -67,4 +70,18 @@ class FileHasher {
byte [] hashList = output.toByteArray()
InfoHash.fromHashList(hashList)
}
public static void main(String[] args) {
if (args.length != 1) {
println "This utility computes an infohash of a file"
println "Pass absolute path to a file as an argument"
System.exit(1)
}
def file = new File(args[0])
file = file.getAbsoluteFile()
def hasher = new FileHasher()
def infohash = hasher.hashFile(file)
println Base64.encode(infohash.getRoot())
}
}

View File

@@ -2,7 +2,9 @@ package com.muwire.core.files
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchIndex
@@ -14,27 +16,31 @@ class FileManager {
final EventBus eventBus
final MuWireSettings settings
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
final Map<String, Set<File>> nameToFiles = new HashMap<>()
final SearchIndex index = new SearchIndex()
FileManager(EventBus eventBus) {
FileManager(EventBus eventBus, MuWireSettings settings) {
this.settings = settings
this.eventBus = eventBus
}
void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
}
void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
}
void onFileLoadedEvent(FileLoadedEvent e) {
addToIndex(e.loadedFile)
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
addToIndex(e.downloadedFile)
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (settings.shareDownloadedFiles) {
addToIndex(e.downloadedFile)
}
}
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())
@@ -100,20 +106,33 @@ class FileManager {
if (e.searchHash != null) {
Set<SharedFile> found
found = rootToFiles.get new InfoHash(e.searchHash)
found = filter(found, e.oobInfohash)
if (found != null && !found.isEmpty())
re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
} else {
def names = index.search e.searchTerms
Set<File> files = new HashSet<>()
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
Set<SharedFile> sharedFiles = new HashSet<>()
files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty())
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
}
if (re != null)
eventBus.publish(re)
}
private static Set<SharedFile> filter(Set<SharedFile> files, boolean oob) {
if (!oob)
return files
Set<SharedFile> rv = new HashSet<>()
files.each {
if (it.getPieceSize() != 0)
rv.add(it)
}
rv
}
}

View File

@@ -10,11 +10,13 @@ class HasherService {
final FileHasher hasher
final EventBus eventBus
final FileManager fileManager
Executor executor
HasherService(FileHasher hasher, EventBus eventBus) {
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
this.hasher = hasher
this.eventBus = eventBus
this.fileManager = fileManager
}
void start() {
@@ -22,6 +24,8 @@ class HasherService {
}
void onFileSharedEvent(FileSharedEvent evt) {
if (fileManager.fileToSharedFile.containsKey(evt.file))
return
executor.execute( { -> process(evt.file) } as Runnable)
}
@@ -36,7 +40,7 @@ class HasherService {
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
} else {
def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash))
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
}
}
}

View File

@@ -1,5 +1,8 @@
package com.muwire.core.files
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.logging.Level
import java.util.stream.Collectors
@@ -34,7 +37,7 @@ class PersisterService extends Service {
}
void start() {
timer.schedule({load()} as TimerTask, 1000)
timer.schedule({load()} as TimerTask, 1)
}
void stop() {
@@ -55,10 +58,13 @@ class PersisterService extends Service {
}
}
}
listener.publish(new AllFilesLoadedEvent())
} catch (IllegalArgumentException|NumberFormatException e) {
log.log(Level.WARNING, "couldn't load files",e)
}
}
} else {
listener.publish(new AllFilesLoadedEvent())
}
timer.schedule({persistFiles()} as TimerTask, 0, interval)
loaded = true
}
@@ -94,28 +100,37 @@ class PersisterService extends Service {
if (!Arrays.equals(root, ih.getRoot()))
return null
if (json.sources != null) {
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, sourceSet)
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
return new FileLoadedEvent(loadedFile : df)
}
SharedFile sf = new SharedFile(file, ih)
SharedFile sf = new SharedFile(file, ih, pieceSize)
return new FileLoadedEvent(loadedFile: sf)
}
private void persistFiles() {
location.delete()
def sharedFiles = fileManager.getSharedFiles()
location.withPrintWriter { writer ->
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
}
}
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
}
private def toJson(File f, SharedFile sf) {
@@ -124,6 +139,7 @@ class PersisterService extends Service {
json.length = f.length()
InfoHash ih = sf.getInfoHash()
json.infoHash = Base64.encode ih.getRoot()
json.pieceSize = sf.getPieceSize()
byte [] tmp = new byte [32]
json.hashList = []
for (int i = 0;i < ih.getHashList().length / 32; i++) {

View File

@@ -65,7 +65,7 @@ class CacheClient {
options.setSendLeaseSet(true)
CacheServers.getCacheServers().each {
log.info "Querying hostcache ${it.toBase32()}"
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 0, 0, options)
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
}
}
@@ -140,7 +140,7 @@ class CacheClient {
pong.pongs.asList().each {
Destination dest = new Destination(it)
if (!session.getMyDestination().equals(dest))
eventBus.publish(new HostDiscoveredEvent(destination: dest))
eventBus.publish(new HostDiscoveredEvent(destination: dest, fromHostcache : true))
}
}

View File

@@ -30,4 +30,8 @@ class Host {
synchronized boolean hasSucceeded() {
successes > 0
}
synchronized void clearFailures() {
failures = 0
}
}

View File

@@ -46,8 +46,12 @@ class HostCache extends Service {
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
if (myself == e.destination)
return
if (hosts.containsKey(e.destination))
return
if (hosts.containsKey(e.destination)) {
if (!e.fromHostcache)
return
hosts.get(e.destination).clearFailures()
return
}
Host host = new Host(e.destination)
if (allowHost(host)) {
hosts.put(e.destination, host)
@@ -55,7 +59,7 @@ class HostCache extends Service {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.incoming || e.leaf)
if (e.leaf)
return
Destination dest = e.endpoint.destination
Host host = hosts.get(dest)

View File

@@ -7,9 +7,10 @@ import net.i2p.data.Destination
class HostDiscoveredEvent extends Event {
Destination destination
boolean fromHostcache
@Override
public String toString() {
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()}"
"HostDiscoveredEvent ${super.toString()} destination:${destination.toBase32()} from hostcache $fromHostcache"
}
}

View File

@@ -1,6 +1,7 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.Persona
import net.i2p.data.Destination
@@ -9,6 +10,11 @@ class QueryEvent extends Event {
SearchEvent searchEvent
boolean firstHop
Destination replyTo
Persona originator
Destination receivedOn
String toString() {
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
"originator: ${originator.getHumanReadableName()} receivedOn: ${receivedOn.toBase32()}"
}
}

View File

@@ -5,6 +5,7 @@ import com.muwire.core.SharedFile
class ResultsEvent extends Event {
SearchEvent searchEvent
SharedFile[] results
UUID uuid
}

View File

@@ -12,8 +12,19 @@ class ResultsParser {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
if (json.type != "Result")
throw new InvalidSearchResultException("not a result json")
if (json.version != 1)
throw new InvalidSearchResultException("unknown version $json.version")
switch(json.version) {
case 1:
return parseV1(p, uuid, json)
case 2:
return parseV2(p, uuid, json)
default:
throw new InvalidSearchResultException("unknown version $json.version")
}
}
private static parseV1(Persona p, UUID uuid, def json) {
if (json.name == null)
throw new InvalidSearchResultException("name missing")
if (json.size == null)
@@ -52,4 +63,33 @@ class ResultsParser {
throw new InvalidSearchResultException("parsing search result failed",e)
}
}
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
if (json.name == null)
throw new InvalidSearchResultException("name missing")
if (json.size == null)
throw new InvalidSearchResultException("length missing")
if (json.infohash == null)
throw new InvalidSearchResultException("infohash missing")
if (json.pieceSize == null)
throw new InvalidSearchResultException("pieceSize missing")
if (json.hashList != null)
throw new InvalidSearchResultException("V2 result with hashlist")
try {
String name = DataUtil.readi18nString(Base64.decode(json.name))
long size = json.size
byte [] infoHash = Base64.decode(json.infohash)
if (infoHash.length != InfoHash.SIZE)
throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
int pieceSize = json.pieceSize
return new UIResultEvent( sender : p,
name : name,
size : size,
infohash : new InfoHash(infoHash),
pieceSize : pieceSize,
uuid: uuid)
} catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e)
}
}
}

View File

@@ -46,22 +46,26 @@ class ResultsSender {
this.me = me
}
void sendResults(UUID uuid, SharedFile[] results, Destination target) {
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()}")
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
if (target.equals(me.destination)) {
results.each {
long length = it.getFile().length()
int pieceSize = it.getPieceSize()
if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length)
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : FileHasher.getPieceSize(length),
pieceSize : pieceSize,
uuid : uuid
)
eventBus.publish(uiResultEvent)
}
} else {
executor.execute(new ResultSendJob(uuid : uuid, results : results, target: target))
executor.execute(new ResultSendJob(uuid : uuid, results : results,
target: target, oobInfohash : oobInfohash))
}
}
@@ -69,6 +73,7 @@ class ResultsSender {
UUID uuid
SharedFile [] results
Destination target
boolean oobInfohash
@Override
public void run() {
@@ -91,19 +96,20 @@ class ResultsSender {
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = 1
obj.version = oobInfohash ? 2 : 1
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = FileHasher.getPieceSize(it.getFile().length())
byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
}
obj.hashList = hashListB64
}
obj.hashList = hashListB64
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))

View File

@@ -1,10 +1,19 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.InfoHash
class SearchEvent extends Event {
List<String> searchTerms
byte [] searchHash
UUID uuid
boolean oobInfohash
String toString() {
def infoHash = null
if (searchHash != null)
infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
}
}

View File

@@ -42,7 +42,7 @@ class SearchIndex {
terms.each {
Set<String> forWord = keywords.getOrDefault(it,[])
if (rv == null) {
rv = forWord
rv = new HashSet<>(forWord)
} else {
rv.retainAll(forWord)
}

View File

@@ -44,7 +44,7 @@ public class SearchManager {
log.info("No results for search uuid $event.uuid")
return
}
resultsSender.sendResults(event.uuid, event.results, target)
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
}
boolean hasLocalSearch(UUID uuid) {

View File

@@ -11,4 +11,9 @@ class UIResultEvent extends Event {
long size
InfoHash infohash
int pieceSize
@Override
public String toString() {
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
}
}

View File

@@ -0,0 +1,10 @@
package com.muwire.core.update
import com.muwire.core.Event
import com.muwire.core.InfoHash
class UpdateAvailableEvent extends Event {
String version
String signer
String infoHash
}

View File

@@ -0,0 +1,132 @@
package com.muwire.core.update
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.util.VersionComparator
@Log
class UpdateClient {
final EventBus eventBus
final I2PSession session
final String myVersion
final MuWireSettings settings
private final Timer timer
private long lastUpdateCheckTime
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
this.eventBus = eventBus
this.session = session
this.myVersion = myVersion
this.settings = settings
timer = new Timer("update-client",true)
}
void start() {
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
}
void stop() {
timer.cancel()
}
private void checkUpdate() {
final long now = System.currentTimeMillis()
if (lastUpdateCheckTime > 0) {
if (now - lastUpdateCheckTime < settings.updateCheckInterval * 60 * 60 * 1000)
return
}
lastUpdateCheckTime = now
log.info("checking for update")
def ping = [version : 1, myVersion : myVersion]
ping = JsonOutput.toJson(ping)
def maker = new I2PDatagramMaker(session)
ping = maker.makeI2PDatagram(ping.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
}
class Listener implements I2PSessionMuxedListener {
final JsonSlurper slurper = new JsonSlurper()
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning "Received unexpected protocol $proto"
return
}
def payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
if (sender != UpdateServers.UPDATE_SERVER) {
log.warning("received something not from update server " + sender.toBase32())
return
}
log.info("Received something from update server")
payload = dissector.getPayload()
payload = slurper.parse(payload)
if (payload.version == null) {
log.warning("version missing")
return
}
if (payload.signer == null) {
log.warning("signer missing")
}
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
log.info("no new version available")
return
}
log.info("new version $payload.version available, publishing event")
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : payload.infoHash))
} catch (Exception e) {
log.log(Level.WARNING,"Invalid datagram",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
log.severe("I2P session disconnected")
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.update
import net.i2p.data.Destination
class UpdateServers {
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
}

View File

@@ -0,0 +1,5 @@
package com.muwire.core.upload
class ContentRequest extends Request {
Range range
}

View File

@@ -0,0 +1,54 @@
package com.muwire.core.upload
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import com.muwire.core.connection.Endpoint
class ContentUploader extends Uploader {
private final File file
private final ContentRequest request
ContentUploader(File file, ContentRequest request, Endpoint endpoint) {
super(endpoint)
this.file = file
this.request = request
}
@Override
void respond() {
OutputStream os = endpoint.getOutputStream()
Range range = request.getRange()
if (range.start >= file.length() || range.end >= file.length()) {
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
return
}
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
FileChannel channel
try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
byte [] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
int start = mapped.position()
synchronized(this) {
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
}
int read = mapped.position() - start
endpoint.getOutputStream().write(tmp, 0, read)
}
} finally {
try {channel?.close() } catch (IOException ignored) {}
endpoint.getOutputStream().flush()
}
}
}

View File

@@ -0,0 +1,4 @@
package com.muwire.core.upload
class HashListRequest extends Request {
}

View File

@@ -0,0 +1,35 @@
package com.muwire.core.upload
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint
class HashListUploader extends Uploader {
private final InfoHash infoHash
private final HashListRequest request
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
super(endpoint)
this.infoHash = infoHash
mapped = ByteBuffer.wrap(infoHash.getHashList())
}
void respond() {
OutputStream os = endpoint.getOutputStream()
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
byte[]tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
int start = mapped.position()
synchronized(this) {
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
}
int read = mapped.position() - start
endpoint.getOutputStream().write(tmp, 0, read)
}
endpoint.getOutputStream().flush()
}
}

View File

@@ -16,57 +16,12 @@ class Request {
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
InfoHash infoHash
Range range
Persona downloader
Map<String, String> headers
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
Map<String,String> headers = new HashMap<>()
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
while(headers.size() < Constants.MAX_HEADERS) {
boolean r = false
boolean n = false
int idx = 0
while (true) {
byte read = is.read()
if (read == -1)
throw new IOException("Stream closed")
if (!r && read == N)
throw new IOException("Received N before R")
if (read == R) {
if (r)
throw new IOException("double R")
r = true
continue
}
if (r && !n) {
if (read != N)
throw new IOException("R not followed by N")
n = true
break
}
if (idx == 0x1 << 14)
throw new IOException("Header too long")
tmp[idx++] = read
}
if (idx == 0)
break
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
log.fine("Read header $header")
int keyIdx = header.indexOf(":")
if (keyIdx < 1)
throw new IOException("Header key not found")
if (keyIdx == header.length())
throw new IOException("Header value not found")
String key = header.substring(0, keyIdx)
String value = header.substring(keyIdx + 1)
headers.put(key, value)
}
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
Map<String, String> headers = parseHeaders(is)
if (!headers.containsKey("Range"))
throw new IOException("Range header not found")
@@ -93,7 +48,69 @@ class Request {
def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded))
}
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
headers : headers, downloader : downloader)
}
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
Map<String,String> headers = parseHeaders(is)
Persona downloader = null
if (headers.containsKey("X-Persona")) {
def encoded = headers["X-Persona"].trim()
def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded))
}
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
}
private static Map<String, String> parseHeaders(InputStream is) {
Map<String,String> headers = new HashMap<>()
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
while(headers.size() < Constants.MAX_HEADERS) {
boolean r = false
boolean n = false
int idx = 0
while (true) {
byte read = is.read()
if (read == -1)
throw new IOException("Stream closed")
if (!r && read == N)
throw new IOException("Received N before R")
if (read == R) {
if (r)
throw new IOException("double R")
r = true
continue
}
if (r && !n) {
if (read != N)
throw new IOException("R not followed by N")
n = true
break
}
if (idx == 0x1 << 14)
throw new IOException("Header too long")
tmp[idx++] = read
}
if (idx == 0)
break
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
log.fine("Read header $header")
int keyIdx = header.indexOf(":")
if (keyIdx < 1)
throw new IOException("Header key not found")
if (keyIdx == header.length())
throw new IOException("Header value not found")
String key = header.substring(0, keyIdx)
String value = header.substring(keyIdx + 1)
headers.put(key, value)
}
headers
}
}

View File

@@ -23,9 +23,9 @@ public class UploadManager {
this.fileManager = fileManager
}
public void processEndpoint(Endpoint e) throws IOException {
public void processGET(Endpoint e) throws IOException {
byte [] infoHashStringBytes = new byte[44]
DataInputStream dis = new DataInputStream(e.getInputStream())
DataInputStream dis = new DataInputStream(e.getInputStream())
boolean first = true
while(true) {
if (first)
@@ -61,8 +61,13 @@ public class UploadManager {
return
}
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination")
e.close()
return
}
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
@@ -70,7 +75,92 @@ public class UploadManager {
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}
}
}
public void processHashList(Endpoint e) {
byte [] infoHashStringBytes = new byte[44]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(infoHashStringBytes)
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
log.info("Responding to hashlist request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString)
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (sharedFiles == null || sharedFiles.isEmpty()) {
log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
e.close()
return
}
byte [] rn = new byte[2]
dis.readFully(rn)
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
log.warning("Malformed HASHLIST header")
e.close()
return
}
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination")
e.close()
return
}
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
} finally {
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}
// proceed with content
while(true) {
byte[] get = new byte[4]
dis.readFully(get)
if (get != "GET ".getBytes(StandardCharsets.US_ASCII)) {
log.warning("received a method other than GET on subsequent call")
e.close()
return
}
dis.readFully(infoHashStringBytes)
infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
log.info("Responding to upload request for root $infoHashString")
infoHashRoot = Base64.decode(infoHashString)
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (sharedFiles == null || sharedFiles.isEmpty()) {
log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
e.close()
return
}
rn = new byte[2]
dis.readFully(rn)
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
log.warning("Malformed GET header")
e.close()
return
}
request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination")
e.close()
return
}
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
} finally {
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}
}
}
}

View File

@@ -8,49 +8,16 @@ import java.nio.file.StandardOpenOption
import com.muwire.core.connection.Endpoint
class Uploader {
private final File file
private final Request request
private final Endpoint endpoint
private ByteBuffer mapped
abstract class Uploader {
protected final Endpoint endpoint
protected ByteBuffer mapped
Uploader(File file, Request request, Endpoint endpoint) {
this.file = file
this.request = request
Uploader(Endpoint endpoint) {
this.endpoint = endpoint
}
void respond() {
OutputStream os = endpoint.getOutputStream()
Range range = request.getRange()
if (range.start >= file.length() || range.end >= file.length()) {
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
return
}
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
FileChannel channel
try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
byte [] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
int start = mapped.position()
synchronized(this) {
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
}
int read = mapped.position() - start
endpoint.getOutputStream().write(tmp, 0, read)
}
} finally {
try {channel?.close() } catch (IOException ignored) {}
endpoint.getOutputStream().flush()
}
}
abstract void respond()
public synchronized int getPosition() {
if (mapped == null)
return -1

View File

@@ -55,21 +55,21 @@ class JULLog extends Log {
@Override
public boolean shouldDebug() {
level.intValue().intValue() >= Level.FINE.intValue()
level.intValue().intValue() <= Level.FINE.intValue()
}
@Override
public boolean shouldInfo() {
level.intValue().intValue() >= Level.INFO.intValue()
level.intValue().intValue() <= Level.INFO.intValue()
}
@Override
public boolean shouldWarn() {
level.intValue().intValue() >= Level.WARNING.intValue()
level.intValue().intValue() <= Level.WARNING.intValue()
}
@Override
public boolean shouldError() {
level.intValue().intValue() >= Level.SEVERE.intValue()
level.intValue().intValue() <= Level.SEVERE.intValue()
}
}

View File

@@ -9,8 +9,8 @@ public class DownloadedFile extends SharedFile {
private final Set<Destination> sources;
public DownloadedFile(File file, InfoHash infoHash, Set<Destination> sources) {
super(file, infoHash);
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources) {
super(file, infoHash, pieceSize);
this.sources = sources;
}

View File

@@ -7,6 +7,7 @@ import java.util.Arrays;
import java.util.List;
import net.i2p.data.Base32;
import net.i2p.data.Base64;
public class InfoHash {
@@ -76,14 +77,16 @@ public class InfoHash {
}
public String toString() {
String rv = "InfoHash[root:"+Base32.encode(root) + " hashList:";
List<String> b32HashList = new ArrayList<>(hashList.length / SIZE);
byte [] tmp = new byte[SIZE];
for (int i = 0; i < hashList.length / SIZE; i++) {
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
b32HashList.add(Base32.encode(tmp));
String rv = "InfoHash[root:"+Base64.encode(root) + " hashList:";
List<String> b64HashList = new ArrayList<>();
if (hashList != null) {
byte [] tmp = new byte[SIZE];
for (int i = 0; i < hashList.length / SIZE; i++) {
System.arraycopy(hashList, SIZE * i, tmp, 0, SIZE);
b64HashList.add(Base64.encode(tmp));
}
}
rv += b32HashList.toString();
rv += b64HashList.toString();
rv += "]";
return rv;
}

View File

@@ -6,10 +6,12 @@ public class SharedFile {
private final File file;
private final InfoHash infoHash;
private final int pieceSize;
public SharedFile(File file, InfoHash infoHash) {
public SharedFile(File file, InfoHash infoHash, int pieceSize) {
this.file = file;
this.infoHash = infoHash;
this.pieceSize = pieceSize;
}
public File getFile() {
@@ -20,4 +22,7 @@ public class SharedFile {
return infoHash;
}
public int getPieceSize() {
return pieceSize;
}
}

View File

@@ -0,0 +1,11 @@
package com.muwire.core
import net.i2p.data.Base64
class Personas {
private final String encoded1 = "AQADemFiO~pgSoEo8wQfwncYMvBQWkvPY9I7DYUllHp289UE~zBaLdbl~wbliktAUsW-S70f3UeYgHq34~c7zVuUQjgHZ506iG9hX8B9S3a9gQ3CSG0GuDpeNyiXmZkpHp5m8vT9PZ1zMWzxvzZY~fP9yKFKgO4yrso5I9~DGOPeyJZJ4BFsTJDERv41aZqjFLYUBDmeHGgg9RjYy~93h-nQMVYj9JSO3AgowW-ix49rtiKYIXHMa2PxWHUXkUHWJZtIZntNIDEFeMnPdzLxjAl8so2G6pDcTMZPLLwyb73Ee5ZVfxUynPqyp~fIGVP8Rl4rlaGFli2~ATGBz3XY54aObC~0p7us2JnWaTC~oQT5DVDM7gaOO885o-m8BB8b0duzMBelbdnMZFQJ5jIHVKxkC6Niw4fxTOoXTyOqQmVhtK-9xcwxMuN5DF9IewkR5bhpq5rgnfBP5zvyBaAHMq-d3TCOjTsZ-d3liB98xX5p8G5zmS7gfKArQtM5~CcK~AlX-lGLBQAEAAcAAN5MW1Tq983szfZgY1l8tQFqy8I9tdMf7vc1Ktj~TCIvXYw6AYMbMGy3S67FSPLZVmfHEMQKj2KLAdaRKQkHPAY"
private final String encoded2 = "AQAHemxhdGluYiN~3G-hPoBfJ04mhcC52lC6TYSwWxH-WNWno9Y35JS-WrXlnPsodZtwy96ttEaiKTg-hkRqMsaYKpWar1FwayR6qlo0pZCo5pQOLfR7GIM3~wde0JIBEp8BUpgzF1-QXLhuRG1t7tBbenW2tSgp5jQH61RI-c9flyUlOvf6nrhQMZ3aoviZ4aZW23Fx-ajYQBDk7PIxuyn8qYNwWy3kWOhGan05c54NnumS3XCzQWFDDPlADmco1WROeY9qrwwtmLM8lzDCEtJQXJlk~K5yLbyB63hmAeTK7J4iS6f9nnWv7TbB5r-Z3kC6D9TLYrQbu3h4AAxrqso45P8yHQtKUA4QJicS-6NJoBOnlCCU887wx2k9YSxxwNydlIxb1mZsX65Ke4uY0HDFokZHTzUcxvfLB6G~5JkSPDCyZz~2fREgW2-VXu7gokEdEugkuZRrsiQzyfAOOkv53ti5MzTbMOXinBskSb1vZyN2-XcZNaDJvEqUNj~qpfhe-ov2F7FuwQUABAAHAAAfqq-MneIqWBQY92-sy9Z0s~iQsq6lUFa~sYMdY-5o-94fF8a140dm-emF3rO8vuidUIPNaS-37Rl05mAKUCcB"
Persona persona1 = new Persona(new ByteArrayInputStream(Base64.decode(encoded1)))
Persona persona2 = new Persona(new ByteArrayInputStream(Base64.decode(encoded2)))
}

View File

@@ -43,6 +43,9 @@ class ConnectionAcceptorTest {
def uploadManagerMock
UploadManager uploadManager
def connectionEstablisherMock
ConnectionEstablisher connectionEstablisher
ConnectionAcceptor acceptor
List<ConnectionEvent> connectionEvents
@@ -57,6 +60,7 @@ class ConnectionAcceptorTest {
trustServiceMock = new MockFor(TrustService.class)
searchManagerMock = new MockFor(SearchManager.class)
uploadManagerMock = new MockFor(UploadManager.class)
connectionEstablisherMock = new MockFor(ConnectionEstablisher.class)
}
@After
@@ -68,6 +72,7 @@ class ConnectionAcceptorTest {
trustServiceMock.verify trustService
searchManagerMock.verify searchManager
uploadManagerMock.verify uploadManager
connectionEstablisherMock.verify connectionEstablisher
Thread.sleep(100)
}
@@ -87,8 +92,10 @@ class ConnectionAcceptorTest {
trustService = trustServiceMock.proxyInstance()
searchManager = searchManagerMock.proxyInstance()
uploadManager = uploadManagerMock.proxyInstance()
connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor, hostCache, trustService, searchManager, uploadManager)
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
acceptor.start()
Thread.sleep(100)
}
@@ -108,6 +115,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null)
}
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1
false
@@ -150,6 +158,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null)
}
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1
false
@@ -264,6 +273,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null)
}
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1
false
@@ -310,6 +320,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null)
}
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1
false
@@ -356,6 +367,7 @@ class ConnectionAcceptorTest {
new Endpoint(destinations.dest1, is, os, null)
}
i2pAcceptorMock.demand.accept { Thread.sleep(Integer.MAX_VALUE) }
connectionEstablisherMock.demand.isInProgress(destinations.dest1) { false }
connectionManagerMock.demand.isConnected { dest ->
assert dest == destinations.dest1
false

View File

@@ -15,7 +15,7 @@ class DownloadSessionTest {
private File source, target
private InfoHash infoHash
private Endpoint endpoint
private Pieces pieces
private Pieces pieces, claimed
private String rootBase64
private DownloadSession session
@@ -24,7 +24,7 @@ class DownloadSessionTest {
private InputStream fromDownloader, fromUploader
private OutputStream toDownloader, toUploader
private void initSession(int size) {
private void initSession(int size, def claimedPieces = []) {
Random r = new Random()
byte [] content = new byte[size]
r.nextBytes(content)
@@ -48,6 +48,8 @@ class DownloadSessionTest {
else
nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces)
claimedPieces.each {claimed.markDownloaded(it)}
fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream()
@@ -55,7 +57,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
@@ -138,4 +140,29 @@ class DownloadSessionTest {
assert !pieces.isComplete()
assert 1 == pieces.donePieces()
}
@Test
public void testSmallFileClaimed() {
initSession(20, [0])
long now = System.currentTimeMillis()
downloadThread.join(100)
assert 100 > (System.currentTimeMillis() - now)
}
@Test
public void testClaimedPiecesAvoided() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10
initSession(size, [1,2,3,4,5,6,7,8,9])
assert !claimed.isMarked(0)
assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader)
def matcher = (range =~ /^Range: (\d+)-(\d+)$/)
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0)
assert start == 0 && end == (1 << pieceSize) - 1
}
}

View File

@@ -24,9 +24,9 @@ class FileHasherTest extends GroovyTestCase {
@Test
void testPieceSize() {
assert 18 == FileHasher.getPieceSize(1000000)
assert 20 == FileHasher.getPieceSize(100000000)
assert 30 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
assert 17 == FileHasher.getPieceSize(1000000)
assert 17 == FileHasher.getPieceSize(100000000)
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE)
}
@@ -48,7 +48,7 @@ class FileHasherTest extends GroovyTestCase {
fos.write b
fos.close()
def ih = hasher.hashFile tmp
assert ih.getHashList().length == 32
assert ih.getHashList().length == 64
}
@Test
@@ -58,7 +58,7 @@ class FileHasherTest extends GroovyTestCase {
fos.write b
fos.close()
def ih = hasher.hashFile tmp
assert ih.getHashList().length == 64
assert ih.getHashList().length == 96
}
@Test
@@ -68,7 +68,7 @@ class FileHasherTest extends GroovyTestCase {
fos.write b
fos.close()
def ih = hasher.hashFile tmp
assert ih.getHashList().length == 64
assert ih.getHashList().length == 128
}
@Test
@@ -78,6 +78,6 @@ class FileHasherTest extends GroovyTestCase {
fos.write b
fos.close()
def ih = hasher.hashFile tmp
assert ih.getHashList().length == 32 * 3
assert ih.getHashList().length == 160
}
}

View File

@@ -5,6 +5,7 @@ import org.junit.Test
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent
@@ -26,7 +27,7 @@ class FileManagerTest {
void before() {
eventBus = new EventBus()
eventBus.register(ResultsEvent.class, listener)
manager = new FileManager(eventBus)
manager = new FileManager(eventBus, new MuWireSettings())
results = null
}
@@ -34,7 +35,7 @@ class FileManagerTest {
void testHash1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih)
SharedFile sf = new SharedFile(f,ih, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -53,8 +54,8 @@ class FileManagerTest {
@Test
void testHash2Results() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@@ -75,7 +76,7 @@ class FileManagerTest {
void testHash0Results() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih)
SharedFile sf = new SharedFile(f,ih, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -89,7 +90,7 @@ class FileManagerTest {
void testKeyword1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih)
SharedFile sf = new SharedFile(f,ih,0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -107,12 +108,12 @@ class FileManagerTest {
void testKeyword2Results() {
File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1)
SharedFile sf1 = new SharedFile(f1, ih1, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2)
SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
UUID uuid = UUID.randomUUID()
@@ -130,7 +131,7 @@ class FileManagerTest {
void testKeyword0Results() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih)
SharedFile sf = new SharedFile(f,ih,0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -143,8 +144,8 @@ class FileManagerTest {
@Test
void testRemoveFileExistingHash() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih)
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@@ -161,12 +162,12 @@ class FileManagerTest {
void testRemoveFile() {
File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1)
SharedFile sf1 = new SharedFile(f1, ih1, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2)
SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)

View File

@@ -8,6 +8,7 @@ import org.junit.Before
import org.junit.Test
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
class HasherServiceTest {
@@ -24,7 +25,7 @@ class HasherServiceTest {
void before() {
eventBus = new EventBus()
hasher = new FileHasher()
service = new HasherService(hasher, eventBus)
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
eventBus.register(FileHashedEvent.class, listener)
service.start()
}

View File

@@ -99,7 +99,7 @@ class PersisterServiceLoadingTest {
FileHasher fh = new FileHasher()
InfoHash ih1 = fh.hashFile(sharedFile1)
assert ih1.getHashList().length == 2 * 32
assert ih1.getHashList().length == 96
def json = [:]
json.file = getSharedFileJsonName(sharedFile1)
@@ -111,7 +111,9 @@ class PersisterServiceLoadingTest {
String hash1 = Base64.encode(tmp)
System.arraycopy(ih1.getHashList(), 32, tmp, 0, 32)
String hash2 = Base64.encode(tmp)
json.hashList = [hash1, hash2]
System.arraycopy(ih1.getHashList(), 64, tmp, 0, 32)
String hash3 = Base64.encode(tmp)
json.hashList = [hash1, hash2, hash3]
json = JsonOutput.toJson(json)

View File

@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.util.DataUtil
@@ -31,7 +32,7 @@ class PersisterServiceSavingTest {
f = new File("build.gradle")
f = f.getCanonicalFile()
ih = fh.hashFile(f)
fileSource = new FileManager(eventBus) {
fileSource = new FileManager(eventBus, new MuWireSettings()) {
Map<File, SharedFile> getSharedFiles() {
Map<File, SharedFile> rv = new HashMap<>()
rv.put(f, sf)
@@ -54,7 +55,7 @@ class PersisterServiceSavingTest {
@Test
void testSavingSharedFile() {
sf = new SharedFile(f, ih)
sf = new SharedFile(f, ih, 0)
ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start()
@@ -73,7 +74,7 @@ class PersisterServiceSavingTest {
@Test
void testSavingDownloadedFile() {
Destinations dests = new Destinations()
sf = new DownloadedFile(f, ih, new HashSet([dests.dest1, dests.dest2]))
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start()

View File

@@ -30,7 +30,18 @@ class SearchIndexTest {
assert found.size() == 2
assert found.contains("a b.c")
assert found.contains("c d.e")
}
@Test
public void testDrillDownDoesNotModifyIndex() {
initIndex(["a b.c", "c d.e"])
index.search(["c","e"])
def found = index.search(["c"])
assert found.size() == 2
assert found.contains("a b.c")
assert found.contains("c d.e")
}
@Test
void testDrillDown() {

View File

@@ -5,14 +5,17 @@ import org.junit.Before
import org.junit.Test
import com.muwire.core.Destinations
import com.muwire.core.Persona
import com.muwire.core.Personas
import net.i2p.data.Base64
import net.i2p.data.Destination
class TrustServiceTest {
TrustService service
File persistGood, persistBad
Destinations dests = new Destinations()
Personas personas = new Personas()
@Before
void before() {
@@ -33,51 +36,50 @@ class TrustServiceTest {
@Test
void testEmpty() {
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest1)
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest2)
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona1.destination)
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona2.destination)
}
@Test
void testOnEvent() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
}
@Test
void testPersist() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
Thread.sleep(250)
def trusted = new HashSet<>()
persistGood.eachLine {
trusted.add(new Destination(it))
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
def distrusted = new HashSet<>()
persistBad.eachLine {
distrusted.add(new Destination(it))
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
assert trusted.size() == 1
assert trusted.contains(dests.dest1)
assert trusted.contains(personas.persona1)
assert distrusted.size() == 1
assert distrusted.contains(dests.dest2)
assert distrusted.contains(personas.persona2)
}
@Test
void testLoad() {
service.stop()
persistGood.append("${dests.dest1.toBase64()}\n")
persistBad.append("${dests.dest2.toBase64()}\n")
persistGood.append("${personas.persona1.toBase64()}\n")
persistBad.append("${personas.persona2.toBase64()}\n")
service = new TrustService(persistGood, persistBad, 100)
service.start()
Thread.sleep(10)
Thread.sleep(50)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
}
}

37
doc/infohash-upgrade.md Normal file
View File

@@ -0,0 +1,37 @@
# InfoHash Upgrade
An infohash is a list of hashes of the pieces of the file. In MuWire 0.1.0 the piece size is determined by policy based on the file size, with the intention being to keep the list of hashes to maximum 128 in number. The reason for this is that infohashes get returned with search results, and smaller piece size will result in larger infohash which will slow down the transmission of search results.
### The problem
This presents the following problem - larger files have larger piece sizes: a 2GB file will have a 16MB piece size, a 4GB file 32MB and so on. Pieces are atomic, i.e. if a download fails halfway through a piece it will resume since the beginning of the piece. Unfortunately in the current state of I2P the failure rate of streaming connections is too high and transmitting an entire piece over a single connection is not likely to succeed as the size of the piece increases. This makes downloading multi-gigabyte files nearly impossible.
### Out-of-band request proposal
Barring any improvement to the reliability of I2P connections, the following approach can be used to enable smaller piece sizes and the corresponding increase in download success rate of large files:
* Search results do carry the full infohash of the file, instead they carry just the root and the number of 32-byte hashes in the infohash
* When a downloader begins a download, it issues a request for the full infohash first. Only after that is fetched and verified, the download proceeds as usual.
Such approach is more complicated in nature than the current simplistic one, but it has the additional benefit of reducing the size of search results.
### Wire protocol changes
A new request method - "HASHLIST" is introduced. It is immediately followed by the Base64 encoded root of the infohash and '\r\n'. The request may contain HTTP headers in the same format as in HTTP 1.1. One such header may be the X-Persona header, but that is optional. After all the headers a blank line with just '\r\n' is sent.
The response is identical to that of regular GET request, with the same response codes. The response may also carry headers, and is also followed by a blank line with '\r\n'. What follows immediately after that is a binary representation of the hashlist. After sending the full hashlist, the uploader keeps the connection open in anticipation of the first content GET request.
The downloader verifies the hashlist by hashing it with SHA256 and comparing it to the advertised infohash root. If there is a match, it proceeds with the rest of the download as in MuWire 0.1.0.
### Necessary changes to MuWire 0.1.0
To accommodate this proposal in a backwards compatible manner, it is necessary to first de-hardcode the piece count computation logic which is currently hardcoded in a few places. Then it is necessary to:
* persist the piece size to disk when a file is being shared so that it can be returned in search results
* search queries need to carry a flag of some kind that indicates support for out-of-band infohash support
* that in turn requires nodes to support passing of that flag as the queries are being routed through the network
* the returned results need to indicate whether they are returning a full infohash or just a root; the "version" field in the json can be used for that
### Roadmap
Support for this proposal is currently intended for MuWire 0.2.0. However, in order to make rollout smooth, in MuWire 0.1.1 support for the first two items will be introduced. Since there already are users on the network who have shared files without persisting the size of their pieces on disk, those files will not be eligible to participate in this scheme unless re-shared (which implies re-hashing).

View File

@@ -131,12 +131,18 @@ Sent by a leaf or ultrapeer when performing a search. Contains the reply-to per
firstHop: false,
keywords : ["keyword1","keyword2"...]
infohash: "asdfasdf...",
replyTo : "asdfasf...b64"
replyTo : "asdfasf...b64",
originator : "asfasdf...",
"oobHashlist" : true
}
```
A search can contain either the query entered by the user in the UI or the infohash if the user is looking for a specific file. If both are present, the infohash takes precedence and the keyword query is ignored.
The "originator" field contains the Base64-encoded persona of the originator of the query. It is used for display purposes only. The I2P destination in that persona must match the one in the "replyTo" field.
The oobHashlist flag indicates support for out-of-band hashlist delivery, which is not yet implemented. Nevertheless, this flag gets propagated through the network for future-proofing.
### Ultrapeer to leaf
The "Search" message is also sent from an ultrapeer to a leaf.
@@ -175,6 +181,8 @@ Search results are sent through and HTTP POST method from the responder to the o
* The "altlocs" list contains list of alternate personas that the responder thinks may also have the file.
* The "pieceSize" field is the size of the each individual file piece (except possibly the last) in powers of 2
Results version 1 contain the full hashlist, version 2 does not contain that list. See the "infohash-upgrade" document for more information.
### "Who do you trust" query - any node to any node
(See the "web-of-trust" document for more info on this query)

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.0.5
version = 0.2.1
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@@ -41,6 +41,7 @@ griffon {
}
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle'

View File

@@ -21,4 +21,9 @@ mvcGroups {
view = 'com.muwire.gui.SearchTabView'
controller = 'com.muwire.gui.SearchTabController'
}
}
'Options' {
model = 'com.muwire.gui.OptionsModel'
view = 'com.muwire.gui.OptionsView'
controller = 'com.muwire.gui.OptionsController'
}
}

View File

@@ -7,12 +7,15 @@ import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import javax.inject.Inject
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
@@ -36,18 +39,56 @@ class MainFrameController {
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
search = search.trim()
if (search.length() == 0)
return
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
params["uuid"] = uuid.toString()
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
boolean hashSearch = false
byte [] root = null
if (search.length() == 44 && search.indexOf(" ") < 0) {
try {
root = Base64.decode(search)
hashSearch = true
} catch (Exception e) {
// not a hash search
}
}
// this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
def searchEvent
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid)
} else {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true)
}
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination))
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
}
void search(String infoHash, String tabTitle) {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = tabTitle
params["uuid"] = uuid.toString()
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
oobInfohash: true)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
}
private def selectedResult() {
@@ -57,12 +98,20 @@ class MainFrameController {
int row = table.getSelectedRow()
if (row == -1)
return
def sortEvt = group.view.lastSortEvent
if (sortEvt != null) {
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
}
group.model.results[row]
}
private def selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow()
model.downloads[selected].downloader
private int selectedDownload() {
def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow()
def sortEvt = mvcGroup.view.lastDownloadSortEvent
if (sortEvt != null)
selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
selected
}
@ControllerAction
@@ -70,8 +119,15 @@ class MainFrameController {
def result = selectedResult()
if (result == null)
return // TODO disable button
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : result, target : file))
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
def selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group")
def resultsBucket = group.model.hashBucket[result.infohash]
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file))
}
@ControllerAction
@@ -92,13 +148,14 @@ class MainFrameController {
@ControllerAction
void cancel() {
def downloader = selectedDownload()
def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel()
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
}
@ControllerAction
void resume() {
def downloader = selectedDownload()
def downloader = model.downloads[selectedDownload()].downloader
downloader.resume()
}
@@ -129,6 +186,10 @@ class MainFrameController {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
void unshareSelectedFiles() {
println "unsharing selected files"
}
void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e->
core = e.getNewValue()

View File

@@ -0,0 +1,113 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
@ArtifactProviderFor(GriffonController)
class OptionsController {
@MVCMember @Nonnull
OptionsModel model
@MVCMember @Nonnull
OptionsView view
@ControllerAction
void save() {
String text
Core core = application.context.get("core")
def i2pProps = core.i2pOptions
text = view.inboundLengthField.text
model.inboundLength = text
i2pProps["inbound.length"] = text
text = view.inboundQuantityField.text
model.inboundQuantity = text
i2pProps["inbound.quantity"] = text
text = view.outboundQuantityField.text
model.outboundQuantity = text
i2pProps["outbound.quantity"] = text
text = view.outboundLengthField.text
model.outboundLength = text
i2pProps["outbound.length"] = text
File i2pSettingsFile = new File(core.home, "i2p.properties")
i2pSettingsFile.withOutputStream {
i2pProps.store(it,"")
}
text = view.retryField.text
model.downloadRetryInterval = text
def settings = application.context.get("muwire-settings")
settings.downloadRetryInterval = Integer.valueOf(text)
text = view.updateField.text
model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text)
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
model.onlyTrusted = onlyTrusted
settings.setAllowUntrusted(!onlyTrusted)
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
model.shareDownloadedFiles = shareDownloaded
settings.shareDownloadedFiles = shareDownloaded
File settingsFile = new File(core.home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
// UI Setttings
UISettings uiSettings = application.context.get("ui-settings")
text = view.lnfField.text
model.lnf = text
uiSettings.lnf = text
text = view.fontField.text
model.font = text
uiSettings.font = text
boolean showMonitor = view.monitorCheckbox.model.isSelected()
model.showMonitor = showMonitor
uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads
uiSettings.clearCancelledDownloads = clearCancelledDownloads
boolean clearFinishedDownloads = view.clearFinishedDownloadsCheckbox.model.isSelected()
model.clearFinishedDownloads = clearFinishedDownloads
uiSettings.clearFinishedDownloads = clearFinishedDownloads
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult
boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
model.showSearchHashes = showSearchHashes
uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
}
cancel()
}
@ControllerAction
void cancel() {
view.d.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -1,17 +1,25 @@
import griffon.core.GriffonApplication
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.gui.UISettings
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JTable
import javax.swing.LookAndFeel
import javax.swing.UIManager
import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel
import java.awt.Font
import java.util.logging.Level
@Log
class Initialize extends AbstractLifecycleHandler {
@Inject
@@ -21,11 +29,87 @@ class Initialize extends AbstractLifecycleHandler {
@Override
void execute() {
if (isMacOSX()) {
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
} else {
lookAndFeel('system', 'gtk')
log.info "Loading home dir"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
selectHome() :
portableHome
home = new File(home)
if (!home.exists()) {
log.info("creating home dir $home")
home.mkdirs()
}
application.context.put("muwire-home", home.getAbsolutePath())
def guiPropsFile = new File(home, "gui.properties")
UISettings uiSettings
if (guiPropsFile.exists()) {
Properties props = new Properties()
guiPropsFile.withInputStream { props.load(it) }
uiSettings = new UISettings(props)
log.info("settting user-specified lnf $uiSettings.lnf")
try {
lookAndFeel(uiSettings.lnf)
} catch (Throwable bad) {
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad)
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID()
}
if (uiSettings.font != null) {
log.info("setting user-specified font $uiSettings.font")
Font font = new Font(uiSettings.font, Font.PLAIN, 12)
def defaults = UIManager.getDefaults()
defaults.put("Button.font", font)
defaults.put("RadioButton.font", font)
defaults.put("Label.font", font)
defaults.put("CheckBox.font", font)
defaults.put("Table.font", font)
defaults.put("TableHeader.font", font)
// TODO: add others
}
} else {
Properties props = new Properties()
uiSettings = new UISettings(props)
log.info "will try default lnfs"
if (isMacOSX()) {
if (SystemVersion.isJava9()) {
uiSettings.lnf = "metal"
lookAndFeel("metal")
} else {
uiSettings.lnf = "nimbus"
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
}
} else {
LookAndFeel chosen = lookAndFeel('system', 'gtk')
uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name")
}
}
application.context.put("ui-settings", uiSettings)
}
private static String selectHome() {
def home = new File(System.properties["user.home"])
def defaultHome = new File(home, ".MuWire")
if (defaultHome.exists())
return defaultHome.getAbsolutePath()
if (SystemVersion.isMac()) {
def library = new File(home, "Library")
def appSupport = new File(library, "Application Support")
def muwire = new File(appSupport,"MuWire")
return muwire.getAbsolutePath()
}
if (SystemVersion.isWindows()) {
def appData = new File(home,"AppData")
def roaming = new File(appData, "Roaming")
def muwire = new File(roaming, "MuWire")
return muwire.getAbsolutePath()
}
defaultHome.getAbsolutePath()
}
}

View File

@@ -1,10 +1,13 @@
import griffon.core.GriffonApplication
import griffon.core.env.Metadata
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.FileSharedEvent
import javax.annotation.Nonnull
@@ -20,6 +23,9 @@ import java.util.logging.Level
@Log
class Ready extends AbstractLifecycleHandler {
@Inject Metadata metadata
@Inject
Ready(@Nonnull GriffonApplication application) {
super(application)
@@ -28,17 +34,8 @@ class Ready extends AbstractLifecycleHandler {
@Override
void execute() {
log.info "starting core services"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" :
portableHome
home = new File(home)
if (!home.exists()) {
log.info("creating home dir")
home.mkdir()
}
def home = new File(application.getContext().getAsString("muwire-home"))
def props = new Properties()
def propsFile = new File(home, "MuWire.properties")
if (propsFile.exists()) {
@@ -90,9 +87,10 @@ class Ready extends AbstractLifecycleHandler {
props.write(it)
}
}
Core core
try {
core = new Core(props, home)
core = new Core(props, home, metadata["application.version"])
} catch (Exception bad) {
log.log(Level.SEVERE,"couldn't initialize core",bad)
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",
@@ -111,6 +109,8 @@ class Ready extends AbstractLifecycleHandler {
core.eventBus.publish(new FileSharedEvent(file : new File(it)))
}
}
core.eventBus.publish(new UILoadedEvent())
}
}

View File

@@ -4,15 +4,18 @@ import java.util.concurrent.ConcurrentHashMap
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JOptionPane
import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
@@ -20,22 +23,28 @@ import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonModel
import griffon.core.env.Metadata
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.FXObservable
import griffon.transform.Observable
import net.i2p.data.Base64
import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class MainFrameModel {
@Inject Metadata metadata
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
MainFrameController controller
@Inject @Nonnull GriffonApplication application
@Observable boolean coreInitialized = false
@@ -57,7 +66,11 @@ class MainFrameModel {
private final Set<InfoHash> infoHashes = new HashSet<>()
volatile Core core
private long lastRetryTime = System.currentTimeMillis()
UISettings uiSettings
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow()
@@ -67,11 +80,28 @@ class MainFrameModel {
void mvcGroupInit(Map<String, Object> args) {
uiSettings = application.context.get("ui-settings")
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
// remove cancelled or finished downloads
def toRemove = []
downloads.each {
if (uiSettings.clearCancelledDownloads &&
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
toRemove << it
if (uiSettings.clearFinishedDownloads &&
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
toRemove << it
}
toRemove.each {
downloads.remove(it)
}
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")
@@ -94,20 +124,30 @@ class MainFrameModel {
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
core.eventBus.register(FileDownloadedEvent.class, this)
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
timer.schedule({
runInsideUIAsync {
downloads.each {
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
it.downloader.resume()
timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
runInsideUIAsync {
downloads.each {
def state = it.downloader.currentState
if (state == Downloader.DownloadState.FAILED ||
state == Downloader.DownloadState.DOWNLOADING)
it.downloader.resume()
updateTablePreservingSelection("downloads-table")
}
}
}
}, retryInterval, retryInterval)
}
}
}
}, 60000, 60000)
runInsideUIAsync {
trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values())
@@ -138,7 +178,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination)
UIConnection con = new UIConnection(destination : e.endpoint.destination, incoming : e.incoming)
connectionList.add(con)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
@@ -153,7 +194,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination)
UIConnection con = new UIConnection(destination : e.destination)
connectionList.remove(con)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
@@ -219,20 +261,76 @@ class MainFrameModel {
void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination)
return
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
def search
if (e.searchEvent.searchHash != null) {
if (!uiSettings.showSearchHashes) {
return
}
search = Base64.encode(e.searchEvent.searchHash)
} else {
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
}
search = sb.toString()
if (search.trim().size() == 0)
return
}
def search = sb.toString()
if (search.trim().size() == 0)
return
runInsideUIAsync {
searches.addFirst(search)
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200)
searches.removeLast()
JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged()
}
}
class IncomingSearch {
String search
Destination replyTo
Persona originator
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
runInsideUIAsync {
int option = JOptionPane.showConfirmDialog(null,
"MuWire $e.version is available from $e.signer. You have "+ metadata["application.version"]+" Update?",
"New MuWire version availble", JOptionPane.OK_CANCEL_OPTION)
if (option == JOptionPane.CANCEL_OPTION)
return
controller.search(e.infoHash,"MuWire update")
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (!core.muOptions.shareDownloadedFiles)
return
infoHashes.add(e.downloadedFile.infoHash)
runInsideUIAsync {
shared << e.downloadedFile
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
}
}
private static class UIConnection {
Destination destination
boolean incoming
@Override
public int hashCode() {
destination.hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof UIConnection))
return false
UIConnection other = (UIConnection) o
return destination == other.destination
}
}
}

View File

@@ -0,0 +1,54 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
@Observable String updateCheckInterval
@Observable boolean onlyTrusted
@Observable boolean shareDownloadedFiles
// i2p options
@Observable String inboundLength
@Observable String inboundQuantity
@Observable String outboundLength
@Observable String outboundQuantity
// gui options
@Observable boolean showMonitor
@Observable String lnf
@Observable String font
@Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
@Observable boolean showSearchHashes
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
downloadRetryInterval = settings.downloadRetryInterval
updateCheckInterval = settings.updateCheckInterval
onlyTrusted = !settings.allowUntrusted()
shareDownloadedFiles = settings.shareDownloadedFiles
Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"]
inboundQuantity = core.i2pOptions["inbound.quantity"]
outboundLength = core.i2pOptions["outbound.length"]
outboundQuantity = core.i2pOptions["outbound.quantity"]
UISettings uiSettings = application.context.get("ui-settings")
showMonitor = uiSettings.showMonitor
lnf = uiSettings.lnf
font = uiSettings.font
clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult
showSearchHashes = uiSettings.showSearchHashes
}
}

View File

@@ -19,12 +19,15 @@ class SearchTabModel {
FactoryBuilderSupport builder
Core core
UISettings uiSettings
String uuid
def results = []
def hashBucket = [:]
void mvcGroupInit(Map<String, String> args) {
core = mvcGroup.parentGroup.model.core
uiSettings = application.context.get("ui-settings")
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
}
@@ -33,7 +36,17 @@ class SearchTabModel {
}
void handleResult(UIResultEvent e) {
if (uiSettings.excludeLocalResult &&
e.sender == core.me)
return
runInsideUIAsync {
def bucket = hashBucket.get(e.infohash)
if (bucket == null) {
bucket = []
hashBucket[e.infohash] = bucket
}
bucket << e
results << e
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()

View File

@@ -3,16 +3,24 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JFileChooser
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.border.Border
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.Constants
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent
@@ -22,6 +30,10 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@@ -33,7 +45,12 @@ class MainFrameView {
@MVCMember @Nonnull
MainFrameModel model
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
void initUI() {
UISettings settings = application.context.get("ui-settings")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
@@ -44,6 +61,11 @@ class MainFrameView {
imageIcon('/griffon-icon-16x16.png').image],
pack : false,
visible : bind { model.coreInitialized }) {
menuBar {
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
}
}
borderLayout()
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
borderLayout()
@@ -51,7 +73,8 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
if (settings.showMonitor)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
}
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
@@ -90,19 +113,18 @@ class MainFrameView {
panel (constraints : JSplitPane.BOTTOM) {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "downloads-table") {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
tableModel(list: model.downloads) {
closureColumn(header: "Name", type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", type: String, read : {row -> row.downloader.getCurrentState()})
closureColumn(header: "Progress", type: String, read: { row ->
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
int pieces = row.downloader.nPieces
int done = row.downloader.donePieces()
"$done/$pieces pieces"
})
closureColumn(header: "Piece", type: String, read: { row ->
int position = row.downloader.positionInPiece()
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece
"$position/$pieceSize bytes"
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec"
})
}
}
@@ -122,10 +144,10 @@ class MainFrameView {
button(text : "Click here to share files", actionPerformed : shareFiles)
}
scrollPane ( constraints : BorderLayout.CENTER) {
table(id : "shared-files-table") {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", type : Long, read : {row -> row.file.length()})
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() })
}
}
}
@@ -164,7 +186,13 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
if (row.incoming)
return "In"
else
return "Out"
})
}
}
}
@@ -177,7 +205,17 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : { it })
closureColumn(header : "Keywords", type : String, read : {
sanitized = it.search.replace('<', ' ')
sanitized
})
closureColumn(header : "From", type : String, read : {
if (it.originator != null) {
return it.originator.getHumanReadableName()
} else {
return it.replyTo.toBase32()
}
})
}
}
}
@@ -188,7 +226,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") {
table(id : "trusted-table", autoCreateRowSorter : true) {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -203,7 +241,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") {
table(id : "distrusted-table", autoCreateRowSorter : true) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -235,16 +273,24 @@ class MainFrameView {
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = downloadsTable.getSelectedRow()
def downloader = model.downloads[selectedRow].downloader
int selectedRow = selectedDownloaderRow()
if (selectedRow < 0) {
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
return
}
def downloader = model.downloads[selectedRow]?.downloader
if (downloader == null)
return
switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
case Downloader.DownloadState.HASHLIST:
model.cancelButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = false
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
break
default:
@@ -252,8 +298,94 @@ class MainFrameView {
model.retryButtonEnabled = false
}
})
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
downloadsTable.rowSorter.setSortsOnUpdates(true)
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu sharedFilesMenu = new JPopupMenu()
// JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
// unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard(sharedFilesTable)})
sharedFilesMenu.add(copyHashToClipboard)
sharedFilesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
})
// searches table
def searchesTable = builder.getVariable("searches-table")
JPopupMenu searchTableMenu = new JPopupMenu()
JMenuItem copySearchToClipboard = new JMenuItem("Copy search to clipboard")
copySearchToClipboard.addActionListener({mvcGroup.view.copySearchToClipboard(searchesTable)})
searchTableMenu.add(copySearchToClipboard)
searchesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
})
}
def showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY())
}
def copyHashToClipboard(JTable sharedFilesTable) {
int selected = sharedFilesTable.getSelectedRow()
if (selected < 0)
return
if (lastSharedSortEvent != null)
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
String root = Base64.encode(model.shared[selected].infoHash.getRoot())
StringSelection selection = new StringSelection(root)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
def copySearchToClipboard(JTable searchesTable) {
int selected = searchesTable.getSelectedRow()
if (selected < 0)
return
String search = model.searches[selected].search
StringSelection selection = new StringSelection(search)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
int selectedDownloaderRow() {
int selected = builder.getVariable("downloads-table").getSelectedRow()
if (lastDownloadSortEvent != null)
selected = lastDownloadSortEvent.convertPreviousRowIndexToModel(selected)
selected
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")

View File

@@ -0,0 +1,132 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JPanel
import javax.swing.JTabbedPane
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class OptionsView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
OptionsModel model
def d
def p
def i
def u
def retryField
def updateField
def allowUntrustedCheckbox
def shareDownloadedCheckbox
def inboundLengthField
def inboundQuantityField
def outboundLengthField
def outboundQuantityField
def lnfField
def monitorCheckbox
def fontField
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def buttonsPanel
def mainFrame
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
d = new JDialog(mainFrame, "Options", true)
d.setResizable(false)
p = builder.panel {
gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 2))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 2))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
}
i = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
}
u = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
}
buttonsPanel = builder.panel {
gridBagLayout()
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
}
}
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("MuWire", p)
tabbedPane.addTab("I2P", i)
tabbedPane.addTab("GUI", u)
JPanel panel = new JPanel()
panel.setLayout(new BorderLayout())
panel.add(tabbedPane, BorderLayout.CENTER)
panel.add(buttonsPanel, BorderLayout.SOUTH)
d.getContentPane().add(panel)
d.pack()
d.setLocationRelativeTo(mainFrame)
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
d.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
d.show()
}
}

View File

@@ -4,11 +4,26 @@ import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.annotation.Nonnull
@@ -22,26 +37,32 @@ class SearchTabView {
def pane
def parent
def searchTerms
def resultsTable
def lastSortEvent
void initUI() {
builder.with {
def resultsTable
def pane = scrollPane {
resultsTable = table(id : "results-table") {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 50, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination).toString()
})
}
}
}
this.pane = pane
this.pane.putClientProperty("mvc-group", mvcGroup)
this.pane.putClientProperty("results-table",resultsTable)
this.resultsTable = resultsTable
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( {
@@ -54,7 +75,7 @@ class SearchTabView {
searchTerms = args["search-terms"]
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
parent.addTab(searchTerms, pane)
int index = parent.indexOfTab(searchTerms)
int index = parent.indexOfComponent(pane)
parent.setSelectedIndex(index)
def tabPanel
@@ -70,6 +91,41 @@ class SearchTabView {
}
parent.setTabComponentAt(index, tabPanel)
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu menu = new JPopupMenu()
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
resultsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download()
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
}
})
}
def closeTab = {
@@ -78,4 +134,21 @@ class SearchTabView {
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
def showPopupMenu(JPopupMenu menu, MouseEvent e) {
println "showing popup menu"
menu.show(e.getComponent(), e.getX(), e.getY())
}
def copyHashToClipboard() {
int selected = resultsTable.getSelectedRow()
if (selected < 0)
return
if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot())
StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
}

View File

@@ -0,0 +1,25 @@
package com.muwire.gui
import griffon.core.test.GriffonFestRule
import org.fest.swing.fixture.FrameFixture
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
class OptionsIntegrationTest {
static {
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule()
private FrameFixture window
@Test
void smokeTest() {
fail('Not implemented yet!')
}
}

View File

@@ -0,0 +1,29 @@
package com.muwire.gui
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JTable
import javax.swing.table.DefaultTableCellRenderer
import net.i2p.data.DataHelper
class SizeRenderer extends DefaultTableCellRenderer {
SizeRenderer() {
setHorizontalAlignment(JLabel.CENTER)
}
@Override
JComponent getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Long l = (Long) value
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
setText(formatted)
if (isSelected) {
setForeground(table.getSelectionForeground())
setBackground(table.getSelectionBackground())
} else {
setForeground(table.getForeground())
setBackground(table.getBackground())
}
this
}
}

View File

@@ -0,0 +1,37 @@
package com.muwire.gui
class UISettings {
String lnf
boolean showMonitor
String font
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean excludeLocalResult
boolean showSearchHashes
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
font = props.getProperty("font",null)
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false"))
}
void write(OutputStream out) throws IOException {
Properties props = new Properties()
props.setProperty("lnf", lnf)
props.setProperty("showMonitor", String.valueOf(showMonitor))
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
if (font != null)
props.setProperty("font", font)
props.store(out, "UI Properties")
}
}

View File

@@ -0,0 +1,21 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(OptionsController)
class OptionsControllerTest {
private OptionsController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,2 +1,3 @@
apply plugin : 'application'
mainClassName = 'com.muwire.hostcache.HostCache'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']

View File

@@ -1,9 +1,12 @@
package com.muwire.hostcache
import java.util.logging.Level
import java.util.stream.Collectors
import groovy.util.logging.Log
import net.i2p.data.Destination
@Log
class Crawler {
final def pinger
@@ -22,12 +25,14 @@ class Crawler {
synchronized def handleCrawlerPong(pong, Destination source) {
if (!inFlight.containsKey(source)) {
log.info("response from host that hasn't been crawled")
return
}
Host host = inFlight.remove(source)
if (pong.uuid == null || pong.leafSlots == null || pong.peerSlots == null || pong.peers == null) {
hostPool.fail(host)
log.info("invalid crawler pong")
return
}
@@ -40,6 +45,7 @@ class Crawler {
}
if (!uuid.equals(currentUUID)) {
log.info("uuid mismatch")
hostPool.fail(host)
return
}
@@ -50,7 +56,9 @@ class Crawler {
def peers
try {
peers = pong.peers.stream().map({b64 -> new Destination(b64)}).collect(Collectors.toSet())
log.info("received ${peers.size()} peers")
} catch (Exception e) {
log.log(Level.WARNING,"couldn't parse peers", e)
hostPool.fail(host)
return
}

View File

@@ -1,9 +1,11 @@
package com.muwire.hostcache
import java.util.logging.Level
import java.util.stream.Collectors
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
@@ -12,6 +14,7 @@ import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.util.SystemVersion
import net.i2p.data.*
@Log
public class HostCache {
public static void main(String[] args) {
@@ -53,7 +56,7 @@ public class HostCache {
myDest = session.getMyDestination()
// initialize hostpool and crawler
HostPool hostPool = new HostPool(3, 60 * 1000 * 1000)
HostPool hostPool = new HostPool(3, 60 * 60 * 1000)
Pinger pinger = new Pinger(session)
Crawler crawler = new Crawler(pinger, hostPool, 5)
@@ -64,7 +67,7 @@ public class HostCache {
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
session.connect()
println "INFO: connected, going to sleep"
log.info("connected, going to sleep")
Thread.sleep(Integer.MAX_VALUE)
}
@@ -77,16 +80,16 @@ public class HostCache {
void reportAbuse(I2PSession sesison, int severity) {}
void disconnected(I2PSession session) {
println "ERROR: session disconnected, exiting"
log.severe("session disconnected, exiting")
System.exit(1)
}
void errorOccurred(I2PSession session, String message, Throwable error) {
println "ERROR: ${message} ${error}"
log.warning("${message} ${error}")
}
void messageAvailable(I2PSession session, int msgId, long size, int proto,
int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
println "WARN: received unexpected protocol ${proto}"
log.warning("received unexpected protocol ${proto}")
return
}
@@ -95,19 +98,19 @@ public class HostCache {
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
println "INFO: Received something from ${sender.toBase32()}"
def b32 = sender.toBase32()
payload = dissector.getPayload()
payload = json.parse(payload)
if (payload.type == null) {
println "WARN: type field missing"
log.warning("type field missing from $b32")
return
}
switch(payload.type) {
case "Ping" :
println "Ping"
case "Ping" :
log.info("ping from $b32")
if (payload.leaf == null) {
println "WARN: ping didn't specify if leaf"
log.warning("ping didn't specify if leaf from $b32")
return
}
payload.leaf = Boolean.parseBoolean(payload.leaf.toString())
@@ -116,14 +119,14 @@ public class HostCache {
respond(session, sender, payload)
break
case "CrawlerPong":
println "CrawlerPong"
log.info("CrawlerPong from $b32")
crawler.handleCrawlerPong(payload, sender)
break
default:
println "WARN: Unexpected message type ${payload.type}, dropping"
log.warning("Unexpected message type ${payload.type}, dropping from $b32")
}
} catch (Exception dfe) {
println "WARN: invalid datagram ${dfe}"
log.log(Level.WARNING,"invalid datagram", dfe)
}
}
void messageAvailable(I2PSession session, int msgId, long size) {

View File

@@ -1,4 +1,6 @@
include 'pinger'
include 'host-cache'
include 'update-server'
include 'core'
include 'gui'
include 'cli'

View File

@@ -0,0 +1,3 @@
apply plugin : 'application'
mainClassName = 'com.muwire.update.UpdateServer'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']

View File

@@ -0,0 +1,107 @@
package com.muwire.update
import java.util.logging.Level
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
@Log
class UpdateServer {
public static void main(String[] args) {
def home = System.getProperty("user.home") + "/.MuWireUpdateServer"
home = new File(home)
if (!home.exists())
home.mkdirs()
def keyFile = new File(home, "key.dat")
def i2pClientFactory = new I2PClientFactory()
def i2pClient = i2pClientFactory.createClient()
def myDest
def session
if (!keyFile.exists()) {
def os = new FileOutputStream(keyFile);
myDest = i2pClient.createDestination(os)
os.close()
log.info "No key.dat file was found, so creating a new destination."
log.info "This is the destination you want to give out for your new UpdateServer"
log.info myDest.toBase64()
}
def update = new File(home, "update.json")
if (!update.exists()) {
log.warning("update file doesn't exist, exiting")
System.exit(1)
}
def props = System.getProperties().clone()
props.putAt("inbound.nickname", "MuWire UpdateServer")
session = i2pClient.createSession(new FileInputStream(keyFile), props)
myDest = session.getMyDestination()
session.addMuxedSessionListener(new Listener(update), I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
session.connect()
log.info("Connected, going to sleep")
Thread.sleep(Integer.MAX_VALUE)
}
static class Listener implements I2PSessionMuxedListener {
private final File json
private final def slurper = new JsonSlurper()
Listener(File json) {
this.json = json
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning("received uknown protocol $proto")
return
}
def payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
payload = slurper.parse(dissector.getPayload())
log.info("Got an update ping from "+sender.toBase32() + " reported version "+payload?.myVersion)
def maker = new I2PDatagramMaker(session)
def response = maker.makeI2PDatagram(json.bytes)
session.sendMessage(sender, response, I2PSession.PROTO_DATAGRAM, 0, 2)
} catch (Exception e) {
log.log(Level.WARNING, "exception responding to update request",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
Log.severe("Disconnected from I2P router")
System.exit(1)
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}