Compare commits

...

165 Commits

Author SHA1 Message Date
Zlatin Balevsky
8fedc0c605 Release 0.6.7 2019-11-26 09:55:10 +00:00
Zlatin Balevsky
5831b06842 chat room monitor tool 2019-11-26 09:43:53 +00:00
Zlatin Balevsky
57d5b5f386 do not send /LEAVE messages when leaving private chats 2019-11-26 05:31:22 +00:00
Zlatin Balevsky
c0f6b1ed73 do not rejoin console or private chats, fix NPE when disconnecting with private window open 2019-11-26 05:28:24 +00:00
Zlatin Balevsky
f4cd1c30cd Do not remove connection on distrust so that disconnect can be processed correctly 2019-11-26 05:00:55 +00:00
Zlatin Balevsky
6b717f560e file hashing 2019-11-23 20:28:29 +02:00
Zlatin Balevsky
e8a3db76bb wip on architecture doc 2019-11-23 20:15:45 +02:00
Zlatin Balevsky
5acf7f2953 add clear button 2019-11-20 18:47:14 +00:00
Zlatin Balevsky
e760e9f600 add option to select chat server welcome message 2019-11-19 09:02:42 +00:00
Zlatin Balevsky
8a47972b10 proper group name for manual rejoin 2019-11-19 03:37:35 +00:00
Zlatin Balevsky
f8e0c9524e make text input fields longer 2019-11-18 10:03:01 +00:00
Zlatin Balevsky
919aeaaed5 preserve chat box contents across openings of the chat window 2019-11-18 09:55:11 +00:00
Zlatin Balevsky
9474512cbd default to /SAY command 2019-11-18 09:44:05 +00:00
Zlatin Balevsky
8c50f6c6d6 proper model update 2019-11-18 09:33:25 +00:00
Zlatin Balevsky
01ee7209c8 clear members list on server disconnect 2019-11-18 09:31:25 +00:00
Zlatin Balevsky
ff7c4eae28 stop local server if tab is closed 2019-11-18 09:06:29 +00:00
Zlatin Balevsky
9373d58b53 limit the time a header read can take 2019-11-18 09:00:11 +00:00
Zlatin Balevsky
df71ade69f formatting 2019-11-18 08:46:28 +00:00
Zlatin Balevsky
2ed29be072 selected component can be null when closing a tab 2019-11-17 22:41:31 +00:00
Zlatin Balevsky
a398ab7d4b indentation 2019-11-17 18:38:53 +00:00
Zlatin Balevsky
a0125e7195 document chat protocol 2019-11-17 18:37:30 +00:00
Zlatin Balevsky
cb9a1cfff6 remove stray import 2019-11-17 15:05:29 +00:00
Zlatin Balevsky
445e73521a more links 2019-11-17 13:35:40 +00:00
Zlatin Balevsky
7bdc922d2c add links 2019-11-17 13:32:11 +00:00
Zlatin Balevsky
0c40c8f269 Release 0.6.6 2019-11-16 17:12:21 +00:00
Zlatin Balevsky
681ddb99a2 add ability to say something in a room 2019-11-16 17:10:21 +00:00
Zlatin Balevsky
5dff319746 prevent starting chat server more than once. Implement chat console in the cli 2019-11-16 16:40:07 +00:00
Zlatin Balevsky
57c4a00ac6 do not call disconnect() unless connecting/ed. This prevents trying to connect after closing the tab 2019-11-16 15:25:23 +00:00
Zlatin Balevsky
286a0a8678 redo locking around chat client state 2019-11-16 14:41:47 +00:00
Zlatin Balevsky
17eff7d77f toString 2019-11-16 00:25:50 +00:00
Zlatin Balevsky
2e22369ce0 set flag before submitting to threadpool 2019-11-15 23:12:17 +00:00
Zlatin Balevsky
15c59b440f flush outputstream on chat connect failures or rejections 2019-11-15 21:52:35 +00:00
Zlatin Balevsky
8fb015acbf sort browse host as well 2019-11-15 14:59:52 +00:00
Zlatin Balevsky
f7b11c90fd sort results from search 2019-11-15 14:49:15 +00:00
Zlatin Balevsky
df93a35062 wip on sorting 2019-11-15 14:23:00 +00:00
Zlatin Balevsky
ecb19a8412 do not update badge if the room is a console 2019-11-15 13:50:13 +00:00
Zlatin Balevsky
b1e5b40800 update minimum JDK to 9, version bump 2019-11-15 13:18:34 +00:00
Zlatin Balevsky
daa3a293f2 new messages update taskbar badge 2019-11-15 13:15:27 +00:00
Zlatin Balevsky
907264fc67 enable/disable chat and browse from trusted pane buttons 2019-11-15 02:15:56 +00:00
Zlatin Balevsky
c6becb93dc enable/disable say field when not connected 2019-11-15 01:57:48 +00:00
Zlatin Balevsky
2954bd2f1a smart scrolling the chat text area 2019-11-15 01:44:54 +00:00
Zlatin Balevsky
35322d2c15 fetch group by name,add sequential download checkbox to browse view 2019-11-14 12:40:40 +00:00
Zlatin Balevsky
9f6a7eb368 make sure browse window works from every parent group 2019-11-14 11:04:38 +00:00
Zlatin Balevsky
fec81808e5 Release 0.6.5 2019-11-14 05:15:26 +00:00
Zlatin Balevsky
4db890484d do not rejoin console 2019-11-14 04:49:13 +00:00
Zlatin Balevsky
dfd5e06889 add browse ability from chat room view 2019-11-14 04:40:15 +00:00
Zlatin Balevsky
71da8e14da name button earlier 2019-11-14 04:25:45 +00:00
Zlatin Balevsky
7dc37e3e0d change button to connect/disconnect 2019-11-14 04:20:57 +00:00
Zlatin Balevsky
3de058a078 send rejoins to the console pt2 2019-11-14 03:59:01 +00:00
Zlatin Balevsky
4d70c7adce send rejoins to the console 2019-11-14 03:58:36 +00:00
Zlatin Balevsky
5b41106476 start and stop poller thread on events 2019-11-14 03:45:21 +00:00
Zlatin Balevsky
6240b22e66 fix reconnecting to server, start with fresh member list upon rejoin 2019-11-14 03:13:01 +00:00
Zlatin Balevsky
0e26f5afd7 rejoin rooms on reconnect 2019-11-14 02:40:22 +00:00
Zlatin Balevsky
114bc06dbb If the user explicitly shares a file, remove it form the negative tree. #26 2019-11-13 22:00:10 +00:00
Zlatin Balevsky
5fa2f2753c Release 0.6.4 2019-11-13 20:06:53 +00:00
Zlatin Balevsky
cacdd2a7a9 add browse and chat buttons to trusted panel 2019-11-13 19:40:28 +00:00
Zlatin Balevsky
d56f7c6184 add right-click menu to trusted table 2019-11-13 19:33:34 +00:00
Zlatin Balevsky
f7f4513109 better help and welcome message 2019-11-13 17:50:50 +00:00
Zlatin Balevsky
dd15d893ba Call for help for Web UI 2019-11-13 17:26:14 +00:00
Zlatin Balevsky
bf5ab9c82e ) 2019-11-13 14:10:26 +00:00
Zlatin Balevsky
edd5a29b10 make private chat room ids unique across servers 2019-11-13 14:09:09 +00:00
Zlatin Balevsky
38eb89f2f7 prepend server name to room id in order to make ids unique across server connections 2019-11-13 13:44:22 +00:00
Zlatin Balevsky
73f1d64428 indentation of text field 2019-11-13 12:24:21 +00:00
Zlatin Balevsky
bc1cae2d75 enable sharing of directories from button 2019-11-13 12:03:23 +00:00
Zlatin Balevsky
a0ab07a7c0 show browse status for local results correctly 2019-11-13 11:58:55 +00:00
Zlatin Balevsky
f875c379ce Release 0.6.3 2019-11-12 17:22:38 +00:00
Zlatin Balevsky
0ce9784ccf add right-click menu on the members table 2019-11-12 17:08:38 +00:00
Zlatin Balevsky
be82136e32 limit scrollback 2019-11-12 16:30:55 +00:00
Zlatin Balevsky
7d25bb9364 tidy up views 2019-11-12 16:06:31 +00:00
Zlatin Balevsky
c6e98db9d4 initialize result sender properly 2019-11-12 15:50:58 +00:00
Zlatin Balevsky
35a26e2a47 advertise chat ability in search results 2019-11-12 15:47:38 +00:00
Zlatin Balevsky
beef4af329 ui for chat options 2019-11-12 15:31:20 +00:00
Zlatin Balevsky
cec3c1bc0f disconnect on close tab 2019-11-12 14:21:47 +00:00
Zlatin Balevsky
289b958784 disconnect functionality 2019-11-12 14:19:57 +00:00
Zlatin Balevsky
e9c554d717 proper group name pt3 2019-11-12 13:53:33 +00:00
Zlatin Balevsky
1875fcddb2 proper room name pt2 2019-11-12 13:33:53 +00:00
Zlatin Balevsky
bee6154fa9 set more room tab names correctly 2019-11-12 13:26:07 +00:00
Zlatin Balevsky
1f9b171021 wip on private messages 2019-11-12 13:16:36 +00:00
Zlatin Balevsky
59c03be35e suffix for group ids 2019-11-12 12:33:18 +00:00
Zlatin Balevsky
621af96bdf wip on private chat 2019-11-12 12:20:49 +00:00
Zlatin Balevsky
bcb7016202 add myself to the room member list when joining, fix /SAY 2019-11-12 11:40:28 +00:00
Zlatin Balevsky
b1b2bcaef8 show disconnects 2019-11-12 11:34:23 +00:00
Zlatin Balevsky
eec007e83b update status only if it matches host 2019-11-12 11:11:42 +00:00
Zlatin Balevsky
3d36351a6b fetch the list of current room members when joining 2019-11-12 10:55:21 +00:00
Zlatin Balevsky
d57d2ccb71 print help message on joining 2019-11-12 04:18:35 +00:00
Zlatin Balevsky
d91f15ee54 dispatch joins to the target room 2019-11-12 03:53:38 +00:00
Zlatin Balevsky
6bc61c920d start outgoing connection 2019-11-12 00:11:26 +00:00
Zlatin Balevsky
146ed53e12 connection code 2019-11-11 23:52:34 +00:00
Zlatin Balevsky
8ebae1600b fix up chat room view 2019-11-11 23:46:43 +00:00
Zlatin Balevsky
18d19ca75e wip on joining and leaving rooms 2019-11-11 23:32:23 +00:00
Zlatin Balevsky
29e499fe9d hook up core and backend 2019-11-11 22:42:55 +00:00
Zlatin Balevsky
3db167bade send periodic pings 2019-11-11 17:54:33 +00:00
Zlatin Balevsky
bfe0ab7867 wip on hooking UI with core 2019-11-11 17:48:42 +00:00
Zlatin Balevsky
1fbb1e7932 add chat pane and associated components 2019-11-11 16:35:15 +00:00
Zlatin Balevsky
0632336cd1 add ability to start and stop chat server from UI 2019-11-11 15:16:23 +00:00
Zlatin Balevsky
aa221cd6dc server-side handling of disconnects and trust events 2019-11-11 14:54:10 +00:00
Zlatin Balevsky
29b5c55328 client-side disconnect handling 2019-11-11 13:31:00 +00:00
Zlatin Balevsky
5e7f3587df shutdown chat components 2019-11-11 13:26:25 +00:00
Zlatin Balevsky
8afd387ca6 hook up chat components with core 2019-11-11 13:21:16 +00:00
Zlatin Balevsky
5d16963d1c process join/leave/say server-side 2019-11-11 12:19:32 +00:00
Zlatin Balevsky
6080c8b308 chat client and server 2019-11-11 10:43:52 +00:00
Zlatin Balevsky
915deb1dee update readme for new shadow jar name 2019-11-11 09:13:56 +00:00
Zlatin Balevsky
8afca3dc7f Merge pull request #24 from theosotr/fix
Bugfix: Update plugin version to fix bug about shadow jar
2019-11-11 09:04:42 +00:00
Thodoris Sotiropoulos
f072d0343c Update plugin version to fix bug about shadow jar 2019-11-11 10:52:37 +02:00
Zlatin Balevsky
a549ad3d8d wip on chat 2019-11-11 04:36:43 +00:00
Zlatin Balevsky
b6f5ec7d22 wip on chat 2019-11-10 20:34:24 +00:00
Zlatin Balevsky
761bf0a177 Release 0.6.2 2019-11-10 18:31:30 +00:00
Zlatin Balevsky
bd873211c0 wip on file preview 2019-11-10 14:50:19 +00:00
Zlatin Balevsky
036971cfe5 wip on file preview 2019-11-10 13:59:01 +00:00
Zlatin Balevsky
a2637570b1 Release 0.6.1 2019-11-10 06:23:28 +00:00
Zlatin Balevsky
6012adbeab fix unsharing of files with comments 2019-11-10 06:04:57 +00:00
Zlatin Balevsky
8f6b6b0caa update test for new json format 2019-11-10 05:20:09 +00:00
Zlatin Balevsky
8f3b5aea8d store lowercases in search index 2019-11-10 05:14:31 +00:00
Zlatin Balevsky
ee098ace8e update readme 2019-11-09 20:11:03 +00:00
Zlatin Balevsky
5d8401e4bf avoid NPE, pending further investigation 2019-11-09 20:10:21 +00:00
Zlatin Balevsky
fbf9add82a Release 0.6.0 2019-11-09 19:27:36 +00:00
Zlatin Balevsky
7379263fef extended signature in cli 2019-11-09 18:34:34 +00:00
Zlatin Balevsky
7d50843754 make signed queries mandatory 2019-11-09 17:03:38 +00:00
Zlatin Balevsky
f4a2864942 add extended signature in queries to prevent replay attacks 2019-11-09 16:39:16 +00:00
Zlatin Balevsky
afaadf65a4 only set selected row if the table contains that many rows. That fixes an AIOOBE 2019-11-09 15:14:14 +00:00
Zlatin Balevsky
7bd422d6b4 another instance of unexplained npe 2019-11-09 12:36:59 +00:00
Zlatin Balevsky
3f47274f61 add option to open containing folder 2019-11-09 11:28:12 +00:00
Zlatin Balevsky
419e9a0ce6 prevent npe when..? unclear when this happens 2019-11-09 11:01:55 +00:00
Zlatin Balevsky
ac1068a681 fix show comment/certificate buttons in group-by-file mode 2019-11-09 10:53:38 +00:00
Zlatin Balevsky
549457e36f close output stream silently 2019-11-08 21:46:44 +00:00
Zlatin Balevsky
14d6d10546 Release 0.5.10 2019-11-08 21:11:20 +00:00
Zlatin Balevsky
878e397aa0 preserve selections on update 2019-11-08 21:04:58 +00:00
Zlatin Balevsky
27831b488b add getter and use it; account for the case where a file has no certificates 2019-11-08 19:20:06 +00:00
Zlatin Balevsky
449f46c62b take list updating out of loop 2019-11-08 18:40:59 +00:00
Zlatin Balevsky
5703b85386 workaround? 2019-11-08 18:36:23 +00:00
Zlatin Balevsky
76d8d847bd wip on grouping by file 2019-11-08 18:15:54 +00:00
Zlatin Balevsky
db84d8e5bf wip on grouping by file 2019-11-08 17:33:41 +00:00
Zlatin Balevsky
cc9b384907 wip on grouping by file 2019-11-08 16:09:05 +00:00
Zlatin Balevsky
72960c24a8 implement trust reason in cli 2019-11-08 14:41:10 +00:00
Zlatin Balevsky
71298e5e73 proper rendering of date on subscriptions table 2019-11-08 08:31:00 +00:00
Zlatin Balevsky
11bc672544 say never if timestamp is 0 2019-11-08 08:30:44 +00:00
Zlatin Balevsky
2f6cd311a0 say never if timestamp is 0 2019-11-08 08:30:29 +00:00
Zlatin Balevsky
0448750491 lowercase for consistency 2019-11-08 08:18:33 +00:00
Zlatin Balevsky
800dd1cbba proper date sorting 2019-11-08 08:17:34 +00:00
Zlatin Balevsky
f95e9450f3 OutputStream.write 2019-11-08 07:47:11 +00:00
Zlatin Balevsky
d842e3f2f2 update for new object 2019-11-08 07:42:33 +00:00
Zlatin Balevsky
2017b53a43 pass comments on trust list subscriptions 2019-11-08 07:37:51 +00:00
Zlatin Balevsky
6e2b3f4f33 prompt for reason from review trust list view 2019-11-08 07:12:17 +00:00
Zlatin Balevsky
dbb305139b update for new type 2019-11-08 06:53:22 +00:00
Zlatin Balevsky
0801bfec08 add optinal reason for trusting/distrusting 2019-11-08 06:46:03 +00:00
Zlatin Balevsky
00a8d100fe show certificate comment form file details view 2019-11-08 04:51:37 +00:00
Zlatin Balevsky
e94b7cb0d4 prevent NPE when browsed from an older host 2019-11-08 04:02:11 +00:00
Zlatin Balevsky
b0357f2ecd update readme 2019-11-08 02:50:42 +00:00
Zlatin Balevsky
62e72a7ce0 Release 0.5.9 2019-11-07 20:01:15 +00:00
Zlatin Balevsky
26fa757b13 shared file details panel 2019-11-07 19:15:35 +00:00
Zlatin Balevsky
3b2e1cf98c make sure the persona reported by the browser matches 2019-11-07 18:35:34 +00:00
Zlatin Balevsky
5de8a51e47 account for unknown searchers 2019-11-07 18:34:11 +00:00
Zlatin Balevsky
f5c07f13c0 core side of searchers tracking 2019-11-07 18:31:20 +00:00
Zlatin Balevsky
c7b0ae34af associate persona with a search event, add skeleton for shared file panel 2019-11-07 17:43:37 +00:00
Zlatin Balevsky
cad5301827 rewrite Persona and Name in java 2019-11-07 17:41:32 +00:00
Zlatin Balevsky
c998011873 add right-click and show-in-library option for uploads 2019-11-07 05:02:53 +00:00
Zlatin Balevsky
5802ba7734 show trust status of certificate issuers in cli as well 2019-11-06 18:19:45 +00:00
Zlatin Balevsky
b3f775f59a show trust status in certificates view 2019-11-06 18:13:07 +00:00
Zlatin Balevsky
739dbc7a24 fix serialization of older certificates 2019-11-06 18:09:50 +00:00
Zlatin Balevsky
af99dee4a3 wip on view certificate comments in cli 2019-11-06 17:08:48 +00:00
Zlatin Balevsky
07a6c63357 wip on view certificate comments in cli 2019-11-06 16:58:22 +00:00
Zlatin Balevsky
c4096568f5 initialize group properly 2019-11-06 16:01:43 +00:00
Zlatin Balevsky
30dda180eb Add support for comments in certificates, bump certificate version 2019-11-06 15:32:39 +00:00
Zlatin Balevsky
83ea1bed3e add timestamp to the filename of the certificate 2019-11-06 14:05:17 +00:00
Zlatin Balevsky
9181829e4a split by newlines 2019-11-06 13:59:14 +00:00
125 changed files with 5030 additions and 567 deletions

View File

@@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.6.6 is avaiable for download at https://muwire.com. You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various documentation.
### Building
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew clean assemble
@@ -19,11 +19,11 @@ If you want to run the unit tests, type
./gradlew clean build
```
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
### Running the GUI
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt.
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z-all.jar` in a terminal or command prompt.
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
@@ -31,10 +31,14 @@ If you have an I2P router running on the same machine that is all you need to do
### Running the CLI
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here [cli options]
The CLI is under active development and doesn't have all the features of the GUI.
### Web UI
If you are a Grails/Scala/JRuby/Kotlin developer and are interested in building a Web UI for MuWire, please get in touch. The MuWire core is written in Groovy and should be easy to integrate with any JVM-based language.
### GPG Fingerprint
```
@@ -45,3 +49,7 @@ You can find the full key at https://keybase.io/zlatinb
[Default I2CP port]: https://geti2p.net/en/docs/ports
[Wiki]: https://github.com/zlatinb/muwire/wiki
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options

View File

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

View File

@@ -17,7 +17,7 @@ class BrowseModel {
private final Persona persona
private final Core core
private final TextGUIThread guiThread
private final TableModel model = new TableModel("Name","Size","Hash","Comment")
private final TableModel model = new TableModel("Name","Size","Hash","Comment","Certificates")
private Map<String, UIResultEvent> rootToResult = new HashMap<>()
private int totalResults
@@ -53,7 +53,7 @@ class BrowseModel {
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment)
model.addRow(e.name, size, infoHash, comment, e.certificates)
rootToResult.put(infoHash, e)
String percentageString = ""
@@ -72,4 +72,27 @@ class BrowseModel {
void setPercentageLabel(Label percentage) {
this.percentage = percentage
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
List<UIResultEvent> l = new ArrayList<>(rootToResult.values())
Collections.sort(l, chosen)
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
l.each { e ->
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
String infoHash = Base64.encode(e.infohash.getRoot())
String comment = String.valueOf(e.comment != null)
model.addRow(e.name, size, infoHash, comment, e.certificates)
}
}
}

View File

@@ -49,7 +49,7 @@ class BrowseView extends BasicWindow {
topPanel.addComponent(percentageLabel, layoutData)
contentPanel.addComponent(topPanel, layoutData)
table = new Table("Name","Size","Hash","Comment")
table = new Table("Name","Size","Hash","Comment","Certificates")
table.with {
setCellSelection(false)
setTableModel(model.model)
@@ -58,11 +58,17 @@ class BrowseView extends BasicWindow {
}
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...", {sort()})
Button closeButton = new Button("Close",{
model.unregister()
close()
})
contentPanel.addComponent(closeButton, layoutData)
buttonsPanel.addComponent(sortButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel)
}
@@ -71,19 +77,24 @@ class BrowseView extends BasicWindow {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
String infoHash = row[2]
boolean comment = Boolean.parseBoolean(row[3])
if (comment) {
boolean comment = Boolean.parseBoolean(row[3])
boolean certificates = row[4] > 0
if (comment || certificates) {
Window prompt = new BasicWindow("Download Or View Comment")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
contentPanel.setLayoutManager(new GridLayout(4))
Button downloadButton = new Button("Download", {download(infoHash)})
Button viewButton = new Button("View Comment", {viewComment(infoHash)})
Button viewCertificate = new Button("View Certificates",{viewCertificates(infoHash)})
Button closeButton = new Button("Cancel", {prompt.close()})
contentPanel.with {
addComponent(downloadButton, layoutData)
addComponent(viewButton, layoutData)
if (comment)
addComponent(viewButton, layoutData)
if (certificates)
addComponent(viewCertificate, layoutData)
addComponent(closeButton, layoutData)
}
@@ -105,7 +116,21 @@ class BrowseView extends BasicWindow {
private void viewComment(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
ViewCommentView view = new ViewCommentView(result, terminalSize)
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
textGUI.addWindowAndWait(view)
}
private void viewCertificates(String infoHash) {
UIResultEvent result = model.rootToResult[infoHash]
ViewCertificatesModel model = new ViewCertificatesModel(result, core, textGUI.getGUIThread())
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,4 +78,32 @@ class FilesModel {
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
}
}
private void sort(SortType type) {
Comparator<SharedFile> chosen
switch(type) {
case SortType.NAME_ASC : chosen = NAME_ASC; break
case SortType.NAME_DESC : chosen = NAME_DESC; break
case SortType.SIZE_ASC : chosen = SIZE_ASC; break
case SortType.SIZE_DESC : chosen = SIZE_DESC; break
}
Collections.sort(sharedFiles, chosen)
}
private static final Comparator<SharedFile> NAME_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
a.getFile().getName().compareTo(b.getFile().getName())
}
}
private static final Comparator<SharedFile> NAME_DESC = NAME_ASC.reversed()
private static final Comparator<SharedFile> SIZE_ASC = new Comparator<SharedFile>() {
public int compare(SharedFile a, SharedFile b) {
Long.compare(a.getCachedLength(), b.getCachedLength())
}
}
private static final Comparator<SharedFile> SIZE_DESC = SIZE_ASC.reversed()
}

View File

@@ -51,17 +51,19 @@ class FilesView extends BasicWindow {
contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(4))
buttonsPanel.setLayoutManager(new GridLayout(5))
Button shareFile = new Button("Share File", {shareFile()})
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
Button sort = new Button("Sort...",{sort()})
Button close = new Button("Close", {close()})
buttonsPanel.with {
addComponent(shareFile, layoutData)
addComponent(shareDirectory, layoutData)
addComponent(unshareDirectory, layoutData)
addComponent(sort, layoutData)
addComponent(close, layoutData)
}
@@ -134,4 +136,11 @@ class FilesView extends BasicWindow {
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -44,6 +44,7 @@ class MainWindowView extends BasicWindow {
private final UploadsModel uploadsModel
private final FilesModel filesModel
private final TrustModel trustModel
private final ChatConsoleModel chatModel
private final Label connectionCount, incoming, outgoing
private final Label known, failing, hopeless
@@ -63,6 +64,9 @@ class MainWindowView extends BasicWindow {
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
filesModel = new FilesModel(textGUI.getGUIThread(),core)
trustModel = new TrustModel(textGUI.getGUIThread(), core)
chatModel = new ChatConsoleModel(core, textGUI.getGUIThread())
if (core.muOptions.startChatServer)
core.chatServer.start()
setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel()
@@ -74,7 +78,7 @@ class MainWindowView extends BasicWindow {
Panel buttonsPanel = new Panel()
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
GridLayout gridLayout = new GridLayout(7)
GridLayout gridLayout = new GridLayout(8)
buttonsPanel.setLayoutManager(gridLayout)
searchTextBox = new TextBox(new TerminalSize(40, 1))
@@ -83,6 +87,7 @@ class MainWindowView extends BasicWindow {
Button uploadsButton = new Button("Uploads", {upload()})
Button filesButton = new Button("Files", { files() })
Button trustButton = new Button("Trust", {trust()})
Button chatButton = new Button("Chat", {chat()})
Button quitButton = new Button("Quit", {close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
@@ -94,6 +99,7 @@ class MainWindowView extends BasicWindow {
addComponent(uploadsButton, layoutData)
addComponent(filesButton, layoutData)
addComponent(trustButton, layoutData)
addComponent(chatButton, layoutData)
addComponent(quitButton, layoutData)
}
@@ -271,6 +277,10 @@ class MainWindowView extends BasicWindow {
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
}
private void chat() {
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
}
private void refreshStats() {
int inCon = 0
int outCon = 0

View File

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

View File

@@ -16,7 +16,27 @@ class ResultsModel {
ResultsModel(UIResultBatchEvent results) {
this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
results.results.each {
updateModel()
}
void sort(SortType type) {
Comparator<UIResultEvent> chosen
switch(type) {
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
}
Arrays.sort(results.results, chosen)
updateModel()
}
private void updateModel() {
int rowCount = model.getRowCount()
rowCount.times { model.removeRow(0) }
results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot())
String sources = String.valueOf(it.sources.size())

View File

@@ -43,9 +43,14 @@ class ResultsView extends BasicWindow {
table.setTableModel(model.model)
table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...",{sort()})
buttonsPanel.addComponent(sortButton)
Button closeButton = new Button("Close", {close()})
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
buttonsPanel.addComponent(closeButton)
contentPanel.addComponent(buttonsPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
setComponent(contentPanel)
closeButton.takeFocus()
@@ -99,7 +104,7 @@ class ResultsView extends BasicWindow {
private void viewComment(String infohash) {
UIResultEvent result = model.rootToResult[infohash]
ViewCommentView view = new ViewCommentView(result, terminalSize)
ViewCommentView view = new ViewCommentView(result.comment, result.name, terminalSize)
textGUI.addWindowAndWait(view)
}
@@ -109,4 +114,11 @@ class ResultsView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view)
}
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
}

View File

@@ -7,6 +7,7 @@ import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
@@ -45,13 +46,16 @@ class SearchModel {
def searchEvent
byte [] payload
UUID uuid = UUID.randomUUID()
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
payload = root
} else {
def nonEmpty = SplitPattern.termify(query)
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
@@ -61,7 +65,7 @@ class SearchModel {
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig: sig.data))
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
}
void unregister() {

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,8 @@ class TrustListModel {
this.trustList = trustList
this.core = core
trustedTableModel = new TableModel("Trusted User","Your Trust")
distrustedTableModel = new TableModel("Distrusted User", "Your Trust")
trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
refreshModels()
core.eventBus.register(TrustEvent.class, this)
@@ -36,10 +36,10 @@ class TrustListModel {
distrustRows.times { distrustedTableModel.removeRow(0) }
trustList.good.each {
trustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
trustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
}
trustList.bad.each {
distrustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
distrustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
}
}

View File

@@ -12,6 +12,7 @@ import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.Persona
@@ -48,7 +49,7 @@ class TrustListView extends BasicWindow {
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted User","Your Trust")
trusted = new Table("Trusted User","Reason","Your Trust")
trusted.with {
setCellSelection(false)
setTableModel(model.trustedTableModel)
@@ -57,7 +58,7 @@ class TrustListView extends BasicWindow {
trusted.setSelectAction({ actionsForUser(true) })
topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted User", "Your Trust")
distrusted = new Table("Distrusted User","Reason", "Your Trust")
distrusted.with {
setCellSelection(false)
setTableModel(model.distrustedTableModel)
@@ -90,7 +91,8 @@ class TrustListView extends BasicWindow {
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Button trustButton = new Button("Trust",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK)
})
@@ -100,7 +102,8 @@ class TrustListView extends BasicWindow {
MessageDialogButton.OK)
})
Button distrustButton = new Button("Distrust",{
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK)
})

View File

@@ -17,8 +17,8 @@ class TrustModel {
this.guiThread = guiThread
this.core = core
modelTrusted = new TableModel("Trusted Users")
modelDistrusted = new TableModel("Distrusted Users")
modelTrusted = new TableModel("Trusted Users","Reason")
modelDistrusted = new TableModel("Distrusted Users","Reason")
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
core.eventBus.register(TrustEvent.class, this)
@@ -57,11 +57,11 @@ class TrustModel {
subsRows.times { modelSubscriptions.removeRow(0) }
core.trustService.good.values().each {
modelTrusted.addRow(new PersonaWrapper(it))
modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
}
core.trustService.bad.values().each {
modelDistrusted.addRow(new PersonaWrapper(it))
modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
}
core.trustSubscriber.remoteTrustLists.values().each {

View File

@@ -11,6 +11,7 @@ import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.Label
import com.googlecode.lanterna.gui2.table.Table
@@ -44,14 +45,14 @@ class TrustView extends BasicWindow {
Panel topPanel = new Panel()
topPanel.setLayoutManager(new GridLayout(2))
trusted = new Table("Trusted Users")
trusted = new Table("Trusted Users","Reason")
trusted.setCellSelection(false)
trusted.setSelectAction({trustedActions()})
trusted.setTableModel(model.modelTrusted)
trusted.setVisibleRows(tableSize)
topPanel.addComponent(trusted, layoutData)
distrusted = new Table("Distrusted users")
distrusted = new Table("Distrusted users","Reason")
distrusted.setCellSelection(false)
distrusted.setSelectAction({distrustedActions()})
distrusted.setTableModel(model.modelDistrusted)
@@ -106,7 +107,8 @@ class TrustView extends BasicWindow {
MessageDialogButton.OK)
})
Button markDistrusted = new Button("Mark Distrusted", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
MessageDialogButton.OK)
})
@@ -122,7 +124,7 @@ class TrustView extends BasicWindow {
}
private void distrustedActions() {
int selectedRow = trusted.getSelectedRow()
int selectedRow = distrusted.getSelectedRow()
def row = model.modelDistrusted.getRow(selectedRow)
Persona persona = row[0].persona
@@ -138,7 +140,8 @@ class TrustView extends BasicWindow {
MessageDialogButton.OK)
})
Button markDistrusted = new Button("Mark Trusted", {
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
MessageDialogButton.OK)
})

View File

@@ -17,8 +17,7 @@ class ViewCertificatesModel {
private final Core core
private final TextGUIThread guiThread
private final TableModel model = new TableModel("Issuer","File Name","Timestamp")
private final Map<Persona, Set<Certificate>> byIssuer = new HashMap<>()
private final TableModel model = new TableModel("Issuer","Trust Status","File Name","Comment","Timestamp")
private int totalCerts
@@ -53,14 +52,8 @@ class ViewCertificatesModel {
void onCertificateFetchedEvent(CertificateFetchedEvent e) {
guiThread.invokeLater {
Date date = new Date(e.certificate.timestamp)
model.addRow(new PersonaWrapper(e.certificate.issuer), e.certificate.name.name, date)
Set<Certificate> set = byIssuer.get(e.certificate.issuer)
if (set == null) {
set = new HashSet<>()
byIssuer.put(e.certificate.issuer, set)
}
set.add(e.certificate)
model.addRow(new CertificateWrapper(e.certificate), core.trustService.getLevel(e.certificate.issuer.destination),
e.certificate.name.name, e.certificate.comment != null, date)
String percentageString = ""
if (totalCerts > 0) {

View File

@@ -49,7 +49,7 @@ class ViewCertificatesView extends BasicWindow {
topPanel.addComponent(percentageLabel, layoutData)
contentPanel.addComponent(topPanel, layoutData)
table = new Table("Issuer","File Name","Timestamp")
table = new Table("Issuer","Trust Status","File Name","Comment","Timestamp")
table.with {
setCellSelection(false)
setTableModel(model.model)
@@ -69,16 +69,21 @@ class ViewCertificatesView extends BasicWindow {
private void rowSelected() {
int selectedRow = table.getSelectedRow()
def row = model.model.getRow(selectedRow)
Persona persona = row[0].persona
Certificate certificate = row[0].certificate
Window prompt = new BasicWindow("Import Certificate?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(2))
Button importButton = new Button("Import", {importCert(persona)})
contentPanel.setLayoutManager(new GridLayout(3))
Button importButton = new Button("Import", {importCert(certificate)})
Button viewCommentButton = new Button("View Comment", {viewComment(certificate)})
Button closeButton = new Button("Close", {prompt.close()})
contentPanel.addComponent(importButton, layoutData)
if (certificate.comment != null)
contentPanel.addComponent(viewCommentButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
@@ -86,11 +91,13 @@ class ViewCertificatesView extends BasicWindow {
textGUI.addWindowAndWait(prompt)
}
private void importCert(Persona persona) {
Set<Certificate> certs = model.byIssuer.get(persona)
certs.each {
core.eventBus.publish(new UIImportCertificateEvent(certificate : it))
}
private void importCert(Certificate certificate) {
core.eventBus.publish(new UIImportCertificateEvent(certificate : certificate))
MessageDialog.showMessageDialog(textGUI, "Certificate(s) Imported", "", MessageDialogButton.OK)
}
private void viewComment(Certificate certificate) {
ViewCommentView view = new ViewCommentView(certificate.comment.name, "Certificate Comment", terminalSize)
textGUI.addWindowAndWait(view)
}
}

View File

@@ -19,8 +19,8 @@ class ViewCommentView extends BasicWindow {
private final TextBox textBox
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
ViewCommentView(UIResultEvent result, TerminalSize terminalSize) {
super("View Comments For "+result.getName())
ViewCommentView(String text, String title, TerminalSize terminalSize) {
super("View Comments For "+title)
setHints([Window.Hint.CENTERED])
@@ -28,7 +28,7 @@ class ViewCommentView extends BasicWindow {
contentPanel.setLayoutManager(new GridLayout(1))
TerminalSize boxSize = new TerminalSize((terminalSize.getColumns() / 2).toInteger(), (terminalSize.getRows() / 2).toInteger())
textBox = new TextBox(boxSize, result.comment, TextBox.Style.MULTI_LINE)
textBox = new TextBox(boxSize, text, TextBox.Style.MULTI_LINE)
contentPanel.addComponent(textBox, layoutData)
Button closeButton = new Button("Close", {close()})

View File

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

View File

@@ -3,6 +3,12 @@ package com.muwire.core
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.chat.ChatDisconnectionEvent
import com.muwire.core.chat.ChatManager
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.ChatServer
import com.muwire.core.chat.UIConnectChatEvent
import com.muwire.core.chat.UIDisconnectChatEvent
import com.muwire.core.connection.ConnectionAcceptor
import com.muwire.core.connection.ConnectionEstablisher
import com.muwire.core.connection.ConnectionEvent
@@ -105,6 +111,8 @@ public class Core {
final UploadManager uploadManager
final ContentManager contentManager
final CertificateManager certificateManager
final ChatServer chatServer
final ChatManager chatManager
private final Router router
@@ -278,8 +286,16 @@ public class Core {
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
log.info("initializing chat server")
chatServer = new ChatServer(eventBus, props, trustService, me, spk)
eventBus.with {
register(ChatMessageEvent.class, chatServer)
register(ChatDisconnectionEvent.class, chatServer)
register(TrustEvent.class, chatServer)
}
log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager)
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@@ -302,11 +318,21 @@ public class Core {
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
log.info("initializing chat manager")
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
eventBus.with {
register(UIConnectChatEvent.class, chatManager)
register(UIDisconnectChatEvent.class, chatManager)
register(ChatMessageEvent.class, chatManager)
register(ChatDisconnectionEvent.class, chatManager)
}
log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
certificateManager)
certificateManager, chatServer)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
@@ -331,7 +357,7 @@ public class Core {
eventBus.register(QueryEvent.class, contentManager)
log.info("initializing browse manager")
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
eventBus.register(UIBrowseEvent.class, browseManager)
}
@@ -358,9 +384,9 @@ public class Core {
saveMuSettings()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down download manageer")
log.info("shutting down download manager")
downloadManager.shutdown()
log.info("shutting down connection acceeptor")
log.info("shutting down connection acceptor")
connectionAcceptor.stop()
log.info("shutting down connection establisher")
connectionEstablisher.stop()
@@ -368,6 +394,10 @@ public class Core {
directoryWatcher.stop()
log.info("shutting down cache client")
cacheClient.stop()
log.info("shutting down chat server")
chatServer.stop()
log.info("shutting down chat manager")
chatManager.shutdown()
log.info("shutting down connection manager")
connectionManager.shutdown()
if (router != null) {
@@ -406,7 +436,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.8")
Core core = new Core(props, home, "0.6.7")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -31,6 +31,10 @@ class MuWireSettings {
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
boolean startChatServer
int maxChatConnections
boolean advertiseChat
File chatWelcomeFile
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
@@ -79,7 +83,13 @@ class MuWireSettings {
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
String chatWelcomeProp = props.getProperty("chatWelcomeFile")
if (chatWelcomeProp != null)
chatWelcomeFile = new File(chatWelcomeProp)
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
@@ -127,6 +137,11 @@ class MuWireSettings {
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
props.setProperty("startChatServer", String.valueOf(startChatServer))
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
if (chatWelcomeFile != null)
props.setProperty("chatWelcomeFile", chatWelcomeFile.getAbsolutePath())
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?\r\n]";
private static final Set<Character> SPLIT_CHARS = new HashSet<>()
static {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -153,6 +153,10 @@ abstract class Connection implements Closeable {
query.originator = e.originator.toBase64()
if (e.sig != null)
query.sig = Base64.encode(e.sig)
if (e.queryTime > 0)
query.queryTime = e.queryTime
if (e.sig2 != null)
query.sig2 = Base64.encode(e.sig2)
messages.put(query)
}
@@ -232,7 +236,6 @@ abstract class Connection implements Closeable {
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
// TODO: make this mandatory at some point
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
@@ -247,21 +250,52 @@ abstract class Connection implements Closeable {
return
} else
log.info("query signature verified")
} else
} else {
log.info("no signature in query")
return
}
// TODO: make this mandatory at some point
byte[] sig2 = null
long queryTime = 0
if (search.sig2 != null) {
if (search.queryTime == null) {
log.info("extended signature but no timestamp")
return
}
sig2 = Base64.decode(search.sig2)
queryTime = search.queryTime
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig2)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("extended signature didn't match uuid and timestamp")
return
} else {
log.info("extended query signature verified")
if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) {
log.info("query too old")
return
}
}
} else
log.info("no extended signature in query")
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,
uuid : uuid,
oobInfohash : oob,
searchComments : searchComments,
compressedResults : compressedResults)
compressedResults : compressedResults,
persona : originator)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop,
sig : sig )
sig : sig,
queryTime : queryTime,
sig2 : sig2 )
eventBus.publish(event)
}

View File

@@ -15,6 +15,7 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.chat.ChatServer
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.files.FileManager
@@ -50,6 +51,7 @@ class ConnectionAcceptor {
final FileManager fileManager
final ConnectionEstablisher establisher
final CertificateManager certificateManager
final ChatServer chatServer
final ExecutorService acceptorThread
final ExecutorService handshakerThreads
@@ -61,7 +63,8 @@ class ConnectionAcceptor {
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager) {
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager,
ChatServer chatServer) {
this.eventBus = eventBus
this.manager = manager
this.settings = settings
@@ -73,6 +76,7 @@ class ConnectionAcceptor {
this.uploadManager = uploadManager
this.establisher = establisher
this.certificateManager = certificateManager
this.chatServer = chatServer
acceptorThread = Executors.newSingleThreadExecutor { r ->
def rv = new Thread(r)
@@ -154,12 +158,17 @@ class ConnectionAcceptor {
case (byte)'C':
processCERTIFICATES(e)
break
case (byte)'I':
processIRC(e)
break
default:
throw new Exception("Invalid read $read")
}
} catch (Exception ex) {
log.log(Level.WARNING, "incoming connection failed",ex)
e.getOutputStream().close()
try {
e.getOutputStream().close()
} catch (Exception ignore) {}
e.close()
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
}
@@ -208,7 +217,9 @@ class ConnectionAcceptor {
os.writeShort(json.bytes.length)
os.write(json.bytes)
}
e.outputStream.close()
try {
e.outputStream.close()
} catch (Exception ignored) {}
e.close()
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
}
@@ -288,23 +299,17 @@ class ConnectionAcceptor {
if (!searchManager.hasLocalSearch(resultsUUID))
throw new UnexpectedResultsException(resultsUUID.toString())
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
Map<String,String> headers = DataUtil.readAllHeaders(is);
if (!headers.containsKey("Sender"))
throw new IOException("No Sender header")
if (!headers.containsKey("Count"))
throw new IOException("No Count header")
boolean chat = false
if (headers.containsKey('Chat'))
chat = Boolean.parseBoolean(headers['Chat'])
byte [] personaBytes = Base64.decode(headers['Sender'])
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
@@ -323,6 +328,7 @@ class ConnectionAcceptor {
dis.readFully(payload)
def json = slurper.parse(payload)
results[i] = ResultsParser.parse(sender, resultsUUID, json)
results[i].chat = chat
}
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException bad) {
@@ -340,8 +346,14 @@ class ConnectionAcceptor {
dis.readFully(rowse)
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid BROWSE connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
Persona browser = null
Map<String,String> headers = DataUtil.readAllHeaders(dis);
if (headers.containsKey('Persona')) {
browser = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
if (browser.destination != e.destination)
throw new IOException("browser persona mismatch")
}
OutputStream os = e.getOutputStream()
if (!settings.browseFiles) {
@@ -362,7 +374,7 @@ class ConnectionAcceptor {
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
it.hit()
it.hit(browser, System.currentTimeMillis(), "Browse Host");
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
def json = jsonOutput.toJson(obj)
@@ -383,10 +395,10 @@ class ConnectionAcceptor {
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
Map<String,String> headers = DataUtil.readAllHeaders(dis)
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
@@ -394,22 +406,53 @@ class ConnectionAcceptor {
return
}
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
List<Persona> good = new ArrayList<>(trustService.good.values())
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
DataOutputStream dos = new DataOutputStream(os)
dos.writeShort(size)
good.each {
it.write(dos)
}
List<Persona> bad = new ArrayList<>(trustService.bad.values())
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.write(dos)
if (!json) {
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
dos.writeShort(size)
good.each {
it.persona.write(dos)
}
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.persona.write(dos)
}
} else {
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
good.each {
def obj = [:]
obj.persona = it.persona.toBase64()
obj.reason = it.reason
String toJson = JsonOutput.toJson(obj)
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
dos.writeShort(payload.length)
dos.write(payload)
}
bad.each {
def obj = [:]
obj.persona = it.persona.toBase64()
obj.reason = it.reason
String toJson = JsonOutput.toJson(obj)
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
dos.writeShort(payload.length)
dos.write(payload)
}
}
dos.flush()
@@ -467,5 +510,14 @@ class ConnectionAcceptor {
e.close()
}
}
private void processIRC(Endpoint e) {
byte[] IRC = new byte[4]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(IRC)
if (IRC != "RC\r\n".getBytes(StandardCharsets.US_ASCII))
throw new Exception("Invalid IRC connection")
chatServer.handle(e)
}
}

View File

@@ -276,6 +276,57 @@ public class Downloader {
activeWorkers.put(d, newWorker)
executorService.submit(newWorker)
}
boolean isSequential() {
pieces.ratio == 0f
}
File generatePreview() {
int lastCompletePiece = pieces.firstIncomplete() - 1
if (lastCompletePiece == -1)
return null
if (lastCompletePiece < -1)
return file
long previewableLength = (lastCompletePiece + 1) * ((long)pieceSize)
// generate name
long now = System.currentTimeMillis()
File previewFile
File parentFile = file.getParentFile()
int lastDot = file.getName().lastIndexOf('.')
if (lastDot < 0)
previewFile = new File(parentFile, file.getName() + "." + String.valueOf(now) + ".mwpreview")
else {
String name = file.getName().substring(0, lastDot)
String extension = file.getName().substring(lastDot + 1)
String previewName = name + "." + String.valueOf(now) + ".mwpreview."+extension
previewFile = new File(parentFile, previewName)
}
// copy
InputStream is = null
OutputStream os = null
try {
is = new BufferedInputStream(new FileInputStream(incompleteFile))
os = new BufferedOutputStream(new FileOutputStream(previewFile))
byte [] tmp = new byte[0x1 << 13]
long totalCopied = 0
while(totalCopied < previewableLength) {
int read = is.read(tmp, 0, (int)Math.min(tmp.length, previewableLength - totalCopied))
if (read < 0)
throw new IOException("EOF?")
os.write(tmp, 0, read)
totalCopied += read
}
return previewFile
} catch (IOException bad) {
log.log(Level.WARNING,"Preview failed",bad)
return null
} finally {
try {is?.close() } catch (IOException ignore) {}
try {os?.close() } catch (IOException ignore) {}
}
}
class DownloadWorker implements Runnable {
private final Destination destination

View File

@@ -108,6 +108,10 @@ class Pieces {
partials.clear()
}
synchronized int firstIncomplete() {
done.nextClearBit(0)
}
synchronized void write(PrintWriter writer) {
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
writer.println(i)

View File

@@ -14,7 +14,7 @@ import net.i2p.data.SigningPublicKey
class Certificate {
private final byte version
private final InfoHash infoHash
private final Name name
private final Name name, comment
private final long timestamp
private final Persona issuer
private final byte[] sig
@@ -23,7 +23,7 @@ class Certificate {
Certificate(InputStream is) {
version = (byte) (is.read() & 0xFF)
if (version != Constants.FILE_CERT_VERSION)
if (version > Constants.FILE_CERT_VERSION)
throw new IOException("Unknown version $version")
DataInputStream dis = new DataInputStream(is)
@@ -35,19 +35,29 @@ class Certificate {
name = new Name(dis)
issuer = new Persona(is)
issuer = new Persona(dis)
if (version == 2) {
byte present = (byte)(dis.read() & 0xFF)
if (present != 0) {
comment = new Name(dis)
}
}
sig = new byte[Constants.SIG_TYPE.getSigLen()]
dis.readFully(sig)
if (!verify(version, infoHash, name, timestamp, issuer, sig))
if (!verify(version, infoHash, name, timestamp, issuer, comment, sig))
throw new InvalidSignatureException("certificate for $name.name from ${issuer.getHumanReadableName()} didn't verify")
}
Certificate(InfoHash infoHash, String name, long timestamp, Persona issuer, SigningPrivateKey spk) {
Certificate(InfoHash infoHash, String name, long timestamp, Persona issuer, String comment, SigningPrivateKey spk) {
this.version = Constants.FILE_CERT_VERSION
this.infoHash = infoHash
this.name = new Name(name)
if (comment != null)
this.comment = new Name(comment)
else
this.comment = null
this.timestamp = timestamp
this.issuer = issuer
@@ -59,6 +69,12 @@ class Certificate {
daos.write(infoHash.getRoot())
this.name.write(daos)
issuer.write(daos)
if (this.comment == null) {
daos.write((byte) 0)
} else {
daos.write((byte) 1)
this.comment.write(daos)
}
daos.close()
byte[] payload = baos.toByteArray()
@@ -66,7 +82,7 @@ class Certificate {
this.sig = signature.getData()
}
private static boolean verify(byte version, InfoHash infoHash, Name name, long timestamp, Persona issuer, byte[] sig) {
private static boolean verify(byte version, InfoHash infoHash, Name name, long timestamp, Persona issuer, Name comment, byte[] sig) {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
DataOutputStream daos = new DataOutputStream(baos)
daos.write(version)
@@ -74,6 +90,14 @@ class Certificate {
daos.write(infoHash.getRoot())
name.write(daos)
issuer.write(daos)
if (version == 2) {
if (comment == null) {
daos.write((byte)0)
} else {
daos.write((byte)1)
comment.write(daos)
}
}
daos.close()
byte [] payload = baos.toByteArray()
@@ -91,6 +115,14 @@ class Certificate {
daos.write(infoHash.getRoot())
name.write(daos)
issuer.write(daos)
if (version == 2) {
if (comment == null)
daos.write((byte) 0)
else {
daos.write((byte) 1)
comment.write(daos)
}
}
daos.write(sig)
daos.close()
@@ -101,7 +133,7 @@ class Certificate {
@Override
public int hashCode() {
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode()
version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() ^ Objects.hashCode(comment)
}
@Override
@@ -114,6 +146,7 @@ class Certificate {
infoHash == other.infoHash &&
timestamp == other.timestamp &&
name == other.name &&
issuer == other.issuer
issuer == other.issuer &&
comment == other.comment
}
}

View File

@@ -73,6 +73,7 @@ class CertificateClient {
try {
cert = new Certificate(new ByteArrayInputStream(tmp))
} catch (IOException | InvalidSignatureException ignore) {
log.log(Level.WARNING, "certificate creation failed",ignore)
continue
}
if (cert.infoHash == e.infoHash)

View File

@@ -8,6 +8,7 @@ import com.muwire.core.InfoHash
import com.muwire.core.InvalidSignatureException
import com.muwire.core.Name
import com.muwire.core.Persona
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Base64
@@ -72,7 +73,12 @@ class CertificateManager {
InfoHash infoHash = e.sharedFile.getInfoHash()
String name = e.sharedFile.getFile().getName()
long timestamp = System.currentTimeMillis()
Certificate cert = new Certificate(infoHash, name, timestamp, me, spk)
String comment = null
if (e.sharedFile.getComment() != null)
comment = DataUtil.readi18nString(Base64.decode(e.sharedFile.getComment()))
Certificate cert = new Certificate(infoHash, name, timestamp, me, comment, spk)
if (addToMaps(cert)) {
@@ -90,7 +96,7 @@ class CertificateManager {
private void saveCert(Certificate cert) {
String infoHashString = Base64.encode(cert.infoHash.getRoot())
File certFile = new File(certDir, "${infoHashString}_${cert.issuer.getHumanReadableName()}.mwcert")
File certFile = new File(certDir, "${infoHashString}_${cert.issuer.getHumanReadableName()}_${cert.timestamp}.mwcert")
certFile.withOutputStream { cert.write(it) }
}

View File

@@ -143,6 +143,7 @@ class FileManager {
String comment = sf.getComment()
if (comment != null) {
comment = DataUtil.readi18nString(Base64.decode(comment))
Set<File> existingComment = commentToFile.get(comment)
if (existingComment != null) {
existingComment.remove(sf.getFile())
@@ -198,7 +199,7 @@ class FileManager {
found = rootToFiles.get new InfoHash(e.searchHash)
found = filter(found, e.oobInfohash)
if (found != null && !found.isEmpty()) {
found.each { it.hit() }
found.each { it.hit(e.persona, e.timestamp, "Hash Search") }
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
}
} else {
@@ -214,7 +215,7 @@ class FileManager {
files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty()) {
sharedFiles.each { it.hit() }
sharedFiles.each { it.hit(e.persona, e.timestamp, String.join(" ", e.searchTerms)) }
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
}
@@ -229,7 +230,7 @@ class FileManager {
return files
Set<SharedFile> rv = new HashSet<>()
files.each {
if (it.getPieceSize() != 0)
if (it != null && it.getPieceSize() != 0)
rv.add(it)
}
rv

View File

@@ -7,7 +7,7 @@ class FileTree {
private final TreeNode root = new TreeNode()
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
void add(File file) {
synchronized void add(File file) {
List<File> path = new ArrayList<>()
path.add(file)
while (file.getParentFile() != null) {
@@ -31,7 +31,7 @@ class FileTree {
}
}
boolean remove(File file) {
synchronized boolean remove(File file) {
TreeNode node = fileToNode.remove(file)
if (node == null) {
return false

View File

@@ -12,6 +12,7 @@ import java.util.stream.Collectors
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Service
import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
@@ -129,15 +130,21 @@ class PersisterService extends Service {
return new FileLoadedEvent(loadedFile : df)
}
int hits = 0
if (json.hits != null)
hits = json.hits
SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
sf.hits = hits
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf)
}
@@ -172,6 +179,19 @@ class PersisterService extends Service {
json.hits = sf.getHits()
json.downloaders = sf.getDownloaders()
if (!sf.searches.isEmpty()) {
Set searchers = new HashSet<>()
sf.searches.each {
def search = [:]
if (it.searcher != null)
search.searcher = it.searcher.toBase64()
search.timestamp = it.timestamp
search.query = it.query
searchers.add(search)
}
json.searchers = searchers
}
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
}

View File

@@ -2,6 +2,7 @@ package com.muwire.core.search
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
@@ -20,12 +21,14 @@ class BrowseManager {
private final I2PConnector connector
private final EventBus eventBus
private final Persona me
private final Executor browserThread = Executors.newSingleThreadExecutor()
BrowseManager(I2PConnector connector, EventBus eventBus) {
BrowseManager(I2PConnector connector, EventBus eventBus, Persona me) {
this.connector = connector
this.eventBus = eventBus
this.me = me
}
void onUIBrowseEvent(UIBrowseEvent e) {
@@ -35,7 +38,9 @@ class BrowseManager {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("BROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
@@ -43,16 +48,7 @@ class BrowseManager {
throw new IOException("Invalid code $code")
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
Map<String,String> headers = DataUtil.readAllHeaders(is)
if (!headers.containsKey("Count"))
throw new IOException("No count header")

View File

@@ -13,6 +13,8 @@ class QueryEvent extends Event {
Persona originator
Destination receivedOn
byte[] sig
long queryTime
byte[] sig2
String toString() {
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +

View File

@@ -1,6 +1,7 @@
package com.muwire.core.search
import com.muwire.core.SharedFile
import com.muwire.core.chat.ChatServer
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.filecert.CertificateManager
@@ -48,13 +49,16 @@ class ResultsSender {
private final EventBus eventBus
private final MuWireSettings settings
private final CertificateManager certificateManager
private final ChatServer chatServer
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings, CertificateManager certificateManager) {
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings,
CertificateManager certificateManager, ChatServer chatServer) {
this.connector = connector;
this.eventBus = eventBus
this.me = me
this.settings = settings
this.certificateManager = certificateManager
this.chatServer = chatServer
}
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) {
@@ -80,9 +84,11 @@ class ResultsSender {
infohash : it.getInfoHash(),
pieceSize : pieceSize,
uuid : uuid,
browse : settings.browseFiles,
sources : suggested,
comment : comment,
certificates : certificates
certificates : certificates,
chat : chatServer.running.get() && settings.advertiseChat
)
uiResultEvents << uiResultEvent
}
@@ -130,6 +136,8 @@ class ResultsSender {
os.write("RESULTS $uuid\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Sender: ${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
boolean chat = chatServer.running.get() && settings.advertiseChat
os.write("Chat: $chat\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
results.each {

View File

@@ -2,6 +2,7 @@ package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class SearchEvent extends Event {
@@ -11,6 +12,7 @@ class SearchEvent extends Event {
boolean oobInfohash
boolean searchComments
boolean compressedResults
Persona persona
String toString() {
def infoHash = null

View File

@@ -39,10 +39,11 @@ class SearchIndex {
split.each { if (it.length() > 0) rv << it }
// then just by ' '
source.split(' ').each { if (it.length() > 0) rv << it }
source.toLowerCase().split(' ').each { if (it.length() > 0) rv << it }
// and add original string
rv << source
rv << source.toLowerCase()
rv.toArray(new String[0])
}

View File

@@ -17,6 +17,7 @@ class UIResultEvent extends Event {
String comment
boolean browse
int certificates
boolean chat
@Override
public String toString() {

View File

@@ -3,6 +3,7 @@ package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.Persona
import com.muwire.core.trust.TrustService.TrustEntry
import net.i2p.util.ConcurrentHashSet
@@ -10,7 +11,7 @@ class RemoteTrustList {
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
private final Persona persona
private final Set<Persona> good, bad
private final Set<TrustEntry> good, bad
volatile long timestamp
volatile boolean forceUpdate
Status status = Status.NEW

View File

@@ -7,4 +7,5 @@ class TrustEvent extends Event {
Persona persona
TrustLevel level
String reason
}

View File

@@ -1,21 +1,26 @@
package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import com.muwire.core.Persona
import com.muwire.core.Service
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@Log
class TrustService extends Service {
final File persistGood, persistBad
final long persistInterval
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
final Map<Destination, TrustEntry> good = new ConcurrentHashMap<>()
final Map<Destination, TrustEntry> bad = new ConcurrentHashMap<>()
final Timer timer
@@ -37,18 +42,41 @@ class TrustService extends Service {
}
void load() {
JsonSlurper slurper = new JsonSlurper()
if (persistGood.exists()) {
persistGood.eachLine {
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, persona)
try {
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, new TrustEntry(persona, null))
} catch (Exception e) {
try {
def json = slurper.parseText(it)
byte [] decoded = Base64.decode(json.persona)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, new TrustEntry(persona, json.reason))
} catch (Exception bad) {
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
}
}
}
}
if (persistBad.exists()) {
persistBad.eachLine {
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, persona)
try {
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, new TrustEntry(persona, null))
} catch (Exception e) {
try {
def json = slurper.parseText(it)
byte [] decoded = Base64.decode(json.persona)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, new TrustEntry(persona, json.reason))
} catch (Exception bad) {
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
}
}
}
}
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
@@ -59,13 +87,19 @@ class TrustService extends Service {
persistGood.delete()
persistGood.withPrintWriter { writer ->
good.each {k,v ->
writer.println v.toBase64()
def json = [:]
json.persona = v.persona.toBase64()
json.reason = v.reason
writer.println JsonOutput.toJson(json)
}
}
persistBad.delete()
persistBad.withPrintWriter { writer ->
bad.each { k,v ->
writer.println v.toBase64()
def json = [:]
json.persona = v.persona.toBase64()
json.reason = v.reason
writer.println JsonOutput.toJson(json)
}
}
}
@@ -82,11 +116,11 @@ class TrustService extends Service {
switch(e.level) {
case TrustLevel.TRUSTED:
bad.remove(e.persona.destination)
good.put(e.persona.destination, e.persona)
good.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
break
case TrustLevel.DISTRUSTED:
good.remove(e.persona.destination)
bad.put(e.persona.destination, e.persona)
bad.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
break
case TrustLevel.NEUTRAL:
good.remove(e.persona.destination)
@@ -94,4 +128,24 @@ class TrustService extends Service {
break
}
}
public static class TrustEntry {
private final Persona persona
private final String reason
TrustEntry(Persona persona, String reason) {
this.persona = persona
this.reason = reason
}
public int hashCode() {
persona.hashCode()
}
public boolean equals(Object o) {
if (!(o instanceof TrustEntry))
return false
persona == o.persona
}
}
}

View File

@@ -12,9 +12,12 @@ import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.trust.TrustService.TrustEntry
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
@@ -109,7 +112,9 @@ class TrustSubscriber {
endpoint = i2pConnector.connect(trustList.persona.destination)
OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream()
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("TRUST\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Json:true\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
String codeString = DataUtil.readTillRN(is)
@@ -123,24 +128,47 @@ class TrustSubscriber {
return false
}
// swallow any headers
String header
while (( header = DataUtil.readTillRN(is)) != "");
Map<String,String> headers = DataUtil.readAllHeaders(is)
DataInputStream dis = new DataInputStream(is)
Set<TrustService.TrustEntry> good = new HashSet<>()
Set<TrustService.TrustEntry> bad = new HashSet<>()
if (headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])) {
int countGood = Integer.parseInt(headers['Good'])
int countBad = Integer.parseInt(headers['Bad'])
JsonSlurper slurper = new JsonSlurper()
for (int i = 0; i < countGood; i++) {
int length = dis.readUnsignedShort()
byte []payload = new byte[length]
dis.readFully(payload)
def json = slurper.parse(payload)
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
good.add(new TrustEntry(persona, json.reason))
}
for (int i = 0; i < countBad; i++) {
int length = dis.readUnsignedShort()
byte []payload = new byte[length]
dis.readFully(payload)
def json = slurper.parse(payload)
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
bad.add(new TrustEntry(persona, json.reason))
}
} else {
int nGood = dis.readUnsignedShort()
for (int i = 0; i < nGood; i++) {
Persona p = new Persona(dis)
good.add(new TrustEntry(p,null))
}
Set<Persona> good = new HashSet<>()
int nGood = dis.readUnsignedShort()
for (int i = 0; i < nGood; i++) {
Persona p = new Persona(dis)
good.add(p)
}
Set<Persona> bad = new HashSet<>()
int nBad = dis.readUnsignedShort()
for (int i = 0; i < nBad; i++) {
Persona p = new Persona(dis)
bad.add(p)
int nBad = dis.readUnsignedShort()
for (int i = 0; i < nBad; i++) {
Persona p = new Persona(dis)
bad.add(new TrustEntry(p, null))
}
}
trustList.timestamp = now

View File

@@ -13,6 +13,7 @@ import com.muwire.core.files.FileSharedEvent
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.json.JsonOutput
import groovy.json.JsonSlurper
@@ -176,9 +177,12 @@ class UpdateClient {
signer = payload.signer
log.info("starting search for new version hash $payload.infoHash")
Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk)
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
UUID uuid = UUID.randomUUID()
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, spk)
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : uuid, oobInfohash : true, persona : me)
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
receivedOn : me.destination, originator : me, sig : sig.data)
receivedOn : me.destination, originator : me, sig : sig.data, queryTime : timestamp, sig2 : sig2)
eventBus.publish(queryEvent)
}
}

View File

@@ -4,13 +4,18 @@ import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_VERSION = (byte)1;
public static final byte FILE_CERT_VERSION = (byte)1;
public static final byte FILE_CERT_VERSION = (byte)2;
public static final int CHAT_VERSION = 1;
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
public static final int MAX_HEADER_SIZE = 0x1 << 14;
public static final int MAX_HEADERS = 16;
public static final long MAX_HEADER_TIME = 60 * 1000;
public static final int MAX_RESULTS = 0x1 << 16;
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
}

View File

@@ -1,4 +1,4 @@
package com.muwire.core
package com.muwire.core;
class InvalidSignatureException extends Exception {

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.muwire.core.util.DataUtil;
@@ -26,8 +27,8 @@ public class SharedFile {
private final List<String> b64EncodedHashList;
private volatile String comment;
private volatile int hits;
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file;
@@ -97,17 +98,21 @@ public class SharedFile {
}
public int getHits() {
return hits;
return searches.size();
}
public void hit() {
hits++;
public void hit(Persona searcher, long timestamp, String query) {
searches.add(new SearchEntry(searcher, timestamp, query));
}
public Set<String> getDownloaders() {
return downloaders;
}
public Set<SearchEntry> getSearches() {
return searches;
}
public void addDownloader(String name) {
downloaders.add(name);
}
@@ -124,4 +129,29 @@ public class SharedFile {
SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash);
}
public static class SearchEntry {
private final Persona searcher;
private final long timestamp;
private final String query;
public SearchEntry(Persona searcher, long timestamp, String query) {
this.searcher = searcher;
this.timestamp = timestamp;
this.query = query;
}
public int hashCode() {
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof SearchEntry))
return false;
SearchEntry other = (SearchEntry)o;
return Objects.equals(searcher, other.searcher) &&
timestamp == other.timestamp &&
query.equals(other.query);
}
}
}

View File

@@ -10,14 +10,20 @@ import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.muwire.core.Constants;
import net.i2p.crypto.DSAEngine;
import net.i2p.data.Base64;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.util.ConcurrentHashSet;
public class DataUtil {
@@ -85,9 +91,12 @@ public class DataUtil {
}
public static String readTillRN(InputStream is) throws IOException {
final long start = System.currentTimeMillis();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
int read = is.read();
if (System.currentTimeMillis() - start > Constants.MAX_HEADER_TIME)
throw new IOException("header taking too long");
if (read == -1)
throw new IOException();
if (read == '\r') {
@@ -99,6 +108,20 @@ public class DataUtil {
}
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
}
public static Map<String, String> readAllHeaders(InputStream is) throws IOException {
Map<String, String> headers = new HashMap<>();
String header;
while(!(header = readTillRN(is)).equals("") && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':');
if (colon == -1 || colon == header.length() - 1)
throw new IOException("Invalid header "+ header);
String key = header.substring(0, colon);
String value = header.substring(colon + 1);
headers.put(key, value.trim());
}
return headers;
}
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8;
@@ -187,4 +210,10 @@ public class DataUtil {
.collect(Collectors.joining(","));
props.setProperty(property, encoded);
}
public static byte[] signUUID(UUID uuid, long timestamp, SigningPrivateKey spk) {
byte [] payload = (uuid.toString() + String.valueOf(timestamp)).getBytes(StandardCharsets.US_ASCII);
Signature sig = DSAEngine.getInstance().sign(payload, spk);
return sig.getData();
}
}

View File

@@ -24,4 +24,12 @@ class SplitPatternTest {
assert SplitPattern.termify('"siamese cat" any cat "persian cat"') ==
['siamese cat','any','cat','persian cat']
}
@Test
void testNewLine() {
def s = "first\nsecond"
s = s.replaceAll(SplitPattern.SPLIT_PATTERN, " ")
s = s.split(" ")
assert s.length == 2
}
}

View File

@@ -1,5 +1,7 @@
package com.muwire.core.files
import static org.junit.jupiter.api.Assertions.assertAll
import org.junit.Before
import org.junit.Test
@@ -9,6 +11,9 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class FileManagerTest {
@@ -185,4 +190,39 @@ class FileManagerTest {
assert results == null
}
@Test
void testComplicatedScenario() {
// this tries to reproduce an NPE when un-sharing then sharing again and searching
String comment = "same comment"
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)
sf1.setComment(comment)
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
manager.onFileUnsharedEvent(new FileUnsharedEvent(unsharedFile : sf1, deleted : true))
File f2 = new File("MuWire-0.6.0.AppImage")
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
SharedFile sf2 = new SharedFile(f2, ih2, 0)
sf2.setComment(comment)
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
manager.onSearchEvent(new SearchEvent(searchTerms : ["muwire"]))
Thread.sleep(20)
assert results != null
assert results.results.size() == 1
assert results.results.contains(sf2)
results = null
manager.onSearchEvent(new SearchEvent(searchTerms : ['comment'], searchComments : true, oobInfohash : true))
Thread.sleep(20)
assert results != null
assert results.results.size() == 1
assert results.results.contains(sf2)
}
}

View File

@@ -120,4 +120,26 @@ class SearchIndexTest {
assert index.search(['cat', 'video siamese']).size() == 0
assert index.search(['cat','video','siamese']).size() == 3
}
@Test
void testNewLine() {
initIndex(['first\nsecond'])
assert index.search(['first']).size() == 1
assert index.search(['second']).size() == 1
assert index.search(['first','second']).size() == 1
assert index.search(['second','first']).size() == 1
assert index.search(['second first']).size() == 0
assert index.search(['first second']).size() == 0
}
@Test
void testDosNewLine() {
initIndex(['first\r\nsecond'])
assert index.search(['first']).size() == 1
assert index.search(['second']).size() == 1
assert index.search(['first','second']).size() == 1
assert index.search(['second','first']).size() == 1
assert index.search(['second first']).size() == 0
assert index.search(['first second']).size() == 0
}
}

View File

@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
import com.muwire.core.Persona
import com.muwire.core.Personas
import groovy.json.JsonSlurper
import net.i2p.data.Base64
import net.i2p.data.Destination
@@ -55,13 +56,16 @@ class TrustServiceTest {
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
Thread.sleep(250)
JsonSlurper slurper = new JsonSlurper()
def trusted = new HashSet<>()
persistGood.eachLine {
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
def json = slurper.parseText(it)
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
}
def distrusted = new HashSet<>()
persistBad.eachLine {
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
def json = slurper.parseText(it)
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
}
assert trusted.size() == 1

59
doc/architecture.md Normal file
View File

@@ -0,0 +1,59 @@
# MuWire architecture
### Core-UI separation
The MuWire application is split conceptually into a `core` component and two `ui` components - one graphical component which is build using Swing and one text-only component built using the "lanterna" library.
The core is written in mixture of Java and Groovy and is designed to be easy to embed into any application or language running on a JVM. To achieve this, all communicatioon between the core and the outside world happens over an event bus using event objects.
### Event bus and events
At the heart of the core is the event bus. It allows the different components that comprise the core to be decoupled, and allows the external components like UIs to communicate in asynchronous fashion with the core.
The Core object has a single instance of the `com.muwire.core.EventBus` class. It is responsible for dispatching events to any registered listeners. Events themselves extend the `com.muwire.core.Event` class and carry arbitrary information relevant to the event. See below or an example how to build a custom event:
1. Define the event in a class that extends `com.muwire.core.Event`:
```
package mypackage
import com.muwire.core.Event
class MyEvent extends Event {
// add relevant fields here
}
```
2. Define one or more classes that will be notified of your events:
```
package mypackage
class MyEventListener {
// ... add other logic here
void onMyEvent(MyEvent e) {
// logic to handle your type of event
}
}
```
3. Register your event listener with the event bus:
```
MyEventListener myListener = new MyEventListener()
eventBus.register(MyEvent.class,myListener)
```
You can register more than one listener for the same type of event; they will be notified in the order you register them.
4. Publish events to the event bus
```
MyEvent myEvent = new MyEvent()
// ... set relevant fields of the event ...
eventBus.publish(myEvent)
```
Threading: the event bus creates a dedicated thread and all events are dispatched on that thread, regardless which thread publishes them.
### Sharing files
The UI publishes an event of type `com.muwire.core.files.FileSharedEvent` which contains a `java.io.File` reference to the file the user has chosen to share. A component in the core called `HasherService` listens for these events, and when it receives notification that a FileSharedEvent has been posted it pereforms some sanity checks, then offloads the actual hashing to a dedicated thread.
Before the hashing begins, another event of type `com.muwire.core.files.FileHashingEvent` is published that contains the name of the file. At the moment that event serves only to update the UI with the current file being hashed.
When the hashing completes, a `com.muwire.core.files.FileHashedEvent` is published by the HasherService. The UI listens to this event and updates its list of shared files. Another core component called `FileManager` also listens for such events and updates the interenal search index from the file name.

98
doc/chat.md Normal file
View File

@@ -0,0 +1,98 @@
# MuWire Chat System
Since version 0.6.3 MuWire comes with a built in chat system. It is very similar to the way IRC operates, and the user experience mimics that of IRC as well.
### Design
The chat system uses a client-server model. Each MuWire node can run a chat server which accepts incoming connections; clients wishing to connect to a chat server establish an outgoing streaming connection to the destination where the chat server is running. The local client also connects to server through a special "loopback" connection.
Once connected, the client automatically joins a special room called "__CONSOLE__" which is the server console. In that room users can issue certain commands, but cannot actually chat. In order to chat, the client needs to `/join` a chat room first. The chat room is kept as state in the server and any messages sent to that chat room are forwarded to all other users who have joined the same room. When the last member of a room leaves, the room state is destroyed server-side.
Private messages work by replacing the room of the message with the base64-encoded persona of the recipient of the message.
### Chat Commands
Clients issue commands to the chat server in order to perform operations. Some commands can only be issued in the __CONSOLE__ room, others only in a regular chat room. The server will ignore commands which are not issued in the appropriate place.
There are several chat commands that MuWire supports, more can be added later. Commands consist of a prefix and payload. The prefix always beings with forward slash `/`. Below is the list of commands a MuWire chat server supports as of version 0.6.6:
##### /HELP - this command can be issued only in the __CONSOLE__ room. It results in the server echoing back a help message of the commands it supports.
##### /SAY - this command can be issued only in a regular chat room or private chat. It's payload is the content of what the user wishes to say.
##### /INFO - this command can be issued only in the __CONSOLE__ room. It results in the server printing a status message. As of 0.6.6, this consists of the base64-encoded address of the server as well as a list of user who are currently connected.
##### /LIST - this command can be issued only in the __CONSOLE__ room. It results in the server echoing the list of rooms which currently have at least one member.
##### /JOIN - this command can be issued only in the __CONSOLE_ room. The payload of the command is the name of the room that the user wishes to join. This results in server-side state being updated to add the user to the membership list of the room.
##### /LEAVE - this command can be issued only in a regular room. It has no payload, and the result is that the server removes the user issuing the command from the room.
##### /TRUST - this command can be issued only in the __CONSOLE__ room and only over the loopback connection, i.e. it is reserved for the owner of the server. It's payload is the human-readable representation of a user the owner wishes to mark as trusted. It results in adding the specified user to the owner's trust list.
##### /DISTRUST - similar to /TRUST, this command results in the opposite; the user specified in the payload being added to the distrusted list. This also results in the user getting disconnected from the server, i.e. kick/ban-ned.
There is a command called "/JOINED" which is issued from the server to the client upon the client joining a room. The payload of the command is a comma-separated list of base64-encoded representations of the personas of the users already in that room.
### Protocol
The client wishing to connect to a server establishes an I2P connection and sends the letters "IRC\r\n" in ASCII encoding. These are followed by one more headers, each header consisting of a name, followed by colon, followed by value, terminated with "\r\n". After all headers have been sent, an additional "\r\n" is written to the socket.
As of version 0.6.6 the following headers are required:
* "Version" - this header indicates the version of the chat protocol that will be used over this connection. Currently fixed at 1.
* "Persona" - this header contains the base64-encoded representation of the persona of the client.
The server responds with a status code encoded as an aSCII string, terminated with "\r\n", which can be one of the following:
* 200 - connection accepted
* 400 - connection not allowed. This can be issued if the server is down for example.
* 429 - connection rejected. This can be issued when the server is overloaded or the client is already connected to the server. Clients are encouraged to not re-attempt connecting for a short period of time.
After the code, the server responds with a "Version" header followed by a "\r\n" on an empty line.
### Messages
After the headers have been exchanged, the connection starts transmitting messages back and forth. Messages are encoded in UTF-8 JSON format, and preceeded by two bytes which are the unsigned representation of the number of bytes of JSON.
As of protocol version 1, the following messages are supported:
##### "Keepalive Ping".
This message serves only to prevent the blocking read from I2P sockets from timing out and is sent on regular intervals by both the server and the client. Example payload of such message is:
```
{
"type" : "Ping",
"version" : 1
}
```
##### "Chat Command"
This message is sent by both server and client whenever an event occurs, such as user issuing a command, or another user in a room the user has joined issues a command. The payload is the following:
```
{
"type" : "Chat",
"uuid" : "1234-asdf-...", // unique random UUID of this message
"host" : "asdf123..", // base64-encoded persona of the server owner, i.e. the server this message is destined to
"sender" : "asdf123...", // base64-encoded persona of the sender of the message. The server verifies it matches the destination of the I2P socket it was received from.
"chatTime" : 1235..., // time since epoch in milliseconds when the message was sent.
"room" : "asdf..." // UTF-8 string indicating the room this message is destined to
"payload" : "/SAY asdf..." // UTF-8 string of the chat command being issued by the user.
"sig" : "asdf1234..." // base64-encoded signature.
}
```
In order to prevent spoofing and replay attacks, each Chat Command message contains a signature. The signature covers the following fields in this order:
1. uuid - toString() representation of the UUID
2. host - binary representation of the persona in the host field
3. sender - binary representation of the persona in the sender field
4. chatTime - big endian representation of the timestamp of the message (8 bytes)
5. room - UTF-8 representation of the room field
6. payload - UTF-8 representation of the payload field.
The signature is created with the signing private key (SPK) of the sender.
##### "Leave"
This message is only sent from a server to a client, whenever another client disconnects from the server. It's format is the following:
```
{
"type" : "Leave,
"persona" : "asdf1234..." // base64-encoded persona of the user being disconnected from the server.
}
```
### Future Work
It is possible to extend this protocol to support inter-server relaying of messages. Because every Chat Command message is signed, it will not be possible for malicious server operators to spoof its contents.

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.5.8
version = 0.6.7
i2pVersion = 0.9.43
groovyVersion = 2.4.15
slf4jVersion = 1.7.25

View File

@@ -9,7 +9,7 @@ buildscript {
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'
classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.11.0'
classpath 'org.gradle.api.plugins:gradle-izpack-plugin:0.2.3'
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
classpath 'com.github.cr0:gradle-macappbundle-plugin:3.1.0'
classpath 'org.kordamp.gradle:stats-gradle-plugin:0.2.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'

View File

@@ -101,4 +101,29 @@ mvcGroups {
view = 'com.muwire.gui.CertificateControlView'
controller = 'com.muwire.gui.CertificateControlController'
}
'shared-file' {
model = 'com.muwire.gui.SharedFileModel'
view = 'com.muwire.gui.SharedFileView'
controller = 'com.muwire.gui.SharedFileController'
}
'download-preview' {
model = "com.muwire.gui.DownloadPreviewModel"
view = "com.muwire.gui.DownloadPreviewView"
controller = "com.muwire.gui.DownloadPreviewController"
}
'chat-server' {
model = 'com.muwire.gui.ChatServerModel'
view = 'com.muwire.gui.ChatServerView'
controller = 'com.muwire.gui.ChatServerController'
}
'chat-room' {
model = 'com.muwire.gui.ChatRoomModel'
view = 'com.muwire.gui.ChatRoomView'
controller = 'com.muwire.gui.ChatRoomController'
}
'chat-monitor' {
model = 'com.muwire.gui.ChatMonitorModel'
view = 'com.muwire.gui.ChatMonitorView'
controller = 'com.muwire.gui.ChatMonitorController'
}
}

View File

@@ -8,6 +8,7 @@ import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.EventBus
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.BrowseStatus
@@ -22,18 +23,18 @@ class BrowseController {
@MVCMember @Nonnull
BrowseView view
EventBus eventBus
Core core
void register() {
eventBus.register(BrowseStatusEvent.class, this)
eventBus.register(UIResultEvent.class, this)
eventBus.publish(new UIBrowseEvent(host : model.host))
core.eventBus.register(BrowseStatusEvent.class, this)
core.eventBus.register(UIResultEvent.class, this)
core.eventBus.publish(new UIBrowseEvent(host : model.host))
}
void mvcGroupDestroy() {
eventBus.unregister(BrowseStatusEvent.class, this)
eventBus.unregister(UIResultEvent.class, this)
core.eventBus.unregister(BrowseStatusEvent.class, this)
core.eventBus.unregister(UIResultEvent.class, this)
}
void onBrowseStatusEvent(BrowseStatusEvent e) {
@@ -63,21 +64,24 @@ class BrowseController {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty())
return
def group = application.mvcGroupManager.getGroups()['MainFrame']
selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
!group.model.canDownload(it.infohash)
}
selectedResults.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
eventBus.publish(new UIDownloadEvent(
core.eventBus.publish(new UIDownloadEvent(
result : [result],
sources : [model.host.destination],
target : file,
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
sequential : view.sequentialDownloadCheckbox.model.isSelected()
))
}
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
group.view.showDownloadsWindow.call()
dismiss()
}
@@ -92,7 +96,8 @@ class BrowseController {
String groupId = Base64.encode(result.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = result
params['text'] = result.comment
params['name'] = result.name
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
@@ -108,7 +113,7 @@ class BrowseController {
def params = [:]
params['result'] = result
params['eventBus'] = eventBus
params['core'] = core
mvcGroup.createMVCGroup("fetch-certificates", params)
}
}

View File

@@ -6,6 +6,23 @@ import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.filecert.Certificate
@ArtifactProviderFor(GriffonController)
class CertificateControlController {
@MVCMember @Nonnull
CertificateControlModel model
@MVCMember @Nonnull
CertificateControlView view
@ControllerAction
void showComment() {
Certificate cert = view.getSelectedSertificate()
if (cert == null || cert.comment == null)
return
def params = [:]
params['text'] = cert.comment.name
mvcGroup.createMVCGroup("show-comment", params)
}
}

View File

@@ -0,0 +1,13 @@
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
@ArtifactProviderFor(GriffonController)
class ChatMonitorController {
@MVCMember @Nonnull
ChatMonitorModel model
}

View File

@@ -0,0 +1,282 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import net.i2p.data.Signature
import java.nio.charset.StandardCharsets
import java.util.logging.Level
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Persona
import com.muwire.core.chat.ChatCommand
import com.muwire.core.chat.ChatAction
import com.muwire.core.chat.ChatConnection
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.ChatServer
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
@Log
@ArtifactProviderFor(GriffonController)
class ChatRoomController {
@MVCMember @Nonnull
ChatRoomModel model
@MVCMember @Nonnull
ChatRoomView view
boolean leftRoom
@ControllerAction
void say() {
String words = view.sayField.text
view.sayField.setText(null)
ChatCommand command
try {
command = new ChatCommand(words)
} catch (Exception nope) {
command = new ChatCommand("/SAY $words")
}
if (!command.action.user) {
JOptionPane.showMessageDialog(null, "$words is not a user command","Invalid Command", JOptionPane.ERROR_MESSAGE)
return
}
long now = System.currentTimeMillis()
if (command.action == ChatAction.SAY && command.payload.length() > 0) {
String toShow = DataHelper.formatTime(now) + " <" + model.core.me.getHumanReadableName() + "> "+command.payload
view.roomTextArea.append(toShow)
view.roomTextArea.append('\n')
trimLines()
}
if (command.action == ChatAction.JOIN) {
String newRoom = command.payload
String groupId = model.host.getHumanReadableName()+"-"+newRoom
if (!mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
def params = [:]
params['core'] = model.core
params['tabName'] = model.host.getHumanReadableName() + "-chat-rooms"
params['room'] = newRoom
params['console'] = false
params['host'] = model.host
params['roomTabName'] = newRoom
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
}
}
if (command.action == ChatAction.LEAVE && !model.console) {
leftRoom = true
view.closeTab.call()
}
String room = model.console ? ChatServer.CONSOLE : model.room
UUID uuid = UUID.randomUUID()
byte [] sig = ChatConnection.sign(uuid, now, room, command.source, model.core.me, model.host, model.core.spk)
def event = new ChatMessageEvent(uuid : uuid,
payload : command.source,
sender : model.core.me,
host : model.host,
room : room,
chatTime : now,
sig : sig)
model.core.eventBus.publish(event)
}
@ControllerAction
void privateMessage() {
Persona p = view.getSelectedPersona()
if (p == null)
return
String groupId = model.host.getHumanReadableName() + "-" + p.getHumanReadableName() +"-private-chat"
if (p != model.core.me && !mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
def params = [:]
params['core'] = model.core
params['tabName'] = model.tabName
params['room'] = p.toBase64()
params['privateChat'] = true
params['host'] = model.host
params['roomTabName'] = p.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
}
}
void markTrusted() {
Persona p = view.getSelectedPersona()
if (p == null)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
model.core.eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
view.refreshMembersTable()
}
void markDistrusted() {
Persona p = view.getSelectedPersona()
if (p == null)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
model.core.eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
view.refreshMembersTable()
}
void markNeutral() {
Persona p = view.getSelectedPersona()
if (p == null)
return
model.core.eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.NEUTRAL))
view.refreshMembersTable()
}
void browse() {
Persona p = view.getSelectedPersona()
if (p == null)
return
String groupId = p.getHumanReadableName() + "-browse"
def params = [:]
params['host'] = p
params['core'] = model.core
mvcGroup.createMVCGroup("browse",groupId,params)
}
void leaveRoom() {
if (leftRoom || model.privateChat)
return
leftRoom = true
long now = System.currentTimeMillis()
UUID uuid = UUID.randomUUID()
byte [] sig = ChatConnection.sign(uuid, now, model.room, "/LEAVE", model.core.me, model.host, model.core.spk)
def event = new ChatMessageEvent(uuid : uuid,
payload : "/LEAVE",
sender : model.core.me,
host : model.host,
room : model.room,
chatTime : now,
sig : sig)
model.core.eventBus.publish(event)
}
void handleChatMessage(ChatMessageEvent e) {
ChatCommand command
try {
command = new ChatCommand(e.payload)
} catch (Exception bad) {
log.log(Level.WARNING,"bad chat command",bad)
return
}
log.info("$model.room processing $command.action")
switch(command.action) {
case ChatAction.SAY : processSay(e, command.payload);break
case ChatAction.JOIN : processJoin(e.timestamp, e.sender); break
case ChatAction.JOINED : processJoined(command.payload); break
case ChatAction.LEAVE : processLeave(e.timestamp, e.sender); break
}
}
private void processSay(ChatMessageEvent e, String text) {
String toDisplay = DataHelper.formatTime(e.timestamp) + " <"+e.sender.getHumanReadableName()+"> " + text + "\n"
runInsideUIAsync {
view.roomTextArea.append(toDisplay)
trimLines()
if (!model.console)
view.chatNotificator.onMessage(mvcGroup.mvcId)
}
}
private void processJoin(long timestamp, Persona p) {
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " joined the room\n"
runInsideUIAsync {
model.members.add(p)
view.roomTextArea.append(toDisplay)
trimLines()
view.membersTable?.model?.fireTableDataChanged()
}
}
private void processJoined(String list) {
runInsideUIAsync {
list.split(",").each {
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(it)))
model.members.add(p)
}
view.membersTable?.model?.fireTableDataChanged()
}
}
private void processLeave(long timestamp, Persona p) {
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " left the room\n"
runInsideUIAsync {
model.members.remove(p)
view.roomTextArea.append(toDisplay)
trimLines()
view.membersTable?.model?.fireTableDataChanged()
}
}
void handleLeave(Persona p) {
String toDisplay = DataHelper.formatTime(System.currentTimeMillis()) + " " + p.getHumanReadableName() + " disconnected\n"
runInsideUIAsync {
if (model.members.remove(p)) {
view.roomTextArea.append(toDisplay)
trimLines()
view.membersTable?.model?.fireTableDataChanged()
}
}
}
private void trimLines() {
if (model.settings.maxChatLines < 0)
return
while(view.roomTextArea.getLineCount() > model.settings.maxChatLines) {
int line0Start = view.roomTextArea.getLineStartOffset(0)
int line0End = view.roomTextArea.getLineEndOffset(0)
view.roomTextArea.replaceRange(null, line0Start, line0End)
}
}
void rejoinRoom() {
if (model.console || model.privateChat)
return
model.members.clear()
model.members.add(model.core.me)
UUID uuid = UUID.randomUUID()
long now = System.currentTimeMillis()
String join = "/JOIN $model.room"
byte [] sig = ChatConnection.sign(uuid, now, ChatServer.CONSOLE, join, model.core.me, model.host, model.core.spk)
def event = new ChatMessageEvent(
uuid : uuid,
payload : join,
sender : model.core.me,
host : model.host,
room : ChatServer.CONSOLE,
chatTime : now,
sig : sig
)
model.core.eventBus.publish(event)
}
void serverDisconnected() {
runInsideUIAsync {
model.members.clear()
view.membersTable?.model?.fireTableDataChanged()
}
}
}

View File

@@ -0,0 +1,31 @@
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.chat.UIDisconnectChatEvent
@ArtifactProviderFor(GriffonController)
class ChatServerController {
@MVCMember @Nonnull
ChatServerModel model
@ControllerAction
void disconnect() {
switch(model.buttonText) {
case "Disconnect" :
model.buttonText = "Connect"
mvcGroup.getChildrenGroups().each { k,v ->
v.controller.serverDisconnected()
}
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
break
case "Connect" :
model.connect()
break
}
}
}

View File

@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.EventBus
@@ -83,8 +84,9 @@ class ContentPanelController {
int selectedHit = view.getSelectedHit()
if (selectedHit < 0)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Match m = model.hits[selectedHit]
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED, reason : reason))
}
@ControllerAction
@@ -92,8 +94,9 @@ class ContentPanelController {
int selectedHit = view.getSelectedHit()
if (selectedHit < 0)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Match m = model.hits[selectedHit]
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED, reason : reason))
}
void saveMuWireSettings() {

View File

@@ -0,0 +1,13 @@
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
@ArtifactProviderFor(GriffonController)
class DownloadPreviewController {
@MVCMember @Nonnull
DownloadPreviewModel model
}

View File

@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.EventBus
import com.muwire.core.filecert.CertificateFetchEvent
import com.muwire.core.filecert.CertificateFetchStatus
@@ -21,10 +22,10 @@ class FetchCertificatesController {
@MVCMember @Nonnull
FetchCertificatesView view
EventBus eventBus
Core core
void register() {
eventBus.with {
core.eventBus.with {
register(CertificateFetchEvent.class, this)
register(CertificateFetchedEvent.class, this)
publish(new UIFetchCertificatesEvent(host : model.result.sender, infoHash : model.result.infohash))
@@ -32,8 +33,8 @@ class FetchCertificatesController {
}
void mvcGroupDestroy() {
eventBus.unregister(CertificateFetchEvent.class, this)
eventBus.unregister(CertificateFetchedEvent.class, this)
core.eventBus.unregister(CertificateFetchEvent.class, this)
core.eventBus.unregister(CertificateFetchedEvent.class, this)
}
void onCertificateFetchEvent(CertificateFetchEvent e) {
@@ -58,11 +59,24 @@ class FetchCertificatesController {
if (selectedCerts == null)
return
selectedCerts.each {
eventBus.publish(new UIImportCertificateEvent(certificate : it))
core.eventBus.publish(new UIImportCertificateEvent(certificate : it))
}
JOptionPane.showMessageDialog(null, "Certificates imported.")
}
@ControllerAction
void showComment() {
def selectedCerts = view.selectedCertificates()
if (selectedCerts == null || selectedCerts.size() != 1)
return
String comment = selectedCerts[0].comment.name
def params = [:]
params['text'] = comment
params['name'] = "Certificate Comment"
mvcGroup.createMVCGroup("show-comment", params)
}
@ControllerAction
void dismiss() {
view.dialog.setVisible(false)

View File

@@ -7,10 +7,13 @@ import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import groovy.json.StringEscapeUtils
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import java.awt.Desktop
import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets
@@ -20,6 +23,7 @@ import javax.swing.JOptionPane
import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
@@ -39,6 +43,9 @@ import com.muwire.core.trust.RemoteTrustList
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustSubscriptionEvent
import com.muwire.core.upload.HashListUploader
import com.muwire.core.upload.Uploader
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
class MainFrameController {
@@ -87,6 +94,7 @@ class MainFrameController {
params["search-terms"] = search
params["uuid"] = uuid.toString()
params["core"] = core
params["settings"] = view.settings
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
@@ -104,21 +112,22 @@ class MainFrameController {
def searchEvent
byte [] payload
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true)
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true, persona : core.me)
payload = root
} else {
def nonEmpty = SplitPattern.termify(search)
payload = String.join(" ",nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
searchComments : core.muOptions.searchComments, compressedResults : true, persona : core.me)
}
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))
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
}
@@ -135,14 +144,16 @@ class MainFrameController {
byte [] infoHashBytes = Base64.decode(infoHash)
Signature sig = DSAEngine.getInstance().sign(infoHashBytes, core.spk)
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
oobInfohash: true)
oobInfohash: true, persona : core.me)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig : sig.data))
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2))
}
private int selectedDownload() {
def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow()
@@ -157,8 +168,9 @@ class MainFrameController {
int selected = builder.getVariable("searches-table").getSelectedRow()
if (selected < 0)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason) )
}
@ControllerAction
@@ -166,8 +178,9 @@ class MainFrameController {
int selected = builder.getVariable("searches-table").getSelectedRow()
if (selected < 0)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.searches[selected].originator
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason) )
}
@ControllerAction
@@ -191,6 +204,14 @@ class MainFrameController {
downloader.pause()
core.eventBus.publish(new UIDownloadPausedEvent())
}
@ControllerAction
void preview() {
def downloader = model.downloads[selectedDownload()].downloader
def params = [:]
params['downloader'] = downloader
mvcGroup.createMVCGroup("download-preview", params)
}
@ControllerAction
void clear() {
@@ -213,8 +234,11 @@ class MainFrameController {
int row = view.getSelectedTrustTablesRow(tableName)
if (row < 0)
return
String reason = null
if (level != TrustLevel.NEUTRAL)
reason = JOptionPane.showInputDialog("Enter reason (optional)")
builder.getVariable(tableName).model.fireTableDataChanged()
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
core.eventBus.publish(new TrustEvent(persona : list[row].persona, level : level, reason : reason))
}
@ControllerAction
@@ -252,7 +276,7 @@ class MainFrameController {
int row = view.getSelectedTrustTablesRow("trusted-table")
if (row < 0)
return
Persona p = model.trusted[row]
Persona p = model.trusted[row].persona
core.muOptions.trustSubscriptions.add(p)
saveMuWireSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
@@ -301,6 +325,31 @@ class MainFrameController {
return null
model.subscriptions[row]
}
@ControllerAction
void browseFromTrusted() {
int row = view.getSelectedTrustTablesRow("trusted-table")
if (row < 0)
return
Persona p = model.trusted[row].persona
String groupId = p.getHumanReadableName() + "-browse"
def params = [:]
params['host'] = p
params['core'] = model.core
mvcGroup.createMVCGroup("browse",groupId,params)
}
@ControllerAction
void chatFromTrusted() {
int row = view.getSelectedTrustTablesRow("trusted-table")
if (row < 0)
return
Persona p = model.trusted[row].persona
startChat(p)
view.showChatWindow.call()
}
void unshareSelectedFile() {
def sf = view.selectedSharedFiles()
@@ -329,6 +378,28 @@ class MainFrameController {
model.uploads.removeAll { it.finished }
}
@ControllerAction
void showInLibrary() {
Uploader uploader = view.selectedUploader()
if (uploader == null)
return
SharedFile sf = null
if (uploader instanceof HashListUploader) {
InfoHash infoHash = uploader.infoHash
Set<SharedFile> sfs = core.fileManager.rootToFiles.get(infoHash)
if (sfs != null && !sfs.isEmpty())
sf = sfs.first()
} else {
File f = uploader.file
sf = core.fileManager.fileToSharedFile.get(f)
}
if (sf == null)
return // can happen if user un-shared
view.focusOnSharedFile(sf)
}
@ControllerAction
void restoreSession() {
model.sessionRestored = true
@@ -351,6 +422,79 @@ class MainFrameController {
JOptionPane.showMessageDialog(null, "Certificate(s) have been issued")
}
}
@ControllerAction
void showFileDetails() {
def selected = view.selectedSharedFiles()
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to view it's details")
return
}
def params = [:]
params['sf'] = selected[0]
params['core'] = core
mvcGroup.createMVCGroup("shared-file", params)
}
@ControllerAction
void openContainingFolder() {
def selected = view.selectedSharedFiles()
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to open it's containing folder")
return
}
try {
Desktop.getDesktop().open(selected[0].file.getParentFile())
} catch (Exception ignored) {}
}
@ControllerAction
void startChatServer() {
model.core.chatServer.start()
model.chatServerRunning = true
if (!mvcGroup.getChildrenGroups().containsKey("local-chat-server")) {
def params = [:]
params['core'] = model.core
params['host'] = model.core.me
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
}
}
@ControllerAction
void stopChatServer() {
model.core.chatServer.stop()
model.chatServerRunning = false
}
@ControllerAction
void connectChatServer() {
String address = JOptionPane.showInputDialog("Copy/paste the address of the server here")
if (address == null)
return
Persona p
try {
p = new Persona(new ByteArrayInputStream(Base64.decode(address)))
} catch (Exception bad) {
JOptionPane.showMessageDialog(null, "Invalid server address", "Invalid server address", JOptionPane.ERROR_MESSAGE)
return
}
startChat(p)
}
void startChat(Persona p) {
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
def params = [:]
params['core'] = model.core
params['host'] = p
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
} else
mvcGroup.getChildrenGroups().get(p.getHumanReadableName()).model.connect()
}
void saveMuWireSettings() {
core.saveMuSettings()

View File

@@ -139,6 +139,25 @@ class OptionsController {
String trustListInterval = view.trustListIntervalField.text
model.trustListInterval = trustListInterval
settings.trustListInterval = Integer.parseInt(trustListInterval)
boolean startChatServer = view.startChatServerCheckbox.model.isSelected()
model.startChatServer = startChatServer
settings.startChatServer = startChatServer
String maxChatConnections = view.maxChatConnectionsField.text
model.maxChatConnections = Integer.parseInt(maxChatConnections)
settings.maxChatConnections = Integer.parseInt(maxChatConnections)
boolean advertiseChat = view.advertiseChatCheckbox.model.isSelected()
model.advertiseChat = advertiseChat
settings.advertiseChat = advertiseChat
int maxChatLines = Integer.parseInt(view.maxChatLinesField.text)
model.maxChatLines = maxChatLines
uiSettings.maxChatLines = maxChatLines
if (model.chatWelcomeFile != null)
settings.chatWelcomeFile = new File(model.chatWelcomeFile)
core.saveMuSettings()
@@ -155,6 +174,8 @@ class OptionsController {
uiSettings.autoFontSize = model.automaticFontSize
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
uiSettings.groupByFile = model.groupByFile
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads
uiSettings.clearCancelledDownloads = clearCancelledDownloads
@@ -219,6 +240,19 @@ class OptionsController {
model.incompleteLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void chooseChatFile() {
def chooser = new JFileChooser()
chooser.with {
setFileHidingEnabled(false)
setDialogTitle("Select location of chat server welcome file")
setFileSelectionMode(JFileChooser.FILES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.chatWelcomeFile = getSelectedFile().getAbsolutePath()
}
}
@ControllerAction
void automaticFont() {
model.automaticFontSize = true
@@ -242,6 +276,16 @@ class OptionsController {
model.closeDecisionMade = true
}
@ControllerAction
void groupByFile() {
model.groupByFile = true
}
@ControllerAction
void groupBySender() {
model.groupByFile = false
}
@ControllerAction
void clearHistory() {
uiSettings.searchHistory.clear()

View File

@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.Persona
@@ -26,116 +27,120 @@ class SearchTabController {
Core core
private def selectedResults() {
int[] rows = view.resultsTable.getSelectedRows()
if (rows.length == 0)
return null
def sortEvt = view.lastSortEvent
if (sortEvt != null) {
for (int i = 0; i < rows.length; i++) {
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
if (model.groupedByFile) {
return [view.getSelectedResult()]
} else {
int[] rows = view.resultsTable.getSelectedRows()
if (rows.length == 0)
return null
def sortEvt = view.lastSortEvent
if (sortEvt != null) {
for (int i = 0; i < rows.length; i++) {
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<UIResultEvent> results = new ArrayList<>()
rows.each { results.add(model.results[it]) }
return results
}
List<UIResultEvent> results = new ArrayList<>()
rows.each { results.add(model.results[it]) }
results
}
@ControllerAction
void download() {
def results = selectedResults()
if (results == null)
return
results.removeAll {
!mvcGroup.parentGroup.model.canDownload(it.infohash)
}
@ControllerAction
void download() {
def results = selectedResults()
if (results == null)
return
results.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
results.removeAll {
!mvcGroup.parentGroup.model.canDownload(it.infohash)
}
def resultsBucket = model.hashBucket[result.infohash]
def sources = model.sourcesBucket[result.infohash]
results.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
def resultsBucket = model.hashBucket[result.infohash]
def sources = model.sourcesBucket[result.infohash]
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
}
mvcGroup.parentGroup.view.showDownloadsWindow.call()
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
target : file, sequential : view.sequentialDownload()))
}
mvcGroup.parentGroup.view.showDownloadsWindow.call()
}
@ControllerAction
void trust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
}
@ControllerAction
void trust() {
def sender = view.selectedSender()
if (sender == null)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED, reason : reason))
}
@ControllerAction
void distrust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void distrust() {
def sender = view.selectedSender()
if (sender == null)
return
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED, reason : reason))
}
@ControllerAction
void neutral() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
}
@ControllerAction
void neutral() {
def sender = view.selectedSender()
if (sender == null)
return
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
}
@ControllerAction
void browse() {
def sender = view.selectedSender()
if (sender == null)
return
String groupId = sender.getHumanReadableName() + "-browse"
Map<String,Object> params = new HashMap<>()
params['host'] = sender
params['core'] = core
mvcGroup.createMVCGroup("browse", groupId, params)
}
@ControllerAction
void chat() {
def sender = view.selectedSender()
if (sender == null)
return
@ControllerAction
void browse() {
int selectedSender = view.selectedSenderRow()
if (selectedSender < 0)
return
Persona sender = model.senders[selectedSender]
String groupId = sender.getHumanReadableName()
Map<String,Object> params = new HashMap<>()
params['host'] = sender
params['eventBus'] = core.eventBus
mvcGroup.createMVCGroup("browse", groupId, params)
}
@ControllerAction
void showComment() {
int[] selectedRows = view.resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return
if (view.lastSortEvent != null)
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
UIResultEvent event = model.results[selectedRows[0]]
if (event.comment == null)
return
String groupId = Base64.encode(event.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = event
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
@ControllerAction
void viewCertificates() {
int[] selectedRows = view.resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return
if (view.lastSortEvent != null)
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
UIResultEvent event = model.results[selectedRows[0]]
if (event.certificates <= 0)
return
def params = [:]
params['result'] = event
params['eventBus'] = core.eventBus
mvcGroup.createMVCGroup("fetch-certificates", params)
}
}
def parent = mvcGroup.parentGroup
parent.controller.startChat(sender)
parent.view.showChatWindow.call()
}
@ControllerAction
void showComment() {
UIResultEvent event = view.getSelectedResult()
if (event == null || event.comment == null)
return
String groupId = Base64.encode(event.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['text'] = event.comment
params['name'] = event.name
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
@ControllerAction
void viewCertificates() {
UIResultEvent event = view.getSelectedResult()
if (event == null || event.certificates <= 0)
return
def params = [:]
params['result'] = event
params['core'] = core
mvcGroup.createMVCGroup("fetch-certificates", params)
}
}

View File

@@ -0,0 +1,28 @@
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.filecert.Certificate
@ArtifactProviderFor(GriffonController)
class SharedFileController {
@MVCMember @Nonnull
SharedFileView view
@MVCMember @Nonnull
SharedFileModel model
@ControllerAction
void showComment() {
Certificate cert = view.getSelectedCertificate()
if (cert == null || cert.comment == null)
return
def params = [:]
params['text'] = cert.comment.name
mvcGroup.createMVCGroup('show-comment',params)
}
}

View File

@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.EventBus
import com.muwire.core.Persona
@@ -25,8 +26,9 @@ class TrustListController {
int selectedRow = view.getSelectedRow("trusted-table")
if (selectedRow < 0)
return
Persona p = model.trusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.trusted[selectedRow].persona
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
view.fireUpdate("trusted-table")
}
@@ -35,8 +37,9 @@ class TrustListController {
int selectedRow = view.getSelectedRow("distrusted-table")
if (selectedRow < 0)
return
Persona p = model.distrusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.distrusted[selectedRow].persona
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
view.fireUpdate("distrusted-table")
}
@@ -45,8 +48,9 @@ class TrustListController {
int selectedRow = view.getSelectedRow("trusted-table")
if (selectedRow < 0)
return
Persona p = model.trusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.trusted[selectedRow].persona
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
view.fireUpdate("trusted-table")
}
@@ -55,8 +59,9 @@ class TrustListController {
int selectedRow = view.getSelectedRow("distrusted-table")
if (selectedRow < 0)
return
Persona p = model.distrusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
Persona p = model.distrusted[selectedRow].persona
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
view.fireUpdate("distrusted-table")
}
}

View File

@@ -13,6 +13,8 @@ class CertificateControlModel {
Core core
@Observable boolean showCommentActionEnabled
void mvcGroupInit(Map<String,String> args) {
users.addAll(core.certificateManager.byIssuer.keySet())
}

View File

@@ -0,0 +1,46 @@
package com.muwire.gui
import javax.annotation.Nonnull
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class ChatMonitorModel implements ChatNotificator.Listener {
@MVCMember @Nonnull
ChatMonitorView view
ChatNotificator chatNotificator
def rooms = []
void mvcGroupInit(Map<String,String> args) {
chatNotificator.listener = this
}
void mvcGroupDestroy() {
chatNotificator.listener = null
}
public void update() {
rooms.clear()
chatNotificator.roomsWithMessages.each { room, count ->
int dash = room.indexOf('-')
String server = room.substring(0, dash)
String roomName = room.substring(dash + 1)
rooms.add(new ChatRoomEntry(server, roomName, count))
}
view.updateView()
}
private static class ChatRoomEntry {
private final String server, room
private final int count
ChatRoomEntry(String server, String room, int count) {
this.server = server
this.room = room
this.count = count
}
}
}

View File

@@ -0,0 +1,28 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.Persona
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class ChatRoomModel {
Core core
Persona host
String tabName
String room
boolean console
boolean privateChat
String roomTabName
def members = []
UISettings settings
void mvcGroupInit(Map<String,String> args) {
members.add(core.me)
settings = application.context.get("ui-settings")
}
}

View File

@@ -0,0 +1,157 @@
package com.muwire.gui
import java.util.logging.Level
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.chat.ChatCommand
import com.muwire.core.chat.ChatAction
import com.muwire.core.chat.ChatConnectionAttemptStatus
import com.muwire.core.chat.ChatConnectionEvent
import com.muwire.core.chat.ChatLink
import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent
import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable
import groovy.util.logging.Log
import griffon.metadata.ArtifactProviderFor
@Log
@ArtifactProviderFor(GriffonModel)
class ChatServerModel {
@MVCMember @Nonnull
ChatServerView view
Persona host
Core core
@Observable boolean disconnectActionEnabled
@Observable String buttonText = "Disconnect"
@Observable ChatConnectionAttemptStatus status
@Observable boolean sayActionEnabled
volatile ChatLink link
volatile Thread poller
volatile boolean running
void mvcGroupInit(Map<String, String> params) {
disconnectActionEnabled = host != core.me // can't disconnect from myself
core.eventBus.register(ChatConnectionEvent.class, this)
connect()
}
void connect() {
runInsideUIAsync {
buttonText = "Disconnect"
}
core.eventBus.publish(new UIConnectChatEvent(host : host))
}
void mvcGroupDestroy() {
stopPoller()
core.eventBus.unregister(ChatConnectionEvent.class, this)
}
private void startPoller() {
if (running)
return
running = true
poller = new Thread({eventLoop()} as Runnable)
poller.setDaemon(true)
poller.start()
}
private void stopPoller() {
running = false
poller?.interrupt()
link = null
}
void onChatConnectionEvent(ChatConnectionEvent e) {
if (e.persona != host)
return
runInsideUIAsync {
status = e.status
sayActionEnabled = status == ChatConnectionAttemptStatus.SUCCESSFUL
}
if (e.status == ChatConnectionAttemptStatus.SUCCESSFUL) {
ChatLink link = e.connection
if (link == null)
return
this.link = e.connection
startPoller()
mvcGroup.childrenGroups.each {k,v ->
v.controller.rejoinRoom()
}
} else {
stopPoller()
}
}
private void eventLoop() {
Thread.sleep(1000)
while(running) {
ChatLink link = this.link
if (link == null || !link.isUp()) {
Thread.sleep(100)
continue
}
Object event = link.nextEvent()
if (event instanceof ChatMessageEvent)
handleChatMessage(event)
else if (event instanceof Persona)
handleLeave(event)
else
throw new IllegalArgumentException("event type $event")
}
}
private void handleChatMessage(ChatMessageEvent e) {
ChatCommand chatCommand
try {
chatCommand = new ChatCommand(e.payload)
} catch (Exception badCommand) {
log.log(Level.WARNING,"bad chat command",badCommand)
return
}
String room = e.room
if (chatCommand.action == ChatAction.JOIN) {
room = chatCommand.payload
}
if (chatCommand.action == ChatAction.SAY &&
room == core.me.toBase64()) {
String groupId = host.getHumanReadableName()+"-"+e.sender.getHumanReadableName() + "-private-chat"
if (!mvcGroup.childrenGroups.containsKey(groupId)) {
def params = [:]
params['core'] = core
params['tabName'] = host.getHumanReadableName() + "-chat-rooms"
params['room'] = e.sender.toBase64()
params['privateChat'] = true
params['host'] = host
params['roomTabName'] = e.sender.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-room",groupId, params)
}
room = groupId
} else
room = host.getHumanReadableName()+"-"+room
mvcGroup.childrenGroups[room]?.controller?.handleChatMessage(e)
}
private void handleLeave(Persona p) {
mvcGroup.childrenGroups.each { k, v ->
v.controller.handleLeave(p)
}
}
}

View File

@@ -0,0 +1,12 @@
package com.muwire.gui
import com.muwire.core.download.Downloader
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class DownloadPreviewModel {
Downloader downloader
}

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