Compare commits

..

182 Commits

Author SHA1 Message Date
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
195 changed files with 6882 additions and 891 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,3 +1,5 @@
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.
@@ -49,6 +51,12 @@ MuWire is available as a Docker image. For more information see the [Docker] pa
## 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
```
@@ -67,3 +75,5 @@ You can find the full key at https://keybase.io/zlatinb
[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

View File

@@ -19,7 +19,8 @@ This helps with scalability
* Enum i18n
* Ability to share trust list only with trusted users
* Confidential files visible only to certain users
* Public Feed feature
* Advertise file feed and browseability in upload headers
* Manual polling / shared folder re-scan (because polling NAS doesn't work)
### Chat
* echo "unknown/innappropriate command" in the console
@@ -32,6 +33,8 @@ This helps with scalability
### Swing GUI
* I2P Status panel - display message when connected to external router
* Search box - left identation
* Ability to disable switching of tabs on actions
* Ability to trust/browse/subscribe from uploads tab
### Web UI/Plugin
* HTML 5 media players

View File

@@ -9,6 +9,19 @@ subprojects {
compileGroovy {
groovyOptions.optimizationOptions.indy = true
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 {

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.6.11"
private static final String MW_VERSION = "0.6.15"
private static volatile Core core

View File

@@ -28,7 +28,6 @@ class FilesModel {
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(AllFilesLoadedEvent.class, this)
Runnable refreshModel = {refreshModel()}
Timer timer = new Timer(true)
@@ -38,15 +37,6 @@ class FilesModel {
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
def eventBus = core.eventBus
guiThread.invokeLater {
core.muOptions.watchedDirectories.each {
eventBus.publish(new FileSharedEvent(file: new File(it)))
}
}
}
void onFileLoadedEvent(FileLoadedEvent e) {
guiThread.invokeLater {
sharedFiles.add(e.loadedFile)

View File

@@ -1,12 +1,38 @@
apply plugin : 'application'
mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile "net.i2p:i2p:${i2pVersion}"
compile "net.i2p:router:${i2pVersion}"
compile "net.i2p.client:mstreaming:${i2pVersion}"
compile "net.i2p.client:streaming:${i2pVersion}"
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
plugins {
id 'java-library'
id 'maven-publish'
}
dependencies {
api "net.i2p:i2p:${i2pVersion}"
api "net.i2p:router:${i2pVersion}"
implementation "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:2.4.15'
}
// 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
}
}
}
// publish core to local maven repo for sister projects
publishing {
publications {
muCore(MavenPublication) {
from components.java
}
}
repositories {
mavenLocal()
}
}

View File

@@ -5,6 +5,8 @@ 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
@@ -53,7 +55,11 @@ 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
@@ -79,6 +85,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
@@ -106,19 +113,19 @@ public class Core {
final Properties i2pOptions
final MuWireSettings muOptions
private final I2PSession i2pSession;
final I2PSession i2pSession;
final TrustService trustService
final TrustSubscriber trustSubscriber
private final PersisterService persisterService
private final PersisterFolderService persisterFolderService
private final HostCache hostCache
private final ConnectionManager connectionManager
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
@@ -128,6 +135,9 @@ public class Core {
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
@@ -144,22 +154,26 @@ public class Core {
// Read defaults
def defaultI2PFile = getClass()
.getClassLoader().getResource("defaults/i2p.properties");
defaultI2PFile.withInputStream { i2pOptions.load(it) }
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"
i2pOptions["inbound.nickname"] = tunnelName
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = tunnelName
}
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
&& i2pOptions.hasProperty("i2np.udp.port")
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, "") }
@@ -359,6 +373,9 @@ public class Core {
log.info("initializing upload manager")
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)
@@ -379,11 +396,6 @@ public class Core {
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
certificateManager, chatServer)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
@@ -405,6 +417,28 @@ public class Core {
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() {
@@ -421,6 +455,7 @@ public class Core {
updateClient?.start()
feedManager.start()
feedClient.start()
trackerResponder.start()
}
public void shutdown() {
@@ -448,6 +483,8 @@ public class Core {
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")
@@ -458,6 +495,8 @@ public class Core {
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()
log.info("killing i2p session")
@@ -505,7 +544,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.6.11")
Core core = new Core(props, home, "0.6.15")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -31,6 +31,7 @@ class MuWireSettings {
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
boolean allowTracking
boolean fileFeed
boolean advertiseFeed
@@ -92,6 +93,7 @@ class MuWireSettings {
outBw = Integer.valueOf(props.getProperty("outBw","128"))
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"))
@@ -100,7 +102,7 @@ class MuWireSettings {
defaultFeedAutoDownload = Boolean.valueOf(props.getProperty("defaultFeedAutoDownload", "false"))
defaultFeedItemsToKeep = Integer.valueOf(props.getProperty("defaultFeedItemsToKeep", "1000"))
defaultFeedSequential = Boolean.valueOf(props.getProperty("defaultFeedSequential", "false"))
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60"))
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60000"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
@@ -157,6 +159,7 @@ class MuWireSettings {
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))

View File

@@ -233,7 +233,7 @@ class ChatConnection implements ChatLink {
daos.close()
byte [] signed = baos.toByteArray()
def spk = sender.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
def signature = new Signature(spk.getType(), sig)
DSAEngine.getInstance().verifySignature(signature, signed, spk)
}

View File

@@ -244,7 +244,7 @@ abstract class Connection implements Closeable {
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
def signature = new Signature(spk.getType(), sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
@@ -266,7 +266,7 @@ abstract class Connection implements Closeable {
queryTime = search.queryTime
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig2)
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

View File

@@ -60,7 +60,7 @@ class ConnectionAcceptor {
private volatile shutdown
private volatile int browsed
volatile int browsed
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
@@ -574,7 +574,9 @@ class ConnectionAcceptor {
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)

View File

@@ -242,4 +242,8 @@ public class DownloadManager {
downloaders.values().each { it.stop() }
Downloader.executorService.shutdownNow()
}
public boolean isDownloading(InfoHash infoHash) {
downloaders.containsKey(infoHash)
}
}

View File

@@ -183,15 +183,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.addAndGet(read)
mapped.put(tmp)
dataSinceLastRead.addAndGet(tmp.length)
pieces.markPartial(piece, mapped.position())
}
}

View File

@@ -160,7 +160,7 @@ public class Downloader {
long dataRead = dataSinceLastRead.getAndSet(0)
long now = System.currentTimeMillis()
if (now > lastSpeedRead)
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
currSpeed = (int) (dataRead * 1000.0d / (now - lastSpeedRead))
lastSpeedRead = now
}

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)
@@ -78,6 +79,7 @@ class Pieces {
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)
}
@@ -91,11 +93,11 @@ class Pieces {
}
synchronized boolean isComplete() {
done.cardinality() == nPieces
cachedDone == nPieces
}
synchronized int donePieces() {
done.cardinality()
cachedDone
}
synchronized boolean isDownloaded(int piece) {
@@ -104,6 +106,7 @@ class Pieces {
synchronized void clearAll() {
done.clear()
cachedDone = 0
claimed.clear()
partials.clear()
}

View File

@@ -105,7 +105,7 @@ class Certificate {
byte [] payload = baos.toByteArray()
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
Signature signature = new Signature(Constants.SIG_TYPE, sig)
Signature signature = new Signature(spk.getType(), sig)
DSAEngine.getInstance().verifySignature(signature, payload, spk)
}

View File

@@ -121,8 +121,13 @@ abstract class BasePersisterService extends Service{
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
if (it.searcher != null) {
try {
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
} catch (Exception ignore) {
return
}
}
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)

View File

@@ -4,8 +4,9 @@ import com.muwire.core.Event
class DirectoryUnsharedEvent extends Event {
File directory
boolean deleted
public String toString() {
super.toString() + " unshared directory "+ directory.toString()
super.toString() + " unshared directory "+ directory.toString() + " deleted $deleted"
}
}

View File

@@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
import com.muwire.core.files.directories.WatchedDirectoryManager
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
@@ -33,27 +36,27 @@ class DirectoryWatcher {
}
private final File home
private final MuWireSettings muOptions
private final EventBus eventBus
private final FileManager fileManager
private final WatchedDirectoryManager watchedDirectoryManager
private final Thread watcherThread, publisherThread
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, WatchedDirectoryManager watchedDirectoryManager) {
this.home = home
this.muOptions = muOptions
this.eventBus = eventBus
this.fileManager = fileManager
this.watchedDirectoryManager = watchedDirectoryManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
watcherThread.setDaemon(true)
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
publisherThread.setDaemon(true)
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
watchService = FileSystems.getDefault().newWatchService()
watcherThread.start()
publisherThread.start()
@@ -71,26 +74,26 @@ class DirectoryWatcher {
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds)
watchedDirectories.put(canonical, wk)
if (muOptions.watchedDirectories.add(canonical.toString()))
saveMuSettings()
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
if (muOptions.watchedDirectories.remove(e.directory.toString()))
saveMuSettings()
}
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withPrintWriter("UTF-8", {
muOptions.write(it)
})
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
if (watchService == null)
return // still converting
if (!e.autoWatch) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
} else if (!watchedDirectories.containsKey(e.directory)) {
Path path = e.directory.toPath()
def wk = path.register(watchService, kinds)
watchedDirectories.put(e.directory, wk)
} // else it was already watched
}
private void watch() {
try {
while(!shutdown) {
@@ -115,7 +118,7 @@ class DirectoryWatcher {
File f= join(parent, path)
log.fine("created entry $f")
if (f.isDirectory())
f.toPath().register(watchService, kinds)
eventBus.publish(new FileSharedEvent(file : f, fromWatch : true))
else
waitingFiles.put(f, System.currentTimeMillis())
}
@@ -133,6 +136,10 @@ class DirectoryWatcher {
SharedFile sf = fileManager.fileToSharedFile.get(f)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
else if (watchedDirectoryManager.isWatched(f))
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
else
log.fine("Entry was not relevant");
}
private static File join(Path parent, Path path) {
@@ -149,7 +156,7 @@ class DirectoryWatcher {
waitingFiles.each { file, timestamp ->
if (now - timestamp > WAIT_TIME) {
log.fine("publishing file $file")
eventBus.publish new FileSharedEvent(file : file)
eventBus.publish new FileSharedEvent(file : file, fromWatch: true)
published << file
}
}

View File

@@ -28,6 +28,7 @@ class FileManager {
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex()
final FileTree<Void> negativeTree = new FileTree<>()
final FileTree<SharedFile> positiveTree = new FileTree<>()
final Set<File> sideCarFiles = new HashSet<>()
FileManager(EventBus eventBus, MuWireSettings settings) {
@@ -87,6 +88,7 @@ class FileManager {
}
existing.add(sf)
fileToSharedFile.put(sf.file, sf)
positiveTree.add(sf.file, sf);
negativeTree.remove(sf.file)
String parent = sf.getFile().getParent()
@@ -130,6 +132,7 @@ class FileManager {
}
fileToSharedFile.remove(sf.file)
positiveTree.remove(sf.file)
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
negativeTree.add(sf.file,null)
saveNegativeTree()
@@ -246,14 +249,26 @@ class FileManager {
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
negativeTree.remove(e.directory)
saveNegativeTree()
e.directory.listFiles().each {
if (it.isDirectory())
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
else {
SharedFile sf = fileToSharedFile.get(it)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
if (!e.deleted) {
e.directory.listFiles().each {
if (it.isDirectory())
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
else {
SharedFile sf = fileToSharedFile.get(it)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
}
}
} else {
def cb = new DirDeletionCallback()
positiveTree.traverse(e.directory, cb)
positiveTree.remove(e.directory)
cb.unsharedFiles.each {
eventBus.publish(new FileUnsharedEvent(unsharedFile : it, deleted: true))
}
cb.subDirs.each {
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted : true))
}
}
}
@@ -270,4 +285,25 @@ class FileManager {
collect(Collectors.toList())
}
}
private static class DirDeletionCallback implements FileTreeCallback<SharedFile> {
final List<File> subDirs = new ArrayList<>()
final List<SharedFile> unsharedFiles = new ArrayList<>()
@Override
public void onDirectoryEnter(File file) {
subDirs.add(file)
}
@Override
public void onDirectoryLeave() {
}
@Override
public void onFile(File file, SharedFile value) {
unsharedFiles << value
}
}
}

View File

@@ -5,9 +5,10 @@ import com.muwire.core.Event
class FileSharedEvent extends Event {
File file
boolean fromWatch
@Override
public String toString() {
return super.toString() + " file: "+file.getAbsolutePath()
return super.toString() + " file: "+file.getAbsolutePath() + " fromWatch: $fromWatch"
}
}

View File

@@ -23,6 +23,7 @@ class FileTree<T> {
if (existing == null) {
existing = new TreeNode()
existing.file = element
existing.isFile = element.isFile()
existing.parent = current
fileToNode.put(element, existing)
current.children.add(existing)
@@ -64,7 +65,7 @@ class FileTree<T> {
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
boolean leave = false
if (node.file != null) {
if (node.file.isFile())
if (node.isFile)
callback.onFile(node.file, node.value)
else {
leave = true
@@ -88,7 +89,7 @@ class FileTree<T> {
node = fileToNode.get(parent)
node.children.each {
if (it.file.isFile())
if (it.isFile)
callback.onFile(it.file, it.value)
else
callback.onDirectory(it.file)
@@ -98,6 +99,7 @@ class FileTree<T> {
public static class TreeNode<T> {
TreeNode parent
File file
boolean isFile
T value;
final Set<TreeNode> children = new HashSet<>()

View File

@@ -53,7 +53,6 @@ class HasherService {
private void process(File f) {
if (f.isDirectory()) {
eventBus.publish(new DirectoryWatchedEvent(directory : f))
f.listFiles().each {
eventBus.publish new FileSharedEvent(file: it)
}

View File

@@ -116,7 +116,7 @@ class PersisterFolderService extends BasePersisterService {
try {
_load()
}
catch (IllegalArgumentException e) {
catch (Exception e) {
log.log(Level.WARNING, "couldn't load files", e)
}
} else {

View File

@@ -62,7 +62,7 @@ class PersisterService extends BasePersisterService {
new File(location.absolutePath + ".bak")
)
listener.publish(new PersisterDoneEvent())
} catch (IllegalArgumentException e) {
} catch (Exception e) {
log.log(Level.WARNING, "couldn't load files",e)
}
} else {

View File

@@ -0,0 +1,7 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
class UISyncDirectoryEvent extends Event {
File directory
}

View File

@@ -0,0 +1,37 @@
package com.muwire.core.files.directories
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class WatchedDirectory {
final File directory
final String encodedName
boolean autoWatch
int syncInterval
long lastSync
WatchedDirectory(File directory) {
this.directory = directory.getCanonicalFile()
this.encodedName = Base64.encode(DataUtil.encodei18nString(directory.getAbsolutePath()))
}
def toJson() {
def rv = [:]
rv.directory = encodedName
rv.autoWatch = autoWatch
rv.syncInterval = syncInterval
rv.lastSync = lastSync
rv
}
static WatchedDirectory fromJson(def json) {
String dirName = DataUtil.readi18nString(Base64.decode(json.directory))
File dir = new File(dirName)
def rv = new WatchedDirectory(dir)
rv.autoWatch = json.autoWatch
rv.syncInterval = json.syncInterval
rv.lastSync = json.lastSync
rv
}
}

View File

@@ -0,0 +1,9 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
class WatchedDirectoryConfigurationEvent extends Event {
File directory
boolean autoWatch
int syncInterval
}

View File

@@ -0,0 +1,10 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
/**
* Emitted when converting an old watched directory entry to the
* new format.
*/
class WatchedDirectoryConvertedEvent extends Event {
}

View File

@@ -0,0 +1,27 @@
package com.muwire.core.files.directories
import com.muwire.core.Core
import com.muwire.core.files.AllFilesLoadedEvent
/**
* converts the setting-based format to new folder-based format.
*/
class WatchedDirectoryConverter {
private final Core core
WatchedDirectoryConverter(Core core) {
this.core = core
}
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
core.getMuOptions().getWatchedDirectories().each {
File directory = new File(it)
directory = directory.getCanonicalFile()
core.eventBus.publish(new WatchedDirectoryConfigurationEvent(directory : directory, autoWatch: true))
}
core.getMuOptions().getWatchedDirectories().clear()
core.saveMuSettings()
core.eventBus.publish(new WatchedDirectoryConvertedEvent())
}
}

View File

@@ -0,0 +1,220 @@
package com.muwire.core.files.directories
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.stream.Stream
import com.muwire.core.EventBus
import com.muwire.core.SharedFile
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
import com.muwire.core.files.FileListCallback
import com.muwire.core.files.FileManager
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
@Log
class WatchedDirectoryManager {
private final File home
private final EventBus eventBus
private final FileManager fileManager
private final Map<File, WatchedDirectory> watchedDirs = new ConcurrentHashMap<>()
private final ExecutorService diskIO = Executors.newSingleThreadExecutor({r ->
Thread t = new Thread(r, "disk-io")
t.setDaemon(true)
t
} as ThreadFactory)
private final Timer timer = new Timer("directory-timer", true)
private boolean converting = true
WatchedDirectoryManager(File home, EventBus eventBus, FileManager fileManager) {
this.home = new File(home, "directories")
this.home.mkdir()
this.eventBus = eventBus
this.fileManager = fileManager
}
public boolean isWatched(File f) {
watchedDirs.containsKey(f)
}
public Stream<WatchedDirectory> getWatchedDirsStream() {
watchedDirs.values().stream()
}
public void shutdown() {
diskIO.shutdown()
timer.cancel()
}
void onUISyncDirectoryEvent(UISyncDirectoryEvent e) {
def wd = watchedDirs.get(e.directory)
if (wd == null) {
log.warning("Got a sync event for non-watched dir ${e.directory}")
return
}
diskIO.submit({sync(wd, System.currentTimeMillis())} as Runnable)
}
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
if (converting) {
def newDir = new WatchedDirectory(e.directory)
// conversion is always autowatch really
newDir.autoWatch = e.autoWatch
persist(newDir)
} else {
def wd = watchedDirs.get(e.directory)
if (wd == null) {
log.severe("got a configuration event for a non-watched directory ${e.directory}")
return
}
wd.autoWatch = e.autoWatch
wd.syncInterval = e.syncInterval
persist(wd)
}
}
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
converting = false
diskIO.submit({
def slurper = new JsonSlurper()
Files.walk(home.toPath()).filter({
it.getFileName().toString().endsWith(".json")
}).
forEach {
def parsed = slurper.parse(it.toFile())
WatchedDirectory wd = WatchedDirectory.fromJson(parsed)
watchedDirs.put(wd.directory, wd)
}
watchedDirs.values().stream().filter({it.autoWatch}).forEach {
eventBus.publish(new DirectoryWatchedEvent(directory : it.directory))
eventBus.publish(new FileSharedEvent(file : it.directory))
}
timer.schedule({sync()} as TimerTask, 1000, 1000)
} as Runnable)
}
private void persist(WatchedDirectory dir) {
diskIO.submit({doPersist(dir)} as Runnable)
}
private void doPersist(WatchedDirectory dir) {
def json = JsonOutput.toJson(dir.toJson())
def targetFile = new File(home, dir.getEncodedName() + ".json")
targetFile.text = json
}
void onFileSharedEvent(FileSharedEvent e) {
if (e.file.isFile() || watchedDirs.containsKey(e.file))
return
def wd = new WatchedDirectory(e.file)
if (e.fromWatch) {
// parent should be already watched, copy settings
def parent = watchedDirs.get(e.file.getParentFile())
if (parent == null) {
log.severe("watching found a directory without a watched parent? ${e.file}")
return
}
wd.autoWatch = parent.autoWatch
wd.syncInterval = parent.syncInterval
} else
wd.autoWatch = true
watchedDirs.put(wd.directory, wd)
persist(wd)
if (wd.autoWatch)
eventBus.publish(new DirectoryWatchedEvent(directory: wd.directory))
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
def wd = watchedDirs.remove(e.directory)
if (wd == null) {
log.warning("unshared a directory that wasn't watched? ${e.directory}")
return
}
File persistFile = new File(home, wd.getEncodedName() + ".json")
persistFile.delete()
}
private void sync() {
long now = System.currentTimeMillis()
watchedDirs.values().stream().
filter({!it.autoWatch}).
filter({it.syncInterval > 0}).
filter({it.lastSync + it.syncInterval * 1000 < now}).
forEach({wd -> diskIO.submit({sync(wd, now)} as Runnable )})
}
private void sync(WatchedDirectory wd, long now) {
log.fine("syncing ${wd.directory}")
wd.lastSync = now
doPersist(wd)
eventBus.publish(new WatchedDirectorySyncEvent(directory: wd.directory, when: now))
def cb = new DirSyncCallback()
fileManager.positiveTree.list(wd.directory, cb)
Set<File> filesOnFS = new HashSet<>()
Set<File> dirsOnFS = new HashSet<>()
wd.directory.listFiles().each {
File canonical = it.getCanonicalFile()
if (canonical.isFile())
filesOnFS.add(canonical)
else
dirsOnFS.add(canonical)
}
Set<File> addedFiles = new HashSet<>(filesOnFS)
addedFiles.removeAll(cb.files)
addedFiles.each {
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
}
Set<File> addedDirs = new HashSet<>(dirsOnFS)
addedDirs.removeAll(cb.dirs)
addedDirs.each {
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
}
Set<File> deletedFiles = new HashSet<>(cb.files)
deletedFiles.removeAll(filesOnFS)
deletedFiles.each {
eventBus.publish(new FileUnsharedEvent(unsharedFile : fileManager.getFileToSharedFile().get(it), deleted : true))
}
Set<File> deletedDirs = new HashSet<>(cb.dirs)
deletedDirs.removeAll(dirsOnFS)
deletedDirs.each {
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted: true))
}
}
private static class DirSyncCallback implements FileListCallback<SharedFile> {
private final Set<File> files = new HashSet<>()
private final Set<File> dirs = new HashSet<>()
@Override
public void onFile(File f, SharedFile value) {
files.add(f)
}
@Override
public void onDirectory(File f) {
dirs.add(f)
}
}
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.files.directories
import com.muwire.core.Event
class WatchedDirectorySyncEvent extends Event {
File directory
long when
}

View File

@@ -8,10 +8,8 @@ class CacheServers {
private static Set<Destination> CACHES = [
// zlatinb
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
// sNL
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
// dark_trion
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
// echelon
new Destination("2MJTl8gYVPK43iJZJa~-5K1OchgPaPHXpqZmKIiKFvxyy8BlIJzUSrF4mazdta--shFHISfT0PEeI95j1yDyKMpGxatUyjSt3ZnyTfAehQR-H2kYV9FvjHo68uA9X5AaGYHKRYLuWMkihMXygd8ywoLjZtFP0UbKMPggfOZaWmjHF4081XoUXt~7MEAeYSQowndiUx0AH3HxNEiv0N373JJS61OsIXb5ctqVKkwIiX1R0ZxESzpP9Xwp8-T0ou8fsLksygbKyH~3K1CyTHjTS51Ux-U-CjOPH9rtCOjjAaifdyMpK0PxW1fVdoGswFywTz9Q-6DUMsIu5TsPMF0-UO1Wn8vCpVAWbBJAOtKCfBrGzp-E~GCbfCNs5xY19nLobMD5ehjsBdI1lXwGDCQ7kBOwC58uuC3BOoazgrB6IrGskyMTexawtthO9mhuPm91bq4xhNaCYHAe059xg5emnM7jFBVzQgjaZ5lOLn~HqcWofJ7oc0doE6XI6kOo~YncBQAEAAcAAA==")
]
static List<Destination> getCacheServers() {

View File

@@ -10,7 +10,7 @@ import net.i2p.util.ConcurrentHashSet
class Mesh {
private final InfoHash infoHash
private final Set<Persona> sources = new ConcurrentHashSet<>()
private final Pieces pieces
final Pieces pieces
Mesh(InfoHash infoHash, Pieces pieces) {
this.infoHash = infoHash

View File

@@ -0,0 +1,214 @@
package com.muwire.core.tracker
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Pieces
import com.muwire.core.files.FileManager
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.data.Base64
@Log
class TrackerResponder {
private final I2PSession i2pSession
private final MuWireSettings muSettings
private final FileManager fileManager
private final DownloadManager downloadManager
private final MeshManager meshManager
private final TrustService trustService
private final Persona me
private final Map<UUID,Long> uuids = new HashMap<>()
private final Timer expireTimer = new Timer("tracker-responder-timer", true)
private static final long UUID_LIFETIME = 10 * 60 * 1000
TrackerResponder(I2PSession i2pSession, MuWireSettings muSettings,
FileManager fileManager, DownloadManager downloadManager,
MeshManager meshManager, TrustService trustService,
Persona me) {
this.i2pSession = i2pSession
this.muSettings = muSettings
this.fileManager = fileManager
this.downloadManager = downloadManager
this.meshManager = meshManager
this.trustService = trustService
this.me = me
}
void start() {
i2pSession.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
expireTimer.schedule({expireUUIDs()} as TimerTask, UUID_LIFETIME, UUID_LIFETIME)
}
void stop() {
expireTimer.cancel()
}
private void expireUUIDs() {
final long now = System.currentTimeMillis()
synchronized(uuids) {
for (Iterator<UUID> iter = uuids.keySet().iterator(); iter.hasNext();) {
UUID uuid = iter.next();
Long time = uuids.get(uuid)
if (now - time > UUID_LIFETIME)
iter.remove()
}
}
}
private void respond(host, json) {
log.info("responding to host $host with json $json")
def message = JsonOutput.toJson(json)
def maker = new I2PDatagramMaker(i2pSession)
message = maker.makeI2PDatagram(message.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(false)
i2pSession.sendMessage(host, message, 0, message.length, I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
}
class Listener implements I2PSessionMuxedListener {
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning "Received unexpected protocol $proto"
return
}
byte[] payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
log.info("got a tracker datagram from ${sender.toBase32()}")
// if not trusted, just drop it
TrustLevel trustLevel = trustService.getLevel(sender)
if (trustLevel == TrustLevel.DISTRUSTED ||
(trustLevel == TrustLevel.NEUTRAL && !muSettings.allowUntrusted)) {
log.info("dropping, untrusted")
return
}
payload = dissector.getPayload()
def slurper = new JsonSlurper()
def json = slurper.parse(payload)
if (json.type != "TrackerPing") {
log.warning("unknown type $json.type")
return
}
def response = [:]
response.type = "TrackerPong"
response.me = me.toBase64()
if (json.infoHash == null) {
log.warning("infoHash missing")
return
}
if (json.uuid == null) {
log.warning("uuid missing")
return
}
UUID uuid = UUID.fromString(json.uuid)
synchronized(uuids) {
if (uuids.containsKey(uuid)) {
log.warning("duplicate uuid $uuid")
return
}
uuids.put(uuid, System.currentTimeMillis())
}
response.uuid = json.uuid
if (!muSettings.allowTracking) {
response.code = 403
respond(sender, response)
return
}
if (json.version != 1) {
log.warning("unknown version $json.version")
response.code = 400
response.message = "I only support version 1"
respond(sender,response)
return
}
byte[] infoHashBytes = Base64.decode(json.infoHash)
InfoHash infoHash = new InfoHash(infoHashBytes)
log.info("servicing request for infoHash ${json.infoHash} with uuid ${json.uuid}")
if (!(fileManager.isShared(infoHash) || downloadManager.isDownloading(infoHash))) {
response.code = 404
respond(sender, response)
return
}
Mesh mesh = meshManager.get(infoHash)
if (fileManager.isShared(infoHash))
response.code = 200
else if (mesh != null) {
response.code = 206
Pieces pieces = mesh.getPieces()
response.xHave = DataUtil.encodeXHave(pieces, pieces.getnPieces())
}
if (mesh != null)
response.altlocs = mesh.getRandom(10, me).stream().map({it.toBase64()}).collect(Collectors.toList())
respond(sender,response)
} catch (Exception e) {
log.log(Level.WARNING, "invalid datagram", e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
log.severe("session disconnected")
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}

View File

@@ -2,6 +2,7 @@ package com.muwire.core.update
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
@@ -63,7 +64,7 @@ class UpdateClient {
}
void start() {
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT)
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
}
@@ -108,7 +109,7 @@ class UpdateClient {
ping = maker.makeI2PDatagram(ping.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT, 0, options)
}
class Listener implements I2PSessionMuxedListener {

View File

@@ -106,7 +106,7 @@ class ContentUploader extends Uploader {
return done ? 100 : 0
int position = mapped.position()
int total = request.getRange().end - request.getRange().start
(int)(position * 100.0 / total)
(int)(position * 100.0d / total)
}
@Override

View File

@@ -45,7 +45,7 @@ class HashListUploader extends Uploader {
@Override
public synchronized int getProgress() {
(int)(mapped.position() * 100.0 / mapped.capacity())
(int)(mapped.position() * 100.0d / mapped.capacity())
}
@Override

View File

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

View File

@@ -49,7 +49,7 @@ abstract class Uploader {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
int currSpeed = (int) (dataSinceLastRead * 1000.0d / interval)
dataSinceLastRead = 0
// normalize to speedArr.size

View File

@@ -4,6 +4,8 @@ import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_VERSION = (byte)1;
public static final String INVALID_NICKNAME_CHARS = "'\"();<>=@$%";
public static final int MAX_NICKNAME_LENGTH = 30;
public static final byte FILE_CERT_VERSION = (byte)2;
public static final int CHAT_VERSION = 1;
@@ -17,5 +19,8 @@ public class Constants {
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
public static final int UPDATE_PORT = 2;
public static final int TRACKER_PORT = 3;
}

View File

@@ -0,0 +1,25 @@
package com.muwire.core;
public class InvalidNicknameException extends Exception {
public InvalidNicknameException() {
}
public InvalidNicknameException(String message) {
super(message);
}
public InvalidNicknameException(Throwable cause) {
super(cause);
}
public InvalidNicknameException(String message, Throwable cause) {
super(message, cause);
}
public InvalidNicknameException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -7,6 +7,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.muwire.core.util.DataUtil;
import net.i2p.crypto.DSAEngine;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
@@ -25,12 +27,15 @@ public class Persona {
private volatile String base64;
private volatile byte[] payload;
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException {
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException, InvalidNicknameException {
version = (byte) (personaStream.read() & 0xFF);
if (version != Constants.PERSONA_VERSION)
throw new IOException("Unknown version "+version);
name = new Name(personaStream);
if (!DataUtil.isValidName(name.name))
throw new InvalidNicknameException(name.name + " is not a valid nickname");
destination = Destination.create(personaStream);
sig = new byte[SIG_LEN];
DataInputStream dis = new DataInputStream(personaStream);
@@ -38,7 +43,7 @@ public class Persona {
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)
throws IOException, DataFormatException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -47,7 +52,7 @@ public class Persona {
destination.writeBytes(baos);
byte[] payload = baos.toByteArray();
SigningPublicKey spk = destination.getSigningPublicKey();
Signature signature = new Signature(Constants.SIG_TYPE, sig);
Signature signature = new Signature(spk.getType(), sig);
return DSAEngine.getInstance().verifySignature(signature, payload, spk);
}

View File

@@ -159,6 +159,18 @@ public class SharedFile {
this.query = query;
}
public Persona getSearcher() {
return searcher;
}
public long getTimestamp() {
return timestamp;
}
public String getQuery() {
return query;
}
public int hashCode() {
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
}

View File

@@ -58,9 +58,9 @@ public class DataUtil {
if (header.length != 3)
throw new IllegalArgumentException("header length $header.length");
return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF);
return ((header[0] & 0x7F) << 16) |
((header[1] & 0xFF) << 8) |
(header[2] & 0xFF);
}
public static String readi18nString(byte [] encoded) {
@@ -174,7 +174,7 @@ public class DataUtil {
clean.setAccessible(true);
clean.invoke(cleaner.invoke(cb));
} else {
Class unsafeClass;
Class<?> unsafeClass;
try {
unsafeClass = Class.forName("sun.misc.Unsafe");
} catch(Exception ex) {
@@ -216,4 +216,13 @@ public class DataUtil {
Signature sig = DSAEngine.getInstance().sign(payload, spk);
return sig.getData();
}
public static boolean isValidName(String name) {
if (name.length() > Constants.MAX_NICKNAME_LENGTH)
return false;
for (int i = 0; i < Constants.INVALID_NICKNAME_CHARS.length(); i++)
if (name.indexOf(Constants.INVALID_NICKNAME_CHARS.charAt(i)) >= 0)
return false;
return true;
}
}

View File

@@ -39,13 +39,13 @@ class FileManagerTest {
@Test
void testHash1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih, 0)
byte [] root = new byte[32]
SharedFile sf = new SharedFile(f,root, 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
UUID uuid = UUID.randomUUID()
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
manager.onSearchEvent(se)
Thread.sleep(20)
@@ -58,14 +58,14 @@ class FileManagerTest {
@Test
void testHash2Results() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
byte [] root = new byte[32]
SharedFile sf1 = new SharedFile(new File("a b.c"), root, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), root, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
UUID uuid = UUID.randomUUID()
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
manager.onSearchEvent(se)
Thread.sleep(20)
@@ -81,7 +81,7 @@ class FileManagerTest {
void testHash0Results() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih, 0)
SharedFile sf = new SharedFile(f,ih.getRoot(), 0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -95,7 +95,7 @@ class FileManagerTest {
void testKeyword1Result() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih,0)
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -113,12 +113,12 @@ class FileManagerTest {
void testKeyword2Results() {
File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1, 0)
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
UUID uuid = UUID.randomUUID()
@@ -136,7 +136,7 @@ class FileManagerTest {
void testKeyword0Results() {
File f = new File("a b.c")
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf = new SharedFile(f,ih,0)
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
manager.onFileHashedEvent(fhe)
@@ -149,8 +149,8 @@ class FileManagerTest {
@Test
void testRemoveFileExistingHash() {
InfoHash ih = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
SharedFile sf1 = new SharedFile(new File("a b.c"), ih.getRoot(), 0)
SharedFile sf2 = new SharedFile(new File("d e.f"), ih.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
@@ -167,12 +167,12 @@ class FileManagerTest {
void testRemoveFile() {
File f1 = new File("a b.c")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1, 0)
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
File f2 = new File("c d.e")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
@@ -198,7 +198,7 @@ class FileManagerTest {
comment = Base64.encode(DataUtil.encodei18nString(comment))
File f1 = new File("MuWire-0.5.10.AppImage")
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
SharedFile sf1 = new SharedFile(f1, ih1, 0)
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
sf1.setComment(comment)
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
@@ -206,7 +206,7 @@ class FileManagerTest {
File f2 = new File("MuWire-0.6.0.AppImage")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
sf2.setComment(comment)
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))

View File

@@ -45,7 +45,7 @@ class HasherServiceTest {
def hashed = listener.poll()
assert hashed instanceof FileHashedEvent
assert hashed.sharedFile.file == f.getCanonicalFile()
assert hashed.sharedFile.infoHash != null
assert hashed.sharedFile.root != null
assert listener.isEmpty()
}

View File

@@ -85,7 +85,7 @@ class PersisterServiceLoadingTest {
def loadedFile = listener.publishedFiles[0]
assert loadedFile != null
assert loadedFile.file == sharedFile1.getCanonicalFile()
assert loadedFile.infoHash == ih1
assert loadedFile.root == ih1.getRoot()
}
private static String getSharedFileJsonName(File sharedFile) {
@@ -128,7 +128,7 @@ class PersisterServiceLoadingTest {
def loadedFile = listener.publishedFiles[0]
assert loadedFile != null
assert loadedFile.file == sharedFile1.getCanonicalFile()
assert loadedFile.infoHash == ih1
assert loadedFile.root == ih1.getRoot()
}
@Test
@@ -169,10 +169,10 @@ class PersisterServiceLoadingTest {
assert listener.publishedFiles.size() == 2
def loadedFile1 = listener.publishedFiles[0]
assert loadedFile1.file == sharedFile1.getCanonicalFile()
assert loadedFile1.infoHash == ih1
assert loadedFile1.root == ih1.getRoot()
def loadedFile2 = listener.publishedFiles[1]
assert loadedFile2.file == sharedFile2.getCanonicalFile()
assert loadedFile2.infoHash == ih2
assert loadedFile2.root == ih2.getRoot()
}
@Test

View File

@@ -2,6 +2,7 @@ package com.muwire.core.files
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import com.muwire.core.Destinations
@@ -16,6 +17,7 @@ import groovy.json.JsonSlurper
import net.i2p.data.Base32
import net.i2p.data.Base64
@Ignore
class PersisterServiceSavingTest {
File f

View File

@@ -1,6 +1,6 @@
group = com.muwire
version = 0.6.11
i2pVersion = 0.9.44
version = 0.6.15
i2pVersion = 0.9.45
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
@@ -8,8 +8,10 @@ grailsVersion=4.0.0
gorm.version=7.0.2.RELEASE
griffonEnv=prod
# javac properties
sourceCompatibility=1.8
targetCompatibility=1.8
compilerArgs=-Xlint:unchecked,cast,path,divzero,empty,path,finally,overrides
# plugin properties
author = zab@mail.i2p

View File

@@ -131,4 +131,14 @@ mvcGroups {
view = 'com.muwire.gui.FeedConfigurationView'
controller = 'com.muwire.gui.FeedConfigurationController'
}
'watched-directory' {
model = 'com.muwire.gui.WatchedDirectoryModel'
view = 'com.muwire.gui.WatchedDirectoryView'
controller = 'com.muwire.gui.WatchedDirectoryController'
}
'sign' {
model = 'com.muwire.gui.SignModel'
view = 'com.muwire.gui.SignView'
controller = 'com.muwire.gui.SignController'
}
}

View File

@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.files.directories.UISyncDirectoryEvent
@ArtifactProviderFor(GriffonController)
class AdvancedSharingController {
@@ -14,4 +15,25 @@ class AdvancedSharingController {
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingView view
@ControllerAction
void configure() {
def wd = view.selectedWatchedDirectory()
if (wd == null)
return
def params = [:]
params['core'] = model.core
params['directory'] = wd
mvcGroup.createMVCGroup("watched-directory",params)
}
@ControllerAction
void sync() {
def wd = view.selectedWatchedDirectory()
if (wd == null)
return
def event = new UISyncDirectoryEvent(directory : wd.directory)
model.core.eventBus.publish(event)
}
}

View File

@@ -104,6 +104,10 @@ class OptionsController {
model.browseFiles = browseFiles
settings.browseFiles = browseFiles
boolean allowTracking = view.allowTrackingCheckbox.model.isSelected()
model.allowTracking = allowTracking
settings.allowTracking = allowTracking
text = view.speedSmoothSecondsField.text
model.speedSmoothSeconds = Integer.valueOf(text)
settings.speedSmoothSeconds = Integer.valueOf(text)

View File

@@ -0,0 +1,50 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
class SignController {
Core core
@MVCMember @Nonnull
SignView view
@ControllerAction
void sign() {
String plain = view.plainTextArea.getText()
byte[] payload = plain.trim().getBytes(StandardCharsets.UTF_8)
def sig = DSAEngine.getInstance().sign(payload, core.spk)
view.signedTextArea.setText(Base64.encode(sig.data))
}
@ControllerAction
void copy() {
String signed = view.signedTextArea.getText()
StringSelection selection = new StringSelection(signed)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
@ControllerAction
void close() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -0,0 +1,33 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
@ArtifactProviderFor(GriffonController)
class WatchedDirectoryController {
@MVCMember @Nonnull
WatchedDirectoryModel model
@MVCMember @Nonnull
WatchedDirectoryView view
@ControllerAction
void save() {
def event = new WatchedDirectoryConfigurationEvent(
directory : model.directory.directory,
autoWatch : view.autoWatchCheckbox.model.isSelected(),
syncInterval : Integer.parseInt(view.syncIntervalField.text))
model.core.eventBus.publish(event)
cancel()
}
@ControllerAction
void cancel() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -6,10 +6,12 @@ import net.i2p.util.SystemVersion
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.util.DataUtil
import javax.annotation.Nonnull
import javax.inject.Inject
@@ -116,8 +118,9 @@ class Ready extends AbstractLifecycleHandler {
JOptionPane.WARNING_MESSAGE)
continue
}
if (nickname.contains("@")) {
JOptionPane.showMessageDialog(null, "Nickname cannot contain @, choose another",
if (!DataUtil.isValidName(nickname)) {
JOptionPane.showMessageDialog(null,
"Nickname cannot contain any of ${Constants.INVALID_NICKNAME_CHARS} and must be no longer than ${Constants.MAX_NICKNAME_LENGTH} characters. Choose another.",
"Select another nickname", JOptionPane.WARNING_MESSAGE)
continue
}

View File

@@ -1,32 +1,49 @@
package com.muwire.gui
import javax.annotation.Nonnull
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import com.muwire.core.Core
import com.muwire.core.files.FileTree
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
import com.muwire.core.files.directories.WatchedDirectorySyncEvent
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class AdvancedSharingModel {
@MVCMember @Nonnull
AdvancedSharingView view
def watchedDirectories = []
def treeRoot
def negativeTree
Core core
@Observable boolean syncActionEnabled
void mvcGroupInit(Map<String,String> args) {
watchedDirectories.addAll(core.muOptions.watchedDirectories)
watchedDirectories.addAll(core.watchedDirectoryManager.watchedDirs.values())
core.eventBus.register(WatchedDirectorySyncEvent.class, this)
core.eventBus.register(WatchedDirectoryConfigurationEvent.class, this)
treeRoot = new DefaultMutableTreeNode()
negativeTree = new DefaultTreeModel(treeRoot)
copyTree(treeRoot, core.fileManager.negativeTree.root)
}
void mvcGroupDestroy() {
core.eventBus.unregister(WatchedDirectorySyncEvent.class, this)
core.eventBus.unregister(WatchedDirectoryConfigurationEvent.class, this)
}
private void copyTree(DefaultMutableTreeNode jtreeNode, FileTree.TreeNode fileTreeNode) {
jtreeNode.setUserObject(fileTreeNode.file?.getName())
fileTreeNode.children.each {
@@ -36,4 +53,16 @@ class AdvancedSharingModel {
}
}
void onWatchedDirectorySyncEvent(WatchedDirectorySyncEvent e) {
runInsideUIAsync {
view.watchedDirsTable.model.fireTableDataChanged()
}
}
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
runInsideUIAsync {
view.watchedDirsTable.model.fireTableDataChanged()
}
}
}

View File

@@ -294,8 +294,6 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync {
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
}
@@ -415,7 +413,7 @@ class MainFrameModel {
break
if (parent.getChildCount() == 0) {
File file = parent.getUserObject().file
if (core.muOptions.watchedDirectories.contains(file.toString()))
if (core.watchedDirectoryManager.isWatched(file))
unshared.add(file)
dmtn = parent
continue

View File

@@ -18,6 +18,7 @@ class OptionsModel {
@Observable String incompleteLocation
@Observable boolean searchComments
@Observable boolean browseFiles
@Observable boolean allowTracking
@Observable int speedSmoothSeconds
@Observable int totalUploadSlots
@Observable int uploadSlotsPerUser
@@ -83,6 +84,7 @@ class OptionsModel {
incompleteLocation = settings.incompleteLocation.getAbsolutePath()
searchComments = settings.searchComments
browseFiles = settings.browseFiles
allowTracking = settings.allowTracking
speedSmoothSeconds = settings.speedSmoothSeconds
totalUploadSlots = settings.totalUploadSlots
uploadSlotsPerUser = settings.uploadSlotsPerUser

View File

@@ -0,0 +1,9 @@
package com.muwire.gui
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class SignModel {
}

View File

@@ -0,0 +1,22 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.files.directories.WatchedDirectory
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class WatchedDirectoryModel {
Core core
WatchedDirectory directory
@Observable boolean autoWatch
@Observable int syncInterval
void mvcGroupInit(Map<String,String> args) {
autoWatch = directory.autoWatch
syncInterval = directory.syncInterval
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -3,13 +3,23 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTabbedPane
import javax.swing.JTree
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.files.directories.WatchedDirectory
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
@@ -21,6 +31,8 @@ class AdvancedSharingView {
FactoryBuilderSupport builder
@MVCMember @Nonnull
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingController controller
def mainFrame
def dialog
@@ -28,6 +40,7 @@ class AdvancedSharingView {
def negativeTreePanel
def watchedDirsTable
def watchedDirsTableSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
@@ -43,10 +56,17 @@ class AdvancedSharingView {
scrollPane( constraints : BorderLayout.CENTER ) {
watchedDirsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.watchedDirectories) {
closureColumn(header : "Directory", type : String, read : {it})
closureColumn(header : "Directory", preferredWidth: 350, type : String, read : {it.directory.toString()})
closureColumn(header : "Auto", preferredWidth: 100, type : Boolean, read : {it.autoWatch})
closureColumn(header : "Interval", preferredWidth : 100, type : Integer, read : {it.syncInterval})
closureColumn(header : "Last Sync", preferredWidth: 250, type : Long, read : {it.lastSync})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Configure", configureAction)
button(text : "Sync", enabled : bind{model.syncActionEnabled}, syncAction)
}
}
negativeTreePanel = builder.panel {
@@ -59,6 +79,54 @@ class AdvancedSharingView {
tree(rootVisible : false, rowHeight : rowHeight,jtree)
}
}
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
watchedDirsTable.setDefaultRenderer(Long.class, new DateRenderer())
watchedDirsTable.setDefaultRenderer(Integer.class, centerRenderer)
watchedDirsTable.rowSorter.addRowSorterListener({evt -> watchedDirsTableSortEvent = evt})
def selectionModel = watchedDirsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
def directory = selectedWatchedDirectory()
model.syncActionEnabled = !(directory == null || directory.autoWatch)
})
watchedDirsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
}
private void showMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu()
JMenuItem configure = new JMenuItem("Configure")
configure.addActionListener({controller.configure()})
menu.add(configure)
if (model.syncActionEnabled) {
JMenuItem sync = new JMenuItem("Sync")
sync.addActionListener({controller.sync()})
menu.add(sync)
}
menu.show(e.getComponent(), e.getX(), e.getY())
}
WatchedDirectory selectedWatchedDirectory() {
int row = watchedDirsTable.getSelectedRow()
if (row < 0)
return null
if (watchedDirsTableSortEvent != null)
row = watchedDirsTable.rowSorter.convertRowIndexToModel(row)
model.watchedDirectories[row]
}
void mvcGroupInit(Map<String,String> args) {

View File

@@ -144,6 +144,11 @@ class MainFrameView {
mvcGroup.createMVCGroup("chat-monitor","chat-monitor",env)
}
})
menuItem("Sign Tool", actionPerformed : {
def env = [:]
env['core'] = model.core
mvcGroup.createMVCGroup("sign",env)
})
}
}
borderLayout()
@@ -447,7 +452,7 @@ class MainFrameView {
closureColumn(header : "Publisher", preferredWidth: 350, type : String, read : {it.getPublisher().getHumanReadableName()})
closureColumn(header : "Files", preferredWidth: 10, type : Integer, read : {model.core.feedManager.getFeedItems(it.getPublisher()).size()})
closureColumn(header : "Last Updated", type : Long, read : {it.getLastUpdated()})
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus()})
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus().toString()})
}
}
}

View File

@@ -43,6 +43,7 @@ class OptionsView {
def shareHiddenCheckbox
def searchCommentsCheckbox
def browseFilesCheckbox
def allowTrackingCheckbox
def speedSmoothSecondsField
def totalUploadSlotsField
def uploadSlotsPerUserField
@@ -107,6 +108,10 @@ class OptionsView {
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 1,
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx: 0))
label(text : "Allow tracking", constraints : gbc(gridx: 0, gridy: 2, anchor: GridBagConstraints.LINE_START,
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
allowTrackingCheckbox = checkBox(selected : bind {model.allowTracking}, constraints : gbc(gridx: 1, gridy : 2,
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx : 0))
}
panel (border : titledBorder(title : "Download Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,

View File

@@ -0,0 +1,66 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SignView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
def mainFrame
def dialog
def p
def plainTextArea
def signedTextArea
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Sign Text", true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label("Enter text to be signed")
}
panel (constraints : BorderLayout.CENTER) {
gridLayout(rows : 2, cols: 1)
scrollPane {
plainTextArea = textArea(rows : 10, columns : 50, editable : true, lineWrap: true, wrapStyleWord : true)
}
scrollPane {
signedTextArea = textArea(rows : 10, columns : 50, editable : false, lineWrap : true, wrapStyleWord : true)
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Sign", signAction)
button(text : "Copy To Clipboard", copyAction)
button(text : "Dismiss", closeAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@@ -0,0 +1,74 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import javax.swing.event.ChangeListener
import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class WatchedDirectoryView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
WatchedDirectoryModel model
def dialog
def p
def mainFrame
def autoWatchCheckbox
def syncIntervalField
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, "Watched Directory Configuration", true)
dialog.setResizable(false)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label("Configuration for directory " + model.directory.directory.toString())
}
panel (constraints : BorderLayout.CENTER) {
gridBagLayout()
label(text : "Auto-watch directory using operating system", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoWatchCheckbox = checkBox(selected : bind {model.autoWatch}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Directory sync frequency (seconds, 0 means never)", enabled : bind {!model.autoWatch}, constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
syncIntervalField = textField(text : bind {model.syncInterval}, columns: 4, enabled : bind {!model.autoWatch},
constraints: gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END, insets : [0,10,0,0]))
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Save", saveAction)
button(text : "Cancel", cancelAction)
}
}
}
void mvcGroupInit(Map<String,String> args) {
autoWatchCheckbox.addChangeListener({e ->
model.autoWatch = autoWatchCheckbox.model.isSelected()
} as ChangeListener)
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
}

View File

@@ -6,8 +6,8 @@ class DownloaderComparator implements Comparator<Downloader>{
@Override
public int compare(Downloader o1, Downloader o2) {
double d1 = o1.donePieces() * 1.0 / o1.nPieces
double d2 = o2.donePieces() * 1.0 / o2.nPieces
double d1 = o1.donePieces().toDouble() / o1.nPieces
double d2 = o2.donePieces().toDouble() / o2.nPieces
return Double.compare(d1, d2);
}
}

View File

@@ -6,4 +6,5 @@ dependencies {
compile "net.i2p:i2p:${i2pVersion}"
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
testCompile 'org.codehaus.groovy:groovy-all:2.4.15'
}

View File

@@ -0,0 +1,62 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = hostcache.log
java.util.logging.FileHandler.limit = 5000000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
# <level>: <log message> [<date/time>]
#
#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %2$s %5$s %6$s %n
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE
net.i2p.client.streaming.impl.level = SEVERE

View File

@@ -0,0 +1,23 @@
#!/usr/bin/python3
import os,sys,json
if len(sys.argv) < 2 :
print("This script counts unique hosts in the MuWire network",file = sys.stderr)
print("Pass the prefix of the files to analyse. For example:",file = sys.stderr)
print("\"20200427\" will count unique hosts on 27th of April 2020",file = sys.stderr)
print("\"202004\" will count unique hosts during all of April 2020",file = sys.stderr)
sys.exit(1)
day = sys.argv[1]
files = os.listdir(".")
files = [x for x in files if x.startswith(day)]
hosts = set()
for f in files:
for line in open(f):
host = json.loads(line)
hosts.add(host["destination"])
print(len(hosts))

View File

@@ -40,12 +40,13 @@ class Crawler {
try {
uuid = UUID.fromString(pong.uuid)
} catch (IllegalArgumentException bad) {
log.log(Level.WARNING,"couldn't parse uuid",bad)
hostPool.fail(host)
return
}
if (!uuid.equals(currentUUID)) {
log.info("uuid mismatch")
log.warning("uuid mismatch $uuid expected $currentUUID")
hostPool.fail(host)
return
}
@@ -75,11 +76,12 @@ class Crawler {
}
synchronized def startCrawl() {
currentUUID = UUID.randomUUID()
log.info("starting new crawl with uuid $currentUUID inFlight ${inFlight.size()}")
if (!inFlight.isEmpty()) {
inFlight.values().each { hostPool.fail(it) }
inFlight.clear()
}
currentUUID = UUID.randomUUID()
hostPool.getUnverified(parallel).each {
inFlight.put(it.destination, it)
pinger.ping(it, currentUUID)

View File

@@ -15,4 +15,9 @@ class Host {
public boolean equals(other) {
return destination.equals(other.destination)
}
@Override
public String toString() {
"Host[b32:${destination.toBase32()} verifyTime:$verifyTime verificationFailures:$verificationFailures]"
}
}

View File

@@ -64,8 +64,10 @@ public class HostCache {
Timer timer = new Timer("timer", true)
timer.schedule({hostPool.age()} as TimerTask, 1000,1000)
timer.schedule({crawler.startCrawl()} as TimerTask, 10000, 10000)
File verified = new File("verified.json")
File unverified = new File("unverified.json")
File verified = new File("verified")
File unverified = new File("unverified")
verified.mkdir()
unverified.mkdir()
timer.schedule({hostPool.serialize(verified, unverified)} as TimerTask, 10000, 60 * 60 * 1000)
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),

View File

@@ -1,11 +1,14 @@
package com.muwire.hostcache
import java.text.SimpleDateFormat
import java.util.stream.Collectors
import groovy.json.JsonOutput
class HostPool {
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMdd-HH")
final def maxFailures
final def maxAge
@@ -77,9 +80,11 @@ class HostPool {
}
}
synchronized void serialize(File verifiedFile, File unverifiedFile) {
write(verifiedFile, verified.values())
write(unverifiedFile, unverified.values())
synchronized void serialize(File verifiedPath, File unverifiedPath) {
def now = new Date()
now = SDF.format(now)
write(new File(verifiedPath, now), verified.values())
write(new File(unverifiedPath, now), unverified.values())
}
private void write(File target, Collection hosts) {

View File

@@ -1,17 +1,21 @@
package com.muwire.hostcache
import groovy.json.JsonOutput
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramMaker
@Log
class Pinger {
final def session
Pinger(session) {
final I2PSession session
Pinger(I2PSession session) {
this.session = session
}
def ping(host, uuid) {
log.info("pinging $host with uuid:$uuid")
def maker = new I2PDatagramMaker(session)
def payload = new HashMap()
payload.type = "CrawlerPing"
@@ -19,6 +23,8 @@ class Pinger {
payload.uuid = uuid
payload = JsonOutput.toJson(payload)
payload = maker.makeI2PDatagram(payload.bytes)
session.sendMessage(host.destination, payload, I2PSession.PROTO_DATAGRAM, 0, 0)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
session.sendMessage(host.destination, payload, 0, payload.length, I2PSession.PROTO_DATAGRAM, 0, 0, options)
}
}

View File

@@ -5,5 +5,6 @@ include 'core'
include 'gui'
include 'cli'
include 'cli-lanterna'
include 'tracker'
// include 'webui'
// include 'plug'

47
tracker/build.gradle Normal file
View File

@@ -0,0 +1,47 @@
buildscript {
repositories {
jcenter()
mavenLocal()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
}
}
plugins {
id 'org.springframework.boot' version '2.2.6.RELEASE'
}
apply plugin : 'application'
apply plugin : 'io.spring.dependency-management'
application {
mainClassName = 'com.muwire.tracker.Tracker'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M',"-Dbuild.version=${project.version}"]
applicationName = 'mwtrackerd'
}
apply plugin : 'com.github.johnrengelman.shadow'
springBoot {
buildInfo {
properties {
version = "${project.version}"
name = "mwtrackerd"
}
}
}
dependencies {
compile project(":core")
compile 'com.github.briandilley.jsonrpc4j:jsonrpc4j:1.5.3'
compile 'org.springframework.boot:spring-boot-starter'
compile 'org.springframework.boot:spring-boot-starter-actuator'
compile 'org.springframework.boot:spring-boot-starter-web'
runtime 'javax.jws:jsr181-api:1.0-MR1'
}

View File

@@ -0,0 +1,28 @@
package com.muwire.tracker
import com.muwire.core.Persona
/**
* A participant in a swarm. The same persona can be a member of multiple
* swarms, but in that case it would have multiple Host objects
*/
class Host {
final Persona persona
long lastPinged
long lastResponded
int failures
volatile String xHave
Host(Persona persona) {
this.persona = persona
}
boolean isExpired(long cutoff, int maxFailures) {
lastPinged > lastResponded && lastResponded <= cutoff && failures >= maxFailures
}
@Override
public String toString() {
"Host:[${persona.getHumanReadableName()} lastPinged:$lastPinged lastResponded:$lastResponded failures:$failures xHave:$xHave]"
}
}

View File

@@ -0,0 +1,182 @@
package com.muwire.tracker
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import javax.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.Persona
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.data.Base64
@Component
@Log
class Pinger {
@Autowired
private Core core
@Autowired
private SwarmManager swarmManager
@Autowired
private TrackerProperties trackerProperties
private final Map<UUID, PingInProgress> inFlight = new ConcurrentHashMap<>()
private final Timer expiryTimer = new Timer("pinger-timer",true)
@PostConstruct
private void registerListener() {
core.getI2pSession().addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
expiryTimer.schedule({expirePings()} as TimerTask, 1000, 1000)
}
private void expirePings() {
final long now = System.currentTimeMillis()
for(Iterator<UUID> iter = inFlight.keySet().iterator(); iter.hasNext();) {
UUID uuid = iter.next()
PingInProgress ping = inFlight.get(uuid)
if (now - ping.pingTime > trackerProperties.getSwarmParameters().getPingTimeout() * 1000L) {
iter.remove()
swarmManager.fail(ping.target)
}
}
}
void ping(SwarmManager.HostAndIH target, long now) {
UUID uuid = UUID.randomUUID()
def ping = new PingInProgress(target, now)
inFlight.put(uuid, ping)
def message = [:]
message.type = "TrackerPing"
message.version = 1
message.infoHash = Base64.encode(target.getInfoHash().getRoot())
message.uuid = uuid.toString()
message = JsonOutput.toJson(message)
def maker = new I2PDatagramMaker(core.getI2pSession())
message = maker.makeI2PDatagram(message.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
core.getI2pSession().sendMessage(target.getHost().getPersona().getDestination(), message, 0, message.length, I2PSession.PROTO_DATAGRAM,
Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
}
private static class PingInProgress {
private final SwarmManager.HostAndIH target
private final long pingTime
PingInProgress(SwarmManager.HostAndIH target, long pingTime) {
this.target = target
this.pingTime = pingTime
}
}
private class Listener implements I2PSessionMuxedListener {
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning("received unexpected protocol $proto")
return
}
byte [] payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
log.info("got a response from ${sender.toBase32()}")
payload = dissector.getPayload()
def slurper = new JsonSlurper()
def json = slurper.parse(payload)
if (json.type != "TrackerPong") {
log.warning("unknown type ${json.type}")
return
}
if (json.me == null) {
log.warning("sender persona missing")
return
}
Persona senderPersona = new Persona(new ByteArrayInputStream(Base64.decode(json.me)))
if (sender != senderPersona.getDestination()) {
log.warning("persona in payload does not match sender ${senderPersona.getHumanReadableName()}")
return
}
if (json.uuid == null) {
log.warning("uuid missing")
return
}
UUID uuid = UUID.fromString(json.uuid)
def ping = inFlight.remove(uuid)
if (ping == null) {
log.warning("no ping in progress for $uuid")
return
}
if (json.code == null) {
log.warning("no code")
return
}
int code = json.code
if (json.xHave != null)
ping.target.host.xHave = json.xHave
Set<Persona> altlocs = new HashSet<>()
json.altlocs?.collect(altlocs,{ new Persona(new ByteArrayInputStream(Base64.decode(it))) })
log.info("For ${ping.target.infoHash} received code $code and altlocs ${altlocs.size()}")
swarmManager.handleResponse(ping.target, code, altlocs)
} catch (Exception e) {
log.log(Level.WARNING,"invalid datagram",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
log.warning("reportabuse $session $severity")
}
@Override
public void disconnected(I2PSession session) {
log.severe("disconnected")
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE,message,error)
}
}
}

View File

@@ -0,0 +1,71 @@
package com.muwire.tracker
class SetupWizard {
private final File home
SetupWizard(File home) {
this.home = home
}
Properties performSetup() {
println "**** Welcome to mwtrackerd setup wizard *****"
println "This wizard ask you some questions and configure the settings for the MuWire tracker daemon."
println "The settings will be saved in ${home.getAbsolutePath()} where you can edit them manually if you wish."
println "You can re-run this wizard by launching mwtrackerd with the \"setup\" argument."
println "*****************"
Scanner scanner = new Scanner(System.in)
Properties rv = new Properties()
// nickname
while(true) {
println "Please select a nickname for your tracker"
String nick = scanner.nextLine()
if (nick.trim().length() == 0) {
println "nickname cannot be empty"
continue
}
rv['nickname'] = nick
break
}
// i2cp host and port
println "Enter the address of an I2P or I2Pd router to connect to. (default is 127.0.0.1)"
String i2cpHost = scanner.nextLine()
if (i2cpHost.trim().length() == 0)
i2cpHost = "127.0.0.1"
rv['i2cp.tcp.host'] = i2cpHost
println "Enter the port of the I2CP interface of the I2P[d] router (default is 7654)"
String i2cpPort = scanner.nextLine()
if (i2cpPort.trim().length() == 0)
i2cpPort = "7654"
rv['i2cp.tcp.port'] = i2cpPort
// json-rpc interface
println "Enter the address to which to bind the JSON-RPC interface of the tracker."
println "Default is 127.0.0.1. If you want to allow JSON-RPC connections from other hosts you can enter 0.0.0.0"
String jsonRpcIface = scanner.nextLine()
if (jsonRpcIface.trim().length() == 0)
jsonRpcIface = "127.0.0.1"
rv['jsonrpc.iface'] = jsonRpcIface
println "Enter the port on which the JSON-RPC interface should listen. (default is 12345)"
String jsonRpcPort = scanner.nextLine()
if (jsonRpcPort.trim().length() == 0)
jsonRpcPort = "12345"
rv['jsonrpc.port'] = jsonRpcPort
// that's all
println "*****************"
println "That's all the setup that's required to get the tracker up and running."
println "The tracker has many other settings which can be changed in the config files."
println "Refer to the documentation for their description."
println "*****************"
rv
}
}

View File

@@ -0,0 +1,182 @@
package com.muwire.tracker
import java.util.function.Function
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import groovy.util.logging.Log
/**
* A swarm for a given file
*/
@Log
class Swarm {
final InfoHash infoHash
/**
* Invariant: these four collections are mutually exclusive.
* A given host can be only in one of them at the same time.
*/
private final Map<Persona,Host> seeds = new HashMap<>()
private final Map<Persona,Host> leeches = new HashMap<>()
private final Map<Persona,Host> unknown = new HashMap<>()
private final Set<Persona> negative = new HashSet<>()
/**
* hosts which are currently being pinged. Hosts can be in here
* and in the collections above, except for negative.
*/
private final Map<Persona, Host> inFlight = new HashMap<>()
/**
* Last time a query was made to the MW network for this hash
*/
private long lastQueryTime
/**
* Last time a batch of hosts was pinged
*/
private long lastPingTime
Swarm(InfoHash infoHash) {
this.infoHash = infoHash
}
/**
* @param cutoff expire hosts older than this
*/
synchronized void expire(long cutoff, int maxFailures) {
doExpire(cutoff, maxFailures, seeds)
doExpire(cutoff, maxFailures, leeches)
doExpire(cutoff, maxFailures, unknown)
}
private static void doExpire(long cutoff, int maxFailures, Map<Persona,Host> map) {
for (Iterator<Persona> iter = map.keySet().iterator(); iter.hasNext();) {
Persona p = iter.next()
Host h = map.get(p)
if (h.isExpired(cutoff, maxFailures))
iter.remove()
}
}
synchronized boolean shouldQuery(long queryCutoff, long now) {
if (!(seeds.isEmpty() &&
leeches.isEmpty() &&
inFlight.isEmpty() &&
unknown.isEmpty()))
return false
if (lastQueryTime <= queryCutoff) {
lastQueryTime = now
return true
}
false
}
synchronized boolean isHealthy() {
!seeds.isEmpty()
// TODO add xHave accumulation of leeches
}
synchronized void add(Persona p) {
if (!(seeds.containsKey(p) || leeches.containsKey(p) ||
negative.contains(p) || inFlight.containsKey(p)))
unknown.computeIfAbsent(p, {new Host(it)} as Function)
}
synchronized void handleResponse(Host responder, int code) {
Host h = inFlight.remove(responder.persona)
if (responder != h)
log.warning("received a response mismatch from host $responder vs $h")
responder.lastResponded = System.currentTimeMillis()
responder.failures = 0
switch(code) {
case 200: addSeed(responder); break
case 206 : addLeech(responder); break;
default :
addNegative(responder)
}
}
synchronized void fail(Host failed) {
Host h = inFlight.remove(failed.persona)
if (h != failed)
log.warning("failed a host that wasn't in flight $failed vs $h")
h.failures++
}
private void addSeed(Host h) {
leeches.remove(h.persona)
unknown.remove(h.persona)
seeds.put(h.persona, h)
}
private void addLeech(Host h) {
unknown.remove(h.persona)
seeds.remove(h.persona)
leeches.put(h.persona, h)
}
private void addNegative(Host h) {
unknown.remove(h.persona)
seeds.remove(h.persona)
leeches.remove(h.persona)
negative.add(h.persona)
}
/**
* @param max number of hosts to give back
* @param now what time is it now
* @param cutoff only consider hosts which have been pinged before this time
* @return hosts to be pinged
*/
synchronized List<Host> getBatchToPing(int max, long now, long cutOff) {
List<Host> rv = new ArrayList<>()
rv.addAll(unknown.values())
rv.addAll(seeds.values())
rv.addAll(leeches.values())
rv.removeAll(inFlight.values())
rv.removeAll { it.lastPinged >= cutOff }
Collections.sort(rv, {l, r ->
Long.compare(l.lastPinged, r.lastPinged)
} as Comparator<Host>)
if (rv.size() > max)
rv = rv[0..(max-1)]
rv.each {
it.lastPinged = now
inFlight.put(it.persona, it)
}
if (!rv.isEmpty())
lastPingTime = now
rv
}
synchronized long getLastPingTime() {
lastPingTime
}
public Info info() {
List<String> seeders = seeds.keySet().collect { it.getHumanReadableName() }
List<String> leechers = leeches.keySet().collect { it.getHumanReadableName() }
return new Info(seeders, leechers, unknown.size(), negative.size())
}
public static class Info {
final List<String> seeders, leechers
final int unknown, negative
Info(List<String> seeders, List<String> leechers, int unknown, int negative) {
this.seeders = seeders
this.leechers = leechers
this.unknown = unknown
this.negative = negative
}
}
}

View File

@@ -0,0 +1,165 @@
package com.muwire.tracker
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Function
import javax.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Signature
@Component
@Log
class SwarmManager {
@Autowired
private Core core
@Autowired
private Pinger pinger
@Autowired
private TrackerProperties trackerProperties
private final Map<InfoHash, Swarm> swarms = new ConcurrentHashMap<>()
private final Map<UUID, InfoHash> queries = new ConcurrentHashMap<>()
private final Timer swarmTimer = new Timer("swarm-timer",true)
@PostConstruct
public void postConstruct() {
core.eventBus.register(UIResultBatchEvent.class, this)
swarmTimer.schedule({trackSwarms()} as TimerTask, 10 * 1000, 10 * 1000)
}
void onUIResultBatchEvent(UIResultBatchEvent e) {
InfoHash stored = queries.get(e.uuid)
InfoHash ih = e.results[0].infohash
if (ih != stored) {
log.warning("infohash mismatch in result $ih vs $stored")
return
}
Swarm swarm = swarms.get(ih)
if (swarm == null) {
log.warning("no swarm found for result with infoHash $ih")
return
}
log.info("got a result with uuid ${e.uuid} for infoHash $ih")
swarm.add(e.results[0].sender)
}
int countSwarms() {
swarms.size()
}
private void trackSwarms() {
final long now = System.currentTimeMillis()
final long expiryCutoff = now - trackerProperties.getSwarmParameters().getExpiry() * 60 * 1000L
final int maxFailures = trackerProperties.getSwarmParameters().getMaxFailures()
swarms.values().each { it.expire(expiryCutoff, maxFailures) }
final long queryCutoff = now - trackerProperties.getSwarmParameters().getQueryInterval() * 60 * 60 * 1000L
swarms.values().each {
if (it.shouldQuery(queryCutoff, now))
query(it)
}
List<Swarm> swarmList = new ArrayList<>(swarms.values())
Collections.sort(swarmList,{Swarm x, Swarm y ->
Long.compare(x.getLastPingTime(), y.getLastPingTime())
} as Comparator<Swarm>)
List<HostAndIH> toPing = new ArrayList<>()
final int amount = trackerProperties.getSwarmParameters().getPingParallel()
final long pingCutoff = now - trackerProperties.getSwarmParameters().getPingInterval() * 60 * 1000L
for(int i = 0; i < swarmList.size() && toPing.size() < amount; i++) {
Swarm s = swarmList.get(i)
List<Host> hostsFromSwarm = s.getBatchToPing(amount - toPing.size(), now, pingCutoff)
hostsFromSwarm.collect(toPing, { host -> new HostAndIH(host, s.getInfoHash())})
}
log.info("will ping $toPing")
toPing.each { pinger.ping(it, now) }
}
private void query(Swarm swarm) {
InfoHash infoHash = swarm.getInfoHash()
cleanQueryMap(infoHash)
UUID uuid = UUID.randomUUID()
queries.put(uuid, infoHash)
log.info("will query MW network for $infoHash with uuid $uuid")
def searchEvent = new SearchEvent(searchHash : infoHash.getRoot(), uuid: uuid, oobInfohash: true, compressedResults : true, persona : core.me)
byte [] payload = infoHash.getRoot()
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
long timestamp = System.currentTimeMillis()
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 : DataUtil.signUUID(uuid, timestamp, core.spk)))
}
void track(InfoHash infoHash) {
swarms.computeIfAbsent(infoHash, {new Swarm(it)} as Function)
}
boolean forget(InfoHash infoHash) {
Swarm swarm = swarms.remove(infoHash)
if (swarm != null) {
cleanQueryMap(infoHash)
return true
} else
return false
}
private void cleanQueryMap(InfoHash infoHash) {
queries.values().removeAll {it == infoHash}
}
Swarm.Info info(InfoHash infoHash) {
swarms.get(infoHash)?.info()
}
void fail(HostAndIH target) {
log.info("failing $target")
swarms.get(target.infoHash)?.fail(target.host)
}
void handleResponse(HostAndIH target, int code, Set<Persona> altlocs) {
Swarm swarm = swarms.get(target.infoHash)
swarm?.handleResponse(target.host, code)
altlocs.each {
swarm?.add(it)
}
}
public static class HostAndIH {
final Host host
final InfoHash infoHash
HostAndIH(Host host, InfoHash infoHash) {
this.host = host
this.infoHash = infoHash
}
@Override
public String toString() {
"$host:$infoHash"
}
}
}

View File

@@ -0,0 +1,10 @@
package com.muwire.tracker;
public class TrackRequest {
String infoHash;
@Override
public String toString() {
return "infoHash: " +infoHash;
}
}

View File

@@ -0,0 +1,119 @@
package com.muwire.tracker
import java.nio.charset.StandardCharsets
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.web.server.ConfigurableWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.context.annotation.Bean
import com.googlecode.jsonrpc4j.spring.JsonServiceExporter
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
@SpringBootApplication
class Tracker {
private static final String VERSION = System.getProperty("build.version")
private static Core core
private static TrackerService trackerService
public static void main(String [] args) {
println "Launching MuWire Tracker version $VERSION"
File home = new File(System.getProperty("user.home"))
home = new File(home, ".mwtrackerd")
home.mkdir()
File mwProps = new File(home, "MuWire.properties")
File i2pProps = new File(home, "i2p.properties")
File trackerProps = new File(home, "tracker.properties")
boolean launchSetup = false
if (args.length > 0 && args[0] == "setup") {
println "Setup requested, entering setup wizard"
launchSetup = true
} else if (!(mwProps.exists() && i2pProps.exists() && trackerProps.exists())) {
println "Config files not found, entering setup wizard"
launchSetup = true
}
if (launchSetup) {
SetupWizard wizard = new SetupWizard(home)
Properties props = wizard.performSetup()
// nickname goes to mw.props
MuWireSettings mwSettings = new MuWireSettings()
mwSettings.nickname = props['nickname']
mwProps.withPrintWriter("UTF-8", {
mwSettings.write(it)
})
// i2cp host & port go in i2p.properties
def i2cpProps = new Properties()
i2cpProps['i2cp.tcp.port'] = props['i2cp.tcp.port']
i2cpProps['i2cp.tcp.host'] = props['i2cp.tcp.host']
i2cpProps['inbound.nickname'] = "MuWire Tracker"
i2cpProps['outbound.nickname'] = "MuWire Tracker"
i2pProps.withPrintWriter { i2cpProps.store(it, "") }
// json rcp props go in tracker.properties
def jsonProps = new Properties()
jsonProps['tracker.jsonRpc.iface'] = props['jsonrpc.iface']
jsonProps['tracker.jsonRpc.port'] = props['jsonrpc.port']
trackerProps.withPrintWriter { jsonProps.store(it, "") }
}
Properties p = new Properties()
mwProps.withReader("UTF-8", { p.load(it) } )
MuWireSettings muSettings = new MuWireSettings(p)
p = new Properties()
trackerProps.withInputStream { p.load(it) }
core = new Core(muSettings, home, VERSION)
// init json service object
trackerService = new TrackerServiceImpl(core)
core.eventBus.with {
register(UILoadedEvent.class, trackerService)
}
Thread coreStarter = new Thread({
core.startServices()
core.eventBus.publish(new UILoadedEvent())
} as Runnable)
coreStarter.start()
System.setProperty("spring.config.location", trackerProps.getAbsolutePath())
SpringApplication.run(Tracker.class, args)
}
@Bean
Core core() {
core
}
@Bean
public TrackerService trackerService() {
trackerService
}
@Bean(name = '/tracker')
public JsonServiceExporter jsonServiceExporter() {
def exporter = new JsonServiceExporter()
exporter.setService(trackerService())
exporter.setServiceInterface(TrackerService.class)
exporter
}
}

View File

@@ -0,0 +1,33 @@
package com.muwire.tracker
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
@Component
@ConfigurationProperties("tracker")
class TrackerProperties {
final JsonRpc jsonRpc = new JsonRpc()
public static class JsonRpc {
InetAddress iface
int port
}
final SwarmParameters swarmParameters = new SwarmParameters()
public static class SwarmParameters {
/** how often to kick of queries on the MW net, in hours */
int queryInterval = 1
/** how many hosts to ping in parallel */
int pingParallel = 5
/** interval of time between pinging the same host, in minutes */
int pingInterval = 15
/** how long to wait before declaring a host is dead, in minutes */
int expiry = 60
/** how long to wait for a host to respond to a ping, in seconds */
int pingTimeout = 20
/** Do not expire a host until it has failed this many times */
int maxFailures = 3
}
}

View File

@@ -0,0 +1,8 @@
package com.muwire.tracker;
public interface TrackerService {
public TrackerStatus status();
public void track(String infoHash);
public boolean forget(String infoHash);
public Swarm.Info info(String infoHash);
}

View File

@@ -0,0 +1,54 @@
package com.muwire.tracker
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.UILoadedEvent
import net.i2p.data.Base64
@Component
class TrackerServiceImpl implements TrackerService {
private final TrackerStatus status = new TrackerStatus()
private final Core core
@Autowired
private SwarmManager swarmManager
TrackerServiceImpl(Core core) {
this.core = core
status.status = "Starting"
}
public TrackerStatus status() {
status.connections = core.getConnectionManager().getConnections().size()
status.swarms = swarmManager.countSwarms()
status
}
void onUILoadedEvent(UILoadedEvent e) {
status.status = "Running"
}
@Override
public void track(String infoHash) {
InfoHash ih = new InfoHash(Base64.decode(infoHash))
swarmManager.track(ih)
}
@Override
public boolean forget(String infoHash) {
InfoHash ih = new InfoHash(Base64.decode(infoHash))
swarmManager.forget(ih)
}
@Override
public Swarm.Info info(String infoHash) {
InfoHash ih = new InfoHash(Base64.decode(infoHash))
swarmManager.info(ih)
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.tracker
class TrackerStatus {
volatile String status
int connections
int swarms
}

View File

@@ -0,0 +1,20 @@
package com.muwire.tracker
import org.springframework.boot.web.server.ConfigurableWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.stereotype.Component
@Component
class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
private final TrackerProperties trackerProperties
WebServerConfiguration(TrackerProperties trackerProperties) {
this.trackerProperties = trackerProperties;
}
@Override
public void customize(ConfigurableWebServerFactory factory) {
factory.setAddress(trackerProperties.jsonRpc.getIface())
factory.setPort(trackerProperties.jsonRpc.port)
}
}

View File

@@ -66,6 +66,7 @@ task generateWebXML {
def jasper = new File("$buildDir/tmp_jsp/web.xml.jasper")
templateText = templateText.replaceAll("__JASPER__", jasper.text)
templateText = templateText.replaceAll("__VERSION__", project.version)
templateText = templateText.replaceAll("__BUILD_NUMBER__", project.buildNumber)
def webXml = new File("$buildDir/tmp_jsp/web.xml")
webXml.text = templateText
}

File diff suppressed because it is too large Load Diff

View File

@@ -191,11 +191,21 @@ iframe {
padding: 0 0 22px 28px;
width: 300px;
}
.title-and-help {
display: inline-block;
}
.pagetitle {
display: inline-block;
font-size: 2em;
padding-left: 28px;
}
.pagehelp {
font-size: 2em;
padding-right: 28px;
float: right;
position: absolute;
right:0;
}
.password {
height: 24px;
position: absolute;
@@ -323,28 +333,43 @@ See also .menu-icon
font-weight: 300;
margin: 0 24px 0 48px;
}
.menuitem.identities .menu-icon:before {
content: url("images/identities.png");
}
.menuitem.address-book .menu-icon:before {
content: url("images/addressbook.png");
}
.menuitem.settings .menu-icon:before {
content: url("images/settings.png");
content: url("images/ConfigurationPage.png");
}
.menuitem.advancedSharing .menu-icon:before {
content: url("images/AdvancedSharing.png");
}
.menuitem.downloads .menu-icon:before {
content: url("images/inbox.png");
content: url("images/Downloads.png");
}
.menuitem.uploads .menu-icon:before {
content: url("images/uploads.png");
}
.menuitem.search .menu-icon:before {
content: url("images/delay.png");
content: url("images/Search.png");
}
.menuitem.shared .menu-icon:before {
content: url("images/folder.png");
content: url("images/SharedFiles.png");
}
.menuitem.browse .menu-icon:before {
content : url("images/BrowseHost.png")
}
.menuitem.feeds .menu-icon:before {
content : url("images/Feeds.png")
}
.menuitem.trustUsers .menu-icon:before {
content : url("images/TrustUsers.png")
}
.menuitem.trustList .menu-icon:before {
content : url("images/TrustList.png")
}
.menuitem.aboutMe .menu-icon:before {
content : url("images/AboutMe.png")
}
.menuitem.muStatus .menu-icon:before {
content : url("images/MuStatus.png")
}
/* Main content */
@@ -463,7 +488,7 @@ table td, th {
padding-top: 3px;
background: white;
white-space: nowrap;
overflow-x: hidden;
overflow: hidden;
font-size: 1em;
font-weight: normal;
}

View File

@@ -1,3 +1,10 @@
:root {
--hover-menu-bg : #c8e0ff;
--hover-menu-link-bg : #d8f0ff;
--table-bg : #ceeee8;
}
#table-wrapper {
position:relative;
}
@@ -6,14 +13,31 @@
overflow:auto;
margin-top:20px;
}
.paddedTable {
padding-bottom: 6%;
}
.paddedTable table tbody tr td .dropdown .dropdown-content {
background: var(--hover-menu-bg);
}
.paddedTable table tbody tr td .dropdown .dropdown-content a:hover {
background: var(--hover-menu-link-bg);
}
#table-wrapper table {
width:100%;
}
#table-wrapper table * {
background: #ceeee8;
#table-wrapper table tbody tr td {
background: var(--table-bg);
color:black;
}
#table-wrapper table thead tr th {
background: var(--table-bg);
color:black;
}
#table-wrapper table td, th {
padding-right: 10px;
padding-bottom: 1px;
@@ -32,6 +56,9 @@ div#activeSearches table td:nth-child(2) {
text-align: right;
}
div#topTableSender table thead th:nth-child(1) {
width: 45%;
}
div#topTableSender table thead th:nth-child(2) {
width: 100px;
}
@@ -39,7 +66,10 @@ div#topTableSender table thead th:nth-child(3) {
width: 100px;
}
div#topTableSender table thead th:nth-child(4) {
width: 340px;
width: 100px;
}
div#topTableSender table thead th:nth-child(5) {
width: 20%;
}
div#topTableSender table tbody td:nth-child(1) {
text-overflow: ellipsis;
@@ -51,6 +81,11 @@ div#topTableSender table tbody td:nth-child(3) {
padding-right: 40px;
text-align: right;
}
div#topTableSender table tbody td:nth-child(5) {
text-overflow: ellipsis;
overflow: auto;
text-align: center;
}
div#bottomTableSender table thead th:nth-child(2) {
width: 100px;
@@ -97,7 +132,10 @@ div#bottomTableFile table thead th:nth-child(2) {
width: 100px;
}
div#bottomTableFile table thead th:nth-child(3) {
width: 340px;
width: 100px;
}
div#bottomTableFile table thead th:nth-child(4) {
width: 20%;
}
div#bottomTableFile table tbody td:nth-child(1) {
text-overflow: ellipsis;
@@ -105,6 +143,10 @@ div#bottomTableFile table tbody td:nth-child(1) {
div#bottomTableFile table tbody td:nth-child(2) {
text-align: center;
}
div#bottomTableFile table tbody td:nth-child(4) {
text-overflow:ellipsis;
overflow:auto;
}
div#activeBrowses table thead th:nth-child(2) {
width: 100px;
@@ -128,6 +170,7 @@ div#filesTable table thead th:nth-child(2) {
}
div#filesTable table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#filesTable table tbody td:nth-child(2) {
padding-right: 25px;
@@ -196,6 +239,109 @@ div#downloadDetails table td {
padding-bottom: 5px;
}
div#feedConfig table * {
background: #bcd1e5 !important;
}
div#feedConfig table {
border-collapse: collapse;
width: auto;
}
div#feedConfig table td {
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
}
div#feedsTable table thead th:nth-child(2) {
width: 70px;
}
div#feedsTable table thead th:nth-child(3) {
width: 170px;
}
div#feedsTable table thead th:nth-child(4) {
width: 100px;
}
div#feedsTable table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#itemsTable table thead th:nth-child(2) {
width: 80px;
}
div#itemsTable table thead th:nth-child(3) {
width: 70px;
}
div#itemsTable table thead th:nth-child(4) {
width: 170px;
}
div#itemsTable table tbody td:nth-child(1) {
text-overflow: ellipsis;
}
div#dirConfig table * {
background: #bcd1e5 !important;
}
div#dirConfig table {
border-collapse: collapse;
width: auto;
}
div#dirConfig table td {
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
}
div#dirsTable table thead th:nth-child(2) {
width: 80px;
}
div#dirsTable table thead th:nth-child(3) {
width: 90px;
}
div#dirsTable table thead th:nth-child(4) {
width: 170px;
}
div#dirsTable table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#hitsTable table thead th:nth-child(1) {
width: 270px;
}
div#hitsTable table thead th:nth-child(2) {
width: 170px;
}
div#hitsTable table tbody td:nth-child(1) {
text-overflow: ellipsis;
}
div#hitsTable table tbody td:nth-child(3) {
text-overflow: ellipsis;
}
div#certificatesTable table thead th:nth-child(2) {
width: 170px;
}
div#certificatesTable table tbody td:nth-child(1) {
text-overflow: ellipsis;
}
div#certificatesTable table tbody td:nth-child(3) {
text-overflow: ellipsis;
}
div#uploads table thead th:nth-child(2) {
width: 120px;
}
@@ -237,6 +383,7 @@ div#trustedUsers table tbody td:nth-child(3) {
}
div#trustedUsers table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#trustedUsers table tbody td:nth-child(3) {
text-align: center;
@@ -244,6 +391,7 @@ div#trustedUsers table tbody td:nth-child(3) {
div#distrustedUsers table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#distrustedUsers table thead th:nth-child(2) {
width: 300px;
@@ -263,6 +411,7 @@ div#trustLists table thead th:nth-child(5) {
}
div#trustLists table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#trustLists table tbody td:nth-child(2) {
padding-right: 40px;
@@ -284,6 +433,7 @@ div#trusted table thead th:nth-child(3) {
}
div#trusted table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#trusted table tbody td:nth-child(3) {
text-align: center;
@@ -296,6 +446,7 @@ div#distrusted table thead th:nth-child(3) {
}
div#distrusted table tbody td:nth-child(1) {
text-overflow: ellipsis;
overflow: auto;
}
div#distrusted table tbody td:nth-child(3) {
text-align: center;
@@ -355,6 +506,11 @@ span.right {
float: right;
}
span.center {
display : inline-block;
text-align : center;
}
input.right {
text-align: right;
}
@@ -370,6 +526,10 @@ pre.comment {
word-wrap: break-word;
}
pre.fullId {
overflow: auto;
}
/* File tree CSS */
/* Remove default bullets */
@@ -413,3 +573,129 @@ ul, #sharedTree {
.active {
display: block;
}
ul.fileTree {
margin-left: -1.5%;
}
li.fileTree {
}
.accordion {
cursor: pointer;
transition: 0.4s;
}
.panel {
max-height: 0;
overflow: hidden;
transition: max-height: 0.2s ease-out;
}
.droplink {
cursor : pointer;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
z-index:1;
background: var(--hover-menu-bg);
background-color: var(--hover-menu-bg);
padding: 3px 14px 3px 14px;
width: max-content;
}
.dropdown-content-right {
display: none;
position: absolute;
z-index:1;
background: var(--hover-menu-bg);
background-color: var(--hover-menu-bg);
padding: 3px 14px 3px 14px;
width: max-content;
right: 0;
}
.dropdown-content a {
color: black;
display: block;
}
.dropdown-content-right a {
color: black;
display: block;
}
/* Change color of dropdown links on hover */
.dropdown-content a:hover {
background: var(--hover-menu-link-bg);
background-color: var(--hover-menu-link-bg);
}
.dropdown-content-right a:hover {
background: var(--hover-menu-link-bg);
background-color: var(--hover-menu-link-bg);
}
/* Show the dropdown menu on hover */
.dropdown:hover .dropdown-content {display: block;}
.dropdown:hover .dropdown-content-right {display: block;}
textarea.copypaste {
opacity: 0;
position: absolute;
z-index: -9999;
pointer-events: none;
}
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 240px;
text-align: center;
border-radius: 6px;
padding: 5px 0;
background : #d8f0ff;
/* Position the tooltip */
position: absolute;
z-index: 1;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
.configuration-section table tr td {
overflow: auto;
}
.configuration-section .tooltip {
border-bottom: none;
}
.configuration-section .tooltip .tooltiptext {
white-space: pre-wrap;
padding : 5px 0 5px 5px;
}
.title-and-help .pagehelp .tooltip .tooltiptext {
white-space: pre-wrap;
right : 0;
top:20px;
width: 400px;
font-size: initial;
color: initial;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

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