Compare commits

...

1170 Commits

Author SHA1 Message Date
Zlatin Balevsky
bcb41baca2 update version, add link to Packaging page 2020-09-27 07:26:06 +01:00
Zlatin Balevsky
72985bacb6 add ability to disable updates completely, intended for 3rd party packaging 2020-09-26 17:53:29 +01:00
Zlatin Balevsky
3387d22a6c remove HOPELESS downloads from the download list via an event 2020-09-26 17:10:24 +01:00
Zlatin Balevsky
10bd566d58 Release 0.7.4 2020-09-25 16:51:40 +01:00
Zlatin Balevsky
f4e0c707df fix cleaning up of hopeless downloads in plugin 2020-09-23 15:23:51 +01:00
Zlatin Balevsky
c11a427483 fix cleaning up of hopeless downloads in gui 2020-09-23 15:21:47 +01:00
Zlatin Balevsky
e9db22c562 option for download attempts before giving up in desktop gui 2020-09-23 14:54:19 +01:00
Zlatin Balevsky
fa53a35023 option for download attempts before giving up in plugin 2020-09-23 14:44:51 +01:00
Zlatin Balevsky
94dd6101aa show sequential status and hopeless host count in plugin 2020-09-23 14:33:18 +01:00
Zlatin Balevsky
e65fbe1bd1 show sequential status and hopeless host count in download details panel 2020-09-23 14:26:02 +01:00
Zlatin Balevsky
964e315367 add a hopeless state for a download where all sources are hopeless 2020-09-23 14:17:40 +01:00
Zlatin Balevsky
140231e362 Give up on download sources after a number of attempts 2020-09-23 14:00:52 +01:00
Zlatin Balevsky
c73a821c67 put verified sources in the responder cache as well 2020-09-23 11:30:20 +01:00
Zlatin Balevsky
0ebe00b526 reduce hopeless interval to 1hr and purge interval to 24hrs 2020-09-22 16:08:17 +01:00
Zlatin Balevsky
b2a3bfce54 make sure we have the Persona of the altloc 2020-09-22 16:07:18 +01:00
Zlatin Balevsky
c490a511bd distinguish between discovered sources and verified sources. Only propagate and persist verified sources 2020-09-22 13:34:49 +01:00
Zlatin Balevsky
cbaa3470d2 make responder cache size configurable 2020-09-22 12:17:27 +01:00
Zlatin Balevsky
84d61fccd5 cache recent responders and always forward queries to them. Thanks to qtm for the idea 2020-09-21 15:28:15 +01:00
Zlatin Balevsky
a88db8f50f FixedSizeFIFOSet 2020-09-21 15:13:44 +01:00
Zlatin Balevsky
5a38154e15 make sure the pongs uuid matches the last sent ping uuid 2020-09-20 18:42:26 +01:00
Zlatin Balevsky
e5891de136 send and read up to 2 hosts per pong 2020-09-20 18:26:29 +01:00
Zlatin Balevsky
1e729bae1c implement forgetting of hopeless hosts after some time 2020-09-20 17:49:07 +01:00
Zlatin Balevsky
3e6e0c7e9f cache calls to System.currenTimeMillis() 2020-09-20 17:34:28 +01:00
Zlatin Balevsky
44af23c162 restrict forwarding of queries to sqrt of neighboring connections. Thanks to 'qtm' for the idea 2020-09-19 17:28:19 +01:00
Zlatin Balevsky
a262c99efe reduce limit on peer connections 2020-09-19 17:16:36 +01:00
Zlatin Balevsky
9eff723dd3 Release 0.7.3 2020-09-18 18:44:15 +01:00
Zlatin Balevsky
f2531c80d5 disable Configure button if no directory is selected 2020-09-18 18:35:10 +01:00
Zlatin Balevsky
944cb29901 show error messages if the command is not in the appropriate room 2020-09-18 18:15:15 +01:00
Zlatin Balevsky
2c7cf24942 browse and subscribe actions from upload table for plugin 2020-09-18 17:30:47 +01:00
Zlatin Balevsky
5a94d14b8e pass event correctly 2020-09-18 16:13:48 +01:00
Zlatin Balevsky
f20b23434f one more 2020-09-18 16:13:23 +01:00
Zlatin Balevsky
8d523a6265 update to api 2020-09-18 15:56:39 +01:00
Zlatin Balevsky
38b9ab5200 gui browse/feed/chat actions from upload table 2020-09-18 15:42:35 +01:00
Zlatin Balevsky
f6fdf9e33f parse browse/feed/chat headers on the uploader side 2020-09-18 14:59:58 +01:00
Zlatin Balevsky
b729a89672 Advertise browse/feed/chat abilities in download headers 2020-09-18 14:50:49 +01:00
Zlatin Balevsky
e531093b28 do not try to connect to hopeless hosts 2020-09-17 19:58:36 +01:00
Zlatin Balevsky
b18772465c limit the number of hosts read from each pong 2020-09-17 16:19:48 +01:00
Zlatin Balevsky
20aac03789 send an uuid with pings and echo it in pongs 2020-09-17 16:11:32 +01:00
Zlatin Balevsky
ac8d9c1281 update room scrollback limit to new api, update TODO 2020-09-16 17:41:01 +01:00
Zlatin Balevsky
ad8693d512 some styling of chat lines 2020-09-16 17:15:23 +01:00
Zlatin Balevsky
144ad634c8 fix ambiguous Math.max 2020-09-16 14:46:34 +01:00
Zlatin Balevsky
4cdb383b9f fix minutes-to-milliseconds conversion of feed update interval in desktop gui. Make default feed update interval one hour 2020-09-16 14:37:08 +01:00
Zlatin Balevsky
9c6f6bf266 Make number of connections for ultrapeer a property 2020-09-16 12:30:41 +01:00
Zlatin Balevsky
9a4e6b868b get rid of clear search button to make more space for search field, as it is not very big on Mac LnF 2020-09-14 15:35:49 +01:00
Zlatin Balevsky
31e0962b73 fix test target by excluding testng dependency 2020-09-14 14:38:12 +01:00
Zlatin Balevsky
2acac4b1ea fix java 14 on mac 2020-09-14 13:33:01 +01:00
Zlatin Balevsky
03d00a22d7 try native lnf on all platforms with metal last 2020-09-14 13:32:25 +01:00
Zlatin Balevsky
0e54fb1ed1 update TODO 2020-09-14 12:45:42 +01:00
Zlatin Balevsky
2dee5e2a8a short-circuit logic if monitor is not visible 2020-09-14 12:44:52 +01:00
Zlatin Balevsky
c7406a4838 preserve selection when updating shared files table 2020-09-14 12:39:18 +01:00
Zlatin Balevsky
c9eb702d7c add a section to the TODO for mwtrackerd with some items 2020-09-12 18:16:27 +01:00
Zlatin Balevsky
253603cac7 add a log statement for infohash parsing 2020-09-12 14:37:54 +01:00
Zlatin Balevsky
3af6ee3bce update TODO 2020-09-12 14:26:48 +01:00
Zlatin Balevsky
bfa88b0b7a Release 0.7.2 2020-09-10 16:43:21 +01:00
Zlatin Balevsky
1400967b22 update TODO 2020-09-04 14:47:13 +01:00
Zlatin Balevsky
5739760075 up router version to 0.9.47 2020-09-04 14:46:37 +01:00
Zlatin Balevsky
fec042ec36 check if watched directories disappeared while MW was down 2020-09-04 14:44:04 +01:00
Zlatin Balevsky
d3477b91fc Merge branch 'iconfix' into 'master'
update the muwire icon which appears on the router console itself

See merge request zlatinb/muwire!48
2020-08-14 14:49:33 +00:00
idk
1f973cf076 Merge branch 'iconfix-redux' of i2pgit.org:idk/muwire into iconfix 2020-08-12 21:30:55 -04:00
idk
fdb64f5539 re-try muwire icon fix with update codebase 2020-08-12 21:28:20 -04:00
idk
5b4f3202d6 update the image and create a smaller, icon-ized version 2020-08-12 21:23:42 -04:00
idk
64eb2dad80 add larger muwire logo, use it for the home page 2020-08-11 22:12:52 -04:00
idk
ffe328eee6 update the muwire icon which appears on the router console itself 2020-08-11 19:26:53 -04:00
Zlatin Balevsky
eb1f2fe19d escape download file name 2020-06-18 21:23:51 +01:00
Zlatin Balevsky
17c59102ad one more --add-opens 2020-06-16 14:05:41 +01:00
Zlatin Balevsky
26e8300d18 first pass at a collections proposal 2020-06-12 19:02:05 +01:00
Zlatin Balevsky
47ac0fd9ac fix gui:run target 2020-06-11 19:16:48 +01:00
Zlatin Balevsky
0b8b489169 read i2p.properties from file 2020-06-11 17:38:17 +01:00
Zlatin Balevsky
fb32690c7c de-hardcode groovy versions 2020-06-06 14:01:32 +01:00
Zlatin Balevsky
a11c504271 update all of the gui to groovy 3.0.4 2020-06-06 04:23:14 +01:00
Zlatin Balevsky
76e726b520 switch to the Groovy json library 2020-06-05 20:29:39 +01:00
Zlatin Balevsky
4f626615d8 disable translation instead of throwing an Error if the user is on development i2p router build 2020-06-05 20:05:03 +01:00
Zlatin Balevsky
061a1a88dd disable indy because on groovy 3 it uses too much memory 2020-06-05 14:32:54 +01:00
Zlatin Balevsky
ad20d7cf9a get rid of illegal reflective access warnings 2020-06-05 13:23:54 +01:00
Zlatin Balevsky
895df6cf94 disable pack200 2020-06-05 02:35:04 +01:00
Zlatin Balevsky
59b5d88829 shorter connect message 2020-06-03 12:58:16 +01:00
Zlatin Balevsky
f382d2ecbf move the share button to the left next to the shared files count 2020-06-03 12:55:20 +01:00
Zlatin Balevsky
6740d09479 checkboxes for font style 2020-06-03 12:44:56 +01:00
Zlatin Balevsky
8cbada110e switch to gridBagLayout to hopefully avoid hiding of short name 2020-06-03 02:58:33 +01:00
Zlatin Balevsky
33982dd24b preserve main frame dimensions across restarts 2020-06-03 02:22:59 +01:00
Zlatin Balevsky
274edcc599 preserve initial font+size+style accross restarts 2020-06-03 02:03:44 +01:00
Zlatin Balevsky
af218a369c migrate to groovy 3.0.4 2020-06-01 13:40:28 +01:00
Zlatin Balevsky
f0aaa83b7f clean up most of noise on console when running without a log file 2020-06-01 13:14:33 +01:00
Zlatin Balevsky
b9c34cb944 Add ability to specify default values for the wizard from system property. GitHub issue #32 2020-06-01 12:09:58 +01:00
Zlatin Balevsky
59353a6718 update readme 2020-05-29 12:35:51 +01:00
Zlatin Balevsky
c25546e1e1 Release 0.7.1 2020-05-29 12:06:53 +01:00
Zlatin Balevsky
f9fb9e4f07 get rid of dead code 2020-05-29 12:04:55 +01:00
Zlatin Balevsky
72f2b2bd37 fix manual searching for updates 2020-05-29 12:03:09 +01:00
Zlatin Balevsky
eb242b0889 reduce default speed smoothing interval 2020-05-29 11:14:56 +01:00
Zlatin Balevsky
6508522c9c tunnel quantity and length sliders step 2020-05-29 11:05:58 +01:00
Zlatin Balevsky
f38b8217c2 steps for embedded or external router, formatting 2020-05-29 10:52:58 +01:00
Zlatin Balevsky
c9c5e8617a Directory validation and creation 2020-05-29 01:51:13 +01:00
Zlatin Balevsky
8c4bafda82 move the button enabling logic in view 2020-05-29 01:50:53 +01:00
Zlatin Balevsky
c2044044c0 add a final step 2020-05-29 01:27:36 +01:00
Zlatin Balevsky
cb54b30967 apply steps at the end, add ability to cancel wizard 2020-05-29 01:15:56 +01:00
Zlatin Balevsky
c041f6baaa skeleton of setup wizard 2020-05-28 20:08:57 +01:00
Zlatin Balevsky
bf28278f72 Release 0.7.0 2020-05-26 19:03:05 +01:00
Zlatin Balevsky
6462675091 enable dual keys 2020-05-26 19:01:31 +01:00
Zlatin Balevsky
5adf8d8276 up to router 0.9.46 2020-05-26 18:58:40 +01:00
Zlatin Balevsky
2fbab55f68 remove references to tunnelName variable 2020-05-25 19:44:50 +01:00
Zlatin Balevsky
0d783a6bcd harmonize some strings 2020-05-21 11:31:27 +01:00
Zlatin Balevsky
017454c4b3 use sliders instead of fields in I2P settings 2020-05-18 14:39:16 +01:00
Zlatin Balevsky
ec41985d31 Release 0.6.15 2020-05-17 22:36:08 +01:00
Zlatin Balevsky
5daad35ee2 new icon 2020-05-17 14:30:23 +01:00
Zlatin Balevsky
8df9f63bc7 new icons 2020-05-17 13:11:59 +01:00
Zlatin Balevsky
367a43825f trim whitespaces before signing 2020-05-15 13:21:51 +01:00
Zlatin Balevsky
7b34b0cffc link to mucats 2020-05-14 20:15:54 +01:00
Zlatin Balevsky
bb6692c38e Merge branch 'strings' into 'master'
Prep for push to transifex

See merge request zlatinb/muwire!47
2020-05-13 15:31:37 +00:00
zzz
f1a2b103a8 Prep for push to transifex
Additional string cleanup, regenerate English po file
2020-05-13 10:29:18 -04:00
Zlatin Balevsky
c1324c92ba Merge branch 'csp3' into 'master'
Fix script line changes from previous MR

See merge request zlatinb/muwire!46
2020-05-11 17:02:49 +00:00
zzz
179c3438cd Fix script line changes from previous MR
Fix some missing quotes
Move util.js and tables.js to css.jsi
script files don't require nonce; inline scripts do.
Nonce doesn't matter until we turn on the CSP.
2020-05-11 10:59:28 -04:00
Zlatin Balevsky
7fa6812ee9 merge from zzz/csp2 2020-05-11 13:38:20 +01:00
zzz
a1c714b46e Replace innerHTML part 1 (Gitlab issue #45)
Change all plain text and empty content from innerHTML to textContent
2020-05-11 08:20:03 -04:00
Zlatin Balevsky
4f7cf4fbfc Merge branch 'csp1' into 'master'
Plugin headers and CSP (Gitlab issue #44)

See merge request zlatinb/muwire!44
2020-05-11 12:04:25 +00:00
zzz
2d3e843d64 Plugin headers and CSP (Gitlab issue #44)
Prep for stricter script-src:
Add headers, remove js onload, move init call to the js
Add nonces to all scripts, can't use yet due to innerHTML (see Gitlab issue #45)
2020-05-11 07:50:36 -04:00
Zlatin Balevsky
2e36812740 Merge branch 'sigtype' into 'master'
Signature must be constructed with the sigtype of the signing key

See merge request zlatinb/muwire!43
2020-05-10 11:15:12 +00:00
zzz
61340f346a Signature must be constructed with the sigtype of the signing key 2020-05-10 06:26:35 -04:00
Zlatin Balevsky
992daa1e45 size limit on nicknames 2020-05-10 09:51:56 +01:00
Zlatin Balevsky
3b825263a7 Make the random port selection range match that of the I2P router 2020-05-08 17:37:06 +01:00
Zlatin Balevsky
e1bf6c0821 prevent invalid characters in searchers of persisted files from breaking the loading process. Related to GitHub issue #45 2020-05-08 17:33:57 +01:00
Zlatin Balevsky
a6eca11479 Release 0.6.14 2020-05-07 13:39:17 +01:00
Zlatin Balevsky
11aa6dda70 sign tool in web ui 2020-05-07 13:29:21 +01:00
Zlatin Balevsky
3116e20c7c Fix i2np port change on every restart, github issue #45 2020-05-07 03:12:09 +01:00
Zlatin Balevsky
58a92e7442 disallow certain characters in nicknames 2020-05-06 11:49:52 +01:00
Zlatin Balevsky
d18cdb15cd disallow certain characters in nicknames 2020-05-06 11:39:08 +01:00
Zlatin Balevsky
ed02b718d9 sign raw UTF-8 representation, removing size limit 2020-05-06 05:36:59 +01:00
Zlatin Balevsky
564db3473c publish core to local maven repo 2020-05-05 16:01:23 +01:00
Zlatin Balevsky
6d6063829a convert the core project into a library 2020-05-05 15:39:54 +01:00
Zlatin Balevsky
ecaec1df3b sign tool 2020-05-04 23:16:32 +01:00
Zlatin Balevsky
8b99f83db8 Release 0.6.13 2020-05-04 14:18:08 +01:00
Zlatin Balevsky
33b159477a get test targets to pass, ignoring some tests which are not relevant anymore 2020-05-04 13:08:18 +01:00
Zlatin Balevsky
91d8175cc5 fix method name 2020-05-04 13:07:32 +01:00
Zlatin Balevsky
b4c6c77167 Ability to configure watched directories from swing gui 2020-05-04 12:15:27 +01:00
Zlatin Balevsky
fb59d1ca0c fix the wait window while core is loading 2020-05-04 08:15:25 +01:00
Zlatin Balevsky
3de4c65d2f Merge branch 'accordion' into 'master'
Accordion

See merge request zlatinb/muwire!41
2020-05-03 16:35:13 +00:00
zzz
91ea2c0184 Move accordion javascript to its own file
Open the accordion section for the page you are on
2020-05-03 12:16:05 -04:00
Zlatin Balevsky
4a81a3539e Merge branch 'master' into 'master'
Clean up help text for consistency and translatability

See merge request zlatinb/muwire!40
2020-05-03 12:56:03 +00:00
zzz
fcfb506787 Clean up help text for consistency and translatability 2020-05-03 12:56:03 +00:00
zzz
44dc7b808f Clean up help text for consistency and translatability 2020-05-03 08:28:05 -04:00
Zlatin Balevsky
339f4aaa3e Merge branch 'master' into 'master'
Add top-level groovy and java compile options to build.gradle

See merge request zlatinb/muwire!39
2020-05-02 15:20:25 +00:00
zzz
bf06c3b15f Add top-level groovy and java compile options to build.gradle
Add compilerArgs to gradle.properties
Fix compile warnings in DataUtil
2020-05-02 10:53:00 -04:00
Zlatin Balevsky
b5e41d72b8 typo 2020-04-29 12:57:01 +01:00
Zlatin Balevsky
2fe9309519 update README with link to Tracker wiki page 2020-04-29 12:55:17 +01:00
Zlatin Balevsky
2410ed7199 Merge branch 'tracking-server-side' 2020-04-29 12:37:06 +01:00
Zlatin Balevsky
9167c9edf7 add a max failuires parameter when deciding whether to expire a host. Report the number of negative hosts in the info rpc 2020-04-29 12:20:23 +01:00
Zlatin Balevsky
028a8d5044 handle tracker pongs 2020-04-29 11:24:58 +01:00
Zlatin Balevsky
356d7fe2ff always include the uuid in the tracker response 2020-04-29 11:23:42 +01:00
Zlatin Balevsky
9da7a90653 wip on pinging swarm members 2020-04-29 07:26:51 +01:00
Zlatin Balevsky
2001419f1a Iterate through the swarms in order of last pinged, get hosts which have not been pinged recently, also in chronological order 2020-04-29 06:07:22 +01:00
Zlatin Balevsky
eec9bab081 Start work on timer-based swarm tracking 2020-04-29 05:21:18 +01:00
Zlatin Balevsky
0a66267264 add missing dependency on java11 2020-04-29 03:47:01 +01:00
Zlatin Balevsky
ad698cf1b9 use spring configuration for the tracker properties 2020-04-29 02:58:48 +01:00
Zlatin Balevsky
fd9866c519 implement "info" json-rpc method 2020-04-29 02:03:50 +01:00
Zlatin Balevsky
83bea0c823 report # of swarms in status, add forget method 2020-04-29 00:54:05 +01:00
Zlatin Balevsky
71789d96d2 working injection and query kickoff through json-rpc, wip on swarm monitoring 2020-04-28 23:35:29 +01:00
Zlatin Balevsky
7860aa2b1c prevent replay attacks by attaching an uuid to the crawler pings and pongs 2020-04-28 19:46:13 +01:00
Zlatin Balevsky
301c2ec0e2 make I2PSession visible 2020-04-28 19:29:09 +01:00
Zlatin Balevsky
c306864781 add type to the tracker pong and echo the infohash that was queried 2020-04-28 19:18:37 +01:00
Zlatin Balevsky
acee9a5805 customize port and interface of web server 2020-04-28 18:26:36 +01:00
Zlatin Balevsky
d34c4e1990 hello spring boot 2020-04-28 18:11:26 +01:00
Zlatin Balevsky
7be3821e53 will use spring boot for json-rpc endpoints 2020-04-28 17:03:00 +01:00
Zlatin Balevsky
872e932629 logging.properties for the hostcache and a script to count total hosts 2020-04-27 19:43:33 +01:00
Zlatin Balevsky
84c7da1fe0 * More logging
* Include leaseset in crawler pings
* serialize hourly files in a directory, keep history
2020-04-26 20:15:48 +01:00
Zlatin Balevsky
4aed958319 wip on tracker 2020-04-26 19:31:21 +01:00
Zlatin Balevsky
5fc0283da7 revert change to constructor 2020-04-26 19:30:26 +01:00
Zlatin Balevsky
c4d908f571 switch to simple-json-rpc library, add basic rpc server over tcp 2020-04-15 10:26:47 +01:00
Zlatin Balevsky
4d5497c12f setup wizard 2020-04-14 13:18:13 +01:00
Zlatin Balevsky
1d22abfa88 add ability to change the tunnel name 2020-04-14 13:17:47 +01:00
Zlatin Balevsky
7a7ebc9690 skeleton of tracker project 2020-04-13 19:43:48 +01:00
Zlatin Balevsky
16d3a109ca option to disable tracking in web ui 2020-04-12 11:40:21 +01:00
Zlatin Balevsky
7864eebb24 gui option to disable tracking 2020-04-12 11:26:08 +01:00
Zlatin Balevsky
9f7aaec991 include local persona in tracker response 2020-04-12 06:57:39 +01:00
Zlatin Balevsky
1c214ad68a server side of file tracking 2020-04-12 05:56:06 +01:00
Zlatin Balevsky
3436af75bf remove redundant header parsing code 2020-04-10 08:04:00 +01:00
Zlatin Balevsky
9b6a2fd952 write to memmapped file in 8kb increments 2020-04-08 13:25:08 +01:00
Zlatin Balevsky
85ad3109f9 get rid of sNL and darktrion hostcaches, add echelon's 2020-04-02 12:29:44 +01:00
Zlatin Balevsky
293ff76ae9 Move the wait for client manager in the background thread, hopefully fixes #42 2020-03-30 13:28:22 +01:00
Zlatin Balevsky
acb70f72d6 fix determination if a directory is shared 2020-03-30 12:42:16 +01:00
Zlatin Balevsky
62bb4f9e5f actions dropdown on trust lists page 2020-03-29 21:16:54 +01:00
Zlatin Balevsky
03d6fb15f2 Actions menu on TrustUsers page 2020-03-29 20:58:15 +01:00
Zlatin Balevsky
699f3ce1b6 convert the Mark (Dis)Trusted links on search results page to hover menu 2020-03-29 19:15:34 +01:00
Zlatin Balevsky
7f9c8bddb6 fix the color of the hover menu when hovering over a table 2020-03-29 13:18:40 +01:00
Zlatin Balevsky
d111983d68 help text for each page 2020-03-28 23:33:11 +00:00
Zlatin Balevsky
50148e5603 add a Help tooltip section in the header. To be updated with different text for each page 2020-03-28 22:51:07 +00:00
Zlatin Balevsky
1054fe0935 x -> px 2020-03-28 20:19:01 +00:00
Zlatin Balevsky
2de2badb0b tooltips on config options 2020-03-28 19:55:00 +00:00
Zlatin Balevsky
424922f2e3 start adding tooltips to config options 2020-03-28 19:19:35 +00:00
Zlatin Balevsky
adce4b1574 help tooltips on Browse and Feeds pages 2020-03-28 18:39:30 +00:00
Zlatin Balevsky
355535e660 help tooltips on search box and share input box 2020-03-28 16:14:03 +00:00
Zlatin Balevsky
09db68182c add a description of the advanced sharing page, wording and css tweaks 2020-03-28 03:02:48 +00:00
Zlatin Balevsky
1e67139e74 display Never if directory was never synced 2020-03-28 02:52:22 +00:00
Zlatin Balevsky
9837e1e3d7 emit an event on every dir sync so that UI can update timestamps 2020-03-28 02:47:29 +00:00
Zlatin Balevsky
2c52486476 fix manual syncing 2020-03-27 15:47:58 +00:00
Zlatin Balevsky
a88dc17064 add a sync option, fix sorting of table 2020-03-27 15:42:50 +00:00
Zlatin Balevsky
862967bf8e configure panel for directories 2020-03-27 15:14:36 +00:00
Zlatin Balevsky
9f1f718870 show the dirs in a table, no actions yet 2020-03-27 12:54:01 +00:00
Zlatin Balevsky
2fd0a3833f wip on web ui for advanced sharing 2020-03-27 11:10:25 +00:00
Zlatin Balevsky
435170cb1b update the advanced sharing pane 2020-03-26 17:32:42 +00:00
Zlatin Balevsky
1c5fec7e9a Merge branch 'master' of 127.0.0.1:zlatinb/muwire into watched-directories
So that I can get B0B's icon
2020-03-26 15:41:34 +00:00
Zlatin Balevsky
e2a0a37abf ui force sync event 2020-03-26 15:40:53 +00:00
Zlatin Balevsky
a4bee73b8a process changes in configuration 2020-03-26 15:19:09 +00:00
Zlatin Balevsky
056e5800c2 implement directory polling 2020-03-26 14:55:44 +00:00
Zlatin Balevsky
6e0d51c221 first load all watched directories, only then register and scan the auto-watched 2020-03-26 13:10:56 +00:00
Zlatin Balevsky
496e2e7f91 scan autoWatched directories on startup 2020-03-26 12:53:54 +00:00
Zlatin Balevsky
a560b14d91 hook up directory manager with share & unshare events 2020-03-26 12:24:07 +00:00
Zlatin Balevsky
faad6b6b0e query the manager if a directory is watched instead of settings 2020-03-26 12:23:15 +00:00
Zlatin Balevsky
dfc62b943f wip on persisting and loading of watched directory metadata, emit the event to register on autowatch service 2020-03-26 06:21:41 +00:00
Zlatin Balevsky
244ce43794 persistence of WatchedDirectory object 2020-03-26 05:31:39 +00:00
Zlatin Balevsky
f0c8c11094 get rid of UI-side watching of directories on AllFilesLoadedEvent 2020-03-26 05:31:05 +00:00
Zlatin Balevsky
11e320ef53 wip on directory watching 2020-03-26 04:09:18 +00:00
Zlatin Balevsky
aae88e80ee Merge branch 'master' into 'master'
AdvancedSharing.png icons. Creative Commons CC0.

See merge request zlatinb/muwire!38
2020-03-25 23:37:51 +00:00
Bob
bbf97311d1 AdvancedSharing.png icons. Creative Commons CC0. 2020-03-25 23:37:51 +00:00
Zlatin Balevsky
23b6995bf2 start work on advanced watched directories 2020-03-25 22:39:03 +00:00
Zlatin Balevsky
518bdc44e6 update TODO 2020-03-25 20:27:51 +00:00
Zlatin Balevsky
5368dbe181 CSS tweaks from B0B 2020-03-25 15:37:48 +00:00
Zlatin Balevsky
e216678d9a Release 0.6.12 2020-03-25 08:41:42 +00:00
Zlatin Balevsky
4582cfa0b5 router version 0.9.45 2020-03-25 08:38:51 +00:00
Zlatin Balevsky
5ea64ecb90 update webui for directory deletion 2020-03-25 08:10:26 +00:00
Zlatin Balevsky
bd9315954a add a positive tree so that deleting of shared directories can be detected 2020-03-25 08:09:45 +00:00
Zlatin Balevsky
83bdf76c08 cache the file/dir status when creating a tree node so that traversal can work if the file is deleted 2020-03-25 08:08:55 +00:00
Zlatin Balevsky
a2ed308cd0 only fetch the latest revision number on initialization. This fixes the flicker on first refreshStatus() 2020-03-24 13:28:56 +00:00
Zlatin Balevsky
4020df0a77 update expanded tree paths on file events 2020-03-23 20:44:13 +00:00
Zlatin Balevsky
6f4b4a2c2d update plugin file manager on deleted files 2020-03-23 18:31:42 +00:00
Zlatin Balevsky
83cd5e57a2 only refresh feeds table if something changes. This prevents the hover menu from flickering 2020-03-23 08:47:05 +00:00
Zlatin Balevsky
bb69535874 convert feeds table actions to hover menu 2020-03-23 08:28:13 +00:00
Zlatin Balevsky
b7033e3277 display build number in MuStatus 2020-03-23 07:59:18 +00:00
Zlatin Balevsky
4a9cea7d2e special-case the files table with some padding to make the hover menu visible without scrolling, in some cases. 2020-03-23 07:47:41 +00:00
Zlatin Balevsky
2aea965d72 fix hover menu in files table, break small x-display size 2020-03-23 00:29:26 +00:00
Zlatin Balevsky
9a6a1c8371 fix missing image 2020-03-22 23:07:53 +00:00
Zlatin Balevsky
2042bfccb7 get rid of effect where ellipsis overflow doesn't work 2020-03-22 23:03:29 +00:00
Zlatin Balevsky
0d4b0df19d remove Bote's icons 2020-03-22 21:52:35 +00:00
Zlatin Balevsky
f363296ed1 use new icons 2020-03-22 21:32:20 +00:00
Zlatin Balevsky
8b33a5a284 new icons from B0B, licensed under CC0 2020-03-22 21:32:09 +00:00
Zlatin Balevsky
7e70dbda86 link to repo 2020-03-22 18:40:36 +00:00
Zlatin Balevsky
c23db1293f add note about GitLab mirroring 2020-03-22 17:53:00 +00:00
Zlatin Balevsky
54f4874ad6 count the times a file has been hit due to feed update 2020-03-22 10:46:53 +00:00
Zlatin Balevsky
886effa3b6 size columns 2020-03-22 04:01:17 +00:00
Zlatin Balevsky
64d8b98ee2 show/hide comments in certificates 2020-03-22 03:46:58 +00:00
Zlatin Balevsky
2f2f620ae5 certificates table 2020-03-22 03:01:07 +00:00
Zlatin Balevsky
9a74cc5026 downloaders table 2020-03-22 02:51:26 +00:00
Zlatin Balevsky
e3c5fe291d WIP on file details page 2020-03-22 02:13:23 +00:00
Zlatin Balevsky
c77b848d44 correct comparision 2020-03-21 20:59:28 +00:00
Zlatin Balevsky
cf5b5b164d copy hash to clipboard in files table 2020-03-21 11:46:09 +00:00
Zlatin Balevsky
3a340e40c8 copy hash to clipboard functionality in file tree 2020-03-21 11:41:44 +00:00
Zlatin Balevsky
e9eafe9380 Actions menu in table view 2020-03-20 16:56:00 +00:00
Zlatin Balevsky
270a8519b4 Actions link on folders 2020-03-20 16:27:37 +00:00
Zlatin Balevsky
f8bbeb8ac0 switch to a dropdown menu on file tree 2020-03-20 15:59:54 +00:00
Zlatin Balevsky
2a4db868aa collapsible Trust Configuration and About sections 2020-03-20 15:01:45 +00:00
Zlatin Balevsky
59219da1a2 tighten the file tree a bit 2020-03-20 14:20:28 +00:00
Zlatin Balevsky
a5fb824f71 link 'browsing' links to specific matching table entries 2020-03-19 22:38:51 +00:00
Zlatin Balevsky
68bc0bbf30 open the latest search by default 2020-03-19 22:03:40 +00:00
Zlatin Balevsky
c6c1ac1d93 more descriptive errors on Browse and Feed submit actions 2020-03-19 20:47:44 +00:00
Zlatin Balevsky
9646eadcb1 better config input validation, fixes resetting of checkboxes to default values on invalid input 2020-03-19 20:20:41 +00:00
Zlatin Balevsky
db91c9171d add copy-to-clipboard ability for full id 2020-03-19 19:19:47 +00:00
Zlatin Balevsky
e542a50260 status page with some MW internals 2020-03-19 18:12:52 +00:00
Zlatin Balevsky
a9539c5999 add an About Me page which shows the short and full ids 2020-03-19 17:08:35 +00:00
Zlatin Balevsky
d93dbbeb8b spacing for readability 2020-03-19 16:40:19 +00:00
Zlatin Balevsky
45659f0dca change message for Browse and Feeds input box 2020-03-19 16:39:49 +00:00
Zlatin Balevsky
31a607ed7d canonicalize download / incomplete locations before testing 2020-03-19 16:19:43 +00:00
Zlatin Balevsky
7a6538beff fix sorting by feed status 2020-03-17 19:07:33 +00:00
Zlatin Balevsky
509b5c3b99 avoid more conversions to BigDecimal 2020-03-17 16:43:31 +00:00
Zlatin Balevsky
fbb710cfc8 avoid more conversions to BigDecimal 2020-03-17 16:39:41 +00:00
Zlatin Balevsky
244015465a avoid groovy's implicit conversion to BigDecimal 2020-03-17 16:31:29 +00:00
Zlatin Balevsky
7285c12b97 clear cached cardinality on cancelling 2020-03-16 23:18:07 +00:00
Zlatin Balevsky
aac259c0fe cache the cardinality to speed up UI sorting 2020-03-16 22:45:16 +00:00
Zlatin Balevsky
e3f58f8f5a catch general exceptions because otherwise they get lost in the executor thread 2020-03-15 01:21:20 +00:00
Zlatin Balevsky
045859fe04 more items 2020-03-14 22:59:50 +00:00
Zlatin Balevsky
3a8c66e857 more todo items 2020-03-14 22:16:43 +00:00
Zlatin Balevsky
773513b257 more todo items 2020-03-14 22:16:12 +00:00
Zlatin Balevsky
83fe2e9b75 update TODO 2020-03-13 11:52:48 +00:00
Zlatin Balevsky
455b0ea48e config options for feeds 2020-03-13 11:50:45 +00:00
Zlatin Balevsky
f4c96db841 publish/unpublish functionality 2020-03-13 08:56:45 +00:00
Zlatin Balevsky
fca8870283 fix default feed update interval 2020-03-13 07:32:08 +00:00
Zlatin Balevsky
3efb04d7bb missed a B 2020-03-13 07:29:37 +00:00
Zlatin Balevsky
62ce8ffa46 size columns 2020-03-13 07:26:02 +00:00
Zlatin Balevsky
05b70a4573 Individual feed configuration ability 2020-03-13 06:48:58 +00:00
Zlatin Balevsky
b339784826 view comment functionality 2020-03-13 03:54:30 +00:00
Zlatin Balevsky
488f2964ee Display feed presence in search results, various fixes 2020-03-13 03:40:41 +00:00
Zlatin Balevsky
369779ab6a swallow an exception that happens in plugin mostly 2020-03-13 02:42:46 +00:00
Zlatin Balevsky
f5fe3da09d hook up some actions 2020-03-13 02:04:50 +00:00
Zlatin Balevsky
392deee34c wip on feeds page js side 2020-03-13 00:37:58 +00:00
Zlatin Balevsky
7183f15c5c plumbing for /Feeds page 2020-03-12 23:33:04 +00:00
Zlatin Balevsky
ca33535630 POST hook for downloading feed items 2020-03-12 22:46:48 +00:00
Zlatin Balevsky
54abf82a91 wip on server side of feeds for plugin 2020-03-12 22:28:11 +00:00
Zlatin Balevsky
14546737fd release 0.6.11 2020-03-10 22:31:33 +00:00
Zlatin Balevsky
0f069f2fc9 Merge branch 'file-feeds' 2020-03-10 22:28:38 +00:00
Zlatin Balevsky
9a44603d2f prevent duplicate feed subscriptions 2020-03-10 21:30:27 +00:00
Zlatin Balevsky
38a027c308 context menu on the feed items table 2020-03-10 21:15:59 +00:00
Zlatin Balevsky
2ba81ccc84 context menu for feeds table 2020-03-10 20:50:00 +00:00
Zlatin Balevsky
0408349c07 size columns 2020-03-10 20:04:28 +00:00
Zlatin Balevsky
95cb7f3214 auto-download feed items functionality 2020-03-10 19:48:36 +00:00
Zlatin Balevsky
69810d7203 fix variable name 2020-03-10 19:35:39 +00:00
Zlatin Balevsky
f202fa34f3 auto-publish shared files functionality 2020-03-10 19:12:49 +00:00
Zlatin Balevsky
c082e25c81 individual feed configuration panel 2020-03-10 18:48:20 +00:00
Zlatin Balevsky
2bb07ff7b5 do not trim feed items if setting is negative 2020-03-10 17:53:17 +00:00
Zlatin Balevsky
ff952890bc populate new feeds from defaults 2020-03-10 17:51:14 +00:00
Zlatin Balevsky
fc393619d8 options for feeds 2020-03-10 17:47:08 +00:00
Zlatin Balevsky
2882c73876 enable button when switching to chat window 2020-03-10 17:05:44 +00:00
Zlatin Balevsky
cbb1de046b fetch certificates functionality 2020-03-10 16:53:28 +00:00
Zlatin Balevsky
a272a45928 persist the right number of feed items 2020-03-10 16:41:31 +00:00
Zlatin Balevsky
3133581363 view comment functionality 2020-03-10 16:35:02 +00:00
Zlatin Balevsky
c3d0dce281 store last update attempt and do not retry active feeds 2020-03-10 16:05:32 +00:00
Zlatin Balevsky
8f710e68c2 download feed item action 2020-03-10 15:06:42 +00:00
Zlatin Balevsky
15430d6c03 manual update and unsubscribe actions 2020-03-10 13:51:07 +00:00
Zlatin Balevsky
166b71f128 fix NPE when logging is enabled 2020-03-10 13:47:31 +00:00
Zlatin Balevsky
d724986ec6 proper method name 2020-03-10 13:34:33 +00:00
Zlatin Balevsky
198c5b5538 fix json parsing 2020-03-10 13:06:47 +00:00
Zlatin Balevsky
96d71ed08f fix method name 2020-03-10 13:06:36 +00:00
Zlatin Balevsky
bb7385688c it always points to the innermost closure 2020-03-10 12:55:09 +00:00
Zlatin Balevsky
e70bec3a51 hook up feed subscription 2020-03-10 12:44:25 +00:00
Zlatin Balevsky
ed04c40420 return an empty set if no items are found 2020-03-10 12:43:49 +00:00
Zlatin Balevsky
e9f00c2995 subscribe button in search tab 2020-03-10 12:16:18 +00:00
Zlatin Balevsky
fd75d8229b fix feed checkbox for local results 2020-03-10 12:15:52 +00:00
Zlatin Balevsky
0ff9ca8572 wip on feed items table 2020-03-10 11:50:55 +00:00
Zlatin Balevsky
a07f01b641 utility method to check if an infohash is shared 2020-03-10 11:50:09 +00:00
Zlatin Balevsky
b9333913c6 hook up feeds table to feed items table 2020-03-10 10:47:05 +00:00
Zlatin Balevsky
fcb5c573f9 wip on feeds table 2020-03-10 10:39:18 +00:00
Zlatin Balevsky
1610766e01 wip on feeds table 2020-03-10 07:33:29 +00:00
Zlatin Balevsky
e2a9db8056 add an IDLE status to feeds for display purposes 2020-03-10 07:32:45 +00:00
Zlatin Balevsky
a0cb214e2b placeholder feeds panel 2020-03-10 06:22:40 +00:00
Zlatin Balevsky
f2bf921d4c parse feed flag in results 2020-03-10 06:06:57 +00:00
Zlatin Balevsky
aa0fcfb7de fix capitalization in event name 2020-03-10 05:50:54 +00:00
Zlatin Balevsky
48cfce71a8 emit event on publishing 2020-03-10 05:47:42 +00:00
Zlatin Balevsky
8798ea38e8 button for publishing, column in the shared files table 2020-03-10 01:39:55 +00:00
Zlatin Balevsky
17cd60afe3 deleting of feeds 2020-03-10 00:58:43 +00:00
Zlatin Balevsky
c10c1118e8 feed client 2020-03-09 19:28:42 +00:00
Zlatin Balevsky
28425e93dc persist only as many items as configured to keep 2020-03-09 18:53:43 +00:00
Zlatin Balevsky
032338bb48 Persist feed metadata and items on successful fetch. Register feed manager for various events 2020-03-09 18:31:10 +00:00
Zlatin Balevsky
12e56b1c9a events associated with updating a feed 2020-03-09 17:37:17 +00:00
Zlatin Balevsky
cc8801c48b do not NPE if hashing fails 2020-03-09 16:05:57 +00:00
Zlatin Balevsky
57c75978b6 wip on feed manager deserialization 2020-03-08 20:19:37 +00:00
Zlatin Balevsky
bfe198e1a6 represenation of a feed 2020-03-08 19:38:48 +00:00
Zlatin Balevsky
8e274f940e Feed item representation and serialization 2020-03-08 19:30:04 +00:00
Zlatin Balevsky
9f3942c1c7 settings to disable or not advertise file feed 2020-03-08 17:15:00 +00:00
Zlatin Balevsky
d60d57ee43 wip on server side feed handling 2020-03-08 17:04:11 +00:00
Zlatin Balevsky
8e3a433afb persist shared file on publish/unpublish 2020-03-08 16:06:28 +00:00
Zlatin Balevsky
49cf56fabb UI Publish & Unpublish events 2020-03-08 16:01:50 +00:00
Zlatin Balevsky
2b6565d107 unpublish method 2020-03-08 16:01:23 +00:00
Zlatin Balevsky
366a2ef841 published flag and timestamp in shared files 2020-03-08 15:46:36 +00:00
Zlatin Balevsky
bcd24e56ac TODO updates for plugin 2020-03-07 15:48:20 +00:00
Zlatin Balevsky
c7d1f0c23c Connect to i2p router after creating the Core object, should help with plugin init issues #39 2020-02-23 18:29:09 +00:00
Zlatin Balevsky
853b9f67fc Release 0.6.10 2020-02-23 15:42:03 +00:00
Zlatin Balevsky
a505a2449a persist SharedFile on change of comments #35 2020-02-18 02:14:32 +00:00
Zlatin Balevsky
c11d81c6c3 Release 0.6.9 2020-02-16 16:33:33 +00:00
Zlatin Balevsky
ee5e90c4ab ignore events from old persister service, prevents duplicate entries during migration #35 2020-02-14 18:20:39 +00:00
Zlatin Balevsky
64d2a87d26 more occurrences of SharedFile::getInfoHash #35 2020-02-14 17:53:09 +00:00
Zlatin Balevsky
f0304dbe7d fix copy-hash-to-clipboard #35 2020-02-14 16:14:36 +00:00
Zlatin Balevsky
bdad8d9309 make extended signatures mandatory 2020-02-14 15:34:21 +00:00
Zlatin Balevsky
8c110bbae5 more occurrences of SharedFile::getInfoHash #35 2020-02-14 15:24:39 +00:00
Zlatin Balevsky
2cc1e384bc more occurrences of SharedFile::getInfoHash #35 2020-02-14 15:20:01 +00:00
Zlatin Balevsky
9337d1b74d chase down references to missing infoHash #35 2020-02-14 01:48:02 +00:00
Zlatin Balevsky
16ed5dd346 chase down some usages of deprecated getInfoHash method #35 2020-02-14 01:32:38 +00:00
Zlatin Balevsky
7b55fc9ed8 working uploads #35 2020-02-14 01:15:10 +00:00
Zlatin Balevsky
d5c8050572 wip on separate hashlist storage #35 2020-02-14 00:37:07 +00:00
Zlatin Balevsky
83546d68d2 Merge pull request #37 from LoveIsGrief/change-persister
Introduce persister that uses a directory structure
2020-01-25 14:36:41 +00:00
a891c83518 Only persist downloaded files if sharing thereof is enabled
Otherwise we might inadvertently share downloads
2020-01-25 15:25:48 +01:00
aa56cc23c0 Cache base 64 path hash
Can't do it in constructor without an ugly try/catch
 therefore this is done on demand
2020-01-25 15:20:38 +01:00
a2b37ef567 Persist downloaded files 2020-01-25 15:06:12 +01:00
4bc04ae631 Revert "Reduce log levels in Connection"
This reverts commit dcd233b7
2020-01-25 15:01:21 +01:00
56da9a16b0 Set FileLoadedEvent::source in the subclass
Setting it in the super class means we don't set the right value for every case
2020-01-25 15:00:48 +01:00
2935ee1a1d Remove unnecessary executor
It was doing nothing but starting and stopping
2020-01-25 14:49:59 +01:00
855183397b Remove TODO
There's already an issue open https://github.com/zlatinb/muwire/issues/35
2020-01-22 21:35:54 +01:00
e27704c1af Make sure migration from PersisterService works
this.getClass() and this.class kept resolving to Class.
Using a string is much simpler

mkdirs() is also necessary because the directory structure doesn't exist
 when persistFile is called the first time
2020-01-22 20:59:05 +01:00
5c18b4a141 Add more logs PersisterFolderService 2020-01-22 15:12:22 +01:00
dcd233b7ad Reduce log levels in Connection
Too verbose
2020-01-22 15:12:01 +01:00
7cee8a28ba FileLoadedEvent should include class when coming from old persister
Otherwise the new PersisterFolderService won't migrate
2020-01-22 15:07:00 +01:00
7446fc949a Remove UIPersistFilesEvent
Hashing is done per file now and those are triggered by individual events
2020-01-22 13:00:55 +01:00
598ab90f63 Clear up the event path when starting up the old and new persisters
The new persister won't load anything until the old one has finished
2020-01-22 12:36:34 +01:00
043028c296 Introduce PersisterFolderService to replace PersisterService
An attempt at automatically migrate from PersisterService was made, but the events aren't triggered in the right order.
We need to make sure that we don't trigger the "AllFilesLoadedEvent" before the migration is done
2020-01-21 23:34:33 +01:00
cd1757fac3 Use Java 11
Java9 isn't available on Ubuntu anymore, which would make development harder
2020-01-19 21:46:47 +01:00
9d4b365e63 Log the time it take to persist files and hashes 2020-01-19 21:43:03 +01:00
Zlatin Balevsky
b12d57e30a fix bracket 2020-01-14 20:27:21 +00:00
Zlatin Balevsky
f33d1b6db3 move the docker documentation to the wiki 2020-01-14 20:26:47 +00:00
Zlatin Balevsky
9e451460da change to my repo 2020-01-14 19:24:32 +00:00
Zlatin Balevsky
ffa52c129a Merge pull request #33 from LoveIsGrief/32-docker-image
Docker image
2020-01-14 19:21:35 +00:00
b779fb75a0 docker: Remove incompletes warning from README
#32 - Docker image
2020-01-14 20:11:34 +01:00
fbe6b53278 docker: Make sure build directories are ignored
#32 - Docker image
2020-01-14 19:20:11 +01:00
b2bd95788d docker: Try minimizing size using add-pkg and del-pkg
As described in https://github.com/jlesage/docker-baseimage-gui#addingremoving-packages

#32 - Docker image
2020-01-14 19:19:47 +01:00
83d4a2624b docker: Add bisentenialwrug/muwire to README
To be replaced later by @zlatinb's repo

#32 - Docker image
2020-01-14 18:47:28 +01:00
03e20e21aa Remove unnecessary quotes from properties files
There doesn't seem to be a special treatment of them
 in properties files

#32 - Docker image
2020-01-14 18:42:51 +01:00
8a08955675 Remove quotes from i2cp.tcp.port setting
For some reason it really doesn't like that and
 subsequently can't connect to the host

#32 - Docker image
2020-01-14 17:52:52 +01:00
4ec54ebe54 docker: Quote the IP-address in i2p.properties
#32 - Docker image
2020-01-14 17:36:45 +01:00
758af6f48e docker: Make sure APP_HOME is editable by the user
Otherwise MuWire won't be able to write into the home

#32 - Docker image
2020-01-14 17:14:41 +01:00
a7bdd47fcd docker: Add more files to ignore
Helps with build speed on the local machine

#32 - Docker image
2020-01-14 17:00:07 +01:00
f7caa77a18 docker: Include the MuWire icon for the webview
#32 - Docker image
2020-01-14 16:59:39 +01:00
7641f64536 docker: Add default MuWire.properties without nickname
#32 - Docker image
2020-01-14 16:59:13 +01:00
02baaace48 Merge branch 'master' of https://github.com/zlatinb/muwire into 32-docker-image 2020-01-14 16:48:12 +01:00
Zlatin Balevsky
d90067ff39 prompt for nickname even if MuWire.properties exists so that docker can ship a MuWire.properties #32 2020-01-14 14:17:18 +00:00
c910a215f5 Add the /incompletes docker volume
It won't be used by default though

#32 - Docker image
2020-01-14 13:07:37 +01:00
65e073b1b9 Use defaults for the i2p.properties
This will help writing custom properties
 as not everthing will have to be specified in them

#32 - Docker image
2020-01-14 12:29:05 +01:00
489a7518c3 Attempt to reduce size a bit more
- Ignore the cruft when building
 - Remove the correct temporary directory

#32 - Docker image
2020-01-14 01:09:39 +01:00
3733e48bbd Force set the port
The default isn't used in the code.
That should be fixed, but I'm too tired right now

#32 - Docker image
2020-01-14 00:29:33 +01:00
c3723a1348 Try to minimize image size
#32 - Docker image
2020-01-14 00:15:01 +01:00
0e0f52bc77 Retry: Set a home directory for the "app" user
Apparently it's done differently in the parent image,
 so we just overwrite it.

Hopefully now the app user will have a home

#32 - Docker image
2020-01-13 23:38:04 +01:00
60b9e990cf Set a home directory for the "app" user
#32 - Docker image
2020-01-13 21:34:50 +01:00
28ad0ae30f Add --name to docker run command
#32 - Docker image
2020-01-13 20:29:28 +01:00
9142de85cd Correct the link to the i2cp_config.png
#32 - Docker image
2020-01-13 19:51:20 +01:00
4eb31c11e3 Write README and cleanup inconsistencies
#32 - Docker image
2020-01-13 18:42:30 +01:00
e8afe358a5 First Dockerfile with GUI that starts
It doesn't continue yet as it seems to be waiting for a connection
 to I2P... or something else 🤷#32 - Docker image
2020-01-13 17:07:56 +01:00
Zlatin Balevsky
3db4317fc1 more items 2020-01-01 11:26:59 +00:00
Zlatin Balevsky
5ad2b28527 more items 2020-01-01 09:19:46 +00:00
Zlatin Balevsky
3036765f81 translations 2019-12-27 12:33:22 +01:00
Zlatin Balevsky
8f9b1e5a8b supress exceptions if client is stopped 2019-12-24 17:05:36 +00:00
Zlatin Balevsky
e6d59a2438 stop host persister on shutdown 2019-12-24 05:53:02 +00:00
Zlatin Balevsky
32609b4779 get rid of dependency on groovy-all 2019-12-23 21:16:24 +00:00
Zlatin Balevsky
74ac4cfecf remove size filter which was left over from grails experiments 2019-12-23 20:54:16 +00:00
Zlatin Balevsky
69173c4156 update TODO 2019-12-23 20:09:07 +00:00
Zlatin Balevsky
6283287bee prevent empty input from sharing the I2P working dir 2019-12-22 22:17:57 +00:00
Zlatin Balevsky
8e3f76f68c move plugin build instructions to the wiki 2019-12-22 16:51:40 +00:00
Zlatin Balevsky
574294fdc6 update readme 2019-12-22 16:14:42 +00:00
Zlatin Balevsky
8bd41546cd proper uploader equality check 2019-12-21 23:15:39 +00:00
Zlatin Balevsky
ba5425c958 extra check for stopped cache client 2019-12-21 15:56:09 +00:00
Zlatin Balevsky
22580f002c separate update url from main plugin url 2019-12-21 13:46:42 +00:00
Zlatin Balevsky
5c773cec80 more css changes from zzz 2019-12-20 16:19:00 +00:00
Zlatin Balevsky
7df00e6709 delete duplicate translation 2019-12-19 20:14:24 +00:00
Zlatin Balevsky
5c05bd2562 If a result is for a shared file, display it as Downloaded 2019-12-19 20:12:02 +00:00
Zlatin Balevsky
9df1d043e4 do not initialize the update client if running as a plugin 2019-12-19 18:35:44 +00:00
Zlatin Balevsky
6ea1a15641 do not initialize the update client if running as a plugin 2019-12-19 18:30:07 +00:00
Zlatin Balevsky
c0575facec add Downloaded string 2019-12-19 13:12:01 +00:00
Zlatin Balevsky
09168844e0 css tweaks from zzz 2019-12-19 12:53:51 +00:00
Zlatin Balevsky
e21d482393 Release 0.6.8 2019-12-19 06:04:18 +00:00
Zlatin Balevsky
f5fc3e40c2 add incomplete translations 2019-12-18 11:26:06 +00:00
Zlatin Balevsky
796a0138fa change Trust Users title for clearer translation; add headers for the two tables 2019-12-18 11:25:36 +00:00
Zlatin Balevsky
505b4ddb06 comment clairfying verb/noun for Pause and Query 2019-12-18 10:57:56 +00:00
Zlatin Balevsky
a35216ff56 some translations 2019-12-18 08:23:56 +00:00
Zlatin Balevsky
fba92fe9b9 missing strings 2019-12-17 20:21:01 +00:00
Zlatin Balevsky
1cc511b0ae initialize root node in the init function so that it can be translated 2019-12-17 20:19:38 +00:00
Zlatin Balevsky
fa94c8ebfa persist files after unsharing and pause to let event propagate 2019-12-17 17:24:50 +00:00
Zlatin Balevsky
88b68a3c5c move controls to the right of the tree nodes 2019-12-17 17:14:30 +00:00
Zlatin Balevsky
b3e0d2ee7a display tree in order it arrives from servlet 2019-12-17 15:38:24 +00:00
Zlatin Balevsky
ce293cbda8 sort file tree servlet side 2019-12-17 15:28:49 +00:00
Zlatin Balevsky
3abc617e9f css changes from zzz 2019-12-17 14:25:42 +00:00
Zlatin Balevsky
67ee634f20 separate link text for a single certificate 2019-12-17 13:51:34 +00:00
Zlatin Balevsky
503d54927f fix directories with special characters in them in file tree view 2019-12-17 13:22:31 +00:00
Zlatin Balevsky
5788329e1a add ability to set css class to sortable tables. Make certificates table certificates class 2019-12-17 12:52:33 +00:00
Zlatin Balevsky
f0ffc68122 more strings 2019-12-17 12:43:49 +00:00
Zlatin Balevsky
3d710cebe5 no <pre> if there is no reason 2019-12-17 12:39:35 +00:00
Zlatin Balevsky
7d67573c92 rename 2019-12-17 11:40:47 +00:00
Zlatin Balevsky
3acc676448 table to div.right 2019-12-17 11:38:40 +00:00
Zlatin Balevsky
2bf03b6b84 use div.centercomment for trust comments 2019-12-17 10:30:07 +00:00
Zlatin Balevsky
b8ba6df4d5 link to BrowseHost page 2019-12-17 10:21:48 +00:00
Zlatin Balevsky
9fa7fa07b4 whitespace between links 2019-12-17 10:19:47 +00:00
Zlatin Balevsky
1c7253ea0a more _t 2019-12-17 10:16:17 +00:00
Zlatin Balevsky
d947ad2997 more table->span.right 2019-12-17 10:15:00 +00:00
Zlatin Balevsky
dd0bd6f5f8 use pre.comment for trust reasons 2019-12-17 07:09:37 +00:00
Zlatin Balevsky
f05b6d0b40 word-wrap pre.comment pt.2 2019-12-17 07:05:24 +00:00
Zlatin Balevsky
906c69a482 word-wrap pre.comment 2019-12-17 07:04:13 +00:00
Zlatin Balevsky
5375b7aec0 add class to <pre> blocks 2019-12-16 22:25:10 +00:00
Zlatin Balevsky
ea5da2431a remove ;, thanks to jshint and zzz 2019-12-16 21:35:57 +00:00
Zlatin Balevsky
14b3a9ac9e add more js strings 2019-12-16 21:14:44 +00:00
Zlatin Balevsky
40bbef4583 remove unneeded files 2019-12-16 20:51:22 +00:00
Zlatin Balevsky
f811653247 remove unnecessary strings from Util._x 2019-12-16 20:03:30 +00:00
Zlatin Balevsky
f321000071 sort tables by default 2019-12-16 19:26:24 +00:00
Zlatin Balevsky
6eb85283cd sort tables by default 2019-12-16 19:19:09 +00:00
Zlatin Balevsky
2973759cd9 sort tables by default 2019-12-16 19:16:06 +00:00
Zlatin Balevsky
fe945a9941 sort tables by default 2019-12-16 19:12:48 +00:00
Zlatin Balevsky
5f7e949310 clear tables when closing current search 2019-12-16 19:07:01 +00:00
Zlatin Balevsky
11edb2cb3c convert tables to div.right 2019-12-16 18:48:28 +00:00
Zlatin Balevsky
ff1f801155 convert tables to div.right 2019-12-16 18:43:31 +00:00
Zlatin Balevsky
0a98083c64 convert tables to div.right 2019-12-16 18:39:53 +00:00
Zlatin Balevsky
75b2852f6e convert table to div.right for links 2019-12-16 18:30:39 +00:00
Zlatin Balevsky
5774cdee94 move comment box to the center 2019-12-16 18:11:30 +00:00
Zlatin Balevsky
2b0f4e52ca ellipsis on overflow, input alignment fixes from zzz 2019-12-16 17:51:10 +00:00
Zlatin Balevsky
1d20dc917b make the comment box 50% of available space in table view 2019-12-16 17:28:38 +00:00
Zlatin Balevsky
63e3b3710c bottom table id based on view type 2019-12-16 16:22:51 +00:00
Zlatin Balevsky
0878b89082 different ids for the top table based on view type 2019-12-16 16:05:52 +00:00
Zlatin Balevsky
fecf0ecae8 put trusted and distrusted tables on top of one another 2019-12-16 14:56:54 +00:00
Zlatin Balevsky
fec8d4ef9f Done->Downloaded Pieces 2019-12-16 14:51:22 +00:00
Zlatin Balevsky
067ac8582a Lists->Subscriptions 2019-12-16 14:47:52 +00:00
Zlatin Balevsky
31cac25a23 remove fetch link, make the file name a link 2019-12-16 14:43:38 +00:00
Zlatin Balevsky
6bcc44e01e align comment textarea to the right 2019-12-16 14:32:44 +00:00
Zlatin Balevsky
31652b34d7 column sizing, tags, other changes from zzz 2019-12-16 14:24:27 +00:00
Zlatin Balevsky
41a15fc7d5 clear Speed and ETA columns for finished downloads 2019-12-16 14:22:12 +00:00
Zlatin Balevsky
da3d7d7a50 herf->href 2019-12-16 13:43:16 +00:00
Zlatin Balevsky
3a079d9f21 expand root by default, expand until there is more than one child 2019-12-16 13:16:39 +00:00
Zlatin Balevsky
ba0c85fe07 do not show unshare/comment/certify links for directories that are not shared 2019-12-16 13:01:56 +00:00
Zlatin Balevsky
ecb2283886 comment out help section 2019-12-16 09:18:27 +00:00
Zlatin Balevsky
cf9a18cee5 style init page 2019-12-16 05:11:20 +00:00
Zlatin Balevsky
982a93a04b get rid of static headers in trust list view 2019-12-16 04:58:40 +00:00
Zlatin Balevsky
58137d11d1 space out trust links in search view 2019-12-16 01:49:59 +00:00
Zlatin Balevsky
d87bec927d space out links in trust users view 2019-12-16 01:32:00 +00:00
Zlatin Balevsky
dc8dd96495 space out links in trust lists view 2019-12-16 01:26:43 +00:00
Zlatin Balevsky
add9fb6feb revision is an integer 2019-12-16 01:15:17 +00:00
Zlatin Balevsky
c500e95ab6 register for correct event 2019-12-16 01:05:31 +00:00
Zlatin Balevsky
477c3285d2 do not display empty files table 2019-12-15 23:36:01 +00:00
Zlatin Balevsky
1f5b112bfe fix distrusting 2019-12-15 23:31:07 +00:00
Zlatin Balevsky
b0d09853e4 pause after publishing all trust events 2019-12-15 23:28:00 +00:00
Zlatin Balevsky
b96d997037 do not show empty tables 2019-12-15 23:22:40 +00:00
Zlatin Balevsky
a631ec1e14 do not display empty or stale tables in trust list view 2019-12-15 23:14:24 +00:00
Zlatin Balevsky
62a06bc891 do not show empty or stale tables after closing browses 2019-12-15 22:46:42 +00:00
Zlatin Balevsky
3534b23194 correct string 2019-12-15 22:20:29 +00:00
Zlatin Balevsky
c561ae9140 get possible sources from browse host 2019-12-15 22:19:26 +00:00
Zlatin Balevsky
5926457eb5 send redirect after manual browse input 2019-12-15 22:14:09 +00:00
Zlatin Balevsky
37c93e352b Make Downloading a link 2019-12-15 19:39:07 +00:00
Zlatin Balevsky
be8fecda39 Change Downloading to a link 2019-12-15 19:29:47 +00:00
Zlatin Balevsky
7ec6257ac0 implement closing browses 2019-12-15 19:02:51 +00:00
Zlatin Balevsky
c4ea58c330 add Sources column to group-by-file view 2019-12-15 18:47:52 +00:00
Zlatin Balevsky
a482fe5c93 turn browse link into browsing link 2019-12-15 18:40:19 +00:00
Zlatin Balevsky
2ee84848c4 make Browsing a link to the browse page 2019-12-15 16:55:11 +00:00
Zlatin Balevsky
e29d7f6872 do not display active searches table if it's empty 2019-12-15 16:47:07 +00:00
Zlatin Balevsky
5ded824ef2 display tables side by side 2019-12-15 16:42:16 +00:00
Zlatin Balevsky
c607560cb8 space between trust action links 2019-12-15 16:42:03 +00:00
Zlatin Balevsky
8b341bb125 tell the user the directories will be created 2019-12-15 16:17:56 +00:00
Zlatin Balevsky
6bc5a9075b rewrite welcome jsp to a servlet, add sanity check of inputs 2019-12-15 16:16:11 +00:00
Zlatin Balevsky
6b1d2bc5ce sanitize the i2p tunnel settings 2019-12-15 15:36:03 +00:00
Zlatin Balevsky
0cbbaf6a63 localized error messages 2019-12-15 15:33:15 +00:00
Zlatin Balevsky
3363b99675 sanitize integer and file input 2019-12-15 15:13:44 +00:00
Zlatin Balevsky
4ab4785539 display errors on invalid config input 2019-12-15 15:06:18 +00:00
Zlatin Balevsky
e595fa97e8 change some strings for easier translation 2019-12-15 14:53:35 +00:00
Zlatin Balevsky
65a7088463 can squeeze a few more characters 2019-12-15 13:39:20 +00:00
Zlatin Balevsky
2d5bd653c1 do not display number of results if it's zero 2019-12-15 13:35:16 +00:00
Zlatin Balevsky
a864343c05 get rid of senders and results columns, use ellipsis for very long search strings 2019-12-15 13:29:03 +00:00
Zlatin Balevsky
696b348469 link to translation instructions 2019-12-15 12:41:08 +00:00
Zlatin Balevsky
b08333c5ea download details view 2019-12-15 11:34:04 +00:00
Zlatin Balevsky
0cf368c1af uploads icon 2019-12-15 10:00:26 +00:00
Zlatin Balevsky
62ab957892 clear finished uploads link 2019-12-15 08:57:14 +00:00
Zlatin Balevsky
2b9e722165 clear finished downloads link 2019-12-15 08:29:55 +00:00
Zlatin Balevsky
8cf4b23762 ability to stop a search 2019-12-15 07:58:16 +00:00
Zlatin Balevsky
1285c68521 uploads page 2019-12-15 03:26:55 +00:00
Zlatin Balevsky
daa9e0bafc servlet side of uploader page 2019-12-15 02:45:14 +00:00
Zlatin Balevsky
8efd9c2c88 headers for the sections 2019-12-14 21:42:42 +00:00
Zlatin Balevsky
918549f164 hook up to translations 2019-12-14 21:18:43 +00:00
Zlatin Balevsky
e30a4666cb wip on configuration page 2019-12-14 20:49:54 +00:00
Zlatin Balevsky
26167abc08 fix connections count on settings page 2019-12-14 20:34:57 +00:00
Zlatin Balevsky
93f7c67f37 wip on configuration page 2019-12-14 20:27:13 +00:00
Zlatin Balevsky
f9a0a5e08a wip on settings page 2019-12-14 19:30:11 +00:00
Zlatin Balevsky
d8ae275df2 remove debug println 2019-12-14 19:25:29 +00:00
Zlatin Balevsky
fce879be5d hook up configuration page, under construction 2019-12-14 19:02:15 +00:00
Zlatin Balevsky
0b58e22714 start work on configuration page 2019-12-14 17:47:52 +00:00
Zlatin Balevsky
dd230c4dfc automatic resume of failed downloads 2019-12-14 14:16:29 +00:00
Zlatin Balevsky
fba0b001c0 pause/resume/retry links 2019-12-14 14:02:25 +00:00
Zlatin Balevsky
6978c7b992 switch certify and comment links 2019-12-14 10:17:19 +00:00
Zlatin Balevsky
7355e76e1b add fetch link to shared file table view 2019-12-14 09:09:03 +00:00
Zlatin Balevsky
5147cf21a0 hook up the downloaded content servlet 2019-12-13 23:57:51 +00:00
Zlatin Balevsky
e8dd7d710d pause to give a chance to the event to propagate 2019-12-13 13:10:11 +00:00
Zlatin Balevsky
fc9114eaa5 use Collator for comparing strings 2019-12-13 12:21:11 +00:00
Zlatin Balevsky
20b7104c41 wrong formatting for ETA and progress 2019-12-13 11:37:20 +00:00
Zlatin Balevsky
570616951a sortable certificate table, add extra parameter to Table object 2019-12-13 11:27:29 +00:00
Zlatin Balevsky
e075bfac55 auto-refresh the files table if revision changed 2019-12-13 08:28:00 +00:00
Zlatin Balevsky
b6411a555c hide links on root node 2019-12-13 08:27:35 +00:00
Zlatin Balevsky
d395475727 sortable shared files table 2019-12-13 08:04:19 +00:00
Zlatin Balevsky
8ae0a16b8a sortable trust list tables 2019-12-13 02:23:42 +00:00
Zlatin Balevsky
38fcdfc97a sorting of trust subscriptions table 2019-12-13 00:04:10 +00:00
Zlatin Balevsky
a0fb07cf99 Link helper class 2019-12-12 22:31:42 +00:00
Zlatin Balevsky
3747f9a5d5 sortable tables on trust users page 2019-12-12 21:43:54 +00:00
Zlatin Balevsky
3a738f8f62 sorting downloads table 2019-12-12 17:26:56 +00:00
Zlatin Balevsky
ca56363438 server-side of downloads sorting support 2019-12-12 13:19:25 +00:00
Zlatin Balevsky
e06cb05e2a fix glitch in sorting when new results arrive 2019-12-12 01:16:42 +00:00
Zlatin Balevsky
8ab2dd7900 sort all tables on search page 2019-12-12 00:44:49 +00:00
Zlatin Balevsky
26116d313a avoid an exception 2019-12-11 22:34:16 +00:00
Zlatin Balevsky
738f177d6c update certificate hooks to new architecture 2019-12-11 22:28:54 +00:00
Zlatin Balevsky
62c4579bbd preserve expanded comment state during updates 2019-12-11 21:47:03 +00:00
Zlatin Balevsky
18d84685ec wip on rewriting search page for sortable tables. Some features do not yet work 2019-12-11 20:45:12 +00:00
Zlatin Balevsky
c05a7a021c table styling and caret on the file tree from zzz 2019-12-11 14:40:49 +00:00
Zlatin Balevsky
a9935eba62 wip on restructuring search xhr 2019-12-11 14:38:42 +00:00
Zlatin Balevsky
e3d80bf809 remove fonts 2019-12-11 14:38:05 +00:00
Zlatin Balevsky
a59a1d3f30 sort active searches 2019-12-11 10:55:51 +00:00
Zlatin Balevsky
37ed75a3e8 sort table of active browses 2019-12-11 07:42:42 +00:00
Zlatin Balevsky
cd4b600ba2 working sorting of the browse host results 2019-12-10 23:56:39 +00:00
Zlatin Balevsky
fcd6dbcfbd wip on sortable tables 2019-12-10 23:24:11 +00:00
Zlatin Balevsky
f3ab15bd74 certificates in browse host page 2019-12-10 21:33:36 +00:00
Zlatin Balevsky
cddaad0f29 move certificate code in a separate file 2019-12-10 20:33:38 +00:00
Zlatin Balevsky
ecb597e0a0 preserve shown/hidden certificate comment state 2019-12-10 17:20:10 +00:00
Zlatin Balevsky
ec2a934f73 wip on show/hide certificate comments 2019-12-10 16:54:21 +00:00
Zlatin Balevsky
e1d630fdee wip on showing comments in certificates 2019-12-10 16:13:59 +00:00
Zlatin Balevsky
5807672503 proper ignore pattern 2019-12-10 16:04:42 +00:00
Zlatin Balevsky
2fadb314d3 css and layout changes from zzz 2019-12-10 15:35:54 +00:00
Zlatin Balevsky
ec5c15ff64 importing of certificates 2019-12-10 15:34:51 +00:00
Zlatin Balevsky
c169a7613f wip on importing certificates 2019-12-10 14:59:30 +00:00
Zlatin Balevsky
0f762968ae show fetched certificates in a table 2019-12-10 14:32:35 +00:00
Zlatin Balevsky
8e6517e7d8 content serving servlet, thx to zzz 2019-12-10 12:50:38 +00:00
Zlatin Balevsky
6946bff7f9 hook up periodic certificate update function 2019-12-10 12:47:56 +00:00
Zlatin Balevsky
37dcedb99b remove .orig 2019-12-10 12:26:00 +00:00
Zlatin Balevsky
afb92b0e4e translation updates, images, thanks zzz 2019-12-10 12:24:56 +00:00
Zlatin Balevsky
7c39dff34f wip on rendering certs table 2019-12-10 12:21:20 +00:00
Zlatin Balevsky
e41c122d2d show/hide links for certificates in group-by-sender view 2019-12-10 08:53:27 +00:00
Zlatin Balevsky
117c5eaf67 wip on showing certificates 2019-12-10 08:12:45 +00:00
Zlatin Balevsky
10fab2b47f certification in view by table 2019-12-09 23:21:24 +00:00
Zlatin Balevsky
3f71df3d29 certification support in tree view 2019-12-09 23:00:40 +00:00
Zlatin Balevsky
813e211200 send certified status to the UI 2019-12-09 17:22:30 +00:00
Zlatin Balevsky
1adb130fba ability to certify directories 2019-12-09 17:04:11 +00:00
Zlatin Balevsky
f69d4027db ability to certify files 2019-12-09 16:19:45 +00:00
Zlatin Balevsky
e0d006ec69 translate more strings 2019-12-09 15:39:18 +00:00
Zlatin Balevsky
81d8af57ed Translation infrastructure, thanks to zzz 2019-12-09 15:17:13 +00:00
Zlatin Balevsky
42c48a8e37 certificate backend 2019-12-09 14:26:39 +00:00
Zlatin Balevsky
3b1349b643 ability to force refresh lists 2019-12-09 10:06:56 +00:00
Zlatin Balevsky
0250ea329c fix some nevers and nulls 2019-12-09 09:54:10 +00:00
Zlatin Balevsky
b722c64ad8 comments in trust actions 2019-12-09 09:51:02 +00:00
Zlatin Balevsky
effa3b567e persist subscription lists 2019-12-09 09:20:07 +00:00
Zlatin Balevsky
64f198d599 fix live updating on trust action 2019-12-09 09:06:45 +00:00
Zlatin Balevsky
131b2defbb trust actions 2019-12-09 09:02:41 +00:00
Zlatin Balevsky
df5aab67ac hook up lists page 2019-12-09 08:22:42 +00:00
Zlatin Balevsky
fdc030904c wip on trust lists 2019-12-09 08:19:55 +00:00
Zlatin Balevsky
2a4fae8de4 wip on trust lists 2019-12-09 07:47:47 +00:00
Zlatin Balevsky
662b065116 wip on trust subscriptions 2019-12-09 07:37:04 +00:00
Zlatin Balevsky
300938fa44 wip on trust lists page 2019-12-09 07:04:35 +00:00
Zlatin Balevsky
086e27876d shut down more services explicitly 2019-12-09 05:38:41 +00:00
Zlatin Balevsky
247c62bfb4 hook up trust page 2019-12-09 05:23:40 +00:00
Zlatin Balevsky
a13315c324 describe the textbox 2019-12-09 04:38:14 +00:00
Zlatin Balevsky
65f40ef23a trust/neutral/distrust links 2019-12-09 04:32:35 +00:00
Zlatin Balevsky
96a611ff78 xhr fixes 2019-12-09 00:09:55 +00:00
Zlatin Balevsky
0f4119b74f submit trust functionality 2019-12-08 23:47:49 +00:00
Zlatin Balevsky
6847329093 trust buttons, submitting doesn't work yet 2019-12-08 23:25:06 +00:00
Zlatin Balevsky
9d2bcf70c7 display trust status in results 2019-12-08 22:30:38 +00:00
Zlatin Balevsky
aa33709f04 fix display of query 2019-12-08 21:08:49 +00:00
Zlatin Balevsky
eacaedaf3d automatically update active browse if the revision has changed 2019-12-08 21:05:29 +00:00
Zlatin Balevsky
f9c428cfcd update comment indexing 2019-12-08 20:48:15 +00:00
Zlatin Balevsky
aa1ede46d2 Redesign the XHR architecture by splitting the requests. Separate requests are issued for the status table, then a request is triggered when a user clicks on a search. 2019-12-08 20:41:54 +00:00
Zlatin Balevsky
3c43244631 wip on trust users view 2019-12-08 18:11:12 +00:00
Zlatin Balevsky
b468a6f19b update web.xml 2019-12-08 17:39:04 +00:00
Zlatin Balevsky
cfdc750ac0 post method 2019-12-08 17:36:30 +00:00
Zlatin Balevsky
6f8b006227 post method 2019-12-08 17:35:53 +00:00
Zlatin Balevsky
3f4bf986f3 remove stray orig file, update gitignore 2019-12-08 16:55:47 +00:00
Zlatin Balevsky
bef1033e12 plugin must compile with java 8 2019-12-08 16:41:45 +00:00
Zlatin Balevsky
13061d60a4 add local status to trust list xml 2019-12-08 15:13:30 +00:00
Zlatin Balevsky
5c6917a7e6 wip on trust views 2019-12-08 14:57:21 +00:00
Zlatin Balevsky
2ec15cfbbc remove jsp from urls, thanks to zzz 2019-12-08 13:45:26 +00:00
Zlatin Balevsky
1325a8dc65 resolve conflicts, fix quotes, thanks zzz 2019-12-08 13:03:21 +00:00
Zlatin Balevsky
b5d8fcf25b missed tx/config 2019-12-08 12:51:25 +00:00
Zlatin Balevsky
c22ff0678e mark script executable 2019-12-08 12:44:11 +00:00
Zlatin Balevsky
07051b813a translation infrastructure, thanks to zzz 2019-12-08 12:41:45 +00:00
Zlatin Balevsky
5c22af6576 add link to sidebar 2019-12-08 12:33:46 +00:00
Zlatin Balevsky
c3e1298ea3 browse links from search results 2019-12-08 12:31:02 +00:00
Zlatin Balevsky
949b616fdd fix xml, placeholders for browse links 2019-12-08 11:43:21 +00:00
Zlatin Balevsky
2b1d95e2ef pass sender's b64 and browse status from endpoint 2019-12-08 11:35:30 +00:00
Zlatin Balevsky
3d967da110 move browses table to top of page 2019-12-08 11:17:13 +00:00
Zlatin Balevsky
66fde32b64 comments support in browse host 2019-12-08 10:44:18 +00:00
Zlatin Balevsky
80a89a5ac0 download functionality 2019-12-08 09:38:34 +00:00
Zlatin Balevsky
c59e038c2a wip on browse host 2019-12-08 07:48:59 +00:00
Zlatin Balevsky
844bd8fd6e comments in shared files are encoded 2019-12-08 00:26:17 +00:00
Zlatin Balevsky
7d9ebb5b0b server side of browse host 2019-12-07 23:35:16 +00:00
Zlatin Balevsky
7fd7444dbf unshare directories to make sure files do not end up in the negative tree 2019-12-07 20:48:45 +00:00
Zlatin Balevsky
13af6cce22 stray println 2019-12-07 20:37:24 +00:00
Zlatin Balevsky
458dbec5fd display a refresh link if the table needs updating 2019-12-07 20:23:22 +00:00
Zlatin Balevsky
2137d6d30b comments in table view 2019-12-07 19:34:20 +00:00
Zlatin Balevsky
b28de0c119 add unshare link 2019-12-07 19:16:48 +00:00
Zlatin Balevsky
0fd4695b7c wip on table view 2019-12-07 18:53:32 +00:00
Zlatin Balevsky
74dddc4da4 wip on table view 2019-12-07 18:07:00 +00:00
Zlatin Balevsky
8bff987d30 implement adding comments to files 2019-12-07 17:19:13 +00:00
Zlatin Balevsky
de8684bafc fix multiline comments by not adding <br> tags in the servlet and using <pre> tag in the browser 2019-12-07 15:15:45 +00:00
Zlatin Balevsky
905f559aa9 proper <br /> tags 2019-12-07 14:23:50 +00:00
Zlatin Balevsky
c7f57c0b15 update sidebar, add sidebar to shared files, <br> in comments, thanks to zzz 2019-12-07 13:36:33 +00:00
Zlatin Balevsky
0f0f46f425 rename Files.jsp 2019-12-07 13:10:17 +00:00
Zlatin Balevsky
d6a3c8b24c re-add zzz's changes to FilesServlet 2019-12-07 13:04:31 +00:00
Zlatin Balevsky
8c661ca1ae unescape file names, this fixes unsharing of files with html characters 2019-12-07 12:59:43 +00:00
Zlatin Balevsky
f579c8754f more sidebar work thanks to zzz 2019-12-07 12:18:01 +00:00
Zlatin Balevsky
5c17536683 unsharing of directories 2019-12-07 12:14:49 +00:00
Zlatin Balevsky
8536353c26 unshare individual files 2019-12-07 11:20:56 +00:00
Zlatin Balevsky
84375c0201 fix typo and collapsing 2019-12-07 10:16:28 +00:00
Zlatin Balevsky
9c0c187a18 base64 encode the div ids to account for special characters in names 2019-12-07 10:13:17 +00:00
Zlatin Balevsky
8ae735e5c0 get tree structure to display, no collapsing yet 2019-12-07 09:31:56 +00:00
Zlatin Balevsky
8224dda3fd sidebar, servlet and styling improvements from zzz 2019-12-06 18:26:44 +00:00
Zlatin Balevsky
c852d7474e base64 encoding function 2019-12-06 17:25:18 +00:00
Zlatin Balevsky
71685d2052 clear hashing span when not hashing 2019-12-06 17:20:41 +00:00
Zlatin Balevsky
e57e513ca1 wip on sharing files 2019-12-06 17:02:40 +00:00
Zlatin Balevsky
aa4fb14540 wip on sharing and unsharing of files server-side 2019-12-06 15:58:02 +00:00
Zlatin Balevsky
5f74abc944 hook up files servlet and file manager 2019-12-06 13:44:15 +00:00
Zlatin Balevsky
c4135389a4 wip on shared files display page 2019-12-06 13:16:32 +00:00
Zlatin Balevsky
a6e0834722 add a single-level list traversal of the tree 2019-12-06 12:47:08 +00:00
Zlatin Balevsky
bc628b9c00 layout and escaping, thanks zzz 2019-12-06 11:02:53 +00:00
Zlatin Balevsky
9b2669a8b8 update to new api 2019-12-06 10:51:35 +00:00
Zlatin Balevsky
a0f70f7677 add traversal of the file tree 2019-12-06 10:51:07 +00:00
Zlatin Balevsky
23b2c912e2 genericize file tree 2019-12-06 10:08:27 +00:00
Zlatin Balevsky
ecfd4180c0 update test 2019-12-06 10:07:32 +00:00
Zlatin Balevsky
42489ba6b2 add support for showing/hiding comments 2019-12-06 01:34:35 +00:00
Zlatin Balevsky
61207f893d cancelled downloads do not count as downloading 2019-12-05 22:11:39 +00:00
Zlatin Balevsky
4e32359718 refresh downloads on cancel 2019-12-05 22:08:35 +00:00
Zlatin Balevsky
8d4af48eca cancel downloads via ajax too 2019-12-05 21:50:06 +00:00
Zlatin Balevsky
693f63534d download via ajax for group-by-file view as well 2019-12-05 21:37:17 +00:00
Zlatin Balevsky
b057e848d0 use ajax for starting downloads 2019-12-05 21:18:29 +00:00
Zlatin Balevsky
0114224d1f various html fixes, version the js, thanks zzz 2019-12-05 13:40:37 +00:00
Zlatin Balevsky
beab2be713 null checks on unitialized core, html escaping, move scriptst to <head>, thanks zzz 2019-12-05 12:19:10 +00:00
Zlatin Balevsky
edd4a1ff4b move download js into a separate file 2019-12-05 11:31:19 +00:00
Zlatin Balevsky
85814b7544 move search javascript into a separate file 2019-12-05 11:24:31 +00:00
Zlatin Balevsky
d46fbd66f0 move connection count into a separate js file, thanks zzz 2019-12-05 10:38:24 +00:00
Zlatin Balevsky
06bd9c80e8 move connection count refreshing into the header 2019-12-04 23:34:21 +00:00
Zlatin Balevsky
54b8628435 convert download servlet to xml and page to ajax 2019-12-04 23:15:50 +00:00
Zlatin Balevsky
b37a548771 Some refactoring thanks to zzz plus some wip on migrating downloads page to an xml-based servlet 2019-12-04 22:12:34 +00:00
Zlatin Balevsky
a14689acff set debug parameter to javac task 2019-12-04 19:37:58 +00:00
Zlatin Balevsky
a73bc956bf set the plugin icon 2019-12-04 19:23:43 +00:00
Zlatin Balevsky
d595a768b8 put images in images/ 2019-12-04 19:15:07 +00:00
Zlatin Balevsky
0fd6421fae bundle images in war 2019-12-04 19:03:05 +00:00
Zlatin Balevsky
6e9a36461a get mwClient from application scope 2019-12-04 19:02:51 +00:00
Zlatin Balevsky
d115f54812 copy icon from i2p source tree 2019-12-04 19:00:48 +00:00
Zlatin Balevsky
f627f661f2 add bote's css and images 2019-12-04 18:57:29 +00:00
Zlatin Balevsky
0e7ec3dfb3 move css to its own file 2019-12-04 11:27:08 +00:00
Zlatin Balevsky
0188bd34a9 add download buttons 2019-12-04 10:55:18 +00:00
Zlatin Balevsky
a2becfa6e2 implement grouping by file 2019-12-04 07:45:51 +00:00
Zlatin Balevsky
ea32af9b91 align tables 2019-12-04 05:17:16 +00:00
Zlatin Balevsky
c74c26e4c6 construct a tree structure to match XML received from servlet; populate tables from it 2019-12-04 02:48:28 +00:00
Zlatin Balevsky
382e21225b display list of senders 2019-12-03 23:59:51 +00:00
Zlatin Balevsky
81c406cbf6 refresh search results and connection count with ajax 2019-12-03 23:00:39 +00:00
Zlatin Balevsky
d9eb46d65c update min java version for plugin 2019-12-03 18:17:55 +00:00
Zlatin Balevsky
dadfed20f1 proper plugin build number 2019-12-03 16:25:38 +00:00
Zlatin Balevsky
6dad29a772 instructions for building the plugin 2019-12-03 16:05:26 +00:00
Zlatin Balevsky
884253fe29 easier running instructions 2019-12-03 12:07:39 +00:00
Zlatin Balevsky
a5eccbdc2b sleep a bit to give event chance to propagate 2019-12-03 06:04:14 +00:00
Zlatin Balevsky
d0318e3e83 display direct and possible sources. Pass possible sources to core 2019-12-03 06:00:56 +00:00
Zlatin Balevsky
d1c308f118 access mwClient from the application context 2019-12-02 14:12:23 +00:00
Zlatin Balevsky
3871170e44 show number of connections 2019-12-01 02:51:00 +00:00
Zlatin Balevsky
95dd5c4a7c downloads display, starting and stopping 2019-11-30 23:34:59 +00:00
Zlatin Balevsky
0bff4b55a5 format the results as table, add download buttons 2019-11-30 21:55:41 +00:00
Zlatin Balevsky
a2022415c2 add display of search results grouped by sender 2019-11-30 19:54:50 +00:00
Zlatin Balevsky
2b8bd8144f basic display of how many senders and results have arrived 2019-11-30 19:09:55 +00:00
Zlatin Balevsky
7bf520ac8c skeleton of search manager 2019-11-30 18:16:25 +00:00
Zlatin Balevsky
ad8983e889 wait for client manager to load before connecting 2019-11-30 17:32:02 +00:00
Zlatin Balevsky
d0b62af32e change url to point to servlet 2019-11-30 17:31:43 +00:00
Zlatin Balevsky
bc8e259974 update readme for web ui and version 2019-11-30 15:43:04 +00:00
Zlatin Balevsky
ff0a4661fd offload start to a thread, display wait page while the tunnel is opening 2019-11-30 14:56:04 +00:00
Zlatin Balevsky
9151df6816 kill i2p session on shutdown 2019-11-30 14:27:40 +00:00
Zlatin Balevsky
9c0878408b redirect to I2P log system 2019-11-30 14:07:58 +00:00
Zlatin Balevsky
61baa53076 _logManager cannot be set on RouterContexts (i.e. when running as plugin) 2019-11-30 13:26:20 +00:00
Zlatin Balevsky
b2841ee9ab fix redirects 2019-11-30 13:20:48 +00:00
Zlatin Balevsky
9edea17fb7 switch to using a servlet instead of bean 2019-11-30 13:07:47 +00:00
Zlatin Balevsky
ac17618f0c fix incomplete location setting 2019-11-30 10:50:58 +00:00
Zlatin Balevsky
e94ed4eafa init nickname and download locations 2019-11-30 10:22:19 +00:00
Zlatin Balevsky
8c33a5e62f hook up the mw client app with the jsp 2019-11-30 08:28:22 +00:00
Zlatin Balevsky
f9f1017e5b initialize core if nickname etc. is provided 2019-11-30 06:50:44 +00:00
Zlatin Balevsky
5d2d831b9e pass MW home to the client 2019-11-30 06:21:32 +00:00
Zlatin Balevsky
562d9a0f4a move i2p core dependency one level down, exclude core dependencies from plugin 2019-11-30 03:44:57 +00:00
Zlatin Balevsky
b981f9199b pass version to MW client app and get it to run 2019-11-30 03:16:10 +00:00
Zlatin Balevsky
efef0f3734 include a servlet as well as pre-compiled jsps 2019-11-29 18:00:32 +00:00
Zlatin Balevsky
cd0b860210 skeleton of client app 2019-11-29 17:02:15 +00:00
Zlatin Balevsky
9cb0655cfa get a buildable i2p plugin for mw 2019-11-29 16:49:44 +00:00
Zlatin Balevsky
3775f28af7 add jsp-based webui 2019-11-29 16:40:02 +00:00
Zlatin Balevsky
c33b824871 remove grails webui 2019-11-29 16:37:57 +00:00
Zlatin Balevsky
cf396b739e ability to chat from browse window 2019-11-29 03:41:59 +00:00
Zlatin Balevsky
631963f43c browse host by full nickname 2019-11-29 02:26:34 +00:00
Zlatin Balevsky
06cedb4f41 add buttons to copy short and full nickname to clipboard 2019-11-29 02:19:47 +00:00
Zlatin Balevsky
7a0c60a164 exit if user refuses to choose a nickname 2019-11-28 16:39:38 +00:00
Zlatin Balevsky
4c038ad932 set the geoip.dir property to load geoip 2019-11-27 16:00:56 +00:00
Zlatin Balevsky
f6dd38685a display country and strictness in I2P status 2019-11-27 15:38:51 +00:00
Zlatin Balevsky
2eab0f0567 make the chat monitor a separate frame so that it does not dissappear when MW is minimized 2019-11-26 18:55:20 +00:00
Zlatin Balevsky
8fedc0c605 Release 0.6.7 2019-11-26 09:55:10 +00:00
Zlatin Balevsky
5831b06842 chat room monitor tool 2019-11-26 09:43:53 +00:00
Zlatin Balevsky
57d5b5f386 do not send /LEAVE messages when leaving private chats 2019-11-26 05:31:22 +00:00
Zlatin Balevsky
c0f6b1ed73 do not rejoin console or private chats, fix NPE when disconnecting with private window open 2019-11-26 05:28:24 +00:00
Zlatin Balevsky
f4cd1c30cd Do not remove connection on distrust so that disconnect can be processed correctly 2019-11-26 05:00:55 +00:00
Zlatin Balevsky
6b717f560e file hashing 2019-11-23 20:28:29 +02:00
Zlatin Balevsky
e8a3db76bb wip on architecture doc 2019-11-23 20:15:45 +02:00
Zlatin Balevsky
5acf7f2953 add clear button 2019-11-20 18:47:14 +00:00
Zlatin Balevsky
e760e9f600 add option to select chat server welcome message 2019-11-19 09:02:42 +00:00
Zlatin Balevsky
8a47972b10 proper group name for manual rejoin 2019-11-19 03:37:35 +00:00
Zlatin Balevsky
f8e0c9524e make text input fields longer 2019-11-18 10:03:01 +00:00
Zlatin Balevsky
919aeaaed5 preserve chat box contents across openings of the chat window 2019-11-18 09:55:11 +00:00
Zlatin Balevsky
9474512cbd default to /SAY command 2019-11-18 09:44:05 +00:00
Zlatin Balevsky
8c50f6c6d6 proper model update 2019-11-18 09:33:25 +00:00
Zlatin Balevsky
01ee7209c8 clear members list on server disconnect 2019-11-18 09:31:25 +00:00
Zlatin Balevsky
ff7c4eae28 stop local server if tab is closed 2019-11-18 09:06:29 +00:00
Zlatin Balevsky
9373d58b53 limit the time a header read can take 2019-11-18 09:00:11 +00:00
Zlatin Balevsky
df71ade69f formatting 2019-11-18 08:46:28 +00:00
Zlatin Balevsky
2ed29be072 selected component can be null when closing a tab 2019-11-17 22:41:31 +00:00
Zlatin Balevsky
a398ab7d4b indentation 2019-11-17 18:38:53 +00:00
Zlatin Balevsky
a0125e7195 document chat protocol 2019-11-17 18:37:30 +00:00
Zlatin Balevsky
cb9a1cfff6 remove stray import 2019-11-17 15:05:29 +00:00
Zlatin Balevsky
445e73521a more links 2019-11-17 13:35:40 +00:00
Zlatin Balevsky
7bdc922d2c add links 2019-11-17 13:32:11 +00:00
Zlatin Balevsky
0c40c8f269 Release 0.6.6 2019-11-16 17:12:21 +00:00
Zlatin Balevsky
681ddb99a2 add ability to say something in a room 2019-11-16 17:10:21 +00:00
Zlatin Balevsky
5dff319746 prevent starting chat server more than once. Implement chat console in the cli 2019-11-16 16:40:07 +00:00
Zlatin Balevsky
57c4a00ac6 do not call disconnect() unless connecting/ed. This prevents trying to connect after closing the tab 2019-11-16 15:25:23 +00:00
Zlatin Balevsky
286a0a8678 redo locking around chat client state 2019-11-16 14:41:47 +00:00
Zlatin Balevsky
17eff7d77f toString 2019-11-16 00:25:50 +00:00
Zlatin Balevsky
2e22369ce0 set flag before submitting to threadpool 2019-11-15 23:12:17 +00:00
Zlatin Balevsky
15c59b440f flush outputstream on chat connect failures or rejections 2019-11-15 21:52:35 +00:00
Zlatin Balevsky
8fb015acbf sort browse host as well 2019-11-15 14:59:52 +00:00
Zlatin Balevsky
f7b11c90fd sort results from search 2019-11-15 14:49:15 +00:00
Zlatin Balevsky
df93a35062 wip on sorting 2019-11-15 14:23:00 +00:00
Zlatin Balevsky
ecb19a8412 do not update badge if the room is a console 2019-11-15 13:50:13 +00:00
Zlatin Balevsky
b1e5b40800 update minimum JDK to 9, version bump 2019-11-15 13:18:34 +00:00
Zlatin Balevsky
daa3a293f2 new messages update taskbar badge 2019-11-15 13:15:27 +00:00
Zlatin Balevsky
907264fc67 enable/disable chat and browse from trusted pane buttons 2019-11-15 02:15:56 +00:00
Zlatin Balevsky
c6becb93dc enable/disable say field when not connected 2019-11-15 01:57:48 +00:00
Zlatin Balevsky
2954bd2f1a smart scrolling the chat text area 2019-11-15 01:44:54 +00:00
Zlatin Balevsky
35322d2c15 fetch group by name,add sequential download checkbox to browse view 2019-11-14 12:40:40 +00:00
Zlatin Balevsky
9f6a7eb368 make sure browse window works from every parent group 2019-11-14 11:04:38 +00:00
Zlatin Balevsky
fec81808e5 Release 0.6.5 2019-11-14 05:15:26 +00:00
Zlatin Balevsky
4db890484d do not rejoin console 2019-11-14 04:49:13 +00:00
Zlatin Balevsky
dfd5e06889 add browse ability from chat room view 2019-11-14 04:40:15 +00:00
Zlatin Balevsky
71da8e14da name button earlier 2019-11-14 04:25:45 +00:00
Zlatin Balevsky
7dc37e3e0d change button to connect/disconnect 2019-11-14 04:20:57 +00:00
Zlatin Balevsky
3de058a078 send rejoins to the console pt2 2019-11-14 03:59:01 +00:00
Zlatin Balevsky
4d70c7adce send rejoins to the console 2019-11-14 03:58:36 +00:00
Zlatin Balevsky
5b41106476 start and stop poller thread on events 2019-11-14 03:45:21 +00:00
Zlatin Balevsky
6240b22e66 fix reconnecting to server, start with fresh member list upon rejoin 2019-11-14 03:13:01 +00:00
Zlatin Balevsky
0e26f5afd7 rejoin rooms on reconnect 2019-11-14 02:40:22 +00:00
Zlatin Balevsky
114bc06dbb If the user explicitly shares a file, remove it form the negative tree. #26 2019-11-13 22:00:10 +00:00
Zlatin Balevsky
5fa2f2753c Release 0.6.4 2019-11-13 20:06:53 +00:00
Zlatin Balevsky
cacdd2a7a9 add browse and chat buttons to trusted panel 2019-11-13 19:40:28 +00:00
Zlatin Balevsky
d56f7c6184 add right-click menu to trusted table 2019-11-13 19:33:34 +00:00
Zlatin Balevsky
f7f4513109 better help and welcome message 2019-11-13 17:50:50 +00:00
Zlatin Balevsky
dd15d893ba Call for help for Web UI 2019-11-13 17:26:14 +00:00
Zlatin Balevsky
bf5ab9c82e ) 2019-11-13 14:10:26 +00:00
Zlatin Balevsky
edd5a29b10 make private chat room ids unique across servers 2019-11-13 14:09:09 +00:00
Zlatin Balevsky
38eb89f2f7 prepend server name to room id in order to make ids unique across server connections 2019-11-13 13:44:22 +00:00
Zlatin Balevsky
73f1d64428 indentation of text field 2019-11-13 12:24:21 +00:00
Zlatin Balevsky
bc1cae2d75 enable sharing of directories from button 2019-11-13 12:03:23 +00:00
Zlatin Balevsky
a0ab07a7c0 show browse status for local results correctly 2019-11-13 11:58:55 +00:00
Zlatin Balevsky
f875c379ce Release 0.6.3 2019-11-12 17:22:38 +00:00
Zlatin Balevsky
0ce9784ccf add right-click menu on the members table 2019-11-12 17:08:38 +00:00
Zlatin Balevsky
be82136e32 limit scrollback 2019-11-12 16:30:55 +00:00
Zlatin Balevsky
7d25bb9364 tidy up views 2019-11-12 16:06:31 +00:00
Zlatin Balevsky
c6e98db9d4 initialize result sender properly 2019-11-12 15:50:58 +00:00
Zlatin Balevsky
35a26e2a47 advertise chat ability in search results 2019-11-12 15:47:38 +00:00
Zlatin Balevsky
beef4af329 ui for chat options 2019-11-12 15:31:20 +00:00
Zlatin Balevsky
cec3c1bc0f disconnect on close tab 2019-11-12 14:21:47 +00:00
Zlatin Balevsky
289b958784 disconnect functionality 2019-11-12 14:19:57 +00:00
Zlatin Balevsky
e9c554d717 proper group name pt3 2019-11-12 13:53:33 +00:00
Zlatin Balevsky
1875fcddb2 proper room name pt2 2019-11-12 13:33:53 +00:00
Zlatin Balevsky
bee6154fa9 set more room tab names correctly 2019-11-12 13:26:07 +00:00
Zlatin Balevsky
1f9b171021 wip on private messages 2019-11-12 13:16:36 +00:00
Zlatin Balevsky
59c03be35e suffix for group ids 2019-11-12 12:33:18 +00:00
Zlatin Balevsky
621af96bdf wip on private chat 2019-11-12 12:20:49 +00:00
Zlatin Balevsky
bcb7016202 add myself to the room member list when joining, fix /SAY 2019-11-12 11:40:28 +00:00
Zlatin Balevsky
b1b2bcaef8 show disconnects 2019-11-12 11:34:23 +00:00
Zlatin Balevsky
eec007e83b update status only if it matches host 2019-11-12 11:11:42 +00:00
Zlatin Balevsky
3d36351a6b fetch the list of current room members when joining 2019-11-12 10:55:21 +00:00
Zlatin Balevsky
d57d2ccb71 print help message on joining 2019-11-12 04:18:35 +00:00
Zlatin Balevsky
d91f15ee54 dispatch joins to the target room 2019-11-12 03:53:38 +00:00
Zlatin Balevsky
6bc61c920d start outgoing connection 2019-11-12 00:11:26 +00:00
Zlatin Balevsky
146ed53e12 connection code 2019-11-11 23:52:34 +00:00
Zlatin Balevsky
8ebae1600b fix up chat room view 2019-11-11 23:46:43 +00:00
Zlatin Balevsky
18d19ca75e wip on joining and leaving rooms 2019-11-11 23:32:23 +00:00
Zlatin Balevsky
29e499fe9d hook up core and backend 2019-11-11 22:42:55 +00:00
Zlatin Balevsky
3db167bade send periodic pings 2019-11-11 17:54:33 +00:00
Zlatin Balevsky
bfe0ab7867 wip on hooking UI with core 2019-11-11 17:48:42 +00:00
Zlatin Balevsky
1fbb1e7932 add chat pane and associated components 2019-11-11 16:35:15 +00:00
Zlatin Balevsky
0632336cd1 add ability to start and stop chat server from UI 2019-11-11 15:16:23 +00:00
Zlatin Balevsky
aa221cd6dc server-side handling of disconnects and trust events 2019-11-11 14:54:10 +00:00
Zlatin Balevsky
29b5c55328 client-side disconnect handling 2019-11-11 13:31:00 +00:00
Zlatin Balevsky
5e7f3587df shutdown chat components 2019-11-11 13:26:25 +00:00
Zlatin Balevsky
8afd387ca6 hook up chat components with core 2019-11-11 13:21:16 +00:00
Zlatin Balevsky
5d16963d1c process join/leave/say server-side 2019-11-11 12:19:32 +00:00
Zlatin Balevsky
6080c8b308 chat client and server 2019-11-11 10:43:52 +00:00
Zlatin Balevsky
915deb1dee update readme for new shadow jar name 2019-11-11 09:13:56 +00:00
Zlatin Balevsky
8afca3dc7f Merge pull request #24 from theosotr/fix
Bugfix: Update plugin version to fix bug about shadow jar
2019-11-11 09:04:42 +00:00
Thodoris Sotiropoulos
f072d0343c Update plugin version to fix bug about shadow jar 2019-11-11 10:52:37 +02:00
Zlatin Balevsky
a549ad3d8d wip on chat 2019-11-11 04:36:43 +00:00
Zlatin Balevsky
b6f5ec7d22 wip on chat 2019-11-10 20:34:24 +00:00
Zlatin Balevsky
761bf0a177 Release 0.6.2 2019-11-10 18:31:30 +00:00
Zlatin Balevsky
bd873211c0 wip on file preview 2019-11-10 14:50:19 +00:00
Zlatin Balevsky
036971cfe5 wip on file preview 2019-11-10 13:59:01 +00:00
Zlatin Balevsky
a2637570b1 Release 0.6.1 2019-11-10 06:23:28 +00:00
Zlatin Balevsky
6012adbeab fix unsharing of files with comments 2019-11-10 06:04:57 +00:00
Zlatin Balevsky
8f6b6b0caa update test for new json format 2019-11-10 05:20:09 +00:00
Zlatin Balevsky
8f3b5aea8d store lowercases in search index 2019-11-10 05:14:31 +00:00
Zlatin Balevsky
ee098ace8e update readme 2019-11-09 20:11:03 +00:00
Zlatin Balevsky
5d8401e4bf avoid NPE, pending further investigation 2019-11-09 20:10:21 +00:00
Zlatin Balevsky
fbf9add82a Release 0.6.0 2019-11-09 19:27:36 +00:00
Zlatin Balevsky
7379263fef extended signature in cli 2019-11-09 18:34:34 +00:00
Zlatin Balevsky
7d50843754 make signed queries mandatory 2019-11-09 17:03:38 +00:00
Zlatin Balevsky
f4a2864942 add extended signature in queries to prevent replay attacks 2019-11-09 16:39:16 +00:00
Zlatin Balevsky
afaadf65a4 only set selected row if the table contains that many rows. That fixes an AIOOBE 2019-11-09 15:14:14 +00:00
Zlatin Balevsky
7bd422d6b4 another instance of unexplained npe 2019-11-09 12:36:59 +00:00
Zlatin Balevsky
3f47274f61 add option to open containing folder 2019-11-09 11:28:12 +00:00
Zlatin Balevsky
419e9a0ce6 prevent npe when..? unclear when this happens 2019-11-09 11:01:55 +00:00
Zlatin Balevsky
ac1068a681 fix show comment/certificate buttons in group-by-file mode 2019-11-09 10:53:38 +00:00
Zlatin Balevsky
549457e36f close output stream silently 2019-11-08 21:46:44 +00:00
Zlatin Balevsky
14d6d10546 Release 0.5.10 2019-11-08 21:11:20 +00:00
Zlatin Balevsky
878e397aa0 preserve selections on update 2019-11-08 21:04:58 +00:00
Zlatin Balevsky
27831b488b add getter and use it; account for the case where a file has no certificates 2019-11-08 19:20:06 +00:00
Zlatin Balevsky
449f46c62b take list updating out of loop 2019-11-08 18:40:59 +00:00
Zlatin Balevsky
5703b85386 workaround? 2019-11-08 18:36:23 +00:00
Zlatin Balevsky
76d8d847bd wip on grouping by file 2019-11-08 18:15:54 +00:00
Zlatin Balevsky
db84d8e5bf wip on grouping by file 2019-11-08 17:33:41 +00:00
Zlatin Balevsky
cc9b384907 wip on grouping by file 2019-11-08 16:09:05 +00:00
Zlatin Balevsky
72960c24a8 implement trust reason in cli 2019-11-08 14:41:10 +00:00
Zlatin Balevsky
71298e5e73 proper rendering of date on subscriptions table 2019-11-08 08:31:00 +00:00
Zlatin Balevsky
11bc672544 say never if timestamp is 0 2019-11-08 08:30:44 +00:00
Zlatin Balevsky
2f6cd311a0 say never if timestamp is 0 2019-11-08 08:30:29 +00:00
Zlatin Balevsky
0448750491 lowercase for consistency 2019-11-08 08:18:33 +00:00
Zlatin Balevsky
800dd1cbba proper date sorting 2019-11-08 08:17:34 +00:00
Zlatin Balevsky
f95e9450f3 OutputStream.write 2019-11-08 07:47:11 +00:00
Zlatin Balevsky
d842e3f2f2 update for new object 2019-11-08 07:42:33 +00:00
Zlatin Balevsky
2017b53a43 pass comments on trust list subscriptions 2019-11-08 07:37:51 +00:00
Zlatin Balevsky
6e2b3f4f33 prompt for reason from review trust list view 2019-11-08 07:12:17 +00:00
Zlatin Balevsky
dbb305139b update for new type 2019-11-08 06:53:22 +00:00
Zlatin Balevsky
0801bfec08 add optinal reason for trusting/distrusting 2019-11-08 06:46:03 +00:00
Zlatin Balevsky
00a8d100fe show certificate comment form file details view 2019-11-08 04:51:37 +00:00
Zlatin Balevsky
e94b7cb0d4 prevent NPE when browsed from an older host 2019-11-08 04:02:11 +00:00
Zlatin Balevsky
b0357f2ecd update readme 2019-11-08 02:50:42 +00:00
Zlatin Balevsky
62e72a7ce0 Release 0.5.9 2019-11-07 20:01:15 +00:00
Zlatin Balevsky
26fa757b13 shared file details panel 2019-11-07 19:15:35 +00:00
Zlatin Balevsky
3b2e1cf98c make sure the persona reported by the browser matches 2019-11-07 18:35:34 +00:00
Zlatin Balevsky
5de8a51e47 account for unknown searchers 2019-11-07 18:34:11 +00:00
Zlatin Balevsky
f5c07f13c0 core side of searchers tracking 2019-11-07 18:31:20 +00:00
Zlatin Balevsky
c7b0ae34af associate persona with a search event, add skeleton for shared file panel 2019-11-07 17:43:37 +00:00
Zlatin Balevsky
cad5301827 rewrite Persona and Name in java 2019-11-07 17:41:32 +00:00
Zlatin Balevsky
c998011873 add right-click and show-in-library option for uploads 2019-11-07 05:02:53 +00:00
Zlatin Balevsky
5802ba7734 show trust status of certificate issuers in cli as well 2019-11-06 18:19:45 +00:00
Zlatin Balevsky
b3f775f59a show trust status in certificates view 2019-11-06 18:13:07 +00:00
Zlatin Balevsky
739dbc7a24 fix serialization of older certificates 2019-11-06 18:09:50 +00:00
Zlatin Balevsky
af99dee4a3 wip on view certificate comments in cli 2019-11-06 17:08:48 +00:00
Zlatin Balevsky
07a6c63357 wip on view certificate comments in cli 2019-11-06 16:58:22 +00:00
Zlatin Balevsky
c4096568f5 initialize group properly 2019-11-06 16:01:43 +00:00
Zlatin Balevsky
30dda180eb Add support for comments in certificates, bump certificate version 2019-11-06 15:32:39 +00:00
Zlatin Balevsky
83ea1bed3e add timestamp to the filename of the certificate 2019-11-06 14:05:17 +00:00
Zlatin Balevsky
9181829e4a split by newlines 2019-11-06 13:59:14 +00:00
Zlatin Balevsky
94678bad3c Release 0.5.8 2019-11-06 05:46:52 +00:00
Zlatin Balevsky
e7072803e9 Merge branch 'master' of https://github.com/zlatinb/muwire 2019-11-06 05:42:14 +00:00
Zlatin Balevsky
e9f7a51e16 Always share update files; disable forced update check on startup 2019-11-06 05:41:58 +00:00
Zlatin Balevsky
916fad7d9b more fake padding 2019-11-05 15:54:16 +00:00
Zlatin Balevsky
9feb891c51 support phrases in search 2019-11-05 15:52:23 +00:00
Zlatin Balevsky
b865376d24 more tests 2019-11-05 14:41:27 +00:00
Zlatin Balevsky
8dcba7535c modify indexing and search logic to account for phrases 2019-11-05 13:24:22 +00:00
Zlatin Balevsky
7e881f1fe6 close() output streams on rejection, update test 2019-11-05 12:57:52 +00:00
Zlatin Balevsky
a9aad7d9db test with deleted files 2019-11-05 12:57:16 +00:00
Zlatin Balevsky
e736b42751 view certificates in cli 2019-11-05 05:51:43 +00:00
Zlatin Balevsky
acda64aea7 Add certify button to cli. Make watched directory handling match that of gui 2019-11-05 04:41:25 +00:00
Zlatin Balevsky
d82dc4ce90 Certificates viewer 2019-11-04 21:34:21 +00:00
Zlatin Balevsky
f2ff90795d show a warning when user tries to certify 2019-11-04 20:49:46 +00:00
Zlatin Balevsky
49f51a9f5f view certificates from browse host 2019-11-04 19:39:04 +00:00
Zlatin Balevsky
6fbd1267fa make sure the View Certificates button appears at default size 2019-11-04 19:27:44 +00:00
Zlatin Balevsky
149568520f register necessary event, initialize mvc group, correct name representation 2019-11-04 19:05:53 +00:00
Zlatin Balevsky
c672880db0 statement was in wrong place 2019-11-04 18:45:57 +00:00
Zlatin Balevsky
6cb1674d14 set row height for tables pt2 2019-11-04 18:36:18 +00:00
Zlatin Balevsky
dba863a864 hook up CertClient, check that infohash in cert matches 2019-11-04 18:33:57 +00:00
Zlatin Balevsky
642044b7e2 ui elements for certificate fetching 2019-11-04 18:33:25 +00:00
Zlatin Balevsky
47c14f109a rename column, show certificate count in results 2019-11-04 17:21:37 +00:00
Zlatin Balevsky
36c1a1a288 core side of certificate exchange 2019-11-04 17:17:57 +00:00
Zlatin Balevsky
5d51b1c580 ability to certify shared files 2019-11-04 15:22:24 +00:00
Zlatin Balevsky
bf3502220f sign update queries as well 2019-11-03 22:44:42 +00:00
Zlatin Balevsky
ff1df88601 Release 0.5.7 2019-11-03 12:35:04 +00:00
Zlatin Balevsky
4ed572ba51 clear search button 2019-11-03 12:03:12 +00:00
Zlatin Balevsky
fd3f55ab4d implement restore session 2019-11-03 10:06:55 +00:00
Zlatin Balevsky
1358e14467 add options for search history 2019-11-03 08:12:10 +00:00
Zlatin Balevsky
e22d5fea11 better search box 2019-11-03 01:50:55 +00:00
Zlatin Balevsky
7ade4aa10d set row height to trees 2019-11-02 19:06:26 +00:00
Zlatin Balevsky
a9f623a91a correct method name 2019-11-02 18:51:02 +00:00
Zlatin Balevsky
1ce410e943 wip on signing queries 2019-11-02 18:34:13 +00:00
Zlatin Balevsky
27aad9d75d do not collapse tree on updates pt2 2019-11-02 17:41:04 +00:00
Zlatin Balevsky
24591b10f2 change the griffon environment 2019-11-02 10:13:28 -07:00
Zlatin Balevsky
e4f1ea5c10 make table rows a bit larger 2019-11-02 15:58:48 +00:00
Zlatin Balevsky
c73c44c5f2 base table row height on the size of the font 2019-11-02 15:46:50 +00:00
Zlatin Balevsky
309cbcc580 UTF-8 in props of cli 2019-11-02 15:23:15 +00:00
Zlatin Balevsky
86894f242b support UTF-8 in persona names 2019-11-02 14:43:24 +00:00
Zlatin Balevsky
568255140f visualize the negative tree as well 2019-11-02 12:54:43 +00:00
Zlatin Balevsky
f6d2bac5bb show all watched directories 2019-11-02 12:26:19 +00:00
Zlatin Balevsky
1c396711ed Fix sidecar files larger than the limit from being shared 2019-11-02 11:15:08 +00:00
Zlatin Balevsky
c154d9538d only check negative tree for files, not directories 2019-11-02 10:28:04 +00:00
Zlatin Balevsky
8043782446 logging config with all logs turned off 2019-11-02 08:52:29 +00:00
Zlatin Balevsky
00c529cca1 toString() 2019-11-02 00:40:08 +00:00
Zlatin Balevsky
094b9ac2b0 restore behavior where watched directories get scanned on startup 2019-11-02 00:27:12 +00:00
Zlatin Balevsky
0dae0a561b more accurate speed measurement. Makes a difference if MW is minimized for a long time 2019-11-01 18:39:41 +00:00
Zlatin Balevsky
82eaafc2c3 Release 0.5.6 2019-10-31 23:22:13 +00:00
Zlatin Balevsky
a3fc1a62e7 format the I2P bandwidths 2019-10-31 21:52:22 +00:00
Zlatin Balevsky
2fd8f45107 update text in cli 2019-10-31 21:22:50 +00:00
Zlatin Balevsky
2429bbf59e Add update notification window 2019-10-31 20:51:09 +00:00
Zlatin Balevsky
f7e28e04f6 add a system status panel 2019-10-31 14:14:14 +00:00
Zlatin Balevsky
cc0188f20e show used memory, not free memory 2019-10-31 13:46:16 +00:00
Zlatin Balevsky
af9b4f4679 change package name for cli 2019-10-31 13:05:42 +00:00
Zlatin Balevsky
625a559d02 change package name 2019-10-31 13:02:44 +00:00
Zlatin Balevsky
6e20193d57 properly set Xmx 2019-10-31 07:15:54 +00:00
Zlatin Balevsky
88ac267f99 show java version and ram usage in cli 2019-10-31 07:14:52 +00:00
Zlatin Balevsky
9b3a7473d1 limit Xmx on cli-lanterna too 2019-10-31 06:52:56 +00:00
Zlatin Balevsky
5b0180280e fix changing font and size on metal lnf 2019-10-30 22:20:27 +00:00
Zlatin Balevsky
d0462034fc enforce comment length in cli as well 2019-10-30 21:51:16 +00:00
Zlatin Balevsky
f3e4098107 refresh gui when processing a sidecar file 2019-10-30 21:45:38 +00:00
Zlatin Balevsky
26e7ca0b21 enforce maximum comment length in the gui 2019-10-30 21:22:08 +00:00
Zlatin Balevsky
11007e5f19 allow up to exact max comment length 2019-10-30 21:20:09 +00:00
Zlatin Balevsky
ae651cb6bd implement sidecar files 2019-10-30 21:07:59 +00:00
Zlatin Balevsky
cad3a88517 Xmx256M by default 2019-10-30 21:06:33 +00:00
Zlatin Balevsky
29c81646af word-wrap the comment views 2019-10-30 19:52:37 +00:00
Zlatin Balevsky
8a0257927b Link to CLI configuration options 2019-10-30 19:43:51 +00:00
Zlatin Balevsky
3b882ae644 Release 0.5.5 2019-10-29 16:16:36 +00:00
Zlatin Balevsky
5b61738ca9 skip downloaders that can't start 2019-10-29 15:56:19 +00:00
Zlatin Balevsky
c77d79513e more long arithmetic fixes 2019-10-29 15:34:48 +00:00
Zlatin Balevsky
9f12442897 long arithmetic 2019-10-29 15:07:29 +00:00
Zlatin Balevsky
477b0a47ad more logging 2019-10-29 14:33:23 +00:00
Zlatin Balevsky
7f1041dd96 @Log 2019-10-29 14:22:28 +00:00
Zlatin Balevsky
99393c59bd log when skipping a download 2019-10-29 14:15:43 +00:00
Zlatin Balevsky
a78d8c84ca unmap before flushing 2019-10-29 13:12:59 +00:00
Zlatin Balevsky
fa9c697bfa do not flush the output stream on Endpoint.close(). This fixes the long shutdown time 2019-10-29 12:38:41 +00:00
Zlatin Balevsky
e5b12701f5 do not crash the core if the XHave in mesh.json fails to parse 2019-10-29 10:28:14 +00:00
Zlatin Balevsky
f69727ab43 wait less time for reset() 2019-10-29 09:35:57 +00:00
Zlatin Balevsky
d7c7afe2c0 move the connections closing to a separate threadpool and limit the time we wait for reset() to complete 2019-10-29 09:01:41 +00:00
Zlatin Balevsky
6c806c4441 fix display of uploader progress to reach 100% 2019-10-29 01:00:59 +00:00
Zlatin Balevsky
c4095abdb4 sanity-check the X-Have header 2019-10-29 00:15:00 +00:00
Zlatin Balevsky
8801546854 tighten piece size range 2019-10-28 23:36:40 +00:00
Zlatin Balevsky
f6ee49c0f5 add upper bounds to the file length and piece size 2019-10-28 23:25:32 +00:00
Zlatin Balevsky
2320d650f6 do not serialize meshes that have more downloaded pieces than total pieces. To be investigated further 2019-10-28 23:16:27 +00:00
Zlatin Balevsky
e9e6e6920a <= part 2 2019-10-28 23:12:32 +00:00
Zlatin Balevsky
87e5007f39 <= 2019-10-28 23:06:50 +00:00
Zlatin Balevsky
8df6715e24 guard mesh.json as well 2019-10-28 23:00:03 +00:00
Zlatin Balevsky
6d587bf228 guard against piece size or count of 0 2019-10-28 22:51:24 +00:00
Zlatin Balevsky
8684452848 Add ability to limit the total number of upload slots, as well as per user 2019-10-28 14:48:38 +00:00
Zlatin Balevsky
7d652fabcb add option to close warning dialog to exit app. Add config option for exit behavior in the options 2019-10-28 13:28:03 +00:00
Zlatin Balevsky
5eb8d75bba Show how many times we've been browsed and increment hit counter 2019-10-27 11:26:41 +00:00
Zlatin Balevsky
9ca8d1738c do not re-share watched directories from the cli 2019-10-27 10:42:26 +00:00
Zlatin Balevsky
2bb9480137 the filetree map gets accessed from the directory watcher thread 2019-10-27 09:54:16 +00:00
Zlatin Balevsky
7a6365f87a Implement a negative lookup structure to prevent explicitly unshared files in watched directories from being re-shared 2019-10-27 09:13:22 +00:00
Zlatin Balevsky
56540ca3ca delay initial persistence to give chance to events to reach FileManager 2019-10-27 09:08:57 +00:00
Zlatin Balevsky
eb5a5198b1 more efficient unsharing of nested dirs 2019-10-27 05:12:25 +00:00
Zlatin Balevsky
29562c42ea add toString() 2019-10-27 05:12:01 +00:00
Zlatin Balevsky
f5284f9483 add upload speed column to cli 2019-10-27 03:07:18 +00:00
Zlatin Balevsky
9bd3c4f141 add speed column to uploads table 2019-10-27 03:00:54 +00:00
Zlatin Balevsky
817dd68faf Add a cli settings file, automatic or manual clearing of downloads and uploads 2019-10-27 02:29:20 +00:00
Zlatin Balevsky
5954cdb342 remove requests column, reword option for consistency 2019-10-26 17:41:57 +01:00
Zlatin Balevsky
56d44e6458 Do not clear uploads by default 2019-10-26 16:45:21 +01:00
Zlatin Balevsky
c6fb76610d Add search hit and download count to shared file table in both UIs 2019-10-26 15:02:46 +01:00
Zlatin Balevsky
5e329dfa2c Release 0.5.4 2019-10-26 06:42:14 +01:00
Zlatin Balevsky
742f6da870 update notifications 2019-10-26 06:12:54 +01:00
Zlatin Balevsky
7f46347c0f retry failed downloads 2019-10-26 05:33:22 +01:00
Zlatin Balevsky
b308ac2f37 searches by hash 2019-10-26 05:14:04 +01:00
Zlatin Balevsky
9cdabb51d1 count shared files in dashboard 2019-10-25 22:51:26 +01:00
Zlatin Balevsky
45f0736a5e account for hashing errors 2019-10-25 22:51:15 +01:00
Zlatin Balevsky
fe753ff978 add a download details view 2019-10-25 22:36:25 +01:00
Zlatin Balevsky
ac717b5205 center things horizontally 2019-10-25 22:02:04 +01:00
Zlatin Balevsky
6f624e3afc add some stats to main window 2019-10-25 21:51:16 +01:00
Zlatin Balevsky
623d675ed9 Ability to view comments 2019-10-25 18:57:07 +01:00
Zlatin Balevsky
546b71b632 implement adding comments to shared files 2019-10-25 18:32:55 +01:00
Zlatin Balevsky
804113bb1b typo 2019-10-25 17:46:59 +01:00
Zlatin Balevsky
ab9e10f438 add a note about the CLI 2019-10-25 17:43:15 +01:00
Zlatin Balevsky
00520acdf0 implement browse host 2019-10-25 17:30:16 +01:00
Zlatin Balevsky
8c44d196a7 move gui result processing on gui thread 2019-10-25 13:14:38 +01:00
Zlatin Balevsky
9c5fa0a2ce hook up trust to results 2019-10-25 10:36:26 +01:00
Zlatin Balevsky
d7bca05725 implement trust list review window 2019-10-25 10:00:52 +01:00
Zlatin Balevsky
45fcb2209e Trust List actions 2019-10-25 08:48:07 +01:00
Zlatin Balevsky
7bf0373b80 trust and distrust actions 2019-10-25 08:24:07 +01:00
Zlatin Balevsky
5925b42597 wip on trust window 2019-10-25 07:39:01 +01:00
Zlatin Balevsky
13243b05ad center shutdown dialog 2019-10-25 06:14:14 +01:00
Zlatin Balevsky
43987be463 prevent RejectedExecutionExceptions on shutdown 2019-10-25 06:13:20 +01:00
Zlatin Balevsky
fcd3414e02 refresh number of connections automatically 2019-10-25 06:08:41 +01:00
Zlatin Balevsky
70913ea8fb correct startup sequence, add listeners for allFilesLoadedEvent 2019-10-25 06:01:16 +01:00
Zlatin Balevsky
b30e552498 share and unshare a directory 2019-10-24 22:35:29 +01:00
Zlatin Balevsky
bae66de4eb implement share file dialog 2019-10-24 22:03:20 +01:00
Zlatin Balevsky
626e145e25 properly set size of tables 2019-10-24 19:22:39 +01:00
Zlatin Balevsky
bf72c76f13 limit the size of the table based on the terminal size 2019-10-24 19:12:50 +01:00
Zlatin Balevsky
fce8bbfd97 wip on shared files window 2019-10-24 18:34:27 +01:00
Zlatin Balevsky
1cc7925155 uploads window 2019-10-24 17:24:01 +01:00
Zlatin Balevsky
12b51ceb02 add an ETA column 2019-10-24 16:51:11 +01:00
Zlatin Balevsky
62811861a4 working downloads window 2019-10-24 16:36:10 +01:00
Zlatin Balevsky
837aa6974b display search results in new window 2019-10-24 14:39:25 +01:00
Zlatin Balevsky
94e7c42d19 add option to specify I2CP host and port. Show failure message is I2CP connect fails 2019-10-24 08:15:02 +01:00
Zlatin Balevsky
877bf12a93 fixed progress dialog, wip on search view 2019-10-24 07:49:15 +01:00
Zlatin Balevsky
224266b2dd basic initialization of the core 2019-10-23 22:25:54 +01:00
Zlatin Balevsky
8f16614dc3 start a new project for an interactive cli 2019-10-23 19:38:16 +01:00
Zlatin Balevsky
b412f9fb0c Release 0.5.3 2019-10-23 09:01:19 +01:00
Zlatin Balevsky
b24d04811d set apple quit strategy 2019-10-23 08:55:10 +01:00
Zlatin Balevsky
771f645df0 proper close 2019-10-23 08:48:53 +01:00
Zlatin Balevsky
b6483ad0f4 add an exit menu 2019-10-23 08:45:03 +01:00
Zlatin Balevsky
decb72c8ef show a warning that MW will continue running 2019-10-23 08:31:23 +01:00
Zlatin Balevsky
439b3bf18b fixes 2019-10-23 06:46:20 +01:00
Zlatin Balevsky
06679ffee0 only show MW if the core has loaded 2019-10-23 06:39:25 +01:00
Zlatin Balevsky
1d5b12e2d7 if core is not initialized, just shutdown 2019-10-23 06:31:08 +01:00
Zlatin Balevsky
4e6e1b6f5b Do not show warnings if core is already shutting down 2019-10-23 06:15:34 +01:00
Zlatin Balevsky
f0b5361d7b smaller icon 2019-10-23 06:06:37 +01:00
Zlatin Balevsky
e0c6bfbf51 show the clsoing window if tray is disabled 2019-10-23 06:01:21 +01:00
Zlatin Balevsky
2a0ecd8a47 fix constructor 2019-10-23 05:48:14 +01:00
Zlatin Balevsky
fb1804e849 Use explicit event to shutdown the application. This fixes closing on Linux 2019-10-23 05:45:50 +01:00
Zlatin Balevsky
d4eaa0df8d do not shutdown core on awt thread 2019-10-22 23:37:44 +01:00
Zlatin Balevsky
ffde6ac86f show a window while MW is shutting down 2019-10-22 23:26:54 +01:00
Zlatin Balevsky
7ad677ead2 add an explicit menu to show MW 2019-10-22 21:48:51 +01:00
Zlatin Balevsky
ddb0568aab do not auto-shutdown 2019-10-22 21:40:47 +01:00
Zlatin Balevsky
ff50a84a48 try to get a tray icon working 2019-10-22 21:34:50 +01:00
Zlatin Balevsky
770396ba41 update test 2019-10-22 10:31:28 +01:00
Zlatin Balevsky
b55852e993 typo 2019-10-22 10:16:41 +01:00
Zlatin Balevsky
a6945275a4 i2p 0.9.43 2019-10-22 08:27:08 +01:00
Zlatin Balevsky
7241809e55 update readme 2019-10-22 00:42:18 +01:00
Zlatin Balevsky
54073af933 Release 0.5.2 2019-10-22 00:28:53 +01:00
Zlatin Balevsky
a32903fc8c prettier i2p status panel 2019-10-22 00:11:57 +01:00
Zlatin Balevsky
e40520be46 count hopeless and failing hosts, prettier status panel 2019-10-21 23:57:15 +01:00
Zlatin Balevsky
97482b949a de-capitalize for consistency 2019-10-21 22:50:21 +01:00
Zlatin Balevsky
92ee107312 remove duplicate variable 2019-10-21 22:23:29 +01:00
Zlatin Balevsky
2e8082af64 use titled borders everywhere for consistency 2019-10-21 22:12:39 +01:00
Zlatin Balevsky
8da5a428c9 make the i2p version a variable 2019-10-21 21:02:37 +01:00
Zlatin Balevsky
fd46b3c7d6 do not display fractions in percentage 2019-10-21 20:37:30 +01:00
Zlatin Balevsky
eea3b2563b allign router-specific settings 2019-10-21 20:16:36 +01:00
Zlatin Balevsky
50719f3828 move settings to top of panel 2019-10-21 20:12:08 +01:00
Zlatin Balevsky
01a45a89a8 reorganize the options view 2019-10-21 19:44:33 +01:00
Zlatin Balevsky
66bd249ed3 show percentage of fetched results 2019-10-21 18:28:37 +01:00
Zlatin Balevsky
265cd6ee15 more accurate description 2019-10-20 20:19:47 +01:00
Zlatin Balevsky
1dc88cb96b make speed smoothing interval configurable 2019-10-20 20:09:24 +01:00
Zlatin Balevsky
3e10d497b1 add an ETA column to downloads table 2019-10-20 19:11:32 +01:00
Zlatin Balevsky
9a0b3bb9d6 fix download table selection when sorted 2019-10-20 18:47:48 +01:00
Zlatin Balevsky
a1fe3c01b9 if no incompletes are in serialized json, use the default one, assuming an upgrade 2019-10-20 18:24:16 +01:00
Zlatin Balevsky
ab323db62a add ability to choose the incompletes location 2019-10-20 18:16:07 +01:00
Zlatin Balevsky
d954387e41 fix showing of local files in results 2019-10-20 11:59:48 +01:00
Zlatin Balevsky
ea9db21a18 wip on compressed results 2019-10-20 01:01:34 +01:00
Zlatin Balevsky
136cf89c9b groovy != java 2019-10-20 00:55:31 +01:00
Zlatin Balevsky
46de1baf88 compressed results 2019-10-20 00:54:32 +01:00
Zlatin Balevsky
13f7b8563c fix a bug where disabled browsing was shown as browsable. Log the response code if it's not 200 2019-10-19 22:33:47 +01:00
Zlatin Balevsky
9c15208f3a Release 0.5.1 2019-10-19 19:11:04 +01:00
Zlatin Balevsky
a9ce9d96b3 wip on menu; close zlib stream 2019-10-19 18:54:58 +01:00
Zlatin Balevsky
4d2a5a8018 MainFrameModel doesn't need to listen to single result events anymore 2019-10-19 18:12:30 +01:00
Zlatin Balevsky
8395047386 compress results in browse connections 2019-10-19 17:59:08 +01:00
Zlatin Balevsky
cb23aa44f0 enable SEVERE log messages if no config file specified 2019-10-19 05:53:33 +01:00
Zlatin Balevsky
dbcb8508b8 add a view comment button 2019-10-19 05:35:04 +01:00
Zlatin Balevsky
47d406d93b add a border around the two panels 2019-10-19 04:59:37 +01:00
Zlatin Balevsky
e06f1805c2 redirect griffon logging to jul 2019-10-19 04:45:45 +01:00
Zlatin Balevsky
2b04374e23 add option to disable browsing of files, make the dialog bigger 2019-10-19 00:53:13 +01:00
Zlatin Balevsky
383addbc37 implement view comment from browse window 2019-10-19 00:30:03 +01:00
Zlatin Balevsky
cc39cd7f8e implement downloading from browse window 2019-10-19 00:23:43 +01:00
Zlatin Balevsky
83665d7524 wip on browse host 2019-10-18 23:55:07 +01:00
Zlatin Balevsky
94340480b4 wip on browse host 2019-10-18 23:25:26 +01:00
Zlatin Balevsky
8850d49c63 wip on browse host 2019-10-18 23:16:37 +01:00
Zlatin Balevsky
f0f9d840f0 wip on browse host 2019-10-18 22:35:17 +01:00
Zlatin Balevsky
7f4cd4f331 wip on browse host 2019-10-18 21:17:34 +01:00
Zlatin Balevsky
e6162503f6 wip on browse host 2019-10-18 20:29:39 +01:00
Zlatin Balevsky
7a5d71dc36 add copy name to clipboard option 2019-10-17 19:01:53 +01:00
Zlatin Balevsky
6fa39a5e35 turn off logging if there is no config file 2019-10-17 18:39:28 +01:00
Zlatin Balevsky
c5ae804f61 Implement automatic font sizing; set all font properties on change of font 2019-10-17 18:15:04 +01:00
Zlatin Balevsky
d7695b448d remove my DS_Store 2019-10-17 05:50:29 +01:00
Zlatin Balevsky
946d9c8f32 disable sharing of hidden files by default, add option to enable 2019-10-17 05:46:27 +01:00
Zlatin Balevsky
02441ca1e3 add option to disable searching in comments 2019-10-16 19:57:18 +01:00
Zlatin Balevsky
5fa21b2360 keep tree expanded on modifications 2019-10-16 14:42:40 +01:00
Zlatin Balevsky
d4c08f4fe6 only remove from index if no more files have the same comment pt.2 2019-10-16 14:23:12 +01:00
Zlatin Balevsky
942de287c6 only remove from index if no more files have the same comment 2019-10-16 14:21:50 +01:00
Zlatin Balevsky
d0299f80c6 search through comments 2019-10-16 14:06:11 +01:00
Zlatin Balevsky
1227cf9263 Release 0.5.0 2019-10-15 12:38:25 +01:00
Zlatin Balevsky
a05575485f move things around 2019-10-15 10:40:50 +01:00
Zlatin Balevsky
f5bccd8126 All shared directories are watched directories. Fix manipulation of tree structure 2019-10-15 08:38:23 +01:00
Zlatin Balevsky
70fb789abf remove the watched directories table 2019-10-15 04:51:21 +01:00
Zlatin Balevsky
feb712c253 Move persisting of files on dedicated thread. Introduce an event to forcefully persist files. Do that immediately after unsharing anything 2019-10-15 04:21:40 +01:00
Zlatin Balevsky
d22b403e2a stop watching multiple directories at once 2019-10-14 23:16:05 +01:00
Zlatin Balevsky
a24982e0df fix comments for local results 2019-10-14 22:47:52 +01:00
Zlatin Balevsky
6c26019164 allow switching without restart 2019-10-14 21:40:03 +01:00
Zlatin Balevsky
965fa79bbf fix count of shared files in tree view mode 2019-10-14 20:57:50 +01:00
Zlatin Balevsky
60ddb85461 Tree view of the shared files. The count is wrong for some reason 2019-10-14 20:13:25 +01:00
Zlatin Balevsky
c7284623bc Release 0.4.16 2019-10-13 22:14:33 +01:00
Zlatin Balevsky
3e7f2aa70a Add a note about DND, automatically watch shared directories 2019-10-13 20:21:28 +01:00
Zlatin Balevsky
4f436a636c implement drop on MW -> share files/directories 2019-10-13 20:00:08 +01:00
Zlatin Balevsky
b49dbc30c3 comment already decoded by the time it gets to the gui 2019-10-11 19:01:40 +01:00
Zlatin Balevsky
c25d314e1c typo 2019-10-11 18:56:46 +01:00
Zlatin Balevsky
b28587a275 wip on file comments 2019-10-11 18:42:02 +01:00
Zlatin Balevsky
8b8e5d59be Silence an IllegalArgumentException while sorting downloads table 2019-10-11 11:21:56 +01:00
Zlatin Balevsky
70bbe1f636 update version 2019-10-10 17:33:07 +01:00
Zlatin Balevsky
337605dc0f Release 0.4.15 2019-10-10 16:48:10 +01:00
Zlatin Balevsky
14bdfa6b2e throttle even further - 500/s 2019-10-09 17:34:54 +01:00
Zlatin Balevsky
ed3f9da773 throttle loading even further, to 1000/sec 2019-10-09 16:46:17 +01:00
Zlatin Balevsky
251080d08f throttle loading of files to 500/s 2019-10-09 16:34:09 +01:00
Zlatin Balevsky
f530ab999d operations on multiple selection in shared files table 2019-10-09 03:38:08 +01:00
Zlatin Balevsky
4133384e48 ability to share multiple files and directories 2019-10-08 21:30:34 +01:00
Zlatin Balevsky
600fc98868 update TODO 2019-10-07 12:38:26 +01:00
Zlatin Balevsky
129eeb3b88 JDK needed, not JRE 2019-10-07 12:38:09 +01:00
Zlatin Balevsky
20b51b78a0 reduce priority of file persister thread 2019-10-07 11:59:51 +01:00
Zlatin Balevsky
33fe755b60 implement multiple-selection on downloads table 2019-10-07 04:26:35 +01:00
Zlatin Balevsky
8b0668a134 Rewrite utils into Java, cache the persistable data of shared files to reduce object churn 2019-10-05 22:50:32 +01:00
Zlatin Balevsky
730d2202fd bundles for linux available now 2019-10-05 18:53:43 +01:00
Zlatin Balevsky
69906a986d set i2p.dir.base to prevent router creating files in PWD 2019-10-05 15:03:59 +01:00
Zlatin Balevsky
5bc8fa8633 Preserve selection on refresh #18 2019-10-05 05:13:49 +01:00
Zlatin Balevsky
7de7c9d8f3 Add 'Clear Hits' button to content control panel #18 2019-10-05 05:03:25 +01:00
Zlatin Balevsky
e943f6019d disable all GUI unit tests, enable host-cache unit tests. The 'build' target now succeeds 2019-10-05 04:31:11 +01:00
Zlatin Balevsky
2eec7bec5b fix most core tests 2019-10-05 04:20:14 +01:00
Zlatin Balevsky
c36110cf76 update readme 2019-10-04 16:41:07 +01:00
Zlatin Balevsky
abe28517bc Release 0.4.14 2019-10-04 13:00:57 +01:00
Zlatin Balevsky
15bc4c064d center the button 2019-10-03 21:32:32 +01:00
Zlatin Balevsky
91d771944b add option for sequential download 2019-10-03 20:45:22 +01:00
Zlatin Balevsky
e09c456a13 make the download retry interval in seconds, default still 1 minute 2019-10-03 19:31:15 +01:00
Zlatin Balevsky
d9c1067226 Add Neutral button to search tab, issue #17 2019-10-02 06:02:06 +01:00
Zlatin Balevsky
eda3e7ad3a Add option to not search extra hop, only considered if connecting only to trusted peers, issue #6 2019-10-02 05:45:46 +01:00
Zlatin Balevsky
e9798c7eaa remember last rejection and back off from hosts that reject us. Fix return value of retry and hopelessness predicates 2019-10-01 08:34:43 +01:00
Zlatin Balevsky
66bb4eef5b close outbound establishments on a separate thread 2019-10-01 07:50:29 +01:00
Zlatin Balevsky
55f260b3f4 update version 2019-09-29 19:21:06 +01:00
Zlatin Balevsky
32d4c3965e Release 0.4.13 2019-09-29 19:00:20 +01:00
Zlatin Balevsky
de1534d837 reduce the default host retry interval 2019-09-29 18:45:09 +01:00
Zlatin Balevsky
7b58e8a88a separate setting for the interval after which a host is considered hopeless 2019-09-29 18:43:39 +01:00
Zlatin Balevsky
8a03b89985 clean up the filtering logic; allow serialization of hosts that can be retried 2019-09-29 16:49:02 +01:00
Zlatin Balevsky
1d97374857 track last successful attempt. Only re-attempt hosts if they have ever been successful. Do not serialize hosts considered hopeless 2019-09-29 16:19:19 +01:00
Zlatin Balevsky
549e8c2d98 Release 0.4.12 2019-09-22 16:55:04 +01:00
Zlatin Balevsky
b54d24db0d new update server destination 2019-09-22 16:47:35 +01:00
Zlatin Balevsky
fa12e84345 stronger sig type 2019-09-22 16:23:01 +01:00
Zlatin Balevsky
6430ff2691 bump i2p libs version 2019-09-22 16:13:12 +01:00
Zlatin Balevsky
591313c81c point to the pkg project 2019-09-20 21:09:53 +01:00
Zlatin Balevsky
ce7b6a0c65 change to gasp AA font table, try metal lnf if the others fail 2019-09-16 15:06:45 +01:00
Zlatin Balevsky
5c4d4c4580 embedded router will not work without reseed certificates, so remove it 2019-09-16 15:04:34 +01:00
Zlatin Balevsky
4cb864ff9f update version 2019-09-16 15:03:20 +01:00
Zlatin Balevsky
417675ad07 update dark_trion's hostcache address 2019-07-22 21:48:29 +01:00
Zlatin Balevsky
9513e5ba3c update todo 2019-07-20 13:15:44 +01:00
Zlatin Balevsky
85610cf169 add new host-cache 2019-07-15 22:05:09 +01:00
581 changed files with 69502 additions and 27370 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
# Dot directories
.gradle/
.idea/
.git/
# Build directories
build/
**/build/
# We execute COPY . .
# Modifying these files would unnecessarily invalidate the build context
Dockerfile

6
.gitignore vendored
View File

@@ -2,5 +2,7 @@
**/.settings
**/build
.gradle
.project
.classpath
**/.project
**/.classpath
**/*.rej
**/*.orig

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
lang_map = he: iw, id: in, ru_RU: ru, sv_SE: sv, tr_TR: tr, uk_UA: uk, yi: ji, zh_CN: zh
[I2P.MuWire]
file_filter = webui/locale/messages_<lang>.po
source_file = webui/locale/messages_en.po
source_lang = en
minimum_perc = 10

64
Dockerfile Normal file
View File

@@ -0,0 +1,64 @@
FROM jlesage/baseimage-gui:alpine-3.10-glibc
# Docker image version is provided via build arg.
ARG DOCKER_IMAGE_VERSION=unknown
# JDK version
ARG JDK=11
# Important directories
ARG TMP_DIR=/muwire-tmp
ENV APP_HOME=/muwire
# Define working directory.
WORKDIR $TMP_DIR
# Put sources into dir
COPY . .
# Install final dependencies
RUN add-pkg openjdk${JDK}-jre
# Build and untar in future distribution dir
RUN add-pkg --virtual openjdk${JDK}-jdk \
&& ./gradlew --no-daemon clean assemble \
&& mkdir -p ${APP_HOME} \
# Extract to ${APP_HOME and ignore the first dir
# First dir in tar is the "MuWire-<version>"
&& tar -C ${APP_HOME} --strip 1 -xvf gui/build/distributions/MuWire*.tar \
# Cleanup
&& rm -rf "${TMP_DIR}" /root/.gradle /root/.java \
&& del-pkg openjdk${JDK}-jdk
WORKDIR ${APP_HOME}
# Maximize only the main/initial window.
RUN \
sed-patch 's/<application type="normal">/<application type="normal" title="MuWire">/' \
/etc/xdg/openbox/rc.xml
# Generate and install favicons.
RUN \
APP_ICON_URL=https://github.com/zlatinb/muwire/raw/master/gui/griffon-app/resources/MuWire-128x128.png && \
install_app_icon.sh "$APP_ICON_URL"
# Add files.
COPY docker/rootfs/ /
# Set environment variables.
ENV APP_NAME="MuWire" \
S6_KILL_GRACETIME=8000
# Define mountable directories.
VOLUME ["$APP_HOME/.MuWire"]
VOLUME ["/incompletes"]
VOLUME ["/output"]
# Metadata.
LABEL \
org.label-schema.name="muwire" \
org.label-schema.description="Docker container for MuWire" \
org.label-schema.version="$DOCKER_IMAGE_VERSION" \
org.label-schema.vcs-url="https://github.com/zlatinb/muwire" \
org.label-schema.schema-version="1.0"

View File

@@ -1,14 +1,16 @@
The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and issues at http://git.idk.i2p/zlatinb/muwire
# MuWire - Easy Anonymous File-Sharing
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.7.4 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
The current stable release - 0.4.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
### Building
## Building
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew clean assemble
@@ -19,19 +21,41 @@ If you want to run the unit tests, type
./gradlew clean build
```
Some of the UI tests will fail because they haven't been written yet :-/
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project. If you want to package MuWire for a Linux distribution, see the [Packaging] wiki page.
### Running
## Running the GUI
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt.
Type
```
./gradlew gui:run
```
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
The setup wizard will ask you for the host and port of an I2P or I2Pd router.
[Default I2CP port]\: `7654`
## Running the CLI
If you do not have an I2P router, pass the following switch to the Java process: `-DembeddedRouter=true`. This will launch MuWire's embedded router. Be aware that this causes startup to take a lot longer.
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here [cli options]
### GPG Fingerprint
The CLI is under active development and doesn't have all the features of the GUI.
## Running the Web UI / Plugin
There is a Web-based UI under development. It is intended to be run as a plugin to the Java I2P router. Instructions how to build it are available at the wiki [Plugin] page.
## Docker
MuWire is available as a Docker image. For more information see the [Docker] page.
## Translations
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
## Related Projects
### MuWire Tracker Daemon
The MuWire Tracker Daemon (or mwtrackerd for short) is a project to bring functionality similar to BitTorrent tracking to MuWire. For more info see the [Tracker] page.
### MuCats
MuCats is a project to create a website for hosting hashes of files shared on the MuWire network. For more info see the [MuCats] project.
## GPG Fingerprint
```
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
@@ -41,3 +65,14 @@ You can find the full key at https://keybase.io/zlatinb
[Default I2CP port]: https://geti2p.net/en/docs/ports
[Wiki]: https://github.com/zlatinb/muwire/wiki
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
[Packaging]: https://github.com/zlatinb/muwire/wiki/Packaging
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
[I2P Github]: https://github.com/i2p/i2p.i2p
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
[Tracker]: https://github.com/zlatinb/muwire/wiki/Tracker-Daemon
[MuCats]: https://github.com/zlatinb/mucats

44
TODO.md
View File

@@ -1,8 +1,6 @@
# TODO List
Not in any particular order yet
### Big Items
### Network
##### Bloom Filters
@@ -12,20 +10,36 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability
##### Content Control Panel
### Core
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
* Metadata parsing and search
* Automatic adjustment of number of I2P tunnels
* Persist trust immediately
* Enum i18n
* Ability to share trust list only with trusted users
* Confidential files visible only to certain users
* Download queue with priorities
* Use tracker pings - either embedded logic or external mwtrackerd to add more sources to downloads
##### Web UI, REST Interface, etc.
### Chat
* break up lines on CR/LF, send multiple messages
* enforce # in room names or ignore it
* auto-create/join channel on server start
* jump from notification window to room with message
Basically any non-gui non-cli user interface
### Swing GUI
* I2P Status panel - display message when connected to external router
* Search box - left identation
* Ability to disable switching of tabs on actions
##### Metadata editing and search
### Web UI/Plugin
* HTML 5 media players
* Remove versions from jar names
* Security: POST nonces, CSP headers
* Upload files from browser to plugin via drag-and-drop
* Check permissions, display better errors when sharing local folders
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items
* Wrapper of some kind for in-place upgrades
* Download file sequentially
* Multiple-selection download, Ctrl-A
### mwtrackerd
* `save` and `load` JSON-RPC commands that save and load swarm state respectively
* load-test with many many hashes (1M?)
* evaluate other usage scenarios besides website backend

View File

@@ -2,12 +2,26 @@ subprojects {
apply plugin: 'groovy'
dependencies {
compile 'net.i2p:i2p:0.9.41'
compile 'org.codehaus.groovy:groovy-all:2.4.15'
compile "org.codehaus.groovy:groovy:${groovyVersion}"
compile "org.codehaus.groovy:groovy-jsr223:${groovyVersion}"
compile "org.codehaus.groovy:groovy-json:${groovyVersion}"
}
compileGroovy {
groovyOptions.optimizationOptions.indy = true
groovyOptions.optimizationOptions.indy = false
sourceCompatibility = project.sourceCompatibility
targetCompatibility = project.targetCompatibility
options.compilerArgs += project.compilerArgs
options.deprecation = true
options.encoding = 'UTF-8'
}
compileJava {
sourceCompatibility = project.sourceCompatibility
targetCompatibility = project.targetCompatibility
options.compilerArgs += project.compilerArgs
options.deprecation = true
options.encoding = 'UTF-8'
}
repositories {

27
cli-lanterna/build.gradle Normal file
View File

@@ -0,0 +1,27 @@
buildscript {
repositories {
jcenter()
mavenLocal()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
}
}
apply plugin : 'application'
application {
mainClassName = 'com.muwire.clilanterna.CliLanterna'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire-cli'
}
apply plugin : 'com.github.johnrengelman.shadow'
dependencies {
compile project(":core")
compile 'com.googlecode.lanterna:lanterna:3.0.1'
}

View File

@@ -0,0 +1,73 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class AddCommentView extends BasicWindow {
private final TextGUI textGUI
private final Core core
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
AddCommentView(TextGUI textGUI, Core core, SharedFile sharedFile, TerminalSize terminalSize) {
super("Add Comment To "+sharedFile.getFile().getName())
this.textGUI = textGUI
this.core = core
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
String oldComment = sharedFile.getComment()
if (oldComment == null)
oldComment = ""
else
oldComment = DataUtil.readi18nString(Base64.decode(oldComment))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize,oldComment,TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
contentPanel.addComponent(buttonsPanel, layoutData)
Button saveButton = new Button("Save", {
String newComment = textBox.getText()
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
} else {
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
String encodedOldComment = sharedFile.getComment()
sharedFile.setComment(newComment)
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
close()
}
})
Button cancelButton = new Button("Cancel", {close()})
buttonsPanel.addComponent(saveButton, layoutData)
buttonsPanel.addComponent(cancelButton, layoutData)
setComponent(contentPanel)
}
}

View File

@@ -0,0 +1,98 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.search.BrowseStatus
import com.muwire.core.search.BrowseStatusEvent
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
class BrowseModel {
private final Persona persona
private final Core core
private final TextGUIThread guiThread
private final TableModel model = new TableModel("Name","Size","Hash","Comment","Certificates")
private Map<String, UIResultEvent> rootToResult = new HashMap<>()
private int totalResults
private Label status
private Label percentage
BrowseModel(Persona persona, Core core, TextGUIThread guiThread) {
this.persona = persona
this.core = core
this.guiThread = guiThread
core.eventBus.register(BrowseStatusEvent.class, this)
core.eventBus.register(UIResultEvent.class, this)
core.eventBus.publish(new UIBrowseEvent(host : persona))
}
void unregister() {
core.eventBus.unregister(BrowseStatusEvent.class, this)
core.eventBus.unregister(UIResultEvent.class, this)
}
void onBrowseStatusEvent(BrowseStatusEvent e) {
guiThread.invokeLater {
status.setText(e.status.toString())
if (e.status == BrowseStatus.FETCHING)
totalResults = e.totalResults
}
}
void onUIResultEvent(UIResultEvent e) {
guiThread.invokeLater {
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment, e.certificates)
rootToResult.put(infoHash, e)
String percentageString = ""
if (totalResults != 0) {
double percentage = Math.round( (model.getRowCount() * 100 / totalResults).toDouble() )
percentageString = String.valueOf(percentage)+"%"
}
percentage.setText(percentageString)
}
}
void setStatusLabel(Label status) {
this.status = status
}
void setPercentageLabel(Label percentage) {
this.percentage = percentage
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
List<UIResultEvent> l = new ArrayList<>(rootToResult.values())
Collections.sort(l, chosen)
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
l.each { e ->
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment, e.certificates)
}
}
}

View File

@@ -0,0 +1,136 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
class BrowseView extends BasicWindow {
private final BrowseModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
BrowseView(BrowseModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
super("Browse "+model.persona.getHumanReadableName())
this.model = model
this.textGUI = textGUI
this.core = core
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
Label statusLabel = new Label("")
Label percentageLabel = new Label("")
model.setStatusLabel(statusLabel)
model.setPercentageLabel(percentageLabel)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
topPanel.addComponent(statusLabel, layoutData)
topPanel.addComponent(percentageLabel, layoutData)
contentPanel.addComponent(topPanel, layoutData)
table = new Table("Name","Size","Hash","Comment","Certificates")
table.with {
setCellSelection(false)
setTableModel(model.model)
setVisibleRows(terminalSize.getRows())
setSelectAction({rowSelected()})
}
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...", {sort()})
Button closeButton = new Button("Close",{
model.unregister()
close()
})
buttonsPanel.addComponent(sortButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
String infoHash = row[2]
boolean comment = Boolean.parseBoolean(row[3])
boolean certificates = row[4] > 0
if (comment || certificates) {
Window prompt = new BasicWindow("Download Or View Comment")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
Button downloadButton = new Button("Download", {download(infoHash)})
Button viewButton = new Button("View Comment", {viewComment(infoHash)})
Button viewCertificate = new Button("View Certificates",{viewCertificates(infoHash)})
Button closeButton = new Button("Cancel", {prompt.close()})
contentPanel.with {
addComponent(downloadButton, layoutData)
if (comment)
addComponent(viewButton, layoutData)
if (certificates)
addComponent(viewCertificate, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
downloadButton.takeFocus()
textGUI.addWindowAndWait(prompt)
} else {
download(infoHash)
}
}
private void download(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
def file = new File(core.muOptions.downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
target : file, sequential : false))
MessageDialog.showMessageDialog(textGUI, "Download started", "Started download of "+result.name, MessageDialogButton.OK)
}
private void viewComment(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
textGUI.addWindowAndWait(view)
}
private void viewCertificates(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -0,0 +1,14 @@
package com.muwire.clilanterna
import com.muwire.core.filecert.Certificate
class CertificateWrapper {
private final Certificate certificate
CertificateWrapper(Certificate certificate) {
this.certificate = certificate
}
public String toString() {
certificate.issuer.getHumanReadableName()
}
}

View File

@@ -0,0 +1,88 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUIThread
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionEvent
import com.muwire.core.chat.ChatLink
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent
import net.i2p.data.DataHelper
class ChatConsoleModel {
private final Core core
private final TextGUIThread guiThread
volatile ChatLink link
volatile Thread poller
volatile boolean running
volatile TextBox textBox
ChatConsoleModel(Core core, TextGUIThread guiThread) {
this.core = core
this.guiThread = guiThread
}
void start() {
if (running)
return
running = true
core.chatServer.start()
core.eventBus.with {
register(ChatConnectionEvent.class, this)
publish(new UIConnectChatEvent(host : core.me))
}
}
void onChatConnectionEvent(ChatConnectionEvent e) {
if (e.persona != core.me)
return // can't really happen
link = e.connection
poller = new Thread({eventLoop()} as Runnable)
poller.setDaemon(true)
poller.start()
}
void stop() {
if (!running)
return
running = false
core.chatServer.stop()
poller?.interrupt()
link = null
}
private void eventLoop() {
Thread.sleep(1000)
while(running) {
ChatLink link = this.link
if (link == null || !link.isUp()) {
Thread.sleep(100)
continue
}
Object event = link.nextEvent()
if (event instanceof ChatMessageEvent)
handleChatMessage(event)
else if (event instanceof Persona)
handleLeave(event)
else
throw new IllegalArgumentException("unknown event type $event")
}
}
private void handleChatMessage(ChatMessageEvent e) {
String text = DataHelper.formatTime(e.timestamp)+" <"+e.sender.getHumanReadableName()+ "> ["+
e.room+"] "+e.payload
guiThread.invokeLater({textBox.addLine(text)})
}
private void handleLeave(Persona p) {
guiThread.invokeLater({textBox.addLine(p.getHumanReadableName()+ " disconnected")})
}
}

View File

@@ -0,0 +1,116 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.Core
import com.muwire.core.chat.ChatCommand
import com.muwire.core.chat.ChatConnection
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.ChatServer
import net.i2p.data.DataHelper
class ChatConsoleView extends BasicWindow {
private final TextGUI textGUI
private final ChatConsoleModel model
private final Core core
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
private final LayoutData layoutDataFill = GridLayout.createLayoutData(Alignment.FILL, Alignment.FILL, true, false)
private final TextBox textBox
private final TextBox sayField
private final TextBox roomField
ChatConsoleView(Core core, ChatConsoleModel model, TextGUI textGUI, TerminalSize terminalSize) {
super("Chat Server Console")
this.core = core
this.model = model
this.textGUI = textGUI
TextBox textBox = model.textBox == null ? new TextBox(terminalSize,"", TextBox.Style.MULTI_LINE) : model.textBox
this.textBox = textBox
model.textBox = textBox
model.start()
TerminalSize textFieldSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), 1)
this.sayField = new TextBox(textFieldSize,"", TextBox.Style.SINGLE_LINE)
this.roomField = new TextBox(textFieldSize,"__CONSOLE__", TextBox.Style.SINGLE_LINE)
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
contentPanel.addComponent(textBox, layoutData)
Panel inputPanel = new Panel()
inputPanel.with {
setLayoutManager(new GridLayout(2))
addComponent(new Label("Say something here"), layoutData)
addComponent(sayField, layoutDataFill)
addComponent(new Label("In room:"), layoutData)
addComponent(roomField, layoutDataFill)
}
contentPanel.addComponent(inputPanel, layoutData)
Panel bottomPanel = new Panel()
bottomPanel.setLayoutManager(new GridLayout(5))
Button sayButton = new Button("Say",{say()})
Button startButton = new Button("Start Server",{model.start()})
Button stopButton = new Button("Stop Server", {model.stop()})
Button clearButton = new Button("Clear",{textBox.setText("")})
Button closeButton = new Button("Close",{close()})
bottomPanel.with {
addComponent(sayButton, layoutData)
addComponent(startButton, layoutData)
addComponent(stopButton, layoutData)
addComponent(clearButton, layoutData)
addComponent(closeButton, layoutData)
}
contentPanel.addComponent(bottomPanel, layoutData)
setComponent(contentPanel)
}
private void say() {
String command = sayField.getText()
sayField.setText("")
ChatCommand chatCommand
try {
chatCommand = new ChatCommand(command)
} catch (Exception e) {
chatCommand = new ChatCommand("/SAY $command")
}
command = chatCommand.source
String room = roomField.getText()
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
String toAppend = DataHelper.formatTime(now) + " <" + core.me.getHumanReadableName() + "> [$room] " + command
textBox.addLine(toAppend)
byte[] sig = ChatConnection.sign(uuid, now, room, command, core.me, core.me, core.spk)
def event = new ChatMessageEvent( uuid : uuid,
payload : command,
sender : core.me,
host : core.me,
room : room,
chatTime : now,
sig : sig
)
core.eventBus.publish(event)
}
}

View File

@@ -0,0 +1,193 @@
package com.muwire.clilanterna
import java.nio.charset.StandardCharsets
import java.util.concurrent.CountDownLatch
import java.util.logging.Level
import java.util.logging.LogManager
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Border
import com.googlecode.lanterna.gui2.BorderLayout
import com.googlecode.lanterna.gui2.Borders
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.MultiWindowTextGUI
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.SeparateTextGUIThread
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.WindowBasedTextGUI
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder
import com.googlecode.lanterna.gui2.dialogs.WaitingDialog
import com.googlecode.lanterna.screen.Screen
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
import com.googlecode.lanterna.terminal.Terminal
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.7.4"
private static volatile Core core
private static WindowBasedTextGUI textGUI
public static void main(String[] args) {
if (System.getProperty("java.util.logging.config.file") == null) {
def names = LogManager.getLogManager().getLoggerNames()
while(names.hasMoreElements()) {
def name = names.nextElement()
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
}
}
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
if (!home.exists())
home.mkdirs()
def propsFile = new File(home,"MuWire.properties")
DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory()
Screen screen = terminalFactory.createScreen()
textGUI = new MultiWindowTextGUI( new SeparateTextGUIThread.Factory(), screen)
textGUI.getGUIThread().start()
screen.startScreen()
def props
if (!propsFile.exists()) {
String nickname = TextInputDialog.showDialog(textGUI, "Select a nickname", "", "")
String defaultDownloadLocation = System.getProperty("user.home")+File.separator+"Downloads"
String downloadLocation = TextInputDialog.showDialog(textGUI, "Select download location", "", defaultDownloadLocation)
String defaultIncompletesLocation = System.getProperty("user.home")+File.separator+".MuWire"+File.separator+"incompletes"
String incompletesLocation = TextInputDialog.showDialog(textGUI, "Select incompletes location", "", defaultIncompletesLocation)
File downloadLocationFile = new File(downloadLocation)
if (!downloadLocationFile.exists())
downloadLocationFile.mkdirs()
File incompletesLocationFile = new File(incompletesLocation)
if (!incompletesLocationFile.exists())
incompletesLocationFile.mkdirs()
props = new MuWireSettings()
props.setNickname(nickname)
props.setDownloadLocation(downloadLocationFile)
props.incompleteLocation = incompletesLocationFile
propsFile.withPrintWriter("UTF-8", {
props.write(it)
})
} else {
props = new Properties()
propsFile.withReader("UTF-8", {
props.load(it)
})
props = new MuWireSettings(props)
}
props.updateType = "cli-lanterna"
def i2pPropsFile = new File(home, "i2p.properties")
if (!i2pPropsFile.exists()) {
String i2pHost = TextInputDialog.showDialog(textGUI, "I2P router host", "Specifiy the host I2P router is on", "127.0.0.1")
int i2pPort = TextInputDialog.showNumberDialog(textGUI, "I2CP port", "Specify the I2CP port", "7654").toInteger()
Properties i2pProps = new Properties()
i2pProps["i2cp.tcp.host"] = i2pHost
i2pProps["i2cp.tcp.port"] = String.valueOf(i2pPort)
i2pPropsFile.withOutputStream { i2pProps.store(it, "") }
}
def cliProps
def cliPropsFile = new File(home, "cli.properties")
if (cliPropsFile.exists()) {
Properties p = new Properties()
cliPropsFile.withInputStream {
p.load(it)
}
cliProps = new CliSettings(p)
} else
cliProps = new CliSettings(new Properties())
Window window = new BasicWindow("MuWire "+ MW_VERSION)
window.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.withBorder(Borders.doubleLine())
BorderLayout layout = new BorderLayout()
contentPanel.setLayoutManager(layout)
Panel welcomeNamePanel = new Panel()
contentPanel.addComponent(welcomeNamePanel, BorderLayout.Location.CENTER)
welcomeNamePanel.setLayoutManager(new GridLayout(1))
Label welcomeLabel = new Label("Welcome to MuWire "+ props.nickname)
welcomeNamePanel.addComponent(welcomeLabel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
Panel connectButtonPanel = new Panel()
contentPanel.addComponent(connectButtonPanel, BorderLayout.Location.BOTTOM)
connectButtonPanel.setLayoutManager(new GridLayout(1))
Button connectButton = new Button("Connect", {
WaitingDialog waiting = new WaitingDialog("Connecting", "Please wait")
waiting.showDialog(textGUI, false)
CountDownLatch latch = new CountDownLatch(1)
Thread connector = new Thread({
try {
core = new Core(props, home, MW_VERSION)
} finally {
latch.countDown()
}
})
connector.start()
while(latch.getCount() > 0) {
textGUI.updateScreen()
Thread.sleep(10)
}
waiting.close()
window.close()
} as Runnable)
welcomeNamePanel.addComponent(connectButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
window.setComponent(contentPanel)
textGUI.addWindowAndWait(window)
if (core == null) {
MessageDialog.showMessageDialog(textGUI, "Failed", "MuWire failed to load", MessageDialogButton.Close)
System.exit(1)
}
window = new MainWindowView("MuWire "+MW_VERSION, core, textGUI, screen, cliProps)
core.startServices()
core.eventBus.publish(new UILoadedEvent())
textGUI.addWindowAndWait(window)
CountDownLatch latch = new CountDownLatch(1)
Thread stopper = new Thread({
core.shutdown()
latch.countDown()
} as Runnable)
WaitingDialog waitingForShutdown = new WaitingDialog("MuWire is shutting down","Please wait")
waitingForShutdown.setHints([Window.Hint.CENTERED])
waitingForShutdown.showDialog(textGUI, false)
stopper.start()
while(latch.getCount() > 0) {
textGUI.updateScreen()
Thread.sleep(10)
}
waitingForShutdown.close()
screen.stopScreen()
System.exit(0)
}
}

View File

@@ -0,0 +1,25 @@
package com.muwire.clilanterna
class CliSettings {
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean clearUploads
CliSettings(Properties props) {
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","true"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads", "false"))
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads", "false"))
}
void write(OutputStream os) {
Properties props = new Properties()
props.with {
setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
setProperty("clearUploads", String.valueOf(clearUploads))
store(os, "CLI Properties")
}
}
}

View File

@@ -0,0 +1,67 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.download.Downloader
class DownloadDetailsView extends BasicWindow {
private final Downloader downloader
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
private Label knownSources, activeSources, donePieces
DownloadDetailsView(Downloader downloader) {
super("Download details for "+downloader.file.getName())
this.downloader = downloader
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(2))
knownSources = new Label("0")
activeSources = new Label("0")
donePieces = new Label("0")
refresh()
Button refreshButton = new Button("Refresh",{refresh()})
Button closeButton = new Button("Close", {close()})
contentPanel.with {
addComponent(new Label("Target Location"), layoutData)
addComponent(new Label(downloader.file.getAbsolutePath()), layoutData)
addComponent(new Label("Piece Size"), layoutData)
addComponent(new Label(String.valueOf(downloader.pieceSize)), layoutData)
addComponent(new Label("Total Pieces"), layoutData)
addComponent(new Label(String.valueOf(downloader.nPieces)), layoutData)
addComponent(new Label("Done Pieces"), layoutData)
addComponent(donePieces, layoutData)
addComponent(new Label("Known Sources"), layoutData)
addComponent(knownSources, layoutData)
addComponent(new Label("Active Sources"), layoutData)
addComponent(activeSources, layoutData)
addComponent(refreshButton, layoutData)
addComponent(closeButton, layoutData)
}
setComponent(contentPanel)
}
private void refresh() {
int done = downloader.donePieces()
int known = downloader.activeWorkers.size()
int active = downloader.activeWorkers()
knownSources.setText(String.valueOf(known))
activeSources.setText(String.valueOf(active))
donePieces.setText(String.valueOf(done))
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.clilanterna
import com.muwire.core.download.Downloader
class DownloaderWrapper {
final Downloader downloader
DownloaderWrapper(Downloader downloader) {
this.downloader = downloader
}
@Override
public String toString() {
downloader.file.getName()
}
}

View File

@@ -0,0 +1,100 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileDownloadedEvent
import net.i2p.data.DataHelper
class DownloadsModel {
private final TextGUIThread guiThread
private final Core core
private final CliSettings props
private final List<Downloader> downloaders = new ArrayList<>()
private final TableModel model = new TableModel("Name", "Status", "Progress", "Speed", "ETA")
private long lastRetryTime
DownloadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
this.guiThread = guiThread
this.core = core
this.props = props
core.eventBus.register(DownloadStartedEvent.class, this)
Timer timer = new Timer(true)
Runnable guiRunnable = {
refreshModel()
resumeDownloads()
}
timer.schedule({
if (core.shutdown.get())
return
guiThread.invokeLater(guiRunnable)
} as TimerTask, 1000,1000)
}
void onDownloadStartedEvent(DownloadStartedEvent e) {
guiThread.invokeLater({
downloaders.add(e.downloader)
refreshModel()
})
}
private void refreshModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
if (props.clearCancelledDownloads) {
downloaders.removeAll { it.cancelled }
}
if (props.clearFinishedDownloads) {
downloaders.removeAll { it.getCurrentState() == Downloader.DownloadState.FINISHED }
}
downloaders.each {
String status = it.getCurrentState().toString()
int speedInt = it.speed()
String speed = DataHelper.formatSize2Decimal(speedInt, false) + "B/sec"
int pieces = it.nPieces
int done = it.donePieces()
int percent = -1
if (pieces != 0)
percent = (done * 100 / pieces)
String totalSize = DataHelper.formatSize2Decimal(it.length, false) + "B"
String progress = (String.format("%2d", percent) + "% of ${totalSize}".toString())
String ETA
if (speedInt == 0)
ETA = "Unknown"
else {
long remaining = (pieces - done) * it.pieceSize / speedInt
ETA = DataHelper.formatDuration(remaining * 1000)
}
model.addRow([new DownloaderWrapper(it), status, progress, speed, ETA])
}
}
private void resumeDownloads() {
int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval == 0)
return
retryInterval *= 1000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
downloaders.each {
def state = it.getCurrentState()
if (state == Downloader.DownloadState.FAILED || state == Downloader.DownloadState.DOWNLOADING)
it.resume()
}
}
}
}

View File

@@ -0,0 +1,94 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.download.Downloader
import com.muwire.core.download.UIDownloadCancelledEvent
class DownloadsView extends BasicWindow {
private final Core core
private final DownloadsModel model
private final TextGUI textGUI
private final Table table
DownloadsView(Core core, DownloadsModel model, TextGUI textGUI, TerminalSize terminalSize) {
this.core = core
this.model = model
this.textGUI = textGUI
setHints([Window.Hint.EXPANDED])
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Name","Status","Progress","Speed","ETA")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button clearButton = new Button("Clear Done",{clearDone()})
buttonsPanel.addComponent(clearButton, layoutData)
Button closeButton = new Button("Close",{close()})
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
closeButton.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
Downloader downloader = row[0].downloader
Window prompt = new BasicWindow("Kill Download?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button killDownload = new Button("Kill Download", {
downloader.cancel()
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
MessageDialog.showMessageDialog(textGUI, "Download Killed", downloader.file.getName()+ " has been killed", MessageDialogButton.OK)
})
Button viewDetails = new Button("View Details", {
textGUI.addWindowAndWait(new DownloadDetailsView(downloader))
})
Button close = new Button("Close", {
prompt.close()
})
contentPanel.addComponent(killDownload,layoutData)
contentPanel.addComponent(viewDetails, layoutData)
contentPanel.addComponent(close, layoutData)
prompt.setComponent(contentPanel)
close.takeFocus()
textGUI.addWindowAndWait(prompt)
}
private void clearDone() {
model.downloaders.removeAll {
def state = it.getCurrentState()
state == Downloader.DownloadState.CANCELLED || state == Downloader.DownloadState.FINISHED
}
}
}

View File

@@ -0,0 +1,100 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.trust.TrustSubscriptionEvent
import net.i2p.data.DataHelper
class FilesModel {
private final TextGUIThread guiThread
private final Core core
private final List<SharedFile> sharedFiles = new ArrayList<>()
private final TableModel model = new TableModel("Name","Size","Comment","Certified","Search Hits","Downloaders")
FilesModel(TextGUIThread guiThread, Core core) {
this.guiThread = guiThread
this.core = core
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
Runnable refreshModel = {refreshModel()}
Timer timer = new Timer(true)
timer.schedule({
guiThread.invokeLater(refreshModel)
} as TimerTask, 1000,1000)
}
void onFileLoadedEvent(FileLoadedEvent e) {
guiThread.invokeLater {
sharedFiles.add(e.loadedFile)
}
}
void onFileHashedEvent(FileHashedEvent e) {
guiThread.invokeLater {
if (e.sharedFile != null)
sharedFiles.add(e.sharedFile)
}
}
void onFileUnsharedEvent(FileUnsharedEvent e) {
guiThread.invokeLater {
sharedFiles.remove(e.unsharedFile)
}
}
private void refreshModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
sharedFiles.each {
long size = it.getCachedLength()
boolean comment = it.comment != null
boolean certified = core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
String hits = String.valueOf(it.getHits())
String downloaders = String.valueOf(it.getDownloaders().size())
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
}
}
private void sort(SortType type) {
Comparator<SharedFile> chosen
switch(type) {
case SortType.NAME_ASC : chosen = NAME_ASC; break
case SortType.NAME_DESC : chosen = NAME_DESC; break
case SortType.SIZE_ASC : chosen = SIZE_ASC; break
case SortType.SIZE_DESC : chosen = SIZE_DESC; break
}
Collections.sort(sharedFiles, chosen)
}
private static final Comparator<SharedFile> NAME_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
a.getFile().getName().compareTo(b.getFile().getName())
}
}
private static final Comparator<SharedFile> NAME_DESC = NAME_ASC.reversed()
private static final Comparator<SharedFile> SIZE_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
Long.compare(a.getCachedLength(), b.getCachedLength())
}
}
private static final Comparator<SharedFile> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@@ -0,0 +1,144 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.FileDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
class FilesView extends BasicWindow {
private final FilesModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
FilesView(FilesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
super("Shared Files")
this.model = model
this.core = core
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
table = new Table("Name","Size","Comment","Certified","Search Hits","Downloaders")
table.setCellSelection(false)
table.setTableModel(model.model)
table.setSelectAction({rowSelected()})
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(5))
Button shareFile = new Button("Share File", {shareFile()})
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
Button sort = new Button("Sort...",{sort()})
Button close = new Button("Close", {close()})
buttonsPanel.with {
addComponent(shareFile, layoutData)
addComponent(shareDirectory, layoutData)
addComponent(unshareDirectory, layoutData)
addComponent(sort, layoutData)
addComponent(close, layoutData)
}
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
close.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
SharedFile sf = row[0].sharedFile
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
Button unshareButton = new Button("Unshare", {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
} )
Button addCommentButton = new Button("Add Comment", {
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
textGUI.addWindowAndWait(view)
})
Button certifyButton = new Button("Certify", {
core.eventBus.publish(new UICreateCertificateEvent(sharedFile : sf))
MessageDialog.showMessageDialog(textGUI, "Certificate Created", "Certificate has been issued", MessageDialogButton.OK)
})
Button closeButton = new Button("Close", {prompt.close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
contentPanel.addComponent(unshareButton, layoutData)
contentPanel.addComponent(addCommentButton, layoutData)
contentPanel.addComponent(certifyButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void shareFile() {
TerminalSize terminalSize = new TerminalSize(terminalSize.getColumns() - 10, terminalSize.getRows() - 10)
FileDialog fileDialog = new FileDialog("Share File", "Select a file to share", "Share", terminalSize, false, null)
File f = fileDialog.showDialog(textGUI)
f = f.getCanonicalFile()
core.eventBus.publish(new FileSharedEvent(file : f))
MessageDialog.showMessageDialog(textGUI, "File Shared", f.getName()+" has been shared", MessageDialogButton.OK)
}
private void shareDirectory() {
String directoryName = TextInputDialog.showDialog(textGUI, "Share a directory", "Enter the directory to share", "")
if (directoryName == null)
return
File directory = new File(directoryName)
directory = directory.getCanonicalFile()
core.eventBus.publish(new FileSharedEvent(file : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Shared", directory.getName()+" has been shared", MessageDialogButton.OK)
}
private void unshareDirectory() {
String directoryName = TextInputDialog.showDialog(textGUI, "Unshare a directory", "Enter the directory to unshare", "")
if (directoryName == null)
return
File directory = new File(directoryName)
directory = directory.getCanonicalFile()
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -0,0 +1,321 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalPosition
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.BorderLayout
import com.googlecode.lanterna.gui2.Borders
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.Panels
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.screen.Screen
import com.googlecode.lanterna.gui2.TextBox
import com.muwire.core.Core
import com.muwire.core.DownloadedFile
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.update.UpdateDownloadedEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
class MainWindowView extends BasicWindow {
private final Core core
private final TextGUI textGUI
private final Screen screen
private final TextBox searchTextBox
private final DownloadsModel downloadsModel
private final UploadsModel uploadsModel
private final FilesModel filesModel
private final TrustModel trustModel
private final ChatConsoleModel chatModel
private final Label connectionCount, incoming, outgoing
private final Label known, failing, hopeless
private final Label sharedFiles
private final Label timesBrowsed
private final Label updateStatus
private final Label usedRam, totalRam, maxRam
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
super(title);
this.core = core
this.textGUI = textGUI
this.screen = screen
downloadsModel = new DownloadsModel(textGUI.getGUIThread(),core, props)
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
filesModel = new FilesModel(textGUI.getGUIThread(),core)
trustModel = new TrustModel(textGUI.getGUIThread(), core)
chatModel = new ChatConsoleModel(core, textGUI.getGUIThread())
if (core.muOptions.startChatServer)
core.chatServer.start()
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
setComponent(contentPanel)
BorderLayout borderLayout = new BorderLayout()
contentPanel.setLayoutManager(borderLayout)
Panel buttonsPanel = new Panel()
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
GridLayout gridLayout = new GridLayout(8)
buttonsPanel.setLayoutManager(gridLayout)
searchTextBox = new TextBox(new TerminalSize(40, 1))
Button searchButton = new Button("Search", { search() })
Button downloadsButton = new Button("Downloads", {download()})
Button uploadsButton = new Button("Uploads", {upload()})
Button filesButton = new Button("Files", { files() })
Button trustButton = new Button("Trust", {trust()})
Button chatButton = new Button("Chat", {chat()})
Button quitButton = new Button("Quit", {close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
buttonsPanel.with {
addComponent(searchTextBox, layoutData)
addComponent(searchButton, layoutData)
addComponent(downloadsButton, layoutData)
addComponent(uploadsButton, layoutData)
addComponent(filesButton, layoutData)
addComponent(trustButton, layoutData)
addComponent(chatButton, layoutData)
addComponent(quitButton, layoutData)
}
Panel bottomPanel = new Panel()
contentPanel.addComponent(bottomPanel, BorderLayout.Location.BOTTOM)
BorderLayout bottomLayout = new BorderLayout()
bottomPanel.setLayoutManager(bottomLayout)
Label persona = new Label(core.me.getHumanReadableName())
bottomPanel.addComponent(persona, BorderLayout.Location.LEFT)
Panel connectionsPanel = new Panel()
connectionsPanel.setLayoutManager(new GridLayout(2))
Label connections = new Label("Connections:")
connectionsPanel.addComponent(connections, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
connectionCount = new Label("0")
connectionsPanel.addComponent(connectionCount, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER))
bottomPanel.addComponent(connectionsPanel, BorderLayout.Location.RIGHT)
Panel centralPanel = new Panel()
centralPanel.setLayoutManager(new GridLayout(1))
contentPanel.addComponent(centralPanel, BorderLayout.Location.CENTER)
Panel statusPanel = new Panel()
statusPanel.setLayoutManager(new GridLayout(2))
statusPanel.withBorder(Borders.doubleLine("Stats"))
centralPanel.addComponent(statusPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, true))
incoming = new Label("0")
outgoing = new Label("0")
known = new Label("0")
failing = new Label("0")
hopeless = new Label("0")
sharedFiles = new Label("0")
timesBrowsed = new Label("0")
updateStatus = new Label("Unknown")
usedRam = new Label("0")
maxRam = new Label("0")
totalRam = new Label("0")
statusPanel.with {
addComponent(new Label("Incoming Connections: "), layoutData)
addComponent(incoming, layoutData)
addComponent(new Label("Outgoing Connections: "), layoutData)
addComponent(outgoing, layoutData)
addComponent(new Label("Known Hosts: "), layoutData)
addComponent(known, layoutData)
addComponent(new Label("Failing Hosts: "), layoutData)
addComponent(failing, layoutData)
addComponent(new Label("Hopeless Hosts: "), layoutData)
addComponent(hopeless, layoutData)
addComponent(new Label("Shared Files: "), layoutData)
addComponent(sharedFiles, layoutData)
addComponent(new Label("Times Browsed: "), layoutData)
addComponent(timesBrowsed, layoutData)
addComponent(new Label("Update Status: "), layoutData)
addComponent(updateStatus, layoutData)
addComponent(new Label("Java Version: "), layoutData)
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
addComponent(new Label("Used Memory: "), layoutData)
addComponent(usedRam, layoutData)
addComponent(new Label("Total Memory: "), layoutData)
addComponent(totalRam, layoutData)
addComponent(new Label("Maximum Memory: "), layoutData)
addComponent(maxRam, layoutData)
}
refreshStats()
searchButton.takeFocus()
core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(HostDiscoveredEvent.class, this)
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(FileDownloadedEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
core.eventBus.register(UpdateDownloadedEvent.class, this)
}
void onConnectionEvent(ConnectionEvent e) {
textGUI.getGUIThread().invokeLater {
connectionCount.setText(String.valueOf(core.connectionManager.connections.size()))
refreshStats()
}
}
void onHostDiscoveredEvent(HostDiscoveredEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileLoadedEvent(FileLoadedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileHashedEvent(FileHashedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileUnsharedEvent(FileUnsharedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
textGUI.getGUIThread().invokeLater {
refreshStats()
}
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
textGUI.getGUIThread().invokeLater {
String label = "$e.version is available with hash $e.infoHash"
updateStatus.setText(label)
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
void onUpdateDownloadedEvent(UpdateDownloadedEvent e) {
textGUI.getGUIThread().invokeLater {
String label = "$e.version downloaded"
updateStatus.setText(label)
String message = "MuWire version $e.version has been downloaded. Show details?."
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
private TerminalSize sizeForTables() {
TerminalSize full = screen.getTerminalSize()
return new TerminalSize(full.getColumns(), full.getRows() - 10)
}
private void search() {
String query = searchTextBox.getText()
query = query.trim()
if (query.length() == 0)
return
if (query.length() > 128)
query = query.substring(0, 128)
SearchModel model = new SearchModel(query, core, textGUI.getGUIThread())
textGUI.addWindowAndWait(new SearchView(model,core, textGUI, sizeForTables()))
}
private void download() {
textGUI.addWindowAndWait(new DownloadsView(core, downloadsModel, textGUI, sizeForTables()))
}
private void upload() {
textGUI.addWindowAndWait(new UploadsView(uploadsModel, sizeForTables()))
}
private void files() {
textGUI.addWindowAndWait(new FilesView(filesModel, textGUI, core, sizeForTables()))
}
private void trust() {
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
}
private void chat() {
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
}
private void refreshStats() {
int inCon = 0
int outCon = 0
core.connectionManager.getConnections().each {
if (it.isIncoming())
inCon++
else
outCon++
}
int knownHosts = core.hostCache.hosts.size()
int failingHosts = core.hostCache.countFailingHosts()
int hopelessHosts = core.hostCache.countHopelessHosts()
int shared = core.fileManager.fileToSharedFile.size()
int browsed = core.connectionAcceptor.browsed
long freeMemL = Runtime.getRuntime().freeMemory()
long totalMemL = Runtime.getRuntime().totalMemory()
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
String maxMem
long maxMemL = Runtime.getRuntime().maxMemory()
if (maxMemL >= Long.MAX_VALUE / 2)
maxMem = "Unlimited"
else
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
incoming.setText(String.valueOf(inCon))
outgoing.setText(String.valueOf(outCon))
known.setText(String.valueOf(knownHosts))
failing.setText(String.valueOf(failingHosts))
hopeless.setText(String.valueOf(hopelessHosts))
sharedFiles.setText(String.valueOf(shared))
timesBrowsed.setText(String.valueOf(browsed))
usedRam.setText(usedMem)
totalRam.setText(totalMem)
maxRam.setText(maxMem)
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.clilanterna
import com.muwire.core.Persona
class PersonaWrapper {
private final Persona persona
PersonaWrapper(Persona persona) {
this.persona = persona
}
@Override
public String toString() {
persona.getHumanReadableName()
}
}

View File

@@ -0,0 +1,21 @@
package com.muwire.clilanterna
import com.muwire.core.search.UIResultEvent
class ResultComparators {
public static final Comparator<UIResultEvent> NAME_ASC = new Comparator<UIResultEvent>() {
public int compare(UIResultEvent a, UIResultEvent b) {
a.name.compareTo(b.name)
}
}
public static final Comparator<UIResultEvent> NAME_DESC = NAME_ASC.reversed()
public static final Comparator<UIResultEvent> SIZE_ASC = new Comparator<UIResultEvent>() {
public int compare(UIResultEvent a, UIResultEvent b) {
Long.compare(a.size, b.size)
}
}
public static final Comparator<UIResultEvent> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@@ -0,0 +1,48 @@
package com.muwire.clilanterna
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import com.googlecode.lanterna.gui2.table.TableModel
class ResultsModel {
private final UIResultBatchEvent results
final TableModel model
final Map<String, UIResultEvent> rootToResult = new HashMap<>()
ResultsModel(UIResultBatchEvent results) {
this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
updateModel()
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
Arrays.sort(results.results, chosen)
updateModel()
}
private void updateModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot())
String sources = String.valueOf(it.sources.size())
String comment = String.valueOf(it.comment != null)
model.addRow(it.name, size, infoHash, sources, comment, it.certificates)
rootToResult.put(infoHash, it)
}
}
}

View File

@@ -0,0 +1,124 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
class ResultsView extends BasicWindow {
private final ResultsModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
ResultsView(ResultsModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
super(model.results.results[0].sender.getHumanReadableName() + " Results")
this.model = model
this.core = core
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Name","Size","Hash","Sources","Comment","Certificates")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...",{sort()})
buttonsPanel.addComponent(sortButton)
Button closeButton = new Button("Close", {close()})
buttonsPanel.addComponent(closeButton)
contentPanel.addComponent(buttonsPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def rows = model.model.getRow(selectedRow)
boolean comment = Boolean.parseBoolean(rows[4])
boolean certificates = rows[5] > 0
if (comment || certificates) {
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Window prompt = new BasicWindow("Download Or View Comment/Certificates")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
Button downloadButton = new Button("Download", {download(rows[2])})
contentPanel.addComponent(downloadButton, layoutData)
if (comment) {
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
contentPanel.addComponent(viewButton, layoutData)
}
if (certificates) {
Button certsButton = new Button("View Certificates", {viewCertificates(rows[2])})
contentPanel.addComponent(certsButton, layoutData)
}
Button closeButton = new Button("Cancel", {prompt.close()})
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
downloadButton.takeFocus()
textGUI.addWindowAndWait(prompt)
} else {
download(rows[2])
}
}
private void download(String infohash) {
UIResultEvent result = model.rootToResult[infohash]
def file = new File(core.muOptions.downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : [result], sources : result.sources,
target : file, sequential : false))
MessageDialog.showMessageDialog(textGUI, "Download Started", "Started download of "+result.name, MessageDialogButton.OK)
}
private void viewComment(String infohash) {
UIResultEvent result = model.rootToResult[infohash]
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
textGUI.addWindowAndWait(view)
}
private void viewCertificates(String infohash) {
UIResultEvent result = model.rootToResult[infohash]
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -0,0 +1,87 @@
package com.muwire.clilanterna
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.SplitPattern
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.nio.charset.StandardCharsets
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
class SearchModel {
private final TextGUIThread guiThread
private final String query
private final Core core
final TableModel model
private final Map<Persona, UIResultBatchEvent> resultsPerSender = new HashMap<>()
SearchModel(String query, Core core, TextGUIThread guiThread) {
this.query = query
this.core = core
this.guiThread = guiThread
this.model = new TableModel("Sender","Results","Browse","Trust")
core.eventBus.register(UIResultBatchEvent.class, this)
boolean hashSearch = false
byte [] root = null
if (query.length() == 44 && query.indexOf(" ") < 0) {
try {
root = Base64.decode(query)
hashSearch = true
} catch (Exception e) {
// not hash search
}
}
def searchEvent
byte [] payload
UUID uuid = UUID.randomUUID()
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
payload = root
} else {
def nonEmpty = SplitPattern.termify(query)
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
}
void unregister() {
core.eventBus.unregister(UIResultBatchEvent.class, this)
}
void onUIResultBatchEvent(UIResultBatchEvent e) {
guiThread.invokeLater {
Persona sender = e.results[0].sender
resultsPerSender.put(sender, e)
String browse = String.valueOf(e.results[0].browse)
String results = String.valueOf(e.results.length)
String trust = core.trustService.getLevel(sender.destination).toString()
model.addRow([new PersonaWrapper(sender), results, browse, trust])
}
}
}

View File

@@ -0,0 +1,113 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
class SearchView extends BasicWindow {
private final Core core
private final SearchModel model
private final Table table
private final TextGUI textGUI
private final TerminalSize terminalSize
SearchView(SearchModel model, Core core, TextGUI textGUI, TerminalSize terminalSize) {
super(model.query)
this.core = core
this.model = model
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Sender","Results","Browse","Trust")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Button closeButton = new Button("Close", {
model.unregister()
close()
})
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def rows = model.model.getRow(selectedRow)
Persona persona = rows[0].persona
boolean browse = Boolean.parseBoolean(rows[2])
Window prompt = new BasicWindow("Show Or Browse "+rows[0]+"?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(6))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button showResults = new Button("Show Results", {
showResults(persona)
})
Button browseHost = new Button("Browse Host", {
BrowseModel model = new BrowseModel(persona, core, textGUI.getGUIThread())
BrowseView view = new BrowseView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
})
Button trustHost = new Button("Trust",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + " has been marked trusted",
MessageDialogButton.OK)
})
Button neutralHost = new Button("Neutral",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + " has been marked neutral",
MessageDialogButton.OK)
})
Button distrustHost = new Button("Distrust", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + " has been marked distrusted",
MessageDialogButton.OK)
})
Button closePrompt = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(showResults, layoutData)
if (browse)
addComponent(browseHost, layoutData)
addComponent(trustHost, layoutData)
addComponent(neutralHost, layoutData)
addComponent(distrustHost, layoutData)
addComponent(closePrompt, layoutData)
}
prompt.setComponent(contentPanel)
showResults.takeFocus()
textGUI.addWindowAndWait(prompt)
}
private void showResults(Persona persona) {
def results = model.resultsPerSender.get(persona)
ResultsModel resultsModel = new ResultsModel(results)
ResultsView resultsView = new ResultsView(resultsModel, core, textGUI, terminalSize)
textGUI.addWindowAndWait(resultsView)
}
}

View File

@@ -0,0 +1,16 @@
package com.muwire.clilanterna
import com.muwire.core.SharedFile
class SharedFileWrapper {
private final SharedFile sharedFile
SharedFileWrapper(SharedFile sharedFile) {
this.sharedFile = sharedFile
}
@Override
public String toString() {
sharedFile.getCachedPath()
}
}

View File

@@ -0,0 +1,57 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
class SortPrompt extends BasicWindow {
private final TextGUI textGUI
private SortType type
SortPrompt(TextGUI textGUI) {
super("Select what to sort by")
this.textGUI = textGUI
}
SortType prompt() {
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(5))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button nameAsc = new Button("Name (ascending)",{
type = SortType.NAME_ASC
close()
})
Button nameDesc = new Button("Name (descending)",{
type = SortType.NAME_DESC
close()
})
Button sizeAsc = new Button("Size (ascending)",{
type = SortType.SIZE_ASC
close()
})
Button sizeDesc = new Button("Size (descending)",{
type = SortType.SIZE_DESC
close()
})
Button close = new Button("Cancel",{close()})
contentPanel.with {
addComponent(nameAsc, layoutData)
addComponent(nameDesc, layoutData)
addComponent(sizeAsc, layoutData)
addComponent(sizeDesc, layoutData)
addComponent(close, layoutData)
}
setComponent(contentPanel)
textGUI.addWindowAndWait(this)
type
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.clilanterna;
public enum SortType {
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
}

View File

@@ -0,0 +1,7 @@
package com.muwire.clilanterna
import com.muwire.core.trust.TrustService
class TrustEntryWrapper {
TrustService.TrustEntry entry
}

View File

@@ -0,0 +1,49 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.trust.RemoteTrustList
import com.muwire.core.trust.TrustEvent
class TrustListModel {
private final TextGUIThread guiThread
private final RemoteTrustList trustList
private final Core core
private final TableModel trustedTableModel, distrustedTableModel
TrustListModel(RemoteTrustList trustList, Core core) {
this.trustList = trustList
this.core = core
trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
refreshModels()
core.eventBus.register(TrustEvent.class, this)
}
void onTrustEvent(TrustEvent e) {
guiThread.invokeLater {
refreshModels()
}
}
private void refreshModels() {
int trustRows = trustedTableModel.getRowCount()
trustRows.times { trustedTableModel.removeRow(0) }
int distrustRows = distrustedTableModel.getRowCount()
distrustRows.times { distrustedTableModel.removeRow(0) }
trustList.good.each {
trustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
}
trustList.bad.each {
distrustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
}
}
void unregister() {
core.eventBus.unregister(TrustEvent.class, this)
}
}

View File

@@ -0,0 +1,121 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
class TrustListView extends BasicWindow {
private final TrustListModel model
private final TextGUI textGUI
private final Core core
private final TerminalSize terminalSize
private final Table trusted, distrusted
TrustListView(TrustListModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
this.model = model
this.textGUI = textGUI
this.core = core
this.terminalSize = terminalSize
int tableSize = terminalSize.getRows() - 10
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
Label nameLabel = new Label("Trust list for "+model.trustList.persona.getHumanReadableName())
Label lastUpdatedLabel = new Label("Last updated "+new Date(model.trustList.timestamp))
contentPanel.addComponent(nameLabel, layoutData)
contentPanel.addComponent(lastUpdatedLabel, layoutData)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted User","Reason","Your Trust")
trusted.with {
setCellSelection(false)
setTableModel(model.trustedTableModel)
setVisibleRows(tableSize)
}
trusted.setSelectAction({ actionsForUser(true) })
topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted User","Reason", "Your Trust")
distrusted.with {
setCellSelection(false)
setTableModel(model.distrustedTableModel)
setVisibleRows(tableSize)
}
distrusted.setSelectAction({actionsForUser(false)})
topPanel.addComponent(distrusted, layoutData)
Button closeButton = new Button("Close",{close()})
contentPanel.addComponent(topPanel, layoutData)
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
private void actionsForUser(boolean trustedUser) {
def table = trustedUser ? trusted : distrusted
def model = trustedUser ? model.trustedTableModel : model.distrustedTableModel
int selectedRow = table.getSelectedRow()
def row = model.getRow(selectedRow)
Persona persona = row[0].persona
Window prompt = new BasicWindow("Actions for "+persona.getHumanReadableName())
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button trustButton = new Button("Trust",{
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK)
})
Button neutralButton = new Button("Neutral",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
MessageDialogButton.OK)
})
Button distrustButton = new Button("Distrust",{
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK)
})
Button closeButton = new Button("Close",{prompt.close()})
contentPanel.with {
addComponent(trustButton,layoutData)
addComponent(neutralButton, layoutData)
addComponent(distrustButton, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.clilanterna
import com.muwire.core.trust.RemoteTrustList
class TrustListWrapper {
private final RemoteTrustList trustList
TrustListWrapper(RemoteTrustList trustList) {
this.trustList = trustList
}
@Override
public String toString() {
trustList.persona.getHumanReadableName()
}
}

View File

@@ -0,0 +1,78 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustSubscriptionEvent
import com.muwire.core.trust.TrustSubscriptionUpdatedEvent
class TrustModel {
private final TextGUIThread guiThread
private final Core core
private final TableModel modelTrusted, modelDistrusted, modelSubscriptions
TrustModel(TextGUIThread guiThread, Core core) {
this.guiThread = guiThread
this.core = core
modelTrusted = new TableModel("Trusted Users","Reason")
modelDistrusted = new TableModel("Distrusted Users","Reason")
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(AllFilesLoadedEvent.class, this)
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
}
void onTrustEvent(TrustEvent e) {
guiThread.invokeLater {
refreshModels()
}
}
void onTrustSubscriptionUpdatedEvent(TrustSubscriptionUpdatedEvent e) {
guiThread.invokeLater {
refreshModels()
}
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
guiThread.invokeLater {
refreshModels()
}
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
}
}
private void refreshModels() {
int trustedRows = modelTrusted.getRowCount()
trustedRows.times { modelTrusted.removeRow(0) }
int distrustedRows = modelDistrusted.getRowCount()
distrustedRows.times { modelDistrusted.removeRow(0) }
int subsRows = modelSubscriptions.getRowCount()
subsRows.times { modelSubscriptions.removeRow(0) }
core.trustService.good.values().each {
modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
}
core.trustService.bad.values().each {
modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
}
core.trustSubscriber.remoteTrustLists.values().each {
def name = new TrustListWrapper(it)
String trusted = String.valueOf(it.good.size())
String distrusted = String.valueOf(it.bad.size())
String status = it.status
String lastUpdated = it.timestamp == 0 ? "Never" : new Date(it.timestamp)
modelSubscriptions.addRow(name, trusted, distrusted, status, lastUpdated)
}
}
}

View File

@@ -0,0 +1,207 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustSubscriptionEvent
class TrustView extends BasicWindow {
private final TrustModel model
private final TextGUI textGUI
private final Core core
private final TerminalSize terminalSize
private final Table trusted, distrusted, subscriptions
TrustView(TrustModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
this.model = model
this.textGUI = textGUI
this.core = core
this.terminalSize = terminalSize
int tableSize = (terminalSize.getRows() / 2 - 10).toInteger()
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted Users","Reason")
trusted.setCellSelection(false)
trusted.setSelectAction({trustedActions()})
trusted.setTableModel(model.modelTrusted)
trusted.setVisibleRows(tableSize)
topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted users","Reason")
distrusted.setCellSelection(false)
distrusted.setSelectAction({distrustedActions()})
distrusted.setTableModel(model.modelDistrusted)
distrusted.setVisibleRows(tableSize)
topPanel.addComponent(distrusted, layoutData)
Panel bottomPanel = new Panel()
bottomPanel.setLayoutManager(new GridLayout(1))
Label tableName = new Label("Trust List Subscriptions")
bottomPanel.addComponent(tableName, layoutData)
subscriptions = new Table("Name","Trusted","Distrusted","Status","Last Updated")
subscriptions.setCellSelection(false)
subscriptions.setSelectAction({trustListActions()})
subscriptions.setTableModel(model.modelSubscriptions)
subscriptions.setVisibleRows(tableSize)
bottomPanel.addComponent(subscriptions, layoutData)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(topPanel, layoutData)
contentPanel.addComponent(bottomPanel, layoutData)
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
private void trustedActions() {
int selectedRow = trusted.getSelectedRow()
def row = model.modelTrusted.getRow(selectedRow)
Persona persona = row[0].persona
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button subscribe = new Button("Subscribe", {
core.muOptions.trustSubscriptions.add(persona)
saveMuSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
MessageDialog.showMessageDialog(textGUI, "Subscribed", "Subscribed from trust list of " + persona.getHumanReadableName(),
MessageDialogButton.OK)
model.refreshModels()
})
Button markNeutral = new Button("Mark Neutral", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
MessageDialogButton.OK)
})
Button markDistrusted = new Button("Mark Distrusted", {
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK)
})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(subscribe, layoutData)
addComponent(markNeutral, layoutData)
addComponent(markDistrusted, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void distrustedActions() {
int selectedRow = distrusted.getSelectedRow()
def row = model.modelDistrusted.getRow(selectedRow)
Persona persona = row[0].persona
Window prompt = new BasicWindow("Change Trust For "+persona.getHumanReadableName())
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button markNeutral = new Button("Mark Neutral", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.NEUTRAL))
MessageDialog.showMessageDialog(textGUI, "Marked Neutral", persona.getHumanReadableName() + "has been marked neutral",
MessageDialogButton.OK)
})
Button markDistrusted = new Button("Mark Trusted", {
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK)
})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(markDistrusted, layoutData)
addComponent(markNeutral, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void trustListActions() {
int selectedRow = subscriptions.getSelectedRow()
def row = model.modelSubscriptions.getRow(selectedRow)
def trustList = row[0].trustList
Persona persona = trustList.persona
Window prompt = new BasicWindow("Trust List Actions")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(4))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button reviewButton = new Button("Review",{review(trustList)})
Button updateButton = new Button("Update",{
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : true))
MessageDialog.showMessageDialog(textGUI, "Updating...", "Trust list will update soon", MessageDialogButton.OK)
})
Button unsubscribeButton = new Button("Unsubscribe", {
core.muOptions.trustSubscriptions.remove(persona)
saveMuSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : persona, subscribe : false))
MessageDialog.showMessageDialog(textGUI, "Unsubscribed", "Unsubscribed from trust list of " + persona.getHumanReadableName(),
MessageDialogButton.OK)
model.refreshModels()
})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.with {
addComponent(reviewButton, layoutData)
addComponent(updateButton, layoutData)
addComponent(unsubscribeButton, layoutData)
addComponent(closeButton, layoutData)
}
prompt.setComponent(contentPanel)
textGUI.addWindowAndWait(prompt)
}
private void review(def trustList) {
TrustListModel model = new TrustListModel(trustList, core)
TrustListView view = new TrustListView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
model.unregister()
}
private void saveMuSettings() {
File settingsFile = new File(core.home,"MuWire.properties")
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
}
}

View File

@@ -0,0 +1,34 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.Window
class UpdateTextView extends BasicWindow {
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
UpdateTextView(String text, TerminalSize terminalSize) {
super("Update Details")
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
}

View File

@@ -0,0 +1,107 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
import com.muwire.core.upload.Uploader
import net.i2p.data.DataHelper
class UploadsModel {
private final TextGUIThread guiThread
private final Core core
private CliSettings props
private final List<UploaderWrapper> uploaders = new ArrayList<>()
private final TableModel model = new TableModel("Name","Progress","Downloader","Remote Pieces", "Speed")
UploadsModel(TextGUIThread guiThread, Core core, CliSettings props) {
this.guiThread = guiThread
this.core = core
this.props = props
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
Timer timer = new Timer(true)
Runnable refreshModel = {refreshModel()}
timer.schedule({
guiThread.invokeLater(refreshModel)
} as TimerTask, 1000, 1000)
}
void onUploadEvent(UploadEvent e) {
guiThread.invokeLater {
UploaderWrapper found = null
uploaders.each {
if (it.uploader == e.uploader) {
found = it
return
}
}
if (found != null) {
found.uploader = e.uploader
found.finished = false
} else
uploaders << new UploaderWrapper(uploader : e.uploader)
}
}
void onUploadFinishedEvent(UploadFinishedEvent e) {
guiThread.invokeLater {
uploaders.each {
if (it.uploader == e.uploader) {
it.finished = true
return
}
}
}
}
private void refreshModel() {
int uploadersSize = model.getRowCount()
uploadersSize.times { model.removeRow(0) }
if (props.clearUploads) {
uploaders.removeAll { it.finished }
}
uploaders.each {
String name = it.uploader.getName()
int percent = it.uploader.getProgress()
String percentString = "$percent% of piece".toString()
String downloader = it.uploader.getDownloader()
int pieces = it.uploader.getTotalPieces()
int done = it.uploader.getDonePieces()
if (percent == 100)
done++
int percentTotal = -1
if (pieces != 0)
percentTotal = (done * 100) / pieces
long size = it.uploader.getTotalSize()
String totalSize = ""
if (size > 0)
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
String remotePieces = String.format("%02d", percentTotal) + "% ${totalSize} ($done/$pieces) pcs".toString()
String speed = DataHelper.formatSize2Decimal(it.uploader.speed(), false) + "B/sec"
model.addRow([name, percentString, downloader, remotePieces, speed])
}
}
private static class UploaderWrapper {
Uploader uploader
boolean finished
@Override
public String toString() {
uploader.getName()
}
}
}

View File

@@ -0,0 +1,49 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.table.Table
class UploadsView extends BasicWindow {
private final UploadsModel model
private final Table table
UploadsView(UploadsModel model, TerminalSize terminalSize) {
this.model = model
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
table = new Table("Name","Progress","Downloader","Remote Pieces","Speed")
table.setCellSelection(false)
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button clearDoneButton = new Button("Clear Finished",{
model.uploaders.removeAll { it.finished }
})
Button closeButton = new Button("Close",{close()})
buttonsPanel.addComponent(clearDoneButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
closeButton.takeFocus()
}
}

View File

@@ -0,0 +1,74 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.CertificateFetchEvent
import com.muwire.core.filecert.CertificateFetchStatus
import com.muwire.core.filecert.CertificateFetchedEvent
import com.muwire.core.filecert.UIFetchCertificatesEvent
import com.muwire.core.search.UIResultEvent
class ViewCertificatesModel {
private final UIResultEvent result
private final Core core
private final TextGUIThread guiThread
private final TableModel model = new TableModel("Issuer","Trust Status","File Name","Comment","Timestamp")
private int totalCerts
private Label status
private Label percentage
ViewCertificatesModel(UIResultEvent result, Core core, TextGUIThread guiThread) {
this.result = result
this.core = core
this.guiThread = guiThread
core.eventBus.with {
register(CertificateFetchEvent.class,this)
register(CertificateFetchedEvent.class, this)
publish(new UIFetchCertificatesEvent(host : result.sender, infoHash : result.infohash))
}
}
void unregister() {
core.eventBus.unregister(CertificateFetchEvent.class, this)
core.eventBus.unregister(CertificateFetchedEvent.class, this)
}
void onCertificateFetchEvent(CertificateFetchEvent e) {
guiThread.invokeLater {
status.setText(e.status.toString())
if (e.status == CertificateFetchStatus.FETCHING)
totalCerts = e.count
}
}
void onCertificateFetchedEvent(CertificateFetchedEvent e) {
guiThread.invokeLater {
Date date = new Date(e.certificate.timestamp)
model.addRow(new CertificateWrapper(e.certificate), core.trustService.getLevel(e.certificate.issuer.destination),
e.certificate.name.name, e.certificate.comment != null, date)
String percentageString = ""
if (totalCerts > 0) {
double percentage = Math.round((model.getRowCount() * 100 / totalCerts).toDouble())
percentageString = String.valueOf(percentage) + "%"
}
percentage.setText(percentageString)
}
}
void setStatusLabel(Label status) {
this.status = status
}
void setPercentageLabel(Label percentage) {
this.percentage = percentage
}
}

View File

@@ -0,0 +1,103 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.UIImportCertificateEvent
class ViewCertificatesView extends BasicWindow {
private final ViewCertificatesModel model
private final TextGUI textGUI
private final Core core
private final Table table
private final TerminalSize terminalSize
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
ViewCertificatesView(ViewCertificatesModel model, TextGUI textGUI, Core core, TerminalSize terminalSize) {
super("Certificates")
this.model = model
this.core = core
this.textGUI = textGUI
this.terminalSize = terminalSize
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
Label statusLabel = new Label("")
Label percentageLabel = new Label("")
model.setStatusLabel(statusLabel)
model.setPercentageLabel(percentageLabel)
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
topPanel.addComponent(statusLabel, layoutData)
topPanel.addComponent(percentageLabel, layoutData)
contentPanel.addComponent(topPanel, layoutData)
table = new Table("Issuer","Trust Status","File Name","Comment","Timestamp")
table.with {
setCellSelection(false)
setTableModel(model.model)
setVisibleRows(terminalSize.getRows())
setSelectAction({rowSelected()})
}
contentPanel.addComponent(table, layoutData)
Button closeButton = new Button("Close",{
model.unregister()
close()
})
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
Certificate certificate = row[0].certificate
Window prompt = new BasicWindow("Import Certificate?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
Button importButton = new Button("Import", {importCert(certificate)})
Button viewCommentButton = new Button("View Comment", {viewComment(certificate)})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.addComponent(importButton, layoutData)
if (certificate.comment != null)
contentPanel.addComponent(viewCommentButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
importButton.takeFocus()
textGUI.addWindowAndWait(prompt)
}
private void importCert(Certificate certificate) {
core.eventBus.publish(new UIImportCertificateEvent(certificate : certificate))
MessageDialog.showMessageDialog(textGUI, "Certificate(s) Imported", "", MessageDialogButton.OK)
}
private void viewComment(Certificate certificate) {
ViewCommentView view = new ViewCommentView(certificate.comment.name, "Certificate Comment", terminalSize)
textGUI.addWindowAndWait(view)
}
}

View File

@@ -0,0 +1,39 @@
package com.muwire.clilanterna
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.Window
import com.muwire.core.SharedFile
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class ViewCommentView extends BasicWindow {
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
ViewCommentView(String text, String title, TerminalSize terminalSize) {
super("View Comments For "+title)
setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, layoutData)
setComponent(contentPanel)
}
}

View File

@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
}
}

View File

@@ -35,7 +35,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.4.11")
core = new Core(props, home, "0.5.3")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.4.11")
core = new Core(props, home, "0.5.3")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -1,11 +1,42 @@
apply plugin : 'application'
mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile 'net.i2p:router:0.9.41'
compile 'net.i2p.client:mstreaming:0.9.41'
compile 'net.i2p.client:streaming:0.9.41'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
plugins {
id 'java-library'
id 'maven-publish'
}
dependencies {
api "net.i2p:i2p:${i2pVersion}"
api "net.i2p:router:${i2pVersion}"
api "net.i2p.client:mstreaming:${i2pVersion}"
implementation "net.i2p.client:streaming:${i2pVersion}"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testImplementation 'junit:junit:4.12'
testImplementation 'org.codehaus.groovy:groovy-all:3.0.4'
}
// this is necessary because applying both groovy and java-library doesn't work well
configurations {
apiElements.outgoing.variants {
classes {
artifact file: compileGroovy.destinationDir, builtBy: compileGroovy
}
}
}
configurations.testImplementation {
exclude group:'org.codehaus.groovy', module:'groovy-testng'
}
// publish core to local maven repo for sister projects
publishing {
publications {
muCore(MavenPublication) {
from components.java
}
}
repositories {
mavenLocal()
}
}

View File

@@ -1,13 +0,0 @@
package com.muwire.core
import net.i2p.crypto.SigType
class Constants {
public static final byte PERSONA_VERSION = (byte)1
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
public static final int MAX_HEADER_SIZE = 0x1 << 14
public static final int MAX_HEADERS = 16
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
}

View File

@@ -1,8 +1,19 @@
package com.muwire.core
import com.muwire.core.files.PersisterDoneEvent
import com.muwire.core.files.PersisterFolderService
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import java.util.zip.ZipException
import com.muwire.core.chat.ChatDisconnectionEvent
import com.muwire.core.chat.ChatManager
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.ChatServer
import com.muwire.core.chat.UIConnectChatEvent
import com.muwire.core.chat.UIDisconnectChatEvent
import com.muwire.core.connection.ConnectionAcceptor
import com.muwire.core.connection.ConnectionEstablisher
import com.muwire.core.connection.ConnectionEvent
@@ -12,15 +23,31 @@ import com.muwire.core.connection.I2PAcceptor
import com.muwire.core.connection.I2PConnector
import com.muwire.core.connection.LeafConnectionManager
import com.muwire.core.connection.UltrapeerConnectionManager
import com.muwire.core.download.DownloadHopelessEvent
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.filecert.CertificateClient
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.filecert.UIFetchCertificatesEvent
import com.muwire.core.filecert.UIImportCertificateEvent
import com.muwire.core.filefeeds.FeedClient
import com.muwire.core.filefeeds.FeedFetchEvent
import com.muwire.core.filefeeds.FeedItemFetchedEvent
import com.muwire.core.filefeeds.FeedManager
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.filefeeds.UIFilePublishedEvent
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
import com.muwire.core.filefeeds.UIFeedDeletedEvent
import com.muwire.core.filefeeds.UIFeedUpdateEvent
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileManager
@@ -28,18 +55,29 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.directories.UISyncDirectoryEvent
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
import com.muwire.core.files.directories.WatchedDirectoryConverter
import com.muwire.core.files.directories.WatchedDirectoryManager
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.DirectoryWatcher
import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.mesh.MeshManager
import com.muwire.core.search.BrowseManager
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResponderCache
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
@@ -50,6 +88,7 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.util.MuWireLogManager
import com.muwire.core.content.ContentControlEvent
import com.muwire.core.content.ContentManager
import com.muwire.core.tracker.TrackerResponder
import groovy.util.logging.Log
import net.i2p.I2PAppContext
@@ -57,10 +96,8 @@ import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.streaming.I2PSocketManager
import net.i2p.client.streaming.I2PSocketManagerFactory
import net.i2p.client.streaming.I2PSocketOptions
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType
import net.i2p.data.Destination
import net.i2p.data.PrivateKey
import net.i2p.data.Signature
@@ -74,68 +111,93 @@ public class Core {
final EventBus eventBus
final Persona me
final String version;
final File home
final Properties i2pOptions
final MuWireSettings muOptions
private final TrustService trustService
private final TrustSubscriber trustSubscriber
final I2PSession i2pSession;
private I2PSocketManager i2pSocketManager
final TrustService trustService
final TrustSubscriber trustSubscriber
private final PersisterService persisterService
private final HostCache hostCache
private final ConnectionManager connectionManager
private final PersisterFolderService persisterFolderService
final HostCache hostCache
final ConnectionManager connectionManager
private final CacheClient cacheClient
private final UpdateClient updateClient
private final ConnectionAcceptor connectionAcceptor
final ConnectionAcceptor connectionAcceptor
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
private final DownloadManager downloadManager
final DownloadManager downloadManager
private final DirectoryWatcher directoryWatcher
final FileManager fileManager
final UploadManager uploadManager
final ContentManager contentManager
final CertificateManager certificateManager
final ChatServer chatServer
final ChatManager chatManager
final FeedManager feedManager
private final FeedClient feedClient
private final WatchedDirectoryConverter watchedDirectoryConverter
final WatchedDirectoryManager watchedDirectoryManager
private final TrackerResponder trackerResponder
private final Router router
final AtomicBoolean shutdown = new AtomicBoolean()
final SigningPrivateKey spk
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
this.version = myVersion
this.muOptions = props
i2pOptions = new Properties()
def i2pOptionsFile = new File(home,"i2p.properties")
// Read defaults
def defaultI2PFile = getClass()
.getClassLoader().getResource("defaults/i2p.properties");
try {
defaultI2PFile.withInputStream { i2pOptions.load(it) }
} catch (ZipException mystery) {
log.log(Level.SEVERE, "couldn't load default i2p properties", mystery)
}
def i2pOptionsFile = new File(home, "i2p.properties")
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.containsKey("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "4"
i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "4"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654"
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
}
if (!(i2pOptions.containsKey("i2np.ntcp.port")
&& i2pOptions.containsKey("i2np.udp.port")
)) {
Random r = new Random()
int port = r.nextInt(60000) + 4000
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
i2pOptions["i2np.udp.port"] = String.valueOf(port)
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
}
i2pOptions['i2cp.leaseSetEncType']='4,0'
if (!props.embeddedRouter) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
router = null
}
} else {
log.info("launching embedded router")
Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("geoip.dir", home.getAbsolutePath() + File.separator + "geoip")
routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
@@ -162,18 +224,16 @@ public class Core {
// options like tunnel length and quantity
I2PSession i2pSession
I2PSocketManager socketManager
keyDat.withInputStream {
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
i2pSocketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
}
socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000)
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
i2pSession = socketManager.getSession()
i2pSocketManager.getDefaultOptions().setReadTimeout(60000)
i2pSocketManager.getDefaultOptions().setConnectTimeout(30000)
i2pSocketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
i2pSession = i2pSocketManager.getSession()
def destination = new Destination()
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
spk = new SigningPrivateKey(Constants.SIG_TYPE)
keyDat.withInputStream {
destination.readBytes(it)
def privateKey = new PrivateKey()
@@ -184,8 +244,9 @@ public class Core {
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.write(Constants.PERSONA_VERSION)
daos.writeShort((short)props.getNickname().length())
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
daos.writeShort((short)name.length)
daos.write(name)
destination.writeBytes(daos)
daos.flush()
byte [] payload = baos.toByteArray()
@@ -199,6 +260,12 @@ public class Core {
eventBus = new EventBus()
log.info("initializing certificate manager")
certificateManager = new CertificateManager(eventBus, home, me, spk)
eventBus.register(UICreateCertificateEvent.class, certificateManager)
eventBus.register(UIImportCertificateEvent.class, certificateManager)
log.info("initializing trust service")
File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "distrusted")
@@ -214,25 +281,46 @@ public class Core {
eventBus.register(FileUnsharedEvent.class, fileManager)
eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
eventBus.register(SideCarFileEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
eventBus.register(SourceDiscoveredEvent.class, meshManager)
eventBus.register(SourceVerifiedEvent.class, meshManager)
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
log.info "initializing folder persistence service"
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
eventBus.register(FileLoadedEvent.class, persisterFolderService)
eventBus.register(FileHashedEvent.class, persisterFolderService)
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
eventBus.register(UICommentEvent.class, persisterFolderService)
eventBus.register(UIFilePublishedEvent.class, persisterFolderService)
eventBus.register(UIFileUnpublishedEvent.class, persisterFolderService)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
hostCache = new HostCache(trustService,hostStorage, 30000, props, i2pSession.getMyDestination())
eventBus.register(HostDiscoveredEvent.class, hostCache)
eventBus.register(ConnectionEvent.class, hostCache)
log.info("initializing responder cache")
ResponderCache responderCache = new ResponderCache(props.responderCacheSize)
eventBus.register(UIResultBatchEvent.class, responderCache)
eventBus.register(SourceVerifiedEvent.class, responderCache)
log.info("initializing connection manager")
connectionManager = props.isLeaf() ?
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
new UltrapeerConnectionManager(eventBus, me, props.peerConnections, props.leafConnections, hostCache, responderCache, trustService, props)
eventBus.register(TrustEvent.class, connectionManager)
eventBus.register(ConnectionEvent.class, connectionManager)
eventBus.register(DisconnectionEvent.class, connectionManager)
@@ -241,52 +329,92 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
if (!(props.plugin || props.disableUpdates)) {
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
} else
log.info("running as plugin or updates disabled, not initializing update client")
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
I2PConnector i2pConnector = new I2PConnector(i2pSocketManager)
log.info("initializing certificate client")
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
log.info("initializing chat server")
chatServer = new ChatServer(eventBus, props, trustService, me, spk)
eventBus.with {
register(ChatMessageEvent.class, chatServer)
register(ChatDisconnectionEvent.class, chatServer)
register(TrustEvent.class, chatServer)
}
log.info("initializing feed manager")
feedManager = new FeedManager(eventBus, home)
eventBus.with {
register(FeedItemFetchedEvent.class, feedManager)
register(FeedFetchEvent.class, feedManager)
register(UIFeedConfigurationEvent.class, feedManager)
register(UIFeedDeletedEvent.class, feedManager)
}
log.info("initializing feed client")
feedClient = new FeedClient(i2pConnector, eventBus, me, feedManager)
eventBus.register(UIFeedUpdateEvent.class, feedClient)
log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
eventBus.register(QueryEvent.class, searchManager)
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing chat manager")
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
eventBus.with {
register(UIConnectChatEvent.class, chatManager)
register(UIDisconnectChatEvent.class, chatManager)
register(ChatMessageEvent.class, chatManager)
register(ChatDisconnectionEvent.class, chatManager)
}
log.info("initializing download manager")
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me, chatServer)
eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UIDownloadFeedItemEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
eventBus.register(DownloadHopelessEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
log.info("initializing tracker responder")
trackerResponder = new TrackerResponder(i2pSession, props, fileManager, downloadManager, meshManager, trustService, me)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
I2PAcceptor i2pAcceptor = new I2PAcceptor(i2pSocketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
certificateManager, chatServer)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
eventBus.register(FileSharedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
eventBus.register(FileSharedEvent.class, hasherService)
eventBus.register(FileUnsharedEvent.class, hasherService)
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
log.info("initializing trust subscriber")
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
@@ -297,9 +425,37 @@ public class Core {
contentManager = new ContentManager()
eventBus.register(ContentControlEvent.class, contentManager)
eventBus.register(QueryEvent.class, contentManager)
log.info("initializing browse manager")
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
eventBus.register(UIBrowseEvent.class, browseManager)
log.info("initializing watched directory converter")
watchedDirectoryConverter = new WatchedDirectoryConverter(this)
eventBus.register(AllFilesLoadedEvent.class, watchedDirectoryConverter)
log.info("initializing watched directory manager")
watchedDirectoryManager = new WatchedDirectoryManager(home, eventBus, fileManager)
eventBus.with {
register(WatchedDirectoryConfigurationEvent.class, watchedDirectoryManager)
register(WatchedDirectoryConvertedEvent.class, watchedDirectoryManager)
register(FileSharedEvent.class, watchedDirectoryManager)
register(DirectoryUnsharedEvent.class, watchedDirectoryManager)
register(UISyncDirectoryEvent.class, watchedDirectoryManager)
}
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, watchedDirectoryManager)
eventBus.with {
register(DirectoryWatchedEvent.class, directoryWatcher)
register(WatchedDirectoryConvertedEvent.class, directoryWatcher)
register(DirectoryUnsharedEvent.class, directoryWatcher)
register(WatchedDirectoryConfigurationEvent.class, directoryWatcher)
}
}
public void startServices() {
i2pSession.connect()
hasherService.start()
trustService.start()
trustService.waitForLoad()
@@ -309,7 +465,10 @@ public class Core {
connectionAcceptor.start()
connectionEstablisher.start()
hostCache.waitForLoad()
updateClient.start()
updateClient?.start()
feedManager.start()
feedClient.start()
trackerResponder.start()
}
public void shutdown() {
@@ -317,24 +476,65 @@ public class Core {
log.info("already shutting down")
return
}
log.info("saving settings")
saveMuSettings()
log.info("shutting down host cache")
hostCache.stop()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down download manageer")
log.info("shutting down trust service")
trustService.stop()
log.info("shutting down persister service")
persisterService.stop()
log.info("shutting down persisterFolder service")
persisterFolderService.stop()
log.info("shutting down download manager")
downloadManager.shutdown()
log.info("shutting down connection acceeptor")
log.info("shutting down connection acceptor")
connectionAcceptor.stop()
log.info("shutting down connection establisher")
connectionEstablisher.stop()
log.info("shutting down directory watcher")
directoryWatcher.stop()
log.info("shutting down watch directory manager")
watchedDirectoryManager.shutdown()
log.info("shutting down cache client")
cacheClient.stop()
log.info("shutting down chat server")
chatServer.stop()
log.info("shutting down chat manager")
chatManager.shutdown()
log.info("shutting down feed manager")
feedManager.stop()
log.info("shutting down feed client")
feedClient.stop()
log.info("shutting down tracker responder")
trackerResponder.stop()
log.info("shutting down connection manager")
connectionManager.shutdown()
if (updateClient != null) {
log.info("shutting down update client")
updateClient.stop()
}
log.info("killing socket manager")
i2pSocketManager.destroySocketManager()
if (router != null) {
log.info("shutting down embedded router")
router.shutdown(0)
}
log.info("shutting down event bus");
eventBus.shutdown()
log.info("shutdown complete")
}
public void saveMuSettings() {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
public void saveI2PSettings() {
File f = new File(home, "i2p.properties")
f.withOutputStream { i2pOptions.store(it, "I2P Options") }
}
static main(args) {
@@ -361,7 +561,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.4.11")
Core core = new Core(props, home, "0.7.4")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -2,6 +2,7 @@ package com.muwire.core
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
@@ -12,7 +13,7 @@ import groovy.util.logging.Log
class EventBus {
private Map handlers = new HashMap()
private final Executor executor = Executors.newSingleThreadExecutor {r ->
private final ExecutorService executor = Executors.newSingleThreadExecutor {r ->
def rv = new Thread(r)
rv.setDaemon(true)
rv.setName("event-bus")
@@ -53,4 +54,8 @@ class EventBus {
log.info("Unregistering $handler for type $eventType")
handlers[eventType]?.remove(handler)
}
void shutdown() {
executor.shutdownNow()
}
}

View File

@@ -6,30 +6,62 @@ import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
boolean searchExtraHop
boolean allowTrustLists
int trustListInterval
Set<Persona> trustSubscriptions
int downloadRetryInterval
int downloadRetryInterval, downloadMaxFailures
int totalUploadSlots
int uploadSlotsPerUser
int updateCheckInterval
long lastUpdateCheck
boolean autoDownloadUpdate
String updateType
String nickname
File downloadLocation
File incompleteLocation
CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
boolean allowTracking
boolean fileFeed
boolean advertiseFeed
boolean autoPublishSharedFiles
boolean defaultFeedAutoDownload
long defaultFeedUpdateInterval
int defaultFeedItemsToKeep
boolean defaultFeedSequential
int peerConnections
int leafConnections
int responderCacheSize
boolean startChatServer
int maxChatConnections
boolean advertiseChat
File chatWelcomeFile
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval
int hostClearInterval, hostHopelessInterval, hostRejectInterval, hostHopelessPurgeInterval
int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter
boolean plugin
boolean disableUpdates
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
Set<String> negativeFileTree
MuWireSettings() {
this(new Properties())
@@ -38,27 +70,69 @@ class MuWireSettings {
MuWireSettings(Properties props) {
isLeaf = Boolean.valueOf(props.get("leaf","false"))
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home")))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
String incompleteLocationProp = props.getProperty("incompleteLocation")
if (incompleteLocationProp != null)
incompleteLocation = new File(incompleteLocationProp)
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
downloadMaxFailures = Integer.parseInt(props.getProperty("downloadMaxFailures","10"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "60"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
hostHopelessPurgeInterval = Integer.valueOf(props.getProperty("hostHopelessPurgeInterval","1440"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
disableUpdates = Boolean.valueOf(props.getProperty("disableUpdates","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
watchedRegexes = readEncodedSet(props, "watchedRegexes")
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
allowTracking = Boolean.valueOf(props.getProperty("allowTracking","true"))
// feed settings
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
advertiseFeed = Boolean.valueOf(props.getProperty("advertiseFeed","true"))
autoPublishSharedFiles = Boolean.valueOf(props.getProperty("autoPublishSharedFiles", "false"))
defaultFeedAutoDownload = Boolean.valueOf(props.getProperty("defaultFeedAutoDownload", "false"))
defaultFeedItemsToKeep = Integer.valueOf(props.getProperty("defaultFeedItemsToKeep", "1000"))
defaultFeedSequential = Boolean.valueOf(props.getProperty("defaultFeedSequential", "false"))
defaultFeedUpdateInterval = Long.valueOf(props.getProperty("defaultFeedUpdateInterval", "3600000"))
// ultrapeer connection settings
leafConnections = Integer.valueOf(props.getProperty("leafConnections","512"))
peerConnections = Integer.valueOf(props.getProperty("peerConnections","128"))
// responder cache settings
responderCacheSize = Integer.valueOf(props.getProperty("responderCacheSize","32"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","10"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
String chatWelcomeProp = props.getProperty("chatWelcomeFile")
if (chatWelcomeProp != null)
chatWelcomeFile = new File(chatWelcomeProp)
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
trustSubscriptions = new HashSet<>()
if (props.containsKey("trustSubscriptions")) {
@@ -70,30 +144,70 @@ class MuWireSettings {
}
void write(OutputStream out) throws IOException {
void write(Writer out) throws IOException {
Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString())
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
if (incompleteLocation != null)
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("downloadMaxFailures", String.valueOf(downloadMaxFailures))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("hostHopelessPurgeInterval", String.valueOf(hostHopelessPurgeInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("plugin", String.valueOf(plugin))
props.setProperty("disableUpdates", String.valueOf(disableUpdates))
props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
props.setProperty("allowTracking", String.valueOf(allowTracking))
// feed settings
props.setProperty("fileFeed", String.valueOf(fileFeed))
props.setProperty("advertiseFeed", String.valueOf(advertiseFeed))
props.setProperty("autoPublishSharedFiles", String.valueOf(autoPublishSharedFiles))
props.setProperty("defaultFeedAutoDownload", String.valueOf(defaultFeedAutoDownload))
props.setProperty("defaultFeedItemsToKeep", String.valueOf(defaultFeedItemsToKeep))
props.setProperty("defaultFeedSequential", String.valueOf(defaultFeedSequential))
props.setProperty("defaultFeedUpdateInterval", String.valueOf(defaultFeedUpdateInterval))
// ultrapeer connection settings
props.setProperty("peerConnections", String.valueOf(peerConnections))
props.setProperty("leafConnections", String.valueOf(leafConnections))
// responder cache settings
props.setProperty("responderCacheSize", String.valueOf(responderCacheSize))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
props.setProperty("startChatServer", String.valueOf(startChatServer))
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
if (chatWelcomeFile != null)
props.setProperty("chatWelcomeFile", chatWelcomeFile.getAbsolutePath())
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
if (!trustSubscriptions.isEmpty()) {
String encoded = trustSubscriptions.stream().
@@ -102,25 +216,7 @@ class MuWireSettings {
props.setProperty("trustSubscriptions", encoded)
}
props.store(out, "")
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new HashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
}
rv
}
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return
String encoded = set.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty(property, encoded)
props.store(out, "This file is UTF-8")
}
boolean isLeaf() {

View File

@@ -1,45 +0,0 @@
package com.muwire.core
import java.nio.charset.StandardCharsets
/**
* A name of persona, file or search term
*/
public class Name {
final String name
Name(String name) {
this.name = name
}
Name(InputStream nameStream) throws IOException {
DataInputStream dis = new DataInputStream(nameStream)
int length = dis.readUnsignedShort()
byte [] nameBytes = new byte[length]
dis.readFully(nameBytes)
this.name = new String(nameBytes, StandardCharsets.UTF_8)
}
public void write(OutputStream out) throws IOException {
DataOutputStream dos = new DataOutputStream(out)
dos.writeShort(name.length())
dos.write(name.getBytes(StandardCharsets.UTF_8))
}
public getName() {
name
}
@Override
public int hashCode() {
name.hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Name))
return false
Name other = (Name)o
name.equals(other.name)
}
}

View File

@@ -1,94 +0,0 @@
package com.muwire.core
import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
import net.i2p.data.SigningPublicKey
public class Persona {
private static final int SIG_LEN = Constants.SIG_TYPE.getSigLen()
private final byte version
private final Name name
private final Destination destination
private final byte[] sig
private volatile String humanReadableName
private volatile String base64
private volatile byte[] payload
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
version = (byte) (personaStream.read() & 0xFF)
if (version != Constants.PERSONA_VERSION)
throw new IOException("Unknown version "+version)
name = new Name(personaStream)
destination = Destination.create(personaStream)
sig = new byte[SIG_LEN]
DataInputStream dis = new DataInputStream(personaStream)
dis.readFully(sig)
if (!verify(version, name, destination, sig))
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify")
}
private static boolean verify(byte version, Name name, Destination destination, byte [] sig) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
baos.write(version)
name.write(baos)
destination.writeBytes(baos)
byte[] payload = baos.toByteArray()
SigningPublicKey spk = destination.getSigningPublicKey()
Signature signature = new Signature(Constants.SIG_TYPE, sig)
DSAEngine.getInstance().verifySignature(signature, payload, spk)
}
public void write(OutputStream out) throws IOException {
if (payload == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
baos.write(version)
name.write(baos)
destination.writeBytes(baos)
baos.write(sig)
payload = baos.toByteArray()
}
out.write(payload)
}
public String getHumanReadableName() {
if (humanReadableName == null)
humanReadableName = name.getName() + "@" + destination.toBase32().substring(0,32)
humanReadableName
}
public String toBase64() {
if (base64 == null) {
def baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
base64
}
@Override
public int hashCode() {
name.hashCode() ^ destination.hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Persona))
return false
Persona other = (Persona)o
name.equals(other.name) && destination.equals(other.destination)
}
public static void main(String []args) {
if (args.length != 1) {
println "This utility decodes a bas64-encoded persona"
System.exit(1)
}
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(args[0])))
println p.getHumanReadableName()
}
}

View File

@@ -0,0 +1,91 @@
package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?\r\n]";
private static final Set<Character> SPLIT_CHARS = new HashSet<>()
static {
SPLIT_CHARS.with {
add(' '.toCharacter())
add('*'.toCharacter())
add('+'.toCharacter())
add('-'.toCharacter())
add(','.toCharacter())
add('.'.toCharacter())
add(':'.toCharacter())
add(';'.toCharacter())
add('('.toCharacter())
add(')'.toCharacter())
add('='.toCharacter())
add('_'.toCharacter())
add('/'.toCharacter())
add('\\'.toCharacter())
add('!'.toCharacter())
add('\''.toCharacter())
add('$'.toCharacter())
add('%'.toCharacter())
add('|'.toCharacter())
add('['.toCharacter())
add(']'.toCharacter())
add('{'.toCharacter())
add('}'.toCharacter())
add('?'.toCharacter())
}
}
public static String[] termify(final String source) {
String lowercase = source.toLowerCase().trim()
def rv = []
int pos = 0
int quote = -1
StringBuilder tmp = new StringBuilder()
while(pos < lowercase.length()) {
char c = lowercase.charAt(pos++)
if (quote < 0 && c == '"') {
quote = pos - 1
continue
}
if (quote >= 0) {
if (c == '"') {
quote = -1
if (tmp.length() != 0) {
rv << tmp.toString()
tmp = new StringBuilder()
}
} else
tmp.append(c)
} else if (SPLIT_CHARS.contains(c)) {
if (tmp.length() != 0) {
rv << tmp.toString()
tmp = new StringBuilder()
}
} else
tmp.append c
}
// check if odd number of quotes and re-tokenize from last quote
if (quote >= 0) {
tmp = new StringBuilder()
pos = quote + 1
while(pos < lowercase.length()) {
char c = lowercase.charAt(pos++)
if (SPLIT_CHARS.contains(c)) {
if (tmp.length() > 0) {
rv << tmp.toString()
tmp = new StringBuilder()
}
} else
tmp.append(c)
}
}
if (tmp.length() > 0)
rv << tmp.toString()
rv
}
}

View File

@@ -0,0 +1,24 @@
package com.muwire.core.chat;
enum ChatAction {
JOIN(true, false, true, false),
LEAVE(false, false, true, false),
SAY(false, false, true, false),
LIST(true, true, true, false),
HELP(true, true, true, false),
INFO(true, true, true, false),
JOINED(true, true, false, false),
TRUST(true, false, true, true),
DISTRUST(true, false, true, true);
final boolean console;
final boolean stateless;
final boolean user;
final boolean local;
ChatAction(boolean console, boolean stateless, boolean user, boolean local) {
this.console = console;
this.stateless = stateless;
this.user = user;
this.local = local;
}
}

View File

@@ -0,0 +1,141 @@
package com.muwire.core.chat
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
@Log
class ChatClient implements Closeable {
private static final long REJECTION_BACKOFF = 60 * 1000
private static final Executor CONNECTOR = Executors.newCachedThreadPool()
private final I2PConnector connector
private final EventBus eventBus
private final Persona host, me
private final TrustService trustService
private final MuWireSettings settings
private ChatConnection connection
private boolean connectInProgress
private long lastRejectionTime
private Thread connectThread
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
MuWireSettings settings) {
this.connector = connector
this.eventBus = eventBus
this.host = host
this.me = me
this.trustService = trustService
this.settings = settings
}
synchronized void connectIfNeeded() {
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
return
connectInProgress = true
CONNECTOR.execute({connect()})
}
private void connect() {
synchronized(this) {
if (!connectInProgress)
return
connectThread = Thread.currentThread()
}
Endpoint endpoint = null
try {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
endpoint = connector.connect(host.destination)
DataOutputStream dos = new DataOutputStream(endpoint.getOutputStream())
DataInputStream dis = new DataInputStream(endpoint.getInputStream())
dos.with {
write("IRC\r\n".getBytes(StandardCharsets.US_ASCII))
write("Version:${Constants.CHAT_VERSION}\r\n".getBytes(StandardCharsets.US_ASCII))
write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
write("\r\n".getBytes(StandardCharsets.US_ASCII))
flush()
}
String codeString = DataUtil.readTillRN(dis)
int code = Integer.parseInt(codeString.split(" ")[0])
if (code == 429) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
try { dos.close() } catch (IOException ignore) {}
endpoint.close()
synchronized(this) {
lastRejectionTime = System.currentTimeMillis()
}
return
}
if (code != 200)
throw new Exception("unknown code $code")
Map<String,String> headers = DataUtil.readAllHeaders(dis)
if (!headers.containsKey('Version'))
throw new Exception("Version header missing")
int version = Integer.parseInt(headers['Version'])
if (version != Constants.CHAT_VERSION)
throw new Exception("Unknown chat version $version")
synchronized(this) {
if (!connectInProgress)
return
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
connection.start()
}
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
connection : connection))
} catch (Exception e) {
log.log(java.util.logging.Level.WARNING, "connect failed", e)
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
if (endpoint != null) {
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
endpoint.close()
}
} finally {
synchronized(this) {
connectInProgress = false
connectThread = null
}
}
}
synchronized void disconnected() {
connectInProgress = false
connection = null
}
@Override
synchronized public void close() {
connectInProgress = false
connectThread?.interrupt()
connection?.close()
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
}
synchronized void ping() {
connection?.sendPing()
}
synchronized void sendChat(ChatMessageEvent e) {
connection?.sendChat(e)
}
}

View File

@@ -0,0 +1,28 @@
package com.muwire.core.chat
class ChatCommand {
private final ChatAction action
private final String payload
final String source
ChatCommand(String source) {
if (source.charAt(0) != '/')
throw new Exception("command doesn't start with / $source")
int position = 1
StringBuilder sb = new StringBuilder()
while(position < source.length()) {
char c = source.charAt(position)
if (c == ' ')
break
sb.append(c)
position++
}
String command = sb.toString().toUpperCase()
action = ChatAction.valueOf(command)
if (position < source.length())
payload = source.substring(position + 1)
else
payload = ""
this.source = source
}
}

View File

@@ -0,0 +1,280 @@
package com.muwire.core.chat
import java.nio.charset.StandardCharsets
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
@Log
class ChatConnection implements ChatLink {
private static final long PING_INTERVAL = 20000
private static final long MAX_CHAT_AGE = 5 * 60 * 1000
private final EventBus eventBus
private final Endpoint endpoint
private final Persona persona
private final boolean incoming
private final TrustService trustService
private final MuWireSettings settings
private final AtomicBoolean running = new AtomicBoolean()
private final BlockingQueue messages = new LinkedBlockingQueue()
private final Thread reader, writer
private final LinkedList<Long> timestamps = new LinkedList<>()
private final BlockingQueue incomingEvents = new LinkedBlockingQueue()
private final DataInputStream dis
private final DataOutputStream dos
private final JsonSlurper slurper = new JsonSlurper()
private volatile long lastPingSentTime
ChatConnection(EventBus eventBus, Endpoint endpoint, Persona persona, boolean incoming,
TrustService trustService, MuWireSettings settings) {
this.eventBus = eventBus
this.endpoint = endpoint
this.persona = persona
this.incoming = incoming
this.trustService = trustService
this.settings = settings
this.dis = new DataInputStream(endpoint.getInputStream())
this.dos = new DataOutputStream(endpoint.getOutputStream())
this.reader = new Thread({readLoop()} as Runnable)
this.reader.setName("reader-${persona.getHumanReadableName()}")
this.reader.setDaemon(true)
this.writer = new Thread({writeLoop()} as Runnable)
this.writer.setName("writer-${persona.getHumanReadableName()}")
this.writer.setDaemon(true)
}
void start() {
if (!running.compareAndSet(false, true)) {
log.log(Level.WARNING,"${persona.getHumanReadableName()} already running", new Exception())
return
}
reader.start()
writer.start()
}
@Override
public boolean isUp() {
running.get()
}
@Override
public Persona getPersona() {
persona
}
@Override
public void close() {
if (!running.compareAndSet(true, false)) {
log.log(Level.WARNING,"${persona.getHumanReadableName()} already closed", new Exception())
return
}
log.info("Closing "+persona.getHumanReadableName())
reader.interrupt()
writer.interrupt()
endpoint.close()
eventBus.publish(new ChatDisconnectionEvent(persona : persona))
}
private void readLoop() {
try {
while(running.get())
read()
} catch( InterruptedException | SocketTimeoutException ignored) {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader", e)
} finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close()
}
}
private void writeLoop() {
try {
while(running.get()) {
def message = messages.take()
write(message)
}
} catch (InterruptedException ignore) {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in writer",e)
} finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close()
}
}
private void read() {
int length = dis.readUnsignedShort()
byte [] payload = new byte[length]
dis.readFully(payload)
def json = slurper.parse(payload)
if (json.type == null)
throw new Exception("missing json type")
switch(json.type) {
case "Ping" : break // just ignore
case "Chat" : handleChat(json); break
case "Leave": handleLeave(json); break
default :
throw new Exception("unknown json type ${json.type}")
}
}
private void write(Object message) {
byte [] payload = JsonOutput.toJson(message).bytes
dos.with {
writeShort(payload.length)
write(payload)
flush()
}
}
void sendPing() {
long now = System.currentTimeMillis()
if (now - lastPingSentTime < PING_INTERVAL)
return
def ping = [:]
ping.type = "Ping"
ping.version = 1
messages.put(ping)
lastPingSentTime = now
}
private void handleChat(def json) {
UUID uuid = UUID.fromString(json.uuid)
Persona host = fromString(json.host)
Persona sender = fromString(json.sender)
long chatTime = json.chatTime
String room = json.room
String payload = json.payload
byte [] sig = Base64.decode(json.sig)
if (!verify(uuid,host,sender,chatTime,room,payload,sig)) {
log.warning("chat didn't verify")
return
}
if (incoming) {
if (sender.destination != endpoint.destination) {
log.warning("Sender destination mismatch, dropping message")
return
}
} else {
if (host.destination != endpoint.destination) {
log.warning("Host destination mismatch, dropping message")
return
}
}
if (System.currentTimeMillis() - chatTime > MAX_CHAT_AGE) {
log.warning("Chat too old, dropping")
return
}
switch(trustService.getLevel(sender.destination)) {
case TrustLevel.TRUSTED : break
case TrustLevel.NEUTRAL :
if (!settings.allowUntrusted)
return
else
break
case TrustLevel.DISTRUSTED :
return
}
def event = new ChatMessageEvent( uuid : uuid, payload : payload, sender : sender,
host : host, room : room, chatTime : chatTime, sig : sig)
eventBus.publish(event)
if (!incoming)
incomingEvents.put(event)
}
private void handleLeave(def json) {
Persona leaver = fromString(json.persona)
eventBus.publish(new UserDisconnectedEvent(user : leaver, host : persona))
incomingEvents.put(leaver)
}
private static Persona fromString(String base64) {
new Persona(new ByteArrayInputStream(Base64.decode(base64)))
}
private static boolean verify(UUID uuid, Persona host, Persona sender, long chatTime,
String room, String payload, byte []sig) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
DataOutputStream daos = new DataOutputStream(baos)
daos.write(uuid.toString().bytes)
host.write(daos)
sender.write(daos)
daos.writeLong(chatTime)
daos.write(room.getBytes(StandardCharsets.UTF_8))
daos.write(payload.getBytes(StandardCharsets.UTF_8))
daos.close()
byte [] signed = baos.toByteArray()
def spk = sender.destination.getSigningPublicKey()
def signature = new Signature(spk.getType(), sig)
DSAEngine.getInstance().verifySignature(signature, signed, spk)
}
public static byte[] sign(UUID uuid, long chatTime, String room, String words, Persona sender, Persona host, SigningPrivateKey spk) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
DataOutputStream daos = new DataOutputStream(baos)
daos.with {
write(uuid.toString().bytes)
host.write(daos)
sender.write(daos)
writeLong(chatTime)
write(room.getBytes(StandardCharsets.UTF_8))
write(words.getBytes(StandardCharsets.UTF_8))
close()
}
byte [] payload = baos.toByteArray()
Signature sig = DSAEngine.getInstance().sign(payload, spk)
sig.getData()
}
void sendChat(ChatMessageEvent e) {
def chat = [:]
chat.type = "Chat"
chat.uuid = e.uuid.toString()
chat.host = e.host.toBase64()
chat.sender = e.sender.toBase64()
chat.chatTime = e.chatTime
chat.room = e.room
chat.payload = e.payload
chat.sig = Base64.encode(e.sig)
messages.put(chat)
}
void sendLeave(Persona p) {
def leave = [:]
leave.type = "Leave"
leave.persona = p.toBase64()
messages.put(leave)
}
public Object nextEvent() {
incomingEvents.take()
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.core.chat;
public enum ChatConnectionAttemptStatus {
CONNECTING, SUCCESSFUL, REJECTED, FAILED, DISCONNECTED
}

View File

@@ -0,0 +1,14 @@
package com.muwire.core.chat
import com.muwire.core.Event
import com.muwire.core.Persona
class ChatConnectionEvent extends Event {
ChatConnectionAttemptStatus status
Persona persona
ChatLink connection
public String toString() {
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
}
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.chat
import com.muwire.core.Event
import com.muwire.core.Persona
class ChatDisconnectionEvent extends Event {
Persona persona
}

View File

@@ -0,0 +1,14 @@
package com.muwire.core.chat;
import java.io.Closeable;
import com.muwire.core.Persona;
public interface ChatLink extends Closeable {
public Persona getPersona();
public boolean isUp();
public void sendChat(ChatMessageEvent e);
public void sendLeave(Persona p);
public void sendPing();
public Object nextEvent() throws InterruptedException;
}

View File

@@ -0,0 +1,77 @@
package com.muwire.core.chat
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.connection.I2PConnector
import com.muwire.core.trust.TrustService
class ChatManager {
private final EventBus eventBus
private final Persona me
private final I2PConnector connector
private final TrustService trustService
private final MuWireSettings settings
private final Map<Persona, ChatClient> clients = new ConcurrentHashMap<>()
ChatManager(EventBus eventBus, Persona me, I2PConnector connector, TrustService trustService,
MuWireSettings settings) {
this.eventBus = eventBus
this.me = me
this.connector = connector
this.trustService = trustService
this.settings = settings
Timer timer = new Timer("chat-connector", true)
timer.schedule({connect()} as TimerTask, 1000, 1000)
}
boolean isConnected(Persona p) {
clients.containsKey(p)
}
void onUIConnectChatEvent(UIConnectChatEvent e) {
if (e.host == me) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL,
persona : me, connection : LocalChatLink.INSTANCE))
} else {
ChatClient client = new ChatClient(connector, eventBus, e.host, me, trustService, settings)
clients.put(e.host, client)
}
}
void onUIDisconnectChatEvent(UIDisconnectChatEvent e) {
if (e.host == me)
return
ChatClient client = clients.remove(e.host)
client?.close()
}
void onChatMessageEvent(ChatMessageEvent e) {
if (e.host == me)
return
if (e.sender != me)
return
clients[e.host]?.sendChat(e)
}
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
clients[e.persona]?.disconnected()
}
private void connect() {
clients.each { k, v ->
v.connectIfNeeded()
v.ping()
}
}
void shutdown() {
clients.each { k, v ->
v.close()
}
}
}

View File

@@ -0,0 +1,13 @@
package com.muwire.core.chat
import com.muwire.core.Event
import com.muwire.core.Persona
class ChatMessageEvent extends Event {
UUID uuid
String payload
Persona sender, host
String room
long chatTime
byte [] sig
}

View File

@@ -0,0 +1,344 @@
package com.muwire.core.chat
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.SigningPrivateKey
import net.i2p.util.ConcurrentHashSet
@Log
class ChatServer {
public static final String CONSOLE = "__CONSOLE__"
private static final String DEFAULT_WELCOME = "Welcome to my chat server! Type /HELP for list of available commands"
private final EventBus eventBus
private final MuWireSettings settings
private final TrustService trustService
private final Persona me
private final SigningPrivateKey spk
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
private final Map<Persona, Set<String>> memberships = new ConcurrentHashMap<>()
private final Map<String, Persona> shortNames = new ConcurrentHashMap<>()
private final AtomicBoolean running = new AtomicBoolean()
ChatServer(EventBus eventBus, MuWireSettings settings, TrustService trustService, Persona me, SigningPrivateKey spk) {
this.eventBus = eventBus
this.settings = settings
this.trustService = trustService
this.me = me
this.spk = spk
Timer timer = new Timer("chat-server-pinger", true)
timer.schedule({sendPings()} as TimerTask, 1000, 1000)
}
public void start() {
if (!running.compareAndSet(false, true))
return
connections.put(me.destination, LocalChatLink.INSTANCE)
joinRoom(me, CONSOLE)
shortNames.put(me.getHumanReadableName(), me)
echo(getWelcome(),me.destination)
}
public boolean isRunning() {
running.get()
}
private String getWelcome() {
String welcome = DEFAULT_WELCOME
if (settings.chatWelcomeFile != null)
welcome = settings.chatWelcomeFile.text
"/SAY $welcome"
}
private void sendPings() {
connections.each { k,v ->
v.sendPing()
}
}
public void handle(Endpoint endpoint) {
InputStream is = endpoint.getInputStream()
OutputStream os = endpoint.getOutputStream()
Map<String, String> headers = DataUtil.readAllHeaders(is)
if (!headers.containsKey("Version"))
throw new Exception("Version header missing")
int version = Integer.parseInt(headers['Version'])
if (version != Constants.CHAT_VERSION)
throw new Exception("Unknown chat version $version")
if (!headers.containsKey('Persona'))
throw new Exception("Persona header missing")
Persona client = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
if (client.destination != endpoint.destination)
throw new Exception("Client destination mismatch")
if (!running.get()) {
os.write("400 Chat Not Enabled\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.close()
endpoint.close()
return
}
if (connections.containsKey(client.destination) || connections.size() == settings.maxChatConnections) {
os.write("429 Rejected\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.close()
endpoint.close()
return
}
os.with {
write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
write("Version:${Constants.CHAT_VERSION}\r\n".getBytes(StandardCharsets.US_ASCII))
write("\r\n".getBytes(StandardCharsets.US_ASCII))
flush()
}
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
connections.put(endpoint.destination, connection)
joinRoom(client, CONSOLE)
shortNames.put(client.getHumanReadableName(), client)
connection.start()
echo(getWelcome(),connection.endpoint.destination)
}
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
ChatConnection con = connections.remove(e.persona.destination)
if (con == null)
return
Set<String> rooms = memberships.get(e.persona)
if (rooms != null) {
rooms.each {
leaveRoom(e.persona, it)
}
}
shortNames.remove(e.persona.getHumanReadableName())
connections.each { k, v ->
v.sendLeave(e.persona)
}
}
void onTrustEvent(TrustEvent e) {
if (e.level == TrustLevel.TRUSTED)
return
if (settings.allowUntrusted && e.level == TrustLevel.NEUTRAL)
return
ChatConnection connection = connections.get(e.persona.destination)
connection?.close()
}
private void joinRoom(Persona p, String room) {
Set<Persona> existing = rooms.get(room)
if (existing == null) {
existing = new ConcurrentHashSet<>()
rooms.put(room, existing)
}
existing.add(p)
Set<String> membership = memberships.get(p)
if (membership == null) {
membership = new ConcurrentHashSet<>()
memberships.put(p, membership)
}
membership.add(room)
}
private void leaveRoom(Persona p, String room) {
Set<Persona> existing = rooms.get(room)
if (existing == null) {
log.warning(p.getHumanReadableName() + " leaving room they hadn't joined")
return
}
existing.remove(p)
if (existing.isEmpty())
rooms.remove(room)
Set<String> membership = memberships.get(p)
if (membership == null) {
log.warning(p.getHumanReadableName() + " didn't have any memberships")
return
}
membership.remove(room)
if (membership.isEmpty())
memberships.remove(p)
}
void onChatMessageEvent(ChatMessageEvent e) {
if (e.host != me)
return
ChatCommand command
try {
command = new ChatCommand(e.payload)
} catch (Exception badCommand) {
log.log(Level.WARNING, "bad chat command",badCommand)
return
}
if (command.action.console && e.room != CONSOLE) {
echo("/SAY ERROR: You can only execute that command in the chat console, not in a chat room.",
e.sender.destination, e.room)
return
}
if (!command.action.console && e.room == CONSOLE) {
echo("/SAY ERROR: You need to be in a chat room. Type /LIST for list of rooms or /JOIN to join or create a room.",
e.sender.destination)
return
}
if (!command.action.user)
return
if (command.action.local && e.sender != me)
return
switch(command.action) {
case ChatAction.JOIN : processJoin(command.payload, e); break
case ChatAction.LEAVE : processLeave(e); break
case ChatAction.SAY : processSay(e); break
case ChatAction.LIST : processList(e.sender.destination); break
case ChatAction.INFO : processInfo(e.sender.destination); break
case ChatAction.HELP : processHelp(e.sender.destination); break
case ChatAction.TRUST : processTrust(command.payload, TrustLevel.TRUSTED); break
case ChatAction.DISTRUST : processTrust(command.payload, TrustLevel.DISTRUSTED); break
}
}
private void processJoin(String room, ChatMessageEvent e) {
joinRoom(e.sender, room)
rooms[room].each {
if (it == e.sender)
return
connections[it.destination].sendChat(e)
}
String payload = rooms[room].stream().filter({it != e.sender}).map({it.toBase64()})
.collect(Collectors.joining(","))
if (payload.length() == 0) {
return
}
payload = "/JOINED $payload"
long now = System.currentTimeMillis()
UUID uuid = UUID.randomUUID()
byte [] sig = ChatConnection.sign(uuid, now, room, payload, me, me, spk)
ChatMessageEvent echo = new ChatMessageEvent(
uuid : uuid,
payload : payload,
sender : me,
host : me,
room : room,
chatTime : now,
sig : sig
)
connections[e.sender.destination].sendChat(echo)
}
private void processLeave(ChatMessageEvent e) {
leaveRoom(e.sender, e.room)
rooms.getOrDefault(e.room, []).each {
if (it == e.sender)
return
connections[it.destination].sendChat(e)
}
}
private void processSay(ChatMessageEvent e) {
if (rooms.containsKey(e.room)) {
// not a private message
rooms[e.room].each {
if (it == e.sender)
return
connections[it.destination].sendChat(e)
}
} else {
Persona target = new Persona(new ByteArrayInputStream(Base64.decode(e.room)))
connections[target.destination]?.sendChat(e)
}
}
private void processList(Destination d) {
String roomList = rooms.keySet().stream().filter({it != CONSOLE}).collect(Collectors.joining("\n"))
roomList = "/SAY \nRoom List:\n"+roomList
echo(roomList, d)
}
private void processInfo(Destination d) {
String info = "/SAY \nThe address of this server is\n========\n${me.toBase64()}\n========\nCopy/paste the above and share it\n"
String connectedUsers = memberships.keySet().stream().map({it.getHumanReadableName()}).collect(Collectors.joining("\n"))
info = "${info}\nConnected Users:\n$connectedUsers\n======="
echo(info, d)
}
private void processHelp(Destination d) {
String help = """/SAY
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /TRUST /DISTRUST /HELP
/JOIN <room name> - joins a room, or creates one if it does not exist. You must type this in the console
/LEAVE - leaves a room. You must type this in the room you want to leave
/SAY - optional, says something in the room you're in
/LIST - lists the existing rooms on this server. You must type this in the console
/INFO - shows information about this server. You must type this in the console
/TRUST <user> - marks user as trusted. This is only available to the server owner
/DISTRUST <user> - marks user as distrusted. This is only available to the server owner
/HELP - prints this help message
"""
echo(help, d)
}
private void echo(String payload, Destination d, String room = CONSOLE) {
log.info "echoing $payload"
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
byte [] sig = ChatConnection.sign(uuid, now, room, payload, me, me, spk)
ChatMessageEvent echo = new ChatMessageEvent(
uuid : uuid,
payload : payload,
sender : me,
host : me,
room : room,
chatTime : now,
sig : sig
)
connections[d]?.sendChat(echo)
}
private void processTrust(String shortName, TrustLevel level) {
Persona p = shortNames.get(shortName)
if (p == null)
return
eventBus.publish(new TrustEvent(persona : p, level : level))
}
void stop() {
if (running.compareAndSet(true, false)) {
connections.each { k, v ->
v.close()
}
}
}
}

View File

@@ -0,0 +1,49 @@
package com.muwire.core.chat
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue
import com.muwire.core.Persona
import groovy.util.logging.Log
@Log
class LocalChatLink implements ChatLink {
public static final LocalChatLink INSTANCE = new LocalChatLink()
private final BlockingQueue messages = new LinkedBlockingQueue()
private LocalChatLink() {}
@Override
public void close() throws IOException {
}
@Override
public void sendChat(ChatMessageEvent e) {
messages.put(e)
}
@Override
public void sendLeave(Persona p) {
messages.put(p)
}
@Override
public void sendPing() {}
@Override
public Object nextEvent() {
messages.take()
}
@Override
public boolean isUp() {
true
}
public Persona getPersona() {
null
}
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.chat
import com.muwire.core.Event
import com.muwire.core.Persona
class UIConnectChatEvent extends Event {
Persona host
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.chat
import com.muwire.core.Event
import com.muwire.core.Persona
class UIDisconnectChatEvent extends Event {
Persona host
}

View File

@@ -0,0 +1,9 @@
package com.muwire.core.chat
import com.muwire.core.Event
import com.muwire.core.Persona
class UserDisconnectedEvent extends Event {
Persona user
Persona host
}

View File

@@ -1,10 +1,17 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
@@ -16,12 +23,14 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
@Log
abstract class Connection implements Closeable {
private static final int SEARCHES = 10
private static final long INTERVAL = 1000
@@ -40,6 +49,8 @@ abstract class Connection implements Closeable {
protected final String name
long lastPingSentTime, lastPongReceivedTime
private volatile UUID lastPingUUID
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
@@ -83,6 +94,7 @@ abstract class Connection implements Closeable {
reader.interrupt()
writer.interrupt()
endpoint.close()
log.info("closed $name")
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
}
@@ -91,6 +103,7 @@ abstract class Connection implements Closeable {
while(running.get()) {
read()
}
} catch (InterruptedException ok) {
} catch (SocketTimeoutException e) {
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader",e)
@@ -107,6 +120,7 @@ abstract class Connection implements Closeable {
def message = messages.take()
write(message)
}
} catch (InterruptedException ok) {
} catch (Exception e) {
log.log(Level.WARNING, "unhandled exception in writer",e)
} finally {
@@ -120,6 +134,8 @@ abstract class Connection implements Closeable {
def ping = [:]
ping.type = "Ping"
ping.version = 1
lastPingUUID = UUID.randomUUID()
ping.uuid = lastPingUUID.toString()
messages.put(ping)
lastPingSentTime = System.currentTimeMillis()
}
@@ -132,20 +148,30 @@ abstract class Connection implements Closeable {
query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms()
query.oobInfohash = e.searchEvent.oobInfohash
query.searchComments = e.searchEvent.searchComments
query.compressedResults = e.searchEvent.compressedResults
if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
if (e.sig != null)
query.sig = Base64.encode(e.sig)
if (e.queryTime > 0)
query.queryTime = e.queryTime
if (e.sig2 != null)
query.sig2 = Base64.encode(e.sig2)
messages.put(query)
}
protected void handlePing() {
protected void handlePing(def ping) {
log.fine("$name received ping")
def pong = [:]
pong.type = "Pong"
pong.version = 1
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
if (ping.uuid != null)
pong.uuid = ping.uuid
pong.pongs = hostCache.getGoodHosts(2).collect { d -> d.toBase64() }
messages.put(pong)
}
@@ -154,7 +180,23 @@ abstract class Connection implements Closeable {
lastPongReceivedTime = System.currentTimeMillis()
if (pong.pongs == null)
throw new Exception("Pong doesn't have pongs")
pong.pongs.each {
if (lastPingUUID == null) {
log.fine "$name received an unexpected pong"
return
}
if (pong.uuid == null) {
log.fine "$name pong doesn't have uuid"
return
}
UUID pongUUID = UUID.fromString(pong.uuid)
if (pongUUID != lastPingUUID) {
log.fine "$name ping/pong uuid mismatch"
return
}
lastPingUUID = null
pong.pongs.stream().limit(2).forEach {
def dest = new Destination(it)
eventBus.publish(new HostDiscoveredEvent(destination: dest))
}
@@ -209,16 +251,74 @@ abstract class Connection implements Closeable {
boolean oob = false
if (search.oobInfohash != null)
oob = search.oobInfohash
boolean searchComments = false
if (search.searchComments != null)
searchComments = search.searchComments
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
if (infohash != null)
payload = infohash
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(spk.getType(), sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
} else
log.info("query signature verified")
} else {
log.info("no signature in query")
return
}
byte[] sig2 = null
long queryTime = 0
if (search.sig2 != null) {
if (search.queryTime == null) {
log.info("extended signature but no timestamp")
return
}
sig2 = Base64.decode(search.sig2)
queryTime = search.queryTime
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(spk.getType(), sig2)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("extended signature didn't match uuid and timestamp")
return
} else {
log.info("extended query signature verified")
if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) {
log.info("query too old")
return
}
}
} else {
log.info("no extended signature in query")
return
}
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,
uuid : uuid,
oobInfohash : oob)
oobInfohash : oob,
searchComments : searchComments,
compressedResults : compressedResults,
persona : originator)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
firstHop : search.firstHop,
sig : sig,
queryTime : queryTime,
sig2 : sig2 )
eventBus.publish(event)
}

View File

@@ -1,15 +1,26 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.nio.file.attribute.DosFileAttributes
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.zip.DeflaterOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.InflaterInputStream
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.chat.ChatServer
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.filefeeds.FeedItems
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
@@ -17,6 +28,7 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.util.DataUtil
import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
@@ -25,6 +37,7 @@ import com.muwire.core.search.UnexpectedResultsException
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
@Log
class ConnectionAcceptor {
@@ -37,17 +50,23 @@ class ConnectionAcceptor {
final TrustService trustService
final SearchManager searchManager
final UploadManager uploadManager
final FileManager fileManager
final ConnectionEstablisher establisher
final CertificateManager certificateManager
final ChatServer chatServer
final ExecutorService acceptorThread
final ExecutorService handshakerThreads
private volatile shutdown
volatile int browsed
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
ConnectionEstablisher establisher) {
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager,
ChatServer chatServer) {
this.eventBus = eventBus
this.manager = manager
this.settings = settings
@@ -55,8 +74,11 @@ class ConnectionAcceptor {
this.hostCache = hostCache
this.trustService = trustService
this.searchManager = searchManager
this.fileManager = fileManager
this.uploadManager = uploadManager
this.establisher = establisher
this.certificateManager = certificateManager
this.chatServer = chatServer
acceptorThread = Executors.newSingleThreadExecutor { r ->
def rv = new Thread(r)
@@ -126,14 +148,32 @@ class ConnectionAcceptor {
case (byte)'P':
processPOST(e)
break
case (byte)'R':
processRESULTS(e)
break
case (byte)'T':
processTRUST(e)
break
case (byte)'B':
processBROWSE(e)
break
case (byte)'C':
processCERTIFICATES(e)
break
case (byte)'I':
processIRC(e)
break
case (byte)'F':
processFEED(e)
break
default:
throw new Exception("Invalid read $read")
}
} catch (Exception ex) {
log.log(Level.WARNING, "incoming connection failed",ex)
try {
e.getOutputStream().close()
} catch (Exception ignore) {}
e.close()
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
}
@@ -182,7 +222,9 @@ class ConnectionAcceptor {
os.writeShort(json.bytes.length)
os.write(json.bytes)
}
e.outputStream.flush()
try {
e.outputStream.close()
} catch (Exception ignored) {}
e.close()
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
}
@@ -229,7 +271,7 @@ class ConnectionAcceptor {
Persona sender = new Persona(dis)
if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
int nResults = dis.readUnsignedShort()
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) {
@@ -246,44 +288,306 @@ class ConnectionAcceptor {
e.close()
}
}
private void processRESULTS(Endpoint e) {
InputStream is = e.getInputStream()
DataInputStream dis = new DataInputStream(is)
byte[] esults = new byte[7]
dis.readFully(esults)
if (esults != "ESULTS ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid RESULTS connection")
JsonSlurper slurper = new JsonSlurper()
try {
String uuid = DataUtil.readTillRN(dis)
UUID resultsUUID = UUID.fromString(uuid)
if (!searchManager.hasLocalSearch(resultsUUID))
throw new UnexpectedResultsException(resultsUUID.toString())
// parse all headers
Map<String,String> headers = DataUtil.readAllHeaders(is);
if (!headers.containsKey("Sender"))
throw new IOException("No Sender header")
if (!headers.containsKey("Count"))
throw new IOException("No Count header")
boolean chat = false
if (headers.containsKey('Chat'))
chat = Boolean.parseBoolean(headers['Chat'])
boolean feed = false
if (headers.containsKey('Feed'))
feed = Boolean.parseBoolean(headers['Feed'])
byte [] personaBytes = Base64.decode(headers['Sender'])
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
int nResults = Integer.parseInt(headers['Count'])
if (nResults > Constants.MAX_RESULTS)
throw new IOException("too many results $nResults")
dis = new DataInputStream(new GZIPInputStream(dis))
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) {
int jsonSize = dis.readUnsignedShort()
byte [] payload = new byte[jsonSize]
dis.readFully(payload)
def json = slurper.parse(payload)
results[i] = ResultsParser.parse(sender, resultsUUID, json)
results[i].chat = chat
results[i].feed = feed
}
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException bad) {
log.log(Level.WARNING, "failed to process RESULTS", bad)
} finally {
e.close()
}
}
private void processBROWSE(Endpoint e) {
try {
byte [] rowse = new byte[7]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(rowse)
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid BROWSE connection")
Persona browser = null
Map<String,String> headers = DataUtil.readAllHeaders(dis);
if (headers.containsKey('Persona')) {
browser = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
if (browser.destination != e.destination)
throw new IOException("browser persona mismatch")
}
OutputStream os = e.getOutputStream()
if (!settings.browseFiles) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
browsed++
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
def sharedFiles = fileManager.getSharedFiles().values()
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
boolean chat = chatServer.isRunning() && settings.advertiseChat
os.write("Chat: ${chat}\r\n".getBytes(StandardCharsets.US_ASCII))
boolean feed = settings.fileFeed && settings.advertiseFeed
os.write("Feed: ${feed}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
it.hit(browser, System.currentTimeMillis(), "Browse Host");
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
dos.close()
} finally {
e.close()
}
}
private void processTRUST(Endpoint e) {
byte[] RUST = new byte[6]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
try {
byte[] RUST = new byte[6]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
Map<String,String> headers = DataUtil.readAllHeaders(dis)
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
DataOutputStream dos = new DataOutputStream(os)
if (!json) {
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
dos.writeShort(size)
good.each {
it.persona.write(dos)
}
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.persona.write(dos)
}
} else {
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
good.each {
def obj = [:]
obj.persona = it.persona.toBase64()
obj.reason = it.reason
String toJson = JsonOutput.toJson(obj)
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
dos.writeShort(payload.length)
dos.write(payload)
}
bad.each {
def obj = [:]
obj.persona = it.persona.toBase64()
obj.reason = it.reason
String toJson = JsonOutput.toJson(obj)
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
dos.writeShort(payload.length)
dos.write(payload)
}
}
dos.flush()
} finally {
e.close()
return
}
}
private void processCERTIFICATES(Endpoint e) {
try {
byte [] ERTIFICATES = new byte[12]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(ERTIFICATES)
if (ERTIFICATES != "ERTIFICATES ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid CERTIFICATES connection")
byte [] infoHashStringBytes = new byte[44]
dis.readFully(infoHashStringBytes)
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
List<Persona> good = new ArrayList<>(trustService.good.values())
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
DataOutputStream dos = new DataOutputStream(os)
dos.writeShort(size)
good.each {
it.write(dos)
byte[] rn = new byte[2]
dis.readFully(rn)
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Malformed CERTIFICATES request")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
log.info("responding to certificates request for $infoHashString")
byte [] root = Base64.decode(infoHashString)
Set<Certificate> certs = certificateManager.getByInfoHash(new InfoHash(root))
if (certs.isEmpty()) {
log.info("certs not found")
e.getOutputStream().write("404 Certs Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
return
}
OutputStream os = e.getOutputStream()
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: ${certs.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(os)
certs.each {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
it.write(baos)
byte [] payload = baos.toByteArray()
dos.writeShort(payload.length)
dos.write(payload)
}
dos.close()
} finally {
e.close()
}
}
private void processIRC(Endpoint e) {
byte[] IRC = new byte[4]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(IRC)
if (IRC != "RC\r\n".getBytes(StandardCharsets.US_ASCII))
throw new Exception("Invalid IRC connection")
chatServer.handle(e)
}
private void processFEED(Endpoint e) {
try {
byte[] EED = new byte[5];
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(EED);
if (EED != "EED\r\n".getBytes(StandardCharsets.US_ASCII))
throw new Exception("Invalid FEED connection")
List<Persona> bad = new ArrayList<>(trustService.bad.values())
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.write(dos)
OutputStream os = e.getOutputStream()
Map<String, String> headers = DataUtil.readAllHeaders(dis)
if (!headers.containsKey("Persona"))
throw new Exception("Persona header missing")
Persona requestor = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
if (requestor.destination != e.destination)
throw new Exception("Requestor persona mismatch")
if (!settings.fileFeed) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
long timestamp = 0
if (headers.containsKey("Timestamp")) {
timestamp = Long.parseLong(headers['Timestamp'])
}
List<SharedFile> published = fileManager.getPublishedSince(timestamp)
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: ${published.size()}\r\n".getBytes(StandardCharsets.US_ASCII));
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
final long now = System.currentTimeMillis();
published.each {
it.hit(requestor, now, "Feed Update");
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = FeedItems.sharedFileToObj(it, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
dos.close()
} finally {
e.close()
}
dos.flush()
e.close()
}
}

View File

@@ -31,9 +31,11 @@ class ConnectionEstablisher {
final HostCache hostCache
final Timer timer
final ExecutorService executor
final ExecutorService executor, closer
final Set inProgress = new ConcurrentHashSet()
private volatile boolean shutdown
ConnectionEstablisher(){}
@@ -51,6 +53,8 @@ class ConnectionEstablisher {
rv.setName("connector-${System.currentTimeMillis()}")
rv
} as ThreadFactory)
closer = Executors.newSingleThreadExecutor()
}
void start() {
@@ -58,11 +62,15 @@ class ConnectionEstablisher {
}
void stop() {
shutdown = true
timer.cancel()
executor.shutdownNow()
closer.shutdownNow()
}
private void connectIfNeeded() {
if (shutdown)
return
if (!connectionManager.needsConnections())
return
if (inProgress.size() >= CONCURRENT)
@@ -86,6 +94,8 @@ class ConnectionEstablisher {
}
private void connect(Destination toTry) {
if (shutdown)
return
log.info("starting connect to ${toTry.toBase32()}")
try {
def endpoint = i2pConnector.connect(toTry)
@@ -120,8 +130,14 @@ class ConnectionEstablisher {
}
private void fail(Endpoint endpoint) {
endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
if (shutdown)
return
if (!closer.isShutdown()) {
closer.execute {
endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
}
}
private void readK(Endpoint e) {
@@ -175,7 +191,7 @@ class ConnectionEstablisher {
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
} finally {
// the end
e.close()
closer.execute({e.close()} as Runnable)
}
}

View File

@@ -36,9 +36,8 @@ abstract class ConnectionManager {
timer.schedule({sendPings()} as TimerTask, 1000,1000)
}
void stop() {
void shutdown() {
timer.cancel()
getConnections().each { it.close() }
}
void onTrustEvent(TrustEvent e) {
@@ -62,8 +61,6 @@ abstract class ConnectionManager {
abstract void onDisconnectionEvent(DisconnectionEvent e)
abstract void shutdown()
protected void sendPings() {
final long now = System.currentTimeMillis()
getConnections().each {

View File

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

View File

@@ -53,7 +53,7 @@ class PeerConnection extends Connection {
if (json.type == null)
throw new Exception("missing json type")
switch(json.type) {
case "Ping" : handlePing(); break;
case "Ping" : handlePing(json); break;
case "Pong" : handlePong(json); break;
case "Search": handleSearch(json); break
default :

View File

@@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResponderCache
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
@@ -18,18 +19,22 @@ class UltrapeerConnectionManager extends ConnectionManager {
final int maxPeers, maxLeafs
final TrustService trustService
final ResponderCache responderCache
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
private final Random random = new Random()
UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
HostCache hostCache, ResponderCache responderCache, TrustService trustService, MuWireSettings settings) {
super(eventBus, me, hostCache, settings)
this.maxPeers = maxPeers
this.maxLeafs = maxLeafs
this.trustService = trustService
this.responderCache = responderCache
}
@Override
public void drop(Destination d) {
@@ -44,8 +49,18 @@ class UltrapeerConnectionManager extends ConnectionManager {
if (e.replyTo != me.destination && e.receivedOn != me.destination &&
!leafConnections.containsKey(e.receivedOn))
e.firstHop = false
final int connCount = peerConnections.size()
if (connCount == 0)
return
final int treshold = (int)(Math.sqrt(connCount)) + 1
peerConnections.values().each {
if (e.getReceivedOn() != it.getEndpoint().getDestination())
// 1. do not send query back to originator
// 2. if firstHop forward to everyone
// 3. otherwise to everyone who has recently responded/transferred to us + randomized sqrt of neighbors
if (e.getReceivedOn() != it.getEndpoint().getDestination() &&
(e.firstHop ||
responderCache.hasResponded(it.endpoint.destination) ||
random.nextInt(connCount) < treshold))
it.sendQuery(e)
}
}
@@ -104,8 +119,9 @@ class UltrapeerConnectionManager extends ConnectionManager {
@Override
void shutdown() {
peerConnections.values().stream().parallel().forEach({v -> v.close()})
leafConnections.values().stream().parallel().forEach({v -> v.close()})
super.shutdown()
peerConnections.values().stream().forEach({v -> v.close()})
leafConnections.values().stream().forEach({v -> v.close()})
peerConnections.clear()
leafConnections.clear()
}

View File

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

View File

@@ -1,6 +1,7 @@
package com.muwire.core.download
import com.muwire.core.connection.I2PConnector
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
@@ -12,6 +13,7 @@ import com.muwire.core.util.DataUtil
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@@ -21,36 +23,39 @@ import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import com.muwire.core.chat.ChatManager
import com.muwire.core.chat.ChatServer
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
@Log
public class DownloadManager {
private final EventBus eventBus
private final TrustService trustService
private final MeshManager meshManager
private final MuWireSettings muSettings
final MuWireSettings muSettings
private final I2PConnector connector
private final Executor executor
private final File incompletes, home
private final File home
private final Persona me
private final ChatServer chatServer
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
I2PConnector connector, File home, Persona me) {
I2PConnector connector, File home, Persona me, ChatServer chatServer) {
this.eventBus = eventBus
this.trustService = trustService
this.meshManager = meshManager
this.muSettings = muSettings
this.connector = connector
this.incompletes = new File(home,"incompletes")
this.home = home
this.me = me
incompletes.mkdir()
this.chatServer = chatServer
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
@@ -62,7 +67,7 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
def size = e.result[0].size
def infohash = e.result[0].infohash
def pieceSize = e.result[0].pieceSize
@@ -74,12 +79,29 @@ public class DownloadManager {
destinations.addAll(e.sources)
destinations.remove(me.destination)
Pieces pieces = getPieces(infohash, size, pieceSize)
doDownload(infohash, e.target, size, pieceSize, e.sequential, destinations)
def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations,
incompletes, pieces)
downloaders.put(infohash, downloader)
}
public void onUIDownloadFeedItemEvent(UIDownloadFeedItemEvent e) {
Set<Destination> singleSource = new HashSet<>()
singleSource.add(e.item.getPublisher().getDestination())
doDownload(e.item.getInfoHash(), e.target, e.item.getSize(), e.item.getPieceSize(),
e.sequential, singleSource)
}
private void doDownload(InfoHash infoHash, File target, long size, int pieceSize,
boolean sequential, Set<Destination> destinations) {
File incompletes = muSettings.incompleteLocation
if (incompletes == null)
incompletes = new File(home, "incompletes")
incompletes.mkdirs()
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
def downloader = new Downloader(eventBus, this, chatServer, me, target, size,
infoHash, pieceSize, connector, destinations,
incompletes, pieces, muSettings.downloadMaxFailures)
downloaders.put(infoHash, downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
@@ -90,6 +112,11 @@ public class DownloadManager {
persistDownloaders()
}
public void onDownloadHopelessEvent(DownloadHopelessEvent e) {
downloaders.remove(e.downloader.infoHash)
persistDownloaders()
}
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
persistDownloaders()
}
@@ -122,27 +149,48 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root)
}
boolean sequential = false
if (json.sequential != null)
sequential = json.sequential
File incompletes
if (json.incompletes != null)
incompletes = new File(DataUtil.readi18nString(Base64.decode(json.incompletes)))
else
incompletes = new File(home, "incompletes")
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
if (json.pieceSizePow2 == null || json.pieceSizePow2 == 0) {
log.warning("Skipping $file because pieceSizePow2=$json.pieceSizePow2")
return // skip this download as it's corrupt anyway
}
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
def downloader = new Downloader(eventBus, this, chatServer, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces, muSettings.downloadMaxFailures)
if (json.paused != null)
downloader.paused = json.paused
downloaders.put(infoHash, downloader)
downloader.readPieces()
if (!downloader.paused)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
try {
downloader.readPieces()
if (!downloader.paused)
downloader.download()
downloaders.put(infoHash, downloader)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} catch (IllegalArgumentException bad) {
log.log(Level.WARNING,"cannot start downloader, skipping", bad)
return
}
}
}
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
int pieceSize = 0x1 << pieceSizePow2
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
long pieceSize = 0x1L << pieceSizePow2
int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0)
nPieces++
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
mesh.pieces
}
@@ -188,6 +236,11 @@ public class DownloadManager {
json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused
json.sequential = downloader.pieces.ratio == 0f
json.incompletes = Base64.encode(DataUtil.encodei18nString(downloader.incompletes.getAbsolutePath()))
writer.println(JsonOutput.toJson(json))
}
}
@@ -198,4 +251,8 @@ public class DownloadManager {
downloaders.values().each { it.stop() }
Downloader.executorService.shutdownNow()
}
public boolean isDownloading(InfoHash infoHash) {
downloaders.containsKey(infoHash)
}
}

View File

@@ -21,6 +21,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
@Log
@@ -36,14 +37,15 @@ class DownloadSession {
private final long fileLength
private final Set<Integer> available
private final MessageDigest digest
private final boolean browse, feed, chat
private long lastSpeedRead = System.currentTimeMillis()
private long dataSinceLastRead
private final AtomicLong dataSinceLastRead
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) {
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead,
boolean browse, boolean feed, boolean chat) {
this.eventBus = eventBus
this.meB64 = meB64
this.pieces = pieces
@@ -53,6 +55,10 @@ class DownloadSession {
this.pieceSize = pieceSize
this.fileLength = fileLength
this.available = available
this.dataSinceLastRead = dataSinceLastRead
this.browse = browse
this.feed = feed
this.chat = chat
try {
digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) {
@@ -93,6 +99,12 @@ class DownloadSession {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII))
if (browse)
os.write("Browse: true\r\n".getBytes(StandardCharsets.US_ASCII))
if (feed)
os.write("Feed: true\r\n".getBytes(StandardCharsets.US_ASCII))
if (chat)
os.write("Chat: true\r\n".getBytes(StandardCharsets.US_ASCII))
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
@@ -141,6 +153,8 @@ class DownloadSession {
// parse X-Have if present
if (headers.containsKey("X-Have")) {
DataUtil.decodeXHave(headers["X-Have"]).each {
if (it >= pieces.nPieces)
throw new IOException("Invalid X-Have header, available piece $it/$pieces.nPieces")
available.add(it)
}
if (!available.contains(piece))
@@ -180,15 +194,14 @@ class DownloadSession {
mapped.position(position)
byte[] tmp = new byte[0x1 << 13]
DataInputStream dis = new DataInputStream(is)
while(mapped.hasRemaining()) {
if (mapped.remaining() < tmp.length)
tmp = new byte[mapped.remaining()]
int read = is.read(tmp)
if (read == -1)
throw new IOException()
dis.readFully(tmp)
synchronized(this) {
mapped.put(tmp, 0, read)
dataSinceLastRead += read
mapped.put(tmp)
dataSinceLastRead.addAndGet(tmp.length)
pieces.markPartial(piece, mapped.position())
}
}
@@ -202,6 +215,8 @@ class DownloadSession {
pieces.markPartial(piece, 0)
throw new BadHashException("bad hash on piece $piece")
}
eventBus.publish(new SourceVerifiedEvent(infoHash : infoHash, source : endpoint.destination))
} finally {
try { channel?.close() } catch (IOException ignore) {}
DataUtil.tryUnmap(mapped)
@@ -220,13 +235,4 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
rv
}
}

View File

@@ -2,6 +2,8 @@ package com.muwire.core.download
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.chat.ChatManager
import com.muwire.core.chat.ChatServer
import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
@@ -12,6 +14,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
import com.muwire.core.Constants
@@ -27,7 +30,8 @@ import net.i2p.util.ConcurrentHashSet
@Log
public class Downloader {
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, HOPELESS, CANCELLED, PAUSED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
@@ -39,6 +43,7 @@ public class Downloader {
private final EventBus eventBus
private final DownloadManager downloadManager
private final ChatServer chatServer
private final Persona me
private final File file
private final Pieces pieces
@@ -48,49 +53,70 @@ public class Downloader {
private final I2PConnector connector
private final Set<Destination> destinations
private final int nPieces
private final File incompletes
private final File piecesFile
private final File incompleteFile
final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
/** LOCKING: itself */
private final Map<Destination, Integer> failingDestinations = new HashMap<>()
private final int maxFailures
private volatile boolean cancelled, paused
private final AtomicBoolean eventFired = new AtomicBoolean()
private final AtomicBoolean hopelessEventFired = new AtomicBoolean()
private boolean piecesFileClosed
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
private volatile long lastSpeedRead = System.currentTimeMillis()
private ArrayList speedArr = new ArrayList<Integer>()
private int speedPos = 0
private int speedAvg = 0
private long timestamp = Instant.now().toEpochMilli()
public Downloader(EventBus eventBus, DownloadManager downloadManager,
public Downloader(EventBus eventBus, DownloadManager downloadManager, ChatServer chatServer,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes, Pieces pieces) {
File incompletes, Pieces pieces, int maxFailures) {
this.eventBus = eventBus
this.me = me
this.downloadManager = downloadManager
this.chatServer = chatServer
this.file = file
this.infoHash = infoHash
this.length = length
this.connector = connector
this.destinations = destinations
this.incompletes = incompletes
this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces
this.nPieces = pieces.nPieces
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
// it's easily adjustable by resizing the size of speedArr
this.speedArr = [ 0, 0, 0, 0, 0 ]
this.maxFailures = maxFailures
}
public synchronized InfoHash getInfoHash() {
infoHash
}
public File getFile() {
file
}
public int getNPieces() {
nPieces
}
public int getPieceSize() {
pieceSize
}
public long getLength() {
length
}
private synchronized void setInfoHash(InfoHash infoHash) {
this.infoHash = infoHash
@@ -99,7 +125,7 @@ public class Downloader {
void download() {
readPieces()
destinations.each {
if (it != me.destination) {
if (it != me.destination && !isHopeless(it)) {
def worker = new DownloadWorker(it)
activeWorkers.put(it, worker)
executorService.submit(worker)
@@ -140,10 +166,17 @@ public class Downloader {
public int speed() {
int currSpeed = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
currSpeed += it.speed()
}
long dataRead = dataSinceLastRead.getAndSet(0)
long now = System.currentTimeMillis()
if (now > lastSpeedRead)
currSpeed = (int) (dataRead * 1000.0d / (now - lastSpeedRead))
lastSpeedRead = now
}
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
speedArr.clear()
downloadManager.muSettings.speedSmoothSeconds.times { speedArr.add(0) }
speedPos = 0
}
// normalize to speedArr.size
@@ -182,6 +215,8 @@ public class Downloader {
if (allFinished) {
if (pieces.isComplete())
return DownloadState.FINISHED
if (!hasLiveSources())
return DownloadState.HOPELESS
return DownloadState.FAILED
}
@@ -241,11 +276,26 @@ public class Downloader {
}
active
}
public int getTotalWorkers() {
return activeWorkers.size();
}
public int countHopelessSources() {
synchronized(failingDestinations) {
return destinations.count { isHopeless(it)}
}
}
private boolean hasLiveSources() {
destinations.size() > countHopelessSources()
}
public void resume() {
paused = false
readPieces()
destinations.each { destination ->
destinations.stream().filter({!isHopeless(it)}).forEach { destination ->
log.fine("resuming source ${destination.toBase32()}")
def worker = activeWorkers.get(destination)
if (worker != null) {
if (worker.currentState == WorkerState.FINISHED) {
@@ -262,12 +312,86 @@ public class Downloader {
}
void addSource(Destination d) {
if (activeWorkers.containsKey(d))
if (activeWorkers.containsKey(d) || isHopeless(d))
return
destinations.add(d)
DownloadWorker newWorker = new DownloadWorker(d)
activeWorkers.put(d, newWorker)
executorService.submit(newWorker)
}
boolean isSequential() {
pieces.ratio == 0f
}
File generatePreview() {
int lastCompletePiece = pieces.firstIncomplete() - 1
if (lastCompletePiece == -1)
return null
if (lastCompletePiece < -1)
return file
long previewableLength = (lastCompletePiece + 1) * ((long)pieceSize)
// generate name
long now = System.currentTimeMillis()
File previewFile
File parentFile = file.getParentFile()
int lastDot = file.getName().lastIndexOf('.')
if (lastDot < 0)
previewFile = new File(parentFile, file.getName() + "." + String.valueOf(now) + ".mwpreview")
else {
String name = file.getName().substring(0, lastDot)
String extension = file.getName().substring(lastDot + 1)
String previewName = name + "." + String.valueOf(now) + ".mwpreview."+extension
previewFile = new File(parentFile, previewName)
}
// copy
InputStream is = null
OutputStream os = null
try {
is = new BufferedInputStream(new FileInputStream(incompleteFile))
os = new BufferedOutputStream(new FileOutputStream(previewFile))
byte [] tmp = new byte[0x1 << 13]
long totalCopied = 0
while(totalCopied < previewableLength) {
int read = is.read(tmp, 0, (int)Math.min(tmp.length, previewableLength - totalCopied))
if (read < 0)
throw new IOException("EOF?")
os.write(tmp, 0, read)
totalCopied += read
}
return previewFile
} catch (IOException bad) {
log.log(Level.WARNING,"Preview failed",bad)
return null
} finally {
try {is?.close() } catch (IOException ignore) {}
try {os?.close() } catch (IOException ignore) {}
}
}
private boolean isHopeless(Destination d) {
if (maxFailures < 0)
return false
synchronized(failingDestinations) {
return !successfulDestinations.contains(d) &&
failingDestinations.containsKey(d) &&
failingDestinations[d] >= maxFailures
}
}
private void markFailed(Destination d) {
log.fine("marking failed ${d.toBase32()}")
synchronized(failingDestinations) {
Integer count = failingDestinations.get(d)
if (count == null) {
failingDestinations.put(d, 1)
} else {
failingDestinations.put(d, count + 1)
}
}
}
class DownloadWorker implements Runnable {
private final Destination destination
@@ -294,10 +418,16 @@ public class Downloader {
setInfoHash(received)
}
currentState = WorkerState.DOWNLOADING
boolean browse = downloadManager.muSettings.browseFiles
boolean feed = downloadManager.muSettings.fileFeed && downloadManager.muSettings.advertiseFeed
boolean chat = chatServer.isRunning() && downloadManager.muSettings.advertiseChat
boolean requestPerformed
while(!pieces.isComplete()) {
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available)
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead,
browse, feed, chat)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
@@ -306,6 +436,9 @@ public class Downloader {
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
markFailed(destination)
if (!hasLiveSources() && hopelessEventFired.compareAndSet(false, true))
eventBus.publish(new DownloadHopelessEvent(downloader : Downloader.this))
} finally {
writePieces()
currentState = WorkerState.FINISHED
@@ -326,20 +459,15 @@ public class Downloader {
}
eventBus.publish(
new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this))
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash().getRoot(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this,
infoHash: getInfoHash()))
}
endpoint?.close()
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}

View File

@@ -2,10 +2,11 @@ package com.muwire.core.download
class Pieces {
private final BitSet done, claimed
private final int nPieces
final int nPieces
private final float ratio
private final Random random = new Random()
private final Map<Integer,Integer> partials = new HashMap<>()
private int cachedDone;
Pieces(int nPieces) {
this(nPieces, 1.0f)
@@ -17,7 +18,7 @@ class Pieces {
done = new BitSet(nPieces)
claimed = new BitSet(nPieces)
}
synchronized int[] claim() {
int claimedCardinality = claimed.cardinality()
if (claimedCardinality == nPieces) {
@@ -30,7 +31,7 @@ class Pieces {
}
// if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
int rv = claimed.nextClearBit(0)
claimed.set(rv)
return [rv, partials.getOrDefault(rv, 0), 0]
@@ -59,7 +60,8 @@ class Pieces {
return [rv, partials.getOrDefault(rv, 0), 1]
}
List<Integer> toList = availableCopy.toList()
Collections.shuffle(toList)
if (ratio > 0f)
Collections.shuffle(toList)
int rv = toList[0]
claimed.set(rv)
[rv, partials.getOrDefault(rv, 0), 0]
@@ -74,7 +76,10 @@ class Pieces {
}
synchronized void markDownloaded(int piece) {
if (piece >= nPieces)
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
done.set(piece)
cachedDone = done.cardinality();
claimed.set(piece)
partials.remove(piece)
}
@@ -88,11 +93,11 @@ class Pieces {
}
synchronized boolean isComplete() {
done.cardinality() == nPieces
cachedDone == nPieces
}
synchronized int donePieces() {
done.cardinality()
cachedDone
}
synchronized boolean isDownloaded(int piece) {
@@ -101,10 +106,15 @@ class Pieces {
synchronized void clearAll() {
done.clear()
cachedDone = 0
claimed.clear()
partials.clear()
}
synchronized int firstIncomplete() {
done.nextClearBit(0)
}
synchronized void write(PrintWriter writer) {
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
writer.println(i)

View File

@@ -0,0 +1,11 @@
package com.muwire.core.download
import com.muwire.core.Event
import com.muwire.core.InfoHash
import net.i2p.data.Destination
class SourceVerifiedEvent extends Event {
InfoHash infoHash
Destination source
}

View File

@@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
UIResultEvent[] result
Set<Destination> sources
File target
boolean sequential
}

View File

@@ -0,0 +1,164 @@
package com.muwire.core.filecert
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.InvalidSignatureException
import com.muwire.core.Name
import com.muwire.core.Persona
import net.i2p.crypto.DSAEngine
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import net.i2p.data.SigningPublicKey
import net.i2p.data.Base64
class Certificate {
private final byte version
private final InfoHash infoHash
final Name name, comment
final long timestamp
final Persona issuer
private final byte[] sig
private volatile byte [] payload
private String base64;
Certificate(InputStream is) {
version = (byte) (is.read() & 0xFF)
if (version > Constants.FILE_CERT_VERSION)
throw new IOException("Unknown version $version")
DataInputStream dis = new DataInputStream(is)
timestamp = dis.readLong()
byte [] root = new byte[InfoHash.SIZE]
dis.readFully(root)
infoHash = new InfoHash(root)
name = new Name(dis)
issuer = new Persona(dis)
if (version == 2) {
byte present = (byte)(dis.read() & 0xFF)
if (present != 0) {
comment = new Name(dis)
}
}
sig = new byte[Constants.SIG_TYPE.getSigLen()]
dis.readFully(sig)
if (!verify(version, infoHash, name, timestamp, issuer, comment, sig))
throw new InvalidSignatureException("certificate for $name.name from ${issuer.getHumanReadableName()} didn't verify")
}
Certificate(InfoHash infoHash, String name, long timestamp, Persona issuer, String comment, SigningPrivateKey spk) {
this.version = Constants.FILE_CERT_VERSION
this.infoHash = infoHash
this.name = new Name(name)
if (comment != null)
this.comment = new Name(comment)
else
this.comment = null
this.timestamp = timestamp
this.issuer = issuer
ByteArrayOutputStream baos = new ByteArrayOutputStream()
DataOutputStream daos = new DataOutputStream(baos)
daos.write(version)
daos.writeLong(timestamp)
daos.write(infoHash.getRoot())
this.name.write(daos)
issuer.write(daos)
if (this.comment == null) {
daos.write((byte) 0)
} else {
daos.write((byte) 1)
this.comment.write(daos)
}
daos.close()
byte[] payload = baos.toByteArray()
Signature signature = DSAEngine.getInstance().sign(payload, spk)
this.sig = signature.getData()
}
private static boolean verify(byte version, InfoHash infoHash, Name name, long timestamp, Persona issuer, Name comment, byte[] sig) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
DataOutputStream daos = new DataOutputStream(baos)
daos.write(version)
daos.writeLong(timestamp)
daos.write(infoHash.getRoot())
name.write(daos)
issuer.write(daos)
if (version == 2) {
if (comment == null) {
daos.write((byte)0)
} else {
daos.write((byte)1)
comment.write(daos)
}
}
daos.close()
byte [] payload = baos.toByteArray()
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
Signature signature = new Signature(spk.getType(), sig)
DSAEngine.getInstance().verifySignature(signature, payload, spk)
}
public void write(OutputStream os) {
if (payload == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
DataOutputStream daos = new DataOutputStream(baos)
daos.write(version)
daos.writeLong(timestamp)
daos.write(infoHash.getRoot())
name.write(daos)
issuer.write(daos)
if (version == 2) {
if (comment == null)
daos.write((byte) 0)
else {
daos.write((byte) 1)
comment.write(daos)
}
}
daos.write(sig)
daos.close()
payload = baos.toByteArray()
}
os.write(payload)
}
public String toBase64() {
if (base64 == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
return base64;
}
@Override
public int hashCode() {
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Certificate))
return false
Certificate other = (Certificate)o
version == other.version &&
infoHash == other.infoHash &&
timestamp == other.timestamp &&
name == other.name &&
issuer == other.issuer &&
comment == other.comment
}
}

View File

@@ -0,0 +1,95 @@
package com.muwire.core.filecert
import java.nio.charset.StandardCharsets
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import net.i2p.data.Base64
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.InvalidSignatureException
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
@Log
class CertificateClient {
private final EventBus eventBus
private final I2PConnector connector
private final ExecutorService fetcherThread = Executors.newSingleThreadExecutor()
CertificateClient(EventBus eventBus, I2PConnector connector) {
this.eventBus = eventBus
this.connector = connector
}
void onUIFetchCertificatesEvent(UIFetchCertificatesEvent e) {
fetcherThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.CONNECTING,
user : e.host, infoHash : e.infoHash))
endpoint = connector.connect(e.host.destination)
String infoHashString = Base64.encode(e.infoHash.getRoot())
OutputStream os = endpoint.getOutputStream()
os.write("CERTIFICATES ${infoHashString}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("invalid code $code")
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
if (!headers.containsKey("Count"))
throw new IOException("No count header")
int count = Integer.parseInt(headers['Count'])
// start pulling the certs
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FETCHING, count : count,
user : e.host, infoHash : e.infoHash))
DataInputStream dis = new DataInputStream(is)
for (int i = 0; i < count; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
Certificate cert = null
try {
cert = new Certificate(new ByteArrayInputStream(tmp))
} catch (IOException | InvalidSignatureException ignore) {
log.log(Level.WARNING, "certificate creation failed",ignore)
continue
}
if (cert.infoHash == e.infoHash)
eventBus.publish(new CertificateFetchedEvent(certificate : cert, user : e.host, infoHash : e.infoHash))
}
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.DONE, count : count,
user : e.host, infoHash : e.infoHash))
} catch (Exception bad) {
log.log(Level.WARNING,"Fetching certificates failed", bad)
eventBus.publish(new CertificateFetchEvent(status : CertificateFetchStatus.FAILED,
user : e.host, infoHash : e.infoHash))
} finally {
endpoint?.close()
}
})
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filecert
import com.muwire.core.Event
class CertificateCreatedEvent extends Event {
Certificate certificate
}

View File

@@ -0,0 +1,12 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class CertificateFetchEvent extends Event {
CertificateFetchStatus status
int count
Persona user
InfoHash infoHash
}

View File

@@ -0,0 +1,5 @@
package com.muwire.core.filecert;
public enum CertificateFetchStatus {
CONNECTING, FETCHING, DONE, FAILED
}

View File

@@ -0,0 +1,11 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class CertificateFetchedEvent extends Event {
Certificate certificate
Persona user
InfoHash infoHash
}

View File

@@ -0,0 +1,146 @@
package com.muwire.core.filecert
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.InvalidSignatureException
import com.muwire.core.Name
import com.muwire.core.Persona
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.SigningPrivateKey
import net.i2p.util.ConcurrentHashSet
@Log
class CertificateManager {
private final EventBus eventBus
private final File certDir
private final Persona me
private final SigningPrivateKey spk
final Map<InfoHash, Set<Certificate>> byInfoHash = new ConcurrentHashMap()
final Map<Persona, Set<Certificate>> byIssuer = new ConcurrentHashMap()
CertificateManager(EventBus eventBus, File home, Persona me, SigningPrivateKey spk) {
this.eventBus = eventBus
this.me = me
this.spk = spk
this.certDir = new File(home, "filecerts")
if (!certDir.exists())
certDir.mkdirs()
else
loadCertificates()
}
private void loadCertificates() {
certDir.listFiles({ dir, name ->
name.endsWith("mwcert")
} as FilenameFilter).each { certFile ->
Certificate cert = null
try {
certFile.withInputStream {
cert = new Certificate(it)
}
} catch (IOException | InvalidSignatureException ignore) {
log.log(Level.WARNING, "Certificate failed to load from $certFile", ignore)
return
}
Set<Certificate> existing = byInfoHash.get(cert.infoHash)
if (existing == null) {
existing = new ConcurrentHashSet<>()
byInfoHash.put(cert.infoHash, existing)
}
existing.add(cert)
existing = byIssuer.get(cert.issuer)
if (existing == null) {
existing = new ConcurrentHashSet<>()
byIssuer.put(cert.issuer, existing)
}
existing.add(cert)
eventBus.publish(new CertificateCreatedEvent(certificate : cert))
}
}
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
InfoHash infoHash = new InfoHash(e.sharedFile.getRoot())
String name = e.sharedFile.getFile().getName()
long timestamp = System.currentTimeMillis()
String comment = null
if (e.sharedFile.getComment() != null)
comment = DataUtil.readi18nString(Base64.decode(e.sharedFile.getComment()))
Certificate cert = new Certificate(infoHash, name, timestamp, me, comment, spk)
if (addToMaps(cert)) {
saveCert(cert)
eventBus.publish(new CertificateCreatedEvent(certificate : cert))
}
}
void onUIImportCertificateEvent(UIImportCertificateEvent e) {
Certificate cert = e.certificate
if (!addToMaps(cert))
return
saveCert(cert)
}
private void saveCert(Certificate cert) {
String infoHashString = Base64.encode(cert.infoHash.getRoot())
File certFile = new File(certDir, "${infoHashString}_${cert.issuer.getHumanReadableName()}_${cert.timestamp}.mwcert")
certFile.withOutputStream { cert.write(it) }
}
private boolean addToMaps(Certificate cert) {
boolean added = true
Set<Certificate> existing = byInfoHash.get(cert.infoHash)
if (existing == null) {
existing = new ConcurrentHashSet<>()
byInfoHash.put(cert.infoHash, existing)
}
added &= existing.add(cert)
existing = byIssuer.get(cert.issuer)
if (existing == null) {
existing = new ConcurrentHashSet<>()
byIssuer.put(cert.issuer, existing)
}
added &= existing.add(cert)
added
}
public boolean hasLocalCertificate(InfoHash infoHash) {
if (!byInfoHash.containsKey(infoHash))
return false
Set<Certificate> set = byInfoHash.get(infoHash)
for (Certificate cert : set) {
if (cert.issuer == me)
return true
}
return false
}
public boolean isImported(Certificate certificate) {
Set<Certificate> forInfoHash = byInfoHash.get(certificate.infoHash)
if (forInfoHash == null)
return false
forInfoHash.contains(certificate)
}
Set<Certificate> getByInfoHash(InfoHash infoHash) {
Set<Certificate> rv = new HashSet<>()
if (byInfoHash.containsKey(infoHash))
rv.addAll(byInfoHash.get(infoHash))
rv
}
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.SharedFile
class UICreateCertificateEvent extends Event {
SharedFile sharedFile
}

View File

@@ -0,0 +1,10 @@
package com.muwire.core.filecert
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class UIFetchCertificatesEvent extends Event {
Persona host
InfoHash infoHash
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filecert
import com.muwire.core.Event
class UIImportCertificateEvent extends Event {
Certificate certificate
}

View File

@@ -0,0 +1,110 @@
package com.muwire.core.filefeeds
import java.util.logging.Level
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.zip.GZIPInputStream
import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
@Log
class FeedClient {
private final I2PConnector connector
private final EventBus eventBus
private final Persona me
private final FeedManager feedManager
private final ExecutorService feedFetcher = Executors.newCachedThreadPool()
private final Timer feedUpdater = new Timer("feed-updater", true)
FeedClient(I2PConnector connector, EventBus eventBus, Persona me, FeedManager feedManager) {
this.connector = connector
this.eventBus = eventBus
this.me = me
this.feedManager = feedManager
}
private void start() {
feedUpdater.schedule({updateAnyFeeds()} as TimerTask, 60000, 60000)
}
private void stop() {
feedUpdater.cancel()
feedFetcher.shutdown()
}
private void updateAnyFeeds() {
feedManager.getFeedsToUpdate().each { feed ->
feedFetcher.execute({updateFeed(feed)} as Runnable)
}
}
void onUIFeedUpdateEvent(UIFeedUpdateEvent e) {
Feed feed = feedManager.getFeed(e.host)
if (feed == null) {
log.severe("UI request to update non-existent feed " + e.host.getHumanReadableName())
return
}
feedFetcher.execute({updateFeed(feed)} as Runnable)
}
private void updateFeed(Feed feed) {
log.info("updating feed " + feed.getPublisher().getHumanReadableName())
Endpoint endpoint = null
try {
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.CONNECTING))
feed.setLastUpdateAttempt(System.currentTimeMillis())
endpoint = connector.connect(feed.getPublisher().getDestination())
OutputStream os = endpoint.getOutputStream()
os.write("FEED\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Timestamp:${feed.getLastUpdated()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("Invalid code $code")
// parse all headers
Map<String,String> headers = DataUtil.readAllHeaders(is)
if (!headers.containsKey("Count"))
throw new IOException("No count header")
int items = Integer.parseInt(headers['Count'])
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FETCHING, totalItems: items))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
for (int i = 0; i < items; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
def json = slurper.parse(tmp)
FeedItem item = FeedItems.objToFeedItem(json, feed.getPublisher())
eventBus.publish(new FeedItemFetchedEvent(item: item))
}
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "Feed update failed", bad)
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FAILED))
} finally {
endpoint?.close()
}
}
}

View File

@@ -0,0 +1,10 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
import com.muwire.core.Persona
class FeedFetchEvent extends Event {
Persona host
FeedFetchStatus status
int totalItems
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class FeedItemFetchedEvent extends Event {
FeedItem item
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class FeedItemLoadedEvent extends Event {
FeedItem item
}

View File

@@ -0,0 +1,79 @@
package com.muwire.core.filefeeds
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class FeedItems {
public static def sharedFileToObj(SharedFile sf, int certificates) {
def json = [:]
json.type = "FeedItem"
json.version = 1
json.name = Base64.encode(DataUtil.encodei18nString(sf.getFile().getName()))
json.infoHash = Base64.encode(sf.getRoot())
json.size = sf.getCachedLength()
json.pieceSize = sf.getPieceSize()
if (sf.getComment() != null)
json.comment = sf.getComment()
json.certificates = certificates
json.timestamp = sf.getPublishedTimestamp()
json
}
public static FeedItem objToFeedItem(def obj, Persona publisher) throws InvalidFeedItemException {
if (obj.timestamp == null)
throw new InvalidFeedItemException("No timestamp");
if (obj.name == null)
throw new InvalidFeedItemException("No name");
if (obj.size == null || obj.size <= 0 || obj.size > FileHasher.MAX_SIZE)
throw new InvalidFeedItemException("length missing or invalid ${obj.size}")
if (obj.pieceSize == null || obj.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || obj.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
throw new InvalidFeedItemException("piece size missing or invalid ${obj.pieceSize}")
if (obj.infoHash == null)
throw new InvalidFeedItemException("Infohash missing")
InfoHash infoHash
try {
infoHash = new InfoHash(Base64.decode(obj.infoHash))
} catch (Exception bad) {
throw new InvalidFeedItemException("Invalid infohash", bad)
}
String name
try {
name = DataUtil.readi18nString(Base64.decode(obj.name))
} catch (Exception bad) {
throw new InvalidFeedItemException("Invalid name", bad)
}
int certificates = 0
if (obj.certificates != null)
certificates = obj.certificates
new FeedItem(publisher, obj.timestamp, name, obj.size, obj.pieceSize, infoHash, certificates, obj.comment)
}
public static def feedItemToObj(FeedItem item) {
def json = [:]
json.type = "FeedItem"
json.version = 1
json.name = Base64.encode(DataUtil.encodei18nString(item.getName()))
json.infoHash = Base64.encode(item.getInfoHash().getRoot())
json.size = item.getSize()
json.pieceSize = item.getPieceSize()
json.timestamp = item.getTimestamp()
json.certificates = item.getCertificates()
json.comment = item.getComment()
json
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.filefeeds
import com.muwire.core.Event
class FeedLoadedEvent extends Event {
Feed feed
}

Some files were not shown because too many files have changed in this diff Show More