Compare commits

..

177 Commits

Author SHA1 Message Date
5fa2f2753c Release 0.6.4 2019-11-13 20:06:53 +00:00
cacdd2a7a9 add browse and chat buttons to trusted panel 2019-11-13 19:40:28 +00:00
d56f7c6184 add right-click menu to trusted table 2019-11-13 19:33:34 +00:00
f7f4513109 better help and welcome message 2019-11-13 17:50:50 +00:00
dd15d893ba Call for help for Web UI 2019-11-13 17:26:14 +00:00
bf5ab9c82e ) 2019-11-13 14:10:26 +00:00
edd5a29b10 make private chat room ids unique across servers 2019-11-13 14:09:09 +00:00
38eb89f2f7 prepend server name to room id in order to make ids unique across server connections 2019-11-13 13:44:22 +00:00
73f1d64428 indentation of text field 2019-11-13 12:24:21 +00:00
bc1cae2d75 enable sharing of directories from button 2019-11-13 12:03:23 +00:00
a0ab07a7c0 show browse status for local results correctly 2019-11-13 11:58:55 +00:00
f875c379ce Release 0.6.3 2019-11-12 17:22:38 +00:00
0ce9784ccf add right-click menu on the members table 2019-11-12 17:08:38 +00:00
be82136e32 limit scrollback 2019-11-12 16:30:55 +00:00
7d25bb9364 tidy up views 2019-11-12 16:06:31 +00:00
c6e98db9d4 initialize result sender properly 2019-11-12 15:50:58 +00:00
35a26e2a47 advertise chat ability in search results 2019-11-12 15:47:38 +00:00
beef4af329 ui for chat options 2019-11-12 15:31:20 +00:00
cec3c1bc0f disconnect on close tab 2019-11-12 14:21:47 +00:00
289b958784 disconnect functionality 2019-11-12 14:19:57 +00:00
e9c554d717 proper group name pt3 2019-11-12 13:53:33 +00:00
1875fcddb2 proper room name pt2 2019-11-12 13:33:53 +00:00
bee6154fa9 set more room tab names correctly 2019-11-12 13:26:07 +00:00
1f9b171021 wip on private messages 2019-11-12 13:16:36 +00:00
59c03be35e suffix for group ids 2019-11-12 12:33:18 +00:00
621af96bdf wip on private chat 2019-11-12 12:20:49 +00:00
bcb7016202 add myself to the room member list when joining, fix /SAY 2019-11-12 11:40:28 +00:00
b1b2bcaef8 show disconnects 2019-11-12 11:34:23 +00:00
eec007e83b update status only if it matches host 2019-11-12 11:11:42 +00:00
3d36351a6b fetch the list of current room members when joining 2019-11-12 10:55:21 +00:00
d57d2ccb71 print help message on joining 2019-11-12 04:18:35 +00:00
d91f15ee54 dispatch joins to the target room 2019-11-12 03:53:38 +00:00
6bc61c920d start outgoing connection 2019-11-12 00:11:26 +00:00
146ed53e12 connection code 2019-11-11 23:52:34 +00:00
8ebae1600b fix up chat room view 2019-11-11 23:46:43 +00:00
18d19ca75e wip on joining and leaving rooms 2019-11-11 23:32:23 +00:00
29e499fe9d hook up core and backend 2019-11-11 22:42:55 +00:00
3db167bade send periodic pings 2019-11-11 17:54:33 +00:00
bfe0ab7867 wip on hooking UI with core 2019-11-11 17:48:42 +00:00
1fbb1e7932 add chat pane and associated components 2019-11-11 16:35:15 +00:00
0632336cd1 add ability to start and stop chat server from UI 2019-11-11 15:16:23 +00:00
aa221cd6dc server-side handling of disconnects and trust events 2019-11-11 14:54:10 +00:00
29b5c55328 client-side disconnect handling 2019-11-11 13:31:00 +00:00
5e7f3587df shutdown chat components 2019-11-11 13:26:25 +00:00
8afd387ca6 hook up chat components with core 2019-11-11 13:21:16 +00:00
5d16963d1c process join/leave/say server-side 2019-11-11 12:19:32 +00:00
6080c8b308 chat client and server 2019-11-11 10:43:52 +00:00
915deb1dee update readme for new shadow jar name 2019-11-11 09:13:56 +00:00
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
f072d0343c Update plugin version to fix bug about shadow jar 2019-11-11 10:52:37 +02:00
a549ad3d8d wip on chat 2019-11-11 04:36:43 +00:00
b6f5ec7d22 wip on chat 2019-11-10 20:34:24 +00:00
761bf0a177 Release 0.6.2 2019-11-10 18:31:30 +00:00
bd873211c0 wip on file preview 2019-11-10 14:50:19 +00:00
036971cfe5 wip on file preview 2019-11-10 13:59:01 +00:00
a2637570b1 Release 0.6.1 2019-11-10 06:23:28 +00:00
6012adbeab fix unsharing of files with comments 2019-11-10 06:04:57 +00:00
8f6b6b0caa update test for new json format 2019-11-10 05:20:09 +00:00
8f3b5aea8d store lowercases in search index 2019-11-10 05:14:31 +00:00
ee098ace8e update readme 2019-11-09 20:11:03 +00:00
5d8401e4bf avoid NPE, pending further investigation 2019-11-09 20:10:21 +00:00
fbf9add82a Release 0.6.0 2019-11-09 19:27:36 +00:00
7379263fef extended signature in cli 2019-11-09 18:34:34 +00:00
7d50843754 make signed queries mandatory 2019-11-09 17:03:38 +00:00
f4a2864942 add extended signature in queries to prevent replay attacks 2019-11-09 16:39:16 +00:00
afaadf65a4 only set selected row if the table contains that many rows. That fixes an AIOOBE 2019-11-09 15:14:14 +00:00
7bd422d6b4 another instance of unexplained npe 2019-11-09 12:36:59 +00:00
3f47274f61 add option to open containing folder 2019-11-09 11:28:12 +00:00
419e9a0ce6 prevent npe when..? unclear when this happens 2019-11-09 11:01:55 +00:00
ac1068a681 fix show comment/certificate buttons in group-by-file mode 2019-11-09 10:53:38 +00:00
549457e36f close output stream silently 2019-11-08 21:46:44 +00:00
14d6d10546 Release 0.5.10 2019-11-08 21:11:20 +00:00
878e397aa0 preserve selections on update 2019-11-08 21:04:58 +00:00
27831b488b add getter and use it; account for the case where a file has no certificates 2019-11-08 19:20:06 +00:00
449f46c62b take list updating out of loop 2019-11-08 18:40:59 +00:00
5703b85386 workaround? 2019-11-08 18:36:23 +00:00
76d8d847bd wip on grouping by file 2019-11-08 18:15:54 +00:00
db84d8e5bf wip on grouping by file 2019-11-08 17:33:41 +00:00
cc9b384907 wip on grouping by file 2019-11-08 16:09:05 +00:00
72960c24a8 implement trust reason in cli 2019-11-08 14:41:10 +00:00
71298e5e73 proper rendering of date on subscriptions table 2019-11-08 08:31:00 +00:00
11bc672544 say never if timestamp is 0 2019-11-08 08:30:44 +00:00
2f6cd311a0 say never if timestamp is 0 2019-11-08 08:30:29 +00:00
0448750491 lowercase for consistency 2019-11-08 08:18:33 +00:00
800dd1cbba proper date sorting 2019-11-08 08:17:34 +00:00
f95e9450f3 OutputStream.write 2019-11-08 07:47:11 +00:00
d842e3f2f2 update for new object 2019-11-08 07:42:33 +00:00
2017b53a43 pass comments on trust list subscriptions 2019-11-08 07:37:51 +00:00
6e2b3f4f33 prompt for reason from review trust list view 2019-11-08 07:12:17 +00:00
dbb305139b update for new type 2019-11-08 06:53:22 +00:00
0801bfec08 add optinal reason for trusting/distrusting 2019-11-08 06:46:03 +00:00
00a8d100fe show certificate comment form file details view 2019-11-08 04:51:37 +00:00
e94b7cb0d4 prevent NPE when browsed from an older host 2019-11-08 04:02:11 +00:00
b0357f2ecd update readme 2019-11-08 02:50:42 +00:00
62e72a7ce0 Release 0.5.9 2019-11-07 20:01:15 +00:00
26fa757b13 shared file details panel 2019-11-07 19:15:35 +00:00
3b2e1cf98c make sure the persona reported by the browser matches 2019-11-07 18:35:34 +00:00
5de8a51e47 account for unknown searchers 2019-11-07 18:34:11 +00:00
f5c07f13c0 core side of searchers tracking 2019-11-07 18:31:20 +00:00
c7b0ae34af associate persona with a search event, add skeleton for shared file panel 2019-11-07 17:43:37 +00:00
cad5301827 rewrite Persona and Name in java 2019-11-07 17:41:32 +00:00
c998011873 add right-click and show-in-library option for uploads 2019-11-07 05:02:53 +00:00
5802ba7734 show trust status of certificate issuers in cli as well 2019-11-06 18:19:45 +00:00
b3f775f59a show trust status in certificates view 2019-11-06 18:13:07 +00:00
739dbc7a24 fix serialization of older certificates 2019-11-06 18:09:50 +00:00
af99dee4a3 wip on view certificate comments in cli 2019-11-06 17:08:48 +00:00
07a6c63357 wip on view certificate comments in cli 2019-11-06 16:58:22 +00:00
c4096568f5 initialize group properly 2019-11-06 16:01:43 +00:00
30dda180eb Add support for comments in certificates, bump certificate version 2019-11-06 15:32:39 +00:00
83ea1bed3e add timestamp to the filename of the certificate 2019-11-06 14:05:17 +00:00
9181829e4a split by newlines 2019-11-06 13:59:14 +00:00
94678bad3c Release 0.5.8 2019-11-06 05:46:52 +00:00
e7072803e9 Merge branch 'master' of https://github.com/zlatinb/muwire 2019-11-06 05:42:14 +00:00
e9f7a51e16 Always share update files; disable forced update check on startup 2019-11-06 05:41:58 +00:00
916fad7d9b more fake padding 2019-11-05 15:54:16 +00:00
9feb891c51 support phrases in search 2019-11-05 15:52:23 +00:00
b865376d24 more tests 2019-11-05 14:41:27 +00:00
8dcba7535c modify indexing and search logic to account for phrases 2019-11-05 13:24:22 +00:00
7e881f1fe6 close() output streams on rejection, update test 2019-11-05 12:57:52 +00:00
a9aad7d9db test with deleted files 2019-11-05 12:57:16 +00:00
e736b42751 view certificates in cli 2019-11-05 05:51:43 +00:00
acda64aea7 Add certify button to cli. Make watched directory handling match that of gui 2019-11-05 04:41:25 +00:00
d82dc4ce90 Certificates viewer 2019-11-04 21:34:21 +00:00
f2ff90795d show a warning when user tries to certify 2019-11-04 20:49:46 +00:00
49f51a9f5f view certificates from browse host 2019-11-04 19:39:04 +00:00
6fbd1267fa make sure the View Certificates button appears at default size 2019-11-04 19:27:44 +00:00
149568520f register necessary event, initialize mvc group, correct name representation 2019-11-04 19:05:53 +00:00
c672880db0 statement was in wrong place 2019-11-04 18:45:57 +00:00
6cb1674d14 set row height for tables pt2 2019-11-04 18:36:18 +00:00
dba863a864 hook up CertClient, check that infohash in cert matches 2019-11-04 18:33:57 +00:00
642044b7e2 ui elements for certificate fetching 2019-11-04 18:33:25 +00:00
47c14f109a rename column, show certificate count in results 2019-11-04 17:21:37 +00:00
36c1a1a288 core side of certificate exchange 2019-11-04 17:17:57 +00:00
5d51b1c580 ability to certify shared files 2019-11-04 15:22:24 +00:00
bf3502220f sign update queries as well 2019-11-03 22:44:42 +00:00
ff1df88601 Release 0.5.7 2019-11-03 12:35:04 +00:00
4ed572ba51 clear search button 2019-11-03 12:03:12 +00:00
fd3f55ab4d implement restore session 2019-11-03 10:06:55 +00:00
1358e14467 add options for search history 2019-11-03 08:12:10 +00:00
e22d5fea11 better search box 2019-11-03 01:50:55 +00:00
7ade4aa10d set row height to trees 2019-11-02 19:06:26 +00:00
a9f623a91a correct method name 2019-11-02 18:51:02 +00:00
1ce410e943 wip on signing queries 2019-11-02 18:34:13 +00:00
27aad9d75d do not collapse tree on updates pt2 2019-11-02 17:41:04 +00:00
24591b10f2 change the griffon environment 2019-11-02 10:13:28 -07:00
e4f1ea5c10 make table rows a bit larger 2019-11-02 15:58:48 +00:00
c73c44c5f2 base table row height on the size of the font 2019-11-02 15:46:50 +00:00
309cbcc580 UTF-8 in props of cli 2019-11-02 15:23:15 +00:00
86894f242b support UTF-8 in persona names 2019-11-02 14:43:24 +00:00
568255140f visualize the negative tree as well 2019-11-02 12:54:43 +00:00
f6d2bac5bb show all watched directories 2019-11-02 12:26:19 +00:00
1c396711ed Fix sidecar files larger than the limit from being shared 2019-11-02 11:15:08 +00:00
c154d9538d only check negative tree for files, not directories 2019-11-02 10:28:04 +00:00
8043782446 logging config with all logs turned off 2019-11-02 08:52:29 +00:00
00c529cca1 toString() 2019-11-02 00:40:08 +00:00
094b9ac2b0 restore behavior where watched directories get scanned on startup 2019-11-02 00:27:12 +00:00
0dae0a561b more accurate speed measurement. Makes a difference if MW is minimized for a long time 2019-11-01 18:39:41 +00:00
82eaafc2c3 Release 0.5.6 2019-10-31 23:22:13 +00:00
a3fc1a62e7 format the I2P bandwidths 2019-10-31 21:52:22 +00:00
2fd8f45107 update text in cli 2019-10-31 21:22:50 +00:00
2429bbf59e Add update notification window 2019-10-31 20:51:09 +00:00
f7e28e04f6 add a system status panel 2019-10-31 14:14:14 +00:00
cc0188f20e show used memory, not free memory 2019-10-31 13:46:16 +00:00
af9b4f4679 change package name for cli 2019-10-31 13:05:42 +00:00
625a559d02 change package name 2019-10-31 13:02:44 +00:00
6e20193d57 properly set Xmx 2019-10-31 07:15:54 +00:00
88ac267f99 show java version and ram usage in cli 2019-10-31 07:14:52 +00:00
9b3a7473d1 limit Xmx on cli-lanterna too 2019-10-31 06:52:56 +00:00
5b0180280e fix changing font and size on metal lnf 2019-10-30 22:20:27 +00:00
d0462034fc enforce comment length in cli as well 2019-10-30 21:51:16 +00:00
f3e4098107 refresh gui when processing a sidecar file 2019-10-30 21:45:38 +00:00
26e7ca0b21 enforce maximum comment length in the gui 2019-10-30 21:22:08 +00:00
11007e5f19 allow up to exact max comment length 2019-10-30 21:20:09 +00:00
ae651cb6bd implement sidecar files 2019-10-30 21:07:59 +00:00
cad3a88517 Xmx256M by default 2019-10-30 21:06:33 +00:00
29c81646af word-wrap the comment views 2019-10-30 19:52:37 +00:00
8a0257927b Link to CLI configuration options 2019-10-30 19:43:51 +00:00
154 changed files with 6303 additions and 691 deletions

View File

@ -4,7 +4,7 @@ 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.3 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.6.2 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building
@ -23,7 +23,7 @@ If you want to build binary bundles that do not depend on Java or I2P, see the h
### 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.
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 https://github.com/zlatinb/muwire/wiki/CLI-Configuration-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
```

View File

@ -6,15 +6,19 @@ buildscript {
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
}
}
apply plugin : 'application'
mainClassName = 'com.muwire.clilanterna.CliLanterna'
application {
mainClassName = 'com.muwire.clilanterna.CliLanterna'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire-cli'
}
apply plugin : 'com.github.johnrengelman.shadow'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile project(":core")

View File

@ -3,6 +3,8 @@ package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextBox
import com.googlecode.lanterna.gui2.TextGUI
import com.googlecode.lanterna.gui2.Window
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.gui2.BasicWindow
import com.googlecode.lanterna.gui2.Button
@ -10,6 +12,7 @@ import com.googlecode.lanterna.gui2.GridLayout
import com.googlecode.lanterna.gui2.GridLayout.Alignment
import com.googlecode.lanterna.gui2.LayoutData
import com.googlecode.lanterna.gui2.Panel
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.files.UICommentEvent
@ -49,11 +52,16 @@ class AddCommentView extends BasicWindow {
Button saveButton = new Button("Save", {
String newComment = textBox.getText()
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
String encodedOldComment = sharedFile.getComment()
sharedFile.setComment(newComment)
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
close()
if (newComment.length() > Constants.MAX_COMMENT_LENGTH) {
String error = "Your comment is too long - ${newComment.length()} bytes. Maximum is $Constants.MAX_COMMENT_LENGTH bytes"
MessageDialog.showMessageDialog(textGUI, "Comment Too Long", error, MessageDialogButton.Close)
} else {
newComment = Base64.encode(DataUtil.encodei18nString(newComment))
String encodedOldComment = sharedFile.getComment()
sharedFile.setComment(newComment)
core.eventBus.publish(new UICommentEvent(sharedFile : sharedFile, oldComment : encodedOldComment))
close()
}
})
Button cancelButton = new Button("Cancel", {close()})

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 = ""

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)
@ -71,19 +71,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 +110,14 @@ 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)
}
}

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

@ -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.5"
private static final String MW_VERSION = "0.6.4"
private static volatile Core core
@ -82,14 +82,14 @@ class CliLanterna {
props.setDownloadLocation(downloadLocationFile)
props.incompleteLocation = incompletesLocationFile
propsFile.withOutputStream {
propsFile.withPrintWriter("UTF-8", {
props.write(it)
}
})
} else {
props = new Properties()
propsFile.withInputStream {
propsFile.withReader("UTF-8", {
props.load(it)
}
})
props = new MuWireSettings(props)
}
props.updateType = "cli-lanterna"

View File

@ -18,7 +18,7 @@ class FilesModel {
private final TextGUIThread guiThread
private final Core core
private final List<SharedFile> sharedFiles = new ArrayList<>()
private final TableModel model = new TableModel("Name","Size","Comment","Search Hits","Downloaders")
private final TableModel model = new TableModel("Name","Size","Comment","Certified","Search Hits","Downloaders")
FilesModel(TextGUIThread guiThread, Core core) {
this.guiThread = guiThread
@ -41,7 +41,7 @@ class FilesModel {
def eventBus = core.eventBus
guiThread.invokeLater {
core.muOptions.watchedDirectories.each {
eventBus.publish(new DirectoryWatchedEvent(directory : new File(it)))
eventBus.publish(new FileSharedEvent(file: new File(it)))
}
}
}
@ -72,9 +72,10 @@ class FilesModel {
sharedFiles.each {
long size = it.getCachedLength()
boolean comment = it.comment != null
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
String hits = String.valueOf(it.getHits())
String downloaders = String.valueOf(it.getDownloaders().size())
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, hits, downloaders)
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
}
}
}

View File

@ -17,6 +17,7 @@ import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
import com.googlecode.lanterna.gui2.table.Table
import com.muwire.core.Core
import com.muwire.core.SharedFile
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
@ -42,7 +43,7 @@ class FilesView extends BasicWindow {
contentPanel.setLayoutManager(new GridLayout(1))
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
table = new Table("Name","Size","Comment","Search Hits","Downloaders")
table = new Table("Name","Size","Comment","Certified","Search Hits","Downloaders")
table.setCellSelection(false)
table.setTableModel(model.model)
table.setSelectAction({rowSelected()})
@ -77,7 +78,7 @@ class FilesView extends BasicWindow {
Window prompt = new BasicWindow("Unshare or add comment to "+sf.getFile().getName()+" ?")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
contentPanel.setLayoutManager(new GridLayout(4))
Button unshareButton = new Button("Unshare", {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
@ -88,11 +89,16 @@ class FilesView extends BasicWindow {
AddCommentView view = new AddCommentView(textGUI, core, sf, terminalSize)
textGUI.addWindowAndWait(view)
})
Button certifyButton = new Button("Certify", {
core.eventBus.publish(new UICreateCertificateEvent(sharedFile : sf))
MessageDialog.showMessageDialog(textGUI, "Certificate Created", "Certificate has been issued", MessageDialogButton.OK)
})
Button closeButton = new Button("Close", {prompt.close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
contentPanel.addComponent(unshareButton, layoutData)
contentPanel.addComponent(addCommentButton, layoutData)
contentPanel.addComponent(certifyButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)

View File

@ -30,6 +30,7 @@ import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.update.UpdateDownloadedEvent
import net.i2p.data.Base64
import net.i2p.data.DataHelper
class MainWindowView extends BasicWindow {
@ -49,6 +50,7 @@ class MainWindowView extends BasicWindow {
private final Label sharedFiles
private final Label timesBrowsed
private final Label updateStatus
private final Label usedRam, totalRam, maxRam
public MainWindowView(String title, Core core, TextGUI textGUI, Screen screen, CliSettings props) {
super(title);
@ -130,6 +132,9 @@ class MainWindowView extends BasicWindow {
sharedFiles = new Label("0")
timesBrowsed = new Label("0")
updateStatus = new Label("Unknown")
usedRam = new Label("0")
maxRam = new Label("0")
totalRam = new Label("0")
statusPanel.with {
addComponent(new Label("Incoming Connections: "), layoutData)
@ -148,6 +153,14 @@ class MainWindowView extends BasicWindow {
addComponent(timesBrowsed, layoutData)
addComponent(new Label("Update Status: "), layoutData)
addComponent(updateStatus, layoutData)
addComponent(new Label("Java Version: "), layoutData)
addComponent(new Label(System.getProperty("java.vendor")+ " " + System.getProperty("java.version")), layoutData)
addComponent(new Label("Used Memory: "), layoutData)
addComponent(usedRam, layoutData)
addComponent(new Label("Total Memory: "), layoutData)
addComponent(totalRam, layoutData)
addComponent(new Label("Maximum Memory: "), layoutData)
addComponent(maxRam, layoutData)
}
refreshStats()
@ -204,8 +217,11 @@ class MainWindowView extends BasicWindow {
textGUI.getGUIThread().invokeLater {
String label = "$e.version is available with hash $e.infoHash"
updateStatus.setText(label)
String message = "Version $e.version is available from $e.signer, search for $e.infoHash"
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
String message = "Version $e.version is available, with hash $e.infoHash . Show details?"
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
@ -213,8 +229,11 @@ class MainWindowView extends BasicWindow {
textGUI.getGUIThread().invokeLater {
String label = "$e.version downloaded"
updateStatus.setText(label)
String message = "Version $e.version from $e.signer has been downloaded. You can update now."
MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.OK)
String message = "MuWire version $e.version has been downloaded. Show details?."
def button = MessageDialog.showMessageDialog(textGUI, "Update Available", message, MessageDialogButton.Yes, MessageDialogButton.No)
if (button == MessageDialogButton.No)
return
textGUI.addWindowAndWait(new UpdateTextView(e.text, sizeForTables()))
}
}
@ -266,6 +285,17 @@ class MainWindowView extends BasicWindow {
int hopelessHosts = core.hostCache.countHopelessHosts()
int shared = core.fileManager.fileToSharedFile.size()
int browsed = core.connectionAcceptor.browsed
long freeMemL = Runtime.getRuntime().freeMemory()
long totalMemL = Runtime.getRuntime().totalMemory()
String usedMem = DataHelper.formatSize2Decimal(freeMemL, false) + "B"
String totalMem = DataHelper.formatSize2Decimal(totalMemL, false)+"B"
String maxMem
long maxMemL = Runtime.getRuntime().maxMemory()
if (maxMemL >= Long.MAX_VALUE / 2)
maxMem = "Unlimited"
else
maxMem = DataHelper.formatSize2Decimal(maxMemL, false) + "B"
incoming.setText(String.valueOf(inCon))
outgoing.setText(String.valueOf(outCon))
@ -274,5 +304,8 @@ class MainWindowView extends BasicWindow {
hopeless.setText(String.valueOf(hopelessHosts))
sharedFiles.setText(String.valueOf(shared))
timesBrowsed.setText(String.valueOf(browsed))
usedRam.setText(usedMem)
totalRam.setText(totalMem)
maxRam.setText(maxMem)
}
}

View File

@ -15,13 +15,13 @@ class ResultsModel {
ResultsModel(UIResultBatchEvent results) {
this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment")
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot())
String sources = String.valueOf(it.sources.size())
String comment = String.valueOf(it.comment != null)
model.addRow(it.name, size, infoHash, sources, comment)
model.addRow(it.name, size, infoHash, sources, comment, it.certificates)
rootToResult.put(infoHash, it)
}
}

View File

@ -37,7 +37,7 @@ class ResultsView extends BasicWindow {
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(1))
table = new Table("Name","Size","Hash","Sources","Comment")
table = new Table("Name","Size","Hash","Sources","Comment","Certificates")
table.setCellSelection(false)
table.setSelectAction({rowSelected()})
table.setTableModel(model.model)
@ -55,18 +55,29 @@ class ResultsView extends BasicWindow {
int selectedRow = table.getSelectedRow()
def rows = model.model.getRow(selectedRow)
boolean comment = Boolean.parseBoolean(rows[4])
if (comment) {
Window prompt = new BasicWindow("Download Or View Comment")
boolean certificates = rows[5] > 0
if (comment || certificates) {
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
Window prompt = new BasicWindow("Download Or View Comment/Certificates")
prompt.setHints([Window.Hint.CENTERED])
Panel contentPanel = new Panel()
contentPanel.setLayoutManager(new GridLayout(3))
contentPanel.setLayoutManager(new GridLayout(4))
Button downloadButton = new Button("Download", {download(rows[2])})
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
contentPanel.addComponent(downloadButton, layoutData)
if (comment) {
Button viewButton = new Button("View Comment", {viewComment(rows[2])})
contentPanel.addComponent(viewButton, layoutData)
}
if (certificates) {
Button certsButton = new Button("View Certificates", {viewCertificates(rows[2])})
contentPanel.addComponent(certsButton, layoutData)
}
Button closeButton = new Button("Cancel", {prompt.close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
contentPanel.addComponent(downloadButton, layoutData)
contentPanel.addComponent(viewButton, layoutData)
contentPanel.addComponent(closeButton, layoutData)
prompt.setComponent(contentPanel)
downloadButton.takeFocus()
@ -88,7 +99,14 @@ 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)
}
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)
}
}

View File

@ -7,8 +7,13 @@ import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.nio.charset.StandardCharsets
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
@ -40,20 +45,27 @@ 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 replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
def nonEmpty = SplitPattern.termify(query)
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
}
void unregister() {

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)
})
@ -199,6 +202,6 @@ class TrustView extends BasicWindow {
private void saveMuSettings() {
File settingsFile = new File(core.home,"MuWire.properties")
settingsFile.withOutputStream { core.muOptions.write(it) }
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
}
}

View File

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

View File

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

View File

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

View File

@ -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
@ -18,6 +24,11 @@ import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.filecert.CertificateClient
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.filecert.UIFetchCertificatesEvent
import com.muwire.core.filecert.UIImportCertificateEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
@ -28,6 +39,7 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
@ -98,10 +110,15 @@ public class Core {
final FileManager fileManager
final UploadManager uploadManager
final ContentManager contentManager
final CertificateManager certificateManager
final ChatServer chatServer
final ChatManager chatManager
private final Router router
final AtomicBoolean shutdown = new AtomicBoolean()
final SigningPrivateKey spk
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@ -179,7 +196,7 @@ public class Core {
i2pSession = socketManager.getSession()
def destination = new Destination()
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
spk = new SigningPrivateKey(Constants.SIG_TYPE)
keyDat.withInputStream {
destination.readBytes(it)
def privateKey = new PrivateKey()
@ -190,8 +207,9 @@ public class Core {
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.write(Constants.PERSONA_VERSION)
daos.writeShort((short)props.getNickname().length())
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
daos.writeShort((short)name.length)
daos.write(name)
destination.writeBytes(daos)
daos.flush()
byte [] payload = baos.toByteArray()
@ -205,6 +223,12 @@ public class Core {
eventBus = new EventBus()
log.info("initializing certificate manager")
certificateManager = new CertificateManager(eventBus, home, me, spk)
eventBus.register(UICreateCertificateEvent.class, certificateManager)
eventBus.register(UIImportCertificateEvent.class, certificateManager)
log.info("initializing trust service")
File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "distrusted")
@ -221,6 +245,7 @@ public class Core {
eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
eventBus.register(SideCarFileEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
@ -250,15 +275,27 @@ public class Core {
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me)
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
log.info("initializing certificate client")
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
log.info("initializing chat server")
chatServer = new ChatServer(eventBus, props, trustService, me, spk)
eventBus.with {
register(ChatMessageEvent.class, chatServer)
register(ChatDisconnectionEvent.class, chatServer)
register(TrustEvent.class, chatServer)
}
log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@ -281,10 +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)
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
certificateManager, chatServer)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
@ -309,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)
}
@ -333,13 +381,12 @@ public class Core {
return
}
log.info("saving settings")
File f = new File(home, "MuWire.properties")
f.withOutputStream { muOptions.write(it) }
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()
@ -347,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) {
@ -355,6 +406,11 @@ public class Core {
}
log.info("shutdown complete")
}
public void saveMuSettings() {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
@ -380,7 +436,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.5")
Core core = new Core(props, home, "0.6.4")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -20,6 +20,7 @@ class MuWireSettings {
int totalUploadSlots
int uploadSlotsPerUser
int updateCheckInterval
long lastUpdateCheck
boolean autoDownloadUpdate
String updateType
String nickname
@ -30,6 +31,9 @@ class MuWireSettings {
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
boolean startChatServer
int maxChatConnections
boolean advertiseChat
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
@ -60,6 +64,7 @@ class MuWireSettings {
incompleteLocation = new File(incompleteLocationProp)
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
@ -77,11 +82,14 @@ class MuWireSettings {
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
watchedRegexes = readEncodedSet(props, "watchedRegexes")
negativeFileTree = readEncodedSet(props, "negativeFileTree")
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
trustSubscriptions = new HashSet<>()
if (props.containsKey("trustSubscriptions")) {
@ -93,7 +101,7 @@ class MuWireSettings {
}
void write(OutputStream out) throws IOException {
void write(Writer out) throws IOException {
Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString())
@ -107,6 +115,7 @@ class MuWireSettings {
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
@ -124,11 +133,14 @@ 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))
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
writeEncodedSet(negativeFileTree, "negativeFileTree", props)
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
if (!trustSubscriptions.isEmpty()) {
String encoded = trustSubscriptions.stream().
@ -137,25 +149,7 @@ class MuWireSettings {
props.setProperty("trustSubscriptions", encoded)
}
props.store(out, "")
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
}
rv
}
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return
String encoded = set.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty(property, encoded)
props.store(out, "This file is UTF-8")
}
boolean isLeaf() {

View File

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

View File

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

View File

@ -2,6 +2,90 @@ 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 {
SPLIT_CHARS.with {
add(' '.toCharacter())
add('*'.toCharacter())
add('+'.toCharacter())
add('-'.toCharacter())
add(','.toCharacter())
add('.'.toCharacter())
add(':'.toCharacter())
add(';'.toCharacter())
add('('.toCharacter())
add(')'.toCharacter())
add('='.toCharacter())
add('_'.toCharacter())
add('/'.toCharacter())
add('\\'.toCharacter())
add('!'.toCharacter())
add('\''.toCharacter())
add('$'.toCharacter())
add('%'.toCharacter())
add('|'.toCharacter())
add('['.toCharacter())
add(']'.toCharacter())
add('{'.toCharacter())
add('}'.toCharacter())
add('?'.toCharacter())
}
}
public static String[] termify(final String source) {
String lowercase = source.toLowerCase().trim()
def rv = []
int pos = 0
int quote = -1
StringBuilder tmp = new StringBuilder()
while(pos < lowercase.length()) {
char c = lowercase.charAt(pos++)
if (quote < 0 && c == '"') {
quote = pos - 1
continue
}
if (quote >= 0) {
if (c == '"') {
quote = -1
if (tmp.length() != 0) {
rv << tmp.toString()
tmp = new StringBuilder()
}
} else
tmp.append(c)
} else if (SPLIT_CHARS.contains(c)) {
if (tmp.length() != 0) {
rv << tmp.toString()
tmp = new StringBuilder()
}
} else
tmp.append c
}
// check if odd number of quotes and re-tokenize from last quote
if (quote >= 0) {
tmp = new StringBuilder()
pos = quote + 1
while(pos < lowercase.length()) {
char c = lowercase.charAt(pos++)
if (SPLIT_CHARS.contains(c)) {
if (tmp.length() > 0) {
rv << tmp.toString()
tmp = new StringBuilder()
}
} else
tmp.append(c)
}
}
if (tmp.length() > 0)
rv << tmp.toString()
rv
}
}

View File

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

View File

@ -0,0 +1,119 @@
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 volatile ChatConnection connection
private volatile boolean connectInProgress
private volatile long lastRejectionTime
private volatile 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
}
void connectIfNeeded() {
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
return
CONNECTOR.execute({connect()})
}
private void connect() {
connectInProgress = true
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))
endpoint.close()
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")
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) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
endpoint?.close()
} finally {
connectInProgress = false
connectThread = null
}
}
void disconnected() {
connectInProgress = false
connection = null
}
@Override
public void close() {
connectThread?.interrupt()
connection?.close()
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
}
void ping() {
connection?.sendPing()
}
}

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,278 @@
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 {
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 {
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,10 @@
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
}

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]?.connection?.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,302 @@
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 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 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() {
running.set(true)
connections.put(me.destination, LocalChatLink.INSTANCE)
joinRoom(me, CONSOLE)
echo("/SAY Welcome to my chat server! Type /HELP for list of available commands.",me.destination)
}
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)
connection.start()
echo("/SAY Welcome to my chat server! Type /HELP for help on available commands",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)
}
}
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.remove(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
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
}
}
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 /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
/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)
}
void stop() {
if (running.compareAndSet(true, false)) {
connections.each { k, v ->
v.close()
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
@ -10,6 +11,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
@ -21,8 +23,10 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
@Log
abstract class Connection implements Closeable {
@ -147,6 +151,12 @@ abstract class Connection implements Closeable {
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
if (e.sig != null)
query.sig = Base64.encode(e.sig)
if (e.queryTime > 0)
query.queryTime = e.queryTime
if (e.sig2 != null)
query.sig2 = Base64.encode(e.sig2)
messages.put(query)
}
@ -225,18 +235,67 @@ abstract class Connection implements Closeable {
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
if (infohash != null)
payload = infohash
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
} else
log.info("query signature verified")
} else {
log.info("no signature in query")
return
}
// 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 )
firstHop : search.firstHop,
sig : sig,
queryTime : queryTime,
sig2 : sig2 )
eventBus.publish(event)
}

View File

@ -1,6 +1,7 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.nio.file.attribute.DosFileAttributes
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
@ -11,8 +12,12 @@ import java.util.zip.InflaterInputStream
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.chat.ChatServer
import com.muwire.core.filecert.Certificate
import com.muwire.core.filecert.CertificateManager
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel
@ -45,6 +50,8 @@ class ConnectionAcceptor {
final UploadManager uploadManager
final FileManager fileManager
final ConnectionEstablisher establisher
final CertificateManager certificateManager
final ChatServer chatServer
final ExecutorService acceptorThread
final ExecutorService handshakerThreads
@ -56,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) {
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager,
ChatServer chatServer) {
this.eventBus = eventBus
this.manager = manager
this.settings = settings
@ -67,6 +75,8 @@ class ConnectionAcceptor {
this.fileManager = fileManager
this.uploadManager = uploadManager
this.establisher = establisher
this.certificateManager = certificateManager
this.chatServer = chatServer
acceptorThread = Executors.newSingleThreadExecutor { r ->
def rv = new Thread(r)
@ -145,11 +155,20 @@ class ConnectionAcceptor {
case (byte)'B':
processBROWSE(e)
break
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)
try {
e.getOutputStream().close()
} catch (Exception ignore) {}
e.close()
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
}
@ -198,7 +217,9 @@ class ConnectionAcceptor {
os.writeShort(json.bytes.length)
os.write(json.bytes)
}
e.outputStream.flush()
try {
e.outputStream.close()
} catch (Exception ignored) {}
e.close()
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
}
@ -278,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))
@ -313,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) {
@ -330,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) {
@ -352,8 +374,9 @@ class ConnectionAcceptor {
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
it.hit()
def obj = ResultsSender.sharedFileToObj(it, false)
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)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
@ -372,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()
@ -383,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()
@ -406,5 +460,64 @@ class ConnectionAcceptor {
e.close()
}
}
private void processCERTIFICATES(Endpoint e) {
try {
byte [] ERTIFICATES = new byte[12]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(ERTIFICATES)
if (ERTIFICATES != "ERTIFICATES ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid CERTIFICATES connection")
byte [] infoHashStringBytes = new byte[44]
dis.readFully(infoHashStringBytes)
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
byte[] rn = new byte[2]
dis.readFully(rn)
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Malformed CERTIFICATES request")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
log.info("responding to certificates request for $infoHashString")
byte [] root = Base64.decode(infoHashString)
Set<Certificate> certs = certificateManager.getByInfoHash(new InfoHash(root))
if (certs.isEmpty()) {
log.info("certs not found")
e.getOutputStream().write("404 Certs Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
return
}
OutputStream os = e.getOutputStream()
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: ${certs.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(os)
certs.each {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
it.write(baos)
byte [] payload = baos.toByteArray()
dos.writeShort(payload.length)
dos.write(payload)
}
dos.close()
} finally {
e.close()
}
}
private void processIRC(Endpoint e) {
byte[] IRC = new byte[4]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(IRC)
if (IRC != "RC\r\n".getBytes(StandardCharsets.US_ASCII))
throw new Exception("Invalid IRC connection")
chatServer.handle(e)
}
}

View File

@ -21,6 +21,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
@Log
@ -37,13 +38,12 @@ class DownloadSession {
private final Set<Integer> available
private final MessageDigest digest
private long lastSpeedRead = System.currentTimeMillis()
private long dataSinceLastRead
private final AtomicLong dataSinceLastRead
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) {
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
this.eventBus = eventBus
this.meB64 = meB64
this.pieces = pieces
@ -53,6 +53,7 @@ class DownloadSession {
this.pieceSize = pieceSize
this.fileLength = fileLength
this.available = available
this.dataSinceLastRead = dataSinceLastRead
try {
digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) {
@ -190,7 +191,7 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
dataSinceLastRead += read
dataSinceLastRead.addAndGet(read)
pieces.markPartial(piece, mapped.position())
}
}
@ -222,13 +223,4 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
rv
}
}

View File

@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
import com.muwire.core.Constants
@ -61,10 +62,11 @@ public class Downloader {
private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
private volatile long lastSpeedRead = System.currentTimeMillis()
private ArrayList speedArr = new ArrayList<Integer>()
private int speedPos = 0
private int speedAvg = 0
private long timestamp = Instant.now().toEpochMilli()
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
@ -139,10 +141,11 @@ public class Downloader {
public int speed() {
int currSpeed = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
currSpeed += it.speed()
}
long dataRead = dataSinceLastRead.getAndSet(0)
long now = System.currentTimeMillis()
if (now > lastSpeedRead)
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
lastSpeedRead = now
}
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
@ -273,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
@ -302,7 +356,7 @@ public class Downloader {
boolean requestPerformed
while(!pieces.isComplete()) {
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available)
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
@ -339,12 +393,6 @@ public class Downloader {
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}

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

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package com.muwire.core.filecert
import com.muwire.core.Event
class CertificateFetchEvent extends Event {
CertificateFetchStatus status
int count
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,9 +86,9 @@ class DirectoryWatcher {
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muSettingsFile.withPrintWriter("UTF-8", {
muOptions.write(it)
}
})
}
private void watch() {

View File

@ -25,6 +25,7 @@ class FileManager {
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex()
final FileTree negativeTree = new FileTree()
final Set<File> sideCarFiles = new HashSet<>()
FileManager(EventBus eventBus, MuWireSettings settings) {
this.settings = settings
@ -36,8 +37,15 @@ class FileManager {
}
void onFileHashedEvent(FileHashedEvent e) {
if (e.sharedFile != null)
addToIndex(e.sharedFile)
if (e.sharedFile == null)
return
File f = e.sharedFile.getFile()
if (sideCarFiles.remove(f)) {
File sideCar = new File(f.getParentFile(), f.getName() + ".mwcomment")
if (sideCar.exists())
e.sharedFile.setComment(Base64.encode(DataUtil.encodei18nString(sideCar.text)))
}
addToIndex(e.sharedFile)
}
void onFileLoadedEvent(FileLoadedEvent e) {
@ -49,6 +57,21 @@ class FileManager {
addToIndex(e.downloadedFile)
}
}
void onSideCarFileEvent(SideCarFileEvent e) {
String name = e.file.getName()
name = name.substring(0, name.length() - ".mwcomment".length())
File target = new File(e.file.getParentFile(), name)
SharedFile existing = fileToSharedFile.get(target)
if (existing == null) {
sideCarFiles.add(target)
return
}
String comment = Base64.encode(DataUtil.encodei18nString(e.file.text))
String oldComment = existing.getComment()
existing.setComment(comment)
eventBus.publish(new UICommentEvent(oldComment : oldComment, sharedFile : existing))
}
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())
@ -120,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())
@ -175,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 {
@ -191,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)
}
@ -206,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

@ -45,7 +45,7 @@ class FileTree {
true
}
private static class TreeNode {
public static class TreeNode {
TreeNode parent
File file
final Set<TreeNode> children = new HashSet<>()

View File

@ -3,6 +3,7 @@ package com.muwire.core.files
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.SharedFile
@ -33,7 +34,12 @@ class HasherService {
return
if (fileManager.fileToSharedFile.containsKey(canonical))
return
if (hashed.add(canonical))
if (canonical.isFile() && fileManager.negativeTree.fileToNode.containsKey(canonical))
return
if (canonical.getName().endsWith(".mwcomment")) {
if (canonical.length() <= Constants.MAX_COMMENT_LENGTH)
eventBus.publish(new SideCarFileEvent(file : canonical))
} else if (hashed.add(canonical))
executor.execute( { -> process(canonical) } as Runnable)
}

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

@ -0,0 +1,12 @@
package com.muwire.core.files
import com.muwire.core.Event
class SideCarFileEvent extends Event {
File file
@Override
public String toString() {
return super.toString() + " file: "+file.getAbsolutePath()
}
}

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

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

View File

@ -99,6 +99,10 @@ class ResultsParser {
boolean browse = false
if (json.browse != null)
browse = json.browse
int certificates = 0
if (json.certificates != null)
certificates = json.certificates
return new UIResultEvent( sender : p,
name : name,
@ -108,7 +112,8 @@ class ResultsParser {
sources : sources,
comment : comment,
browse : browse,
uuid: uuid)
uuid: uuid,
certificates : certificates)
} catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e)
}

View File

@ -1,8 +1,10 @@
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
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import com.muwire.core.Persona
@ -46,12 +48,17 @@ class ResultsSender {
private final Persona me
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) {
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) {
@ -70,14 +77,18 @@ class ResultsSender {
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : pieceSize,
uuid : uuid,
browse : settings.browseFiles,
sources : suggested,
comment : comment
comment : comment,
certificates : certificates,
chat : chatServer.running.get() && settings.advertiseChat
)
uiResultEvents << uiResultEvent
}
@ -108,7 +119,8 @@ class ResultsSender {
me.write(os)
os.writeShort((short)results.length)
results.each {
def obj = sharedFileToObj(it, settings.browseFiles)
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
@ -124,10 +136,13 @@ 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 {
def obj = sharedFileToObj(it, settings.browseFiles)
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
@ -143,7 +158,7 @@ class ResultsSender {
}
}
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
public static def sharedFileToObj(SharedFile sf, boolean browseFiles, int certificates) {
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
@ -166,6 +181,7 @@ class ResultsSender {
obj.comment = sf.getComment()
obj.browse = browseFiles
obj.certificates = certificates
obj
}
}

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

@ -31,25 +31,49 @@ class SearchIndex {
}
}
private static String[] split(String source) {
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
String [] split = source.split(" ")
private static String[] split(final String source) {
// first split by split pattern
String sourceSplit = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
String [] split = sourceSplit.split(" ")
def rv = []
split.each { if (it.length() > 0) rv << it }
// then just by ' '
source.toLowerCase().split(' ').each { if (it.length() > 0) rv << it }
// and add original string
rv << source
rv << source.toLowerCase()
rv.toArray(new String[0])
}
String[] search(List<String> terms) {
Set<String> rv = null;
Set<String> powerSet = new HashSet<>()
terms.each {
powerSet.addAll(it.toLowerCase().split(' '))
}
powerSet.each {
Set<String> forWord = keywords.getOrDefault(it,[])
if (rv == null) {
rv = new HashSet<>(forWord)
} else {
rv.retainAll(forWord)
}
}
// now, filter by terms
for (Iterator<String> iter = rv.iterator(); iter.hasNext();) {
String candidate = iter.next()
candidate = candidate.toLowerCase()
boolean keep = true
terms.each {
keep &= candidate.contains(it)
}
if (!keep)
iter.remove()
}
if (rv != null)

View File

@ -16,6 +16,8 @@ class UIResultEvent extends Event {
int pieceSize
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

@ -7,4 +7,5 @@ class UpdateAvailableEvent extends Event {
String version
String signer
String infoHash
String text
}

View File

@ -9,9 +9,11 @@ import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileManager
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
@ -21,7 +23,10 @@ import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import net.i2p.util.VersionComparator
@Log
@ -32,6 +37,7 @@ class UpdateClient {
final MuWireSettings settings
final FileManager fileManager
final Persona me
final SigningPrivateKey spk
private final Timer timer
@ -40,14 +46,19 @@ class UpdateClient {
private volatile InfoHash updateInfoHash
private volatile String version, signer
private volatile boolean updateDownloading
private volatile String text
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings, FileManager fileManager, Persona me) {
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings,
FileManager fileManager, Persona me, SigningPrivateKey spk) {
this.eventBus = eventBus
this.session = session
this.myVersion = myVersion
this.settings = settings
this.fileManager = fileManager
this.me = me
this.spk = spk
this.lastUpdateCheckTime = settings.lastUpdateCheck
timer = new Timer("update-client",true)
}
@ -75,7 +86,9 @@ class UpdateClient {
if (e.downloadedFile.infoHash != updateInfoHash)
return
updateDownloading = false
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer))
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
if (!settings.shareDownloadedFiles)
eventBus.publish(new FileSharedEvent(file : e.downloadedFile))
}
private void checkUpdate() {
@ -85,6 +98,7 @@ class UpdateClient {
return
}
lastUpdateCheckTime = now
settings.lastUpdateCheck = now
log.info("checking for update")
@ -147,23 +161,28 @@ class UpdateClient {
} else
infoHash = payload[settings.updateType]
text = payload.text
if (!settings.autoDownloadUpdate) {
log.info("new version $payload.version available, publishing event")
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash))
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer, infoHash : infoHash, text : text))
} else {
log.info("new version $payload.version available")
updateInfoHash = new InfoHash(Base64.decode(infoHash))
if (fileManager.rootToFiles.containsKey(updateInfoHash))
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer))
eventBus.publish(new UpdateDownloadedEvent(version : payload.version, signer : payload.signer, text : text))
else {
updateDownloading = false
version = payload.version
signer = payload.signer
log.info("starting search for new version hash $payload.infoHash")
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true)
Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk)
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)
receivedOn : me.destination, originator : me, sig : sig.data, queryTime : timestamp, sig2 : sig2)
eventBus.publish(queryEvent)
}
}

View File

@ -5,4 +5,5 @@ import com.muwire.core.Event
class UpdateDownloadedEvent extends Event {
String version
String signer
String text
}

View File

@ -4,10 +4,17 @@ import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_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 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,11 +10,21 @@ 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 {
@ -95,6 +105,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;
@ -165,4 +189,28 @@ public class DataUtil {
} catch(Exception ex) { }
cb = null;
}
public static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>();
if (props.containsKey(property)) {
String [] encoded = props.getProperty(property).split(",");
for(String s : encoded)
rv.add(readi18nString(Base64.decode(s)));
}
return rv;
}
public static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return;
String encoded = set.stream().map(s -> Base64.encode(encodei18nString(s)))
.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

@ -0,0 +1,35 @@
package com.muwire.core
import org.junit.Test
class SplitPatternTest {
@Test
void testReplaceCharacters() {
assert SplitPattern.termify("a_b.c") == ['a','b','c']
}
@Test
void testPhrase() {
assert SplitPattern.termify('"siamese cat"') == ['siamese cat']
}
@Test
void testInvalidPhrase() {
assert SplitPattern.termify('"siamese cat') == ['siamese', 'cat']
}
@Test
void testManyPhrases() {
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

@ -95,7 +95,7 @@ class ConnectionAcceptorTest {
connectionEstablisher = connectionEstablisherMock.proxyInstance()
acceptor = new ConnectionAcceptor(eventBus, connectionManager, settings, i2pAcceptor,
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher)
hostCache, trustService, searchManager, uploadManager, null, connectionEstablisher, null)
acceptor.start()
Thread.sleep(100)
}

View File

@ -2,6 +2,8 @@ package com.muwire.core.download
import static org.junit.Assert.fail
import java.util.concurrent.atomic.AtomicLong
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@ -76,7 +78,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong())
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()

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 {
@ -149,7 +154,7 @@ class FileManagerTest {
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
manager.onSearchEvent new SearchEvent(searchHash : ih.getRoot())
Thread.sleep(20)
@ -170,7 +175,7 @@ class FileManagerTest {
SharedFile sf2 = new SharedFile(f2, ih2, 0)
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(unsharedFile: sf2)
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
// 1 match left
manager.onSearchEvent new SearchEvent(searchTerms: ["c"])
@ -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

@ -90,4 +90,56 @@ class SearchIndexTest {
def found = index.search(["muwire", "0", "3", "jar"])
assert found.size() == 1
}
@Test
void testOriginalText() {
initIndex(["a-b c-d"])
def found = index.search(['a-b'])
assert found.size() == 1
found = index.search(['c-d'])
assert found.size() == 1
}
@Test
void testPhrase() {
initIndex(["a-b c-d e-f"])
def found = index.search(['a-b c-d'])
assert found.size() == 1
assert index.search(['c-d e-f']).size() == 1
assert index.search(['a-b e-f']).size() == 0
}
@Test
void testMixedPhraseAndKeyword() {
initIndex(["My siamese cat video",
"My cat video of a siamese",
"Video of a siamese cat"])
assert index.search(['cat video']).size() == 2
assert index.search(['cat video','siamese']).size() == 2
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

View File

@ -1,11 +1,12 @@
group = com.muwire
version = 0.5.5
version = 0.6.4
i2pVersion = 0.9.43
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
grailsVersion=4.0.0
gorm.version=7.0.2.RELEASE
griffonEnv=prod
sourceCompatibility=1.8
targetCompatibility=1.8

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'
@ -40,8 +40,11 @@ griffon {
]
}
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
application {
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
applicationName = 'MuWire'
}
apply from: 'gradle/publishing.gradle'
// apply from: 'gradle/code-coverage.gradle'

View File

@ -41,6 +41,11 @@ mvcGroups {
view = 'com.muwire.gui.I2PStatusView'
controller = 'com.muwire.gui.I2PStatusController'
}
'system-status' {
model = 'com.muwire.gui.SystemStatusModel'
view = 'com.muwire.gui.SystemStatusView'
controller = 'com.muwire.gui.SystemStatusController'
}
'trust-list' {
model = 'com.muwire.gui.TrustListModel'
view = 'com.muwire.gui.TrustListView'
@ -71,4 +76,49 @@ mvcGroups {
view = 'com.muwire.gui.CloseWarningView'
controller = 'com.muwire.gui.CloseWarningController'
}
'update' {
model = 'com.muwire.gui.UpdateModel'
view = 'com.muwire.gui.UpdateView'
controller = 'com.muwire.gui.UpdateController'
}
'advanced-sharing' {
model = 'com.muwire.gui.AdvancedSharingModel'
view = 'com.muwire.gui.AdvancedSharingView'
controller = 'com.muwire.gui.AdvancedSharingController'
}
'fetch-certificates' {
model = 'com.muwire.gui.FetchCertificatesModel'
view = 'com.muwire.gui.FetchCertificatesView'
controller = 'com.muwire.gui.FetchCertificatesController'
}
'certificate-warning' {
model = 'com.muwire.gui.CertificateWarningModel'
view = 'com.muwire.gui.CertificateWarningView'
controller = 'com.muwire.gui.CertificateWarningController'
}
'certificate-control' {
model = 'com.muwire.gui.CertificateControlModel'
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'
}
}

View File

@ -7,7 +7,9 @@ import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import javax.swing.JOptionPane
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil
@ -24,6 +26,11 @@ class AddCommentController {
@ControllerAction
void save() {
String comment = view.textarea.getText()
if (comment.length() > Constants.MAX_COMMENT_LENGTH ) {
JOptionPane.showMessageDialog(null, "Your comment is too long - ${comment.length()} bytes. The maximum size is $Constants.MAX_COMMENT_LENGTH bytes",
"Comment Too Long", JOptionPane.WARNING_MESSAGE)
return
}
if (comment.trim().length() == 0)
comment = null
else

View File

@ -0,0 +1,17 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
@ArtifactProviderFor(GriffonController)
class AdvancedSharingController {
@MVCMember @Nonnull
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingView view
}

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) {
@ -69,7 +70,7 @@ class BrowseController {
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,
@ -92,8 +93,24 @@ 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)
}
@ControllerAction
void viewCertificates() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.size() != 1)
return
def result = selectedResults[0]
if (result.certificates <= 0)
return
def params = [:]
params['result'] = result
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 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,27 @@
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 CertificateWarningController {
@MVCMember @Nonnull
CertificateWarningView view
UISettings settings
File home
@ControllerAction
void dismiss() {
if (view.checkbox.model.isSelected()) {
settings.certificateWarning = false
File propsFile = new File(home, "gui.properties")
propsFile.withOutputStream { settings.write(it) }
}
view.dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -0,0 +1,236 @@
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
if (!mvcGroup.parentGroup.childrenGroups.containsKey(newRoom)) {
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
mvcGroup.parentGroup.createMVCGroup("chat-room", model.host.getHumanReadableName()+"-"+newRoom, 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()
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 leaveRoom() {
if (leftRoom)
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()
}
}
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)
}
}
}

View File

@ -0,0 +1,20 @@
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() {
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
}
}

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